From 7a38eed18abc83b03c640e8b29f43a2156b8d2d6 Mon Sep 17 00:00:00 2001 From: marcie Date: Fri, 28 Aug 2026 17:53:02 -0500 Subject: [PATCH 01/17] framework tools/git: issue-close R1/R4 conversion (P1 first wrapper) Same contract as issue-comment (PR #1462): -b/--body canonical comment flag with -c/--comment alias; usage errors stderr + exit 2 (unknown option, missing -i, value-less flags); provider failures normalized to exit 1 on the GitHub path (gh's own usage errors exit 2 and would collide with the reserved status). New suite test-issue-close-usage-contract.sh, enrolled in framework-shell CI (enumeration guard OK, population 74). Probe assertions split: parser arms must contact zero providers; sandbox arms may issue detection reads only (tea login list), never gh/curl. Existing fail-closed suite still green. Mirrored to the brain tree. --- .../mosaic/framework/tools/git/issue-close.sh | 47 +++++-- .../git/test-issue-close-usage-contract.sh | 129 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 167 insertions(+), 11 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index 3d014a59..58850292 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -1,6 +1,7 @@ #!/bin/bash # issue-close.sh - Close an issue on GitHub or Gitea -# Usage: issue-close.sh -i [-c ] +# Usage: issue-close.sh -i [-b ] +# (-c/--comment is a backward-compatible alias for -b/--body; R1/R4 2026-08-28) set -e @@ -12,35 +13,49 @@ source "$SCRIPT_DIR/detect-platform.sh" ISSUE_NUMBER="" COMMENT="" +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1), so a +# caller or stop gate can tell an invocation defect from a delivery blocker. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: issue-close.sh -i [-b ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE_NUMBER="$2" shift 2 ;; - -c|--comment) + -b|--body|-c|--comment) + # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays + # a backward-compatible alias. + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" COMMENT="$2" shift 2 ;; -h|--help) - echo "Usage: issue-close.sh -i [-c ]" + echo "Usage: issue-close.sh -i [-b ]" echo "" echo "Options:" echo " -i, --issue Issue number (required)" - echo " -c, --comment Comment to add before closing (optional)" + echo " -b, --body Comment to add before closing (optional; canonical)" + echo " -c, --comment Alias for --body" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$ISSUE_NUMBER" ]]; then - echo "Error: Issue number is required (-i)" - exit 1 + usage_error "issue number is required (-i/--issue)" fi # Detect platform and close issue @@ -82,10 +97,22 @@ gitea_issue_close_api() { } if [[ "$PLATFORM" == "github" ]]; then + # R4: normalize provider failures to exit 1 (gh's own usage errors exit 2 + # and would collide with the reserved usage-error status). if [[ -n "$COMMENT" ]]; then - gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" + gh_rc=0 + gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub comment before close failed (gh exit $gh_rc)" >&2 + exit 1 + fi + fi + gh_rc=0 + gh issue close "$ISSUE_NUMBER" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub issue close failed (gh exit $gh_rc)" >&2 + exit 1 fi - gh issue close "$ISSUE_NUMBER" echo "Closed GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then GITEA_LOGIN_NAME=$(get_gitea_login || true) diff --git a/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh new file mode 100755 index 00000000..1a58618e --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-close.sh (R1/R4, 2026-08-28). +# +# R4: usage errors print to STDERR and exit 2, distinct from provider, +# credential, and verification failures (exit 1). R1: -b/--body is the +# canonical comment flag; -c/--comment remains a compatible alias. +# The comment is OPTIONAL here (an issue may close without one), so unlike +# issue-comment there is no missing-comment arm. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-close-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <> "$PROBE_LOG" +exit 0 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-close.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-close.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr + grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-close.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "unknown option" "unknown option names itself on stderr" + +# 3. Missing required issue number: rc 2, stderr. +expect_rc 2 "missing -i exits 2" +expect_stderr "issue number is required" "missing -i message on stderr" + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -i -b -c --issue --body --comment; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b -c; do + rc=0 + run_wrapper_sandboxed -i 5 "$flag" "closing note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Sandbox arms may issue DETECTION reads only (tea login list via the +# stub); no gh/curl write or read may occur. +if grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" | grep -q .; then + echo "FAIL: a sandbox arm performed a non-detection provider request:" >&2 + grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" >&2 + exit 1 +fi +if grep -qE '^(gh|curl)' "$PROBE_LOG"; then + echo "FAIL: gh or curl was invoked during a sandbox arm:" >&2 + grep -E '^(gh|curl)' "$PROBE_LOG" >&2 + exit 1 +fi + +echo "issue-close.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index ce1a3740..15980f6d 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From 7e4de270dcdcd544d93e4cfe4488d05985992510 Mon Sep 17 00:00:00 2001 From: marcie Date: Fri, 28 Aug 2026 17:55:09 -0500 Subject: [PATCH 02/17] framework tools/git: issue-reopen + pr-close R1/R4 conversions (P1) Same contract as issue-close (7a38eed1): -b/--body canonical comment flag with -c/--comment alias; usage errors stderr + exit 2 (unknown option, missing required, value-less flags); GitHub-path provider failures normalized to exit 1. Both new suites enrolled in framework-shell CI (enumeration guard OK, population 76). pr-close's suite uses FAILING provider stubs (exit 99): its API fallback treats a successful curl as a closed PR, so exit-0 stubs let sandbox arms succeed (measured); post-sandbox contact assertions are correspondingly scoped, parser arms remain zero-contact. Mirrored to the brain tree; all usage-contract suites green in both. --- .../framework/tools/git/issue-reopen.sh | 47 +++++-- .../mosaic/framework/tools/git/pr-close.sh | 47 +++++-- .../git/test-issue-reopen-usage-contract.sh | 129 ++++++++++++++++++ .../tools/git/test-pr-close-usage-contract.sh | 126 +++++++++++++++++ packages/mosaic/package.json | 2 +- 5 files changed, 330 insertions(+), 21 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-reopen.sh b/packages/mosaic/framework/tools/git/issue-reopen.sh index cb92acdc..b686f370 100755 --- a/packages/mosaic/framework/tools/git/issue-reopen.sh +++ b/packages/mosaic/framework/tools/git/issue-reopen.sh @@ -1,6 +1,7 @@ #!/bin/bash # issue-reopen.sh - Reopen a closed issue on GitHub or Gitea -# Usage: issue-reopen.sh -i [-c ] +# Usage: issue-reopen.sh -i [-b ] +# (-c/--comment is a backward-compatible alias for -b/--body; R1/R4 2026-08-28) set -e @@ -11,35 +12,49 @@ source "$SCRIPT_DIR/detect-platform.sh" ISSUE_NUMBER="" COMMENT="" +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1), so a +# caller or stop gate can tell an invocation defect from a delivery blocker. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: issue-reopen.sh -i [-b ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE_NUMBER="$2" shift 2 ;; - -c|--comment) + -b|--body|-c|--comment) + # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays + # a backward-compatible alias. + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" COMMENT="$2" shift 2 ;; -h|--help) - echo "Usage: issue-reopen.sh -i [-c ]" + echo "Usage: issue-reopen.sh -i [-b ]" echo "" echo "Options:" echo " -i, --issue Issue number (required)" - echo " -c, --comment Comment to add when reopening (optional)" + echo " -b, --body Comment to add when reopening (optional; canonical)" + echo " -c, --comment Alias for --body" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$ISSUE_NUMBER" ]]; then - echo "Error: Issue number is required (-i)" - exit 1 + usage_error "issue number is required (-i/--issue)" fi detect_platform >/dev/null @@ -80,10 +95,22 @@ gitea_issue_reopen_api() { } if [[ "$PLATFORM" == "github" ]]; then + # R4: normalize provider failures to exit 1 (gh's own usage errors exit 2 + # and would collide with the reserved usage-error status). if [[ -n "$COMMENT" ]]; then - gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" + gh_rc=0 + gh issue comment "$ISSUE_NUMBER" --body "$COMMENT" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub comment before reopen failed (gh exit $gh_rc)" >&2 + exit 1 + fi + fi + gh_rc=0 + gh issue reopen "$ISSUE_NUMBER" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub issue reopen failed (gh exit $gh_rc)" >&2 + exit 1 fi - gh issue reopen "$ISSUE_NUMBER" echo "Reopened GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then REPO_ARGS=$(get_gitea_repo_args || true) diff --git a/packages/mosaic/framework/tools/git/pr-close.sh b/packages/mosaic/framework/tools/git/pr-close.sh index 9fcb00f4..e2a75c14 100755 --- a/packages/mosaic/framework/tools/git/pr-close.sh +++ b/packages/mosaic/framework/tools/git/pr-close.sh @@ -1,6 +1,7 @@ #!/bin/bash # pr-close.sh - Close a pull request without merging on GitHub or Gitea -# Usage: pr-close.sh -n [-c ] +# Usage: pr-close.sh -n [-b ] +# (-c/--comment is a backward-compatible alias for -b/--body; R1/R4 2026-08-28) set -e @@ -11,44 +12,70 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" COMMENT="" +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1), so a +# caller or stop gate can tell an invocation defect from a delivery blocker. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: pr-close.sh -n [-b ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -n|--number) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" PR_NUMBER="$2" shift 2 ;; - -c|--comment) + -b|--body|-c|--comment) + # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays + # a backward-compatible alias. + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" COMMENT="$2" shift 2 ;; -h|--help) - echo "Usage: pr-close.sh -n [-c ]" + echo "Usage: pr-close.sh -n [-b ]" echo "" echo "Options:" echo " -n, --number PR number (required)" - echo " -c, --comment Comment before closing (optional)" + echo " -b, --body Comment before closing (optional; canonical)" + echo " -c, --comment Alias for --body" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$PR_NUMBER" ]]; then - echo "Error: PR number is required (-n)" - exit 1 + usage_error "PR number is required (-n/--number)" fi detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then + # R4: normalize provider failures to exit 1 (gh's own usage errors exit 2 + # and would collide with the reserved usage-error status). if [[ -n "$COMMENT" ]]; then - gh pr comment "$PR_NUMBER" --body "$COMMENT" + gh_rc=0 + gh pr comment "$PR_NUMBER" --body "$COMMENT" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub PR comment before close failed (gh exit $gh_rc)" >&2 + exit 1 + fi + fi + gh_rc=0 + gh pr close "$PR_NUMBER" || gh_rc=$? + if [[ "$gh_rc" -ne 0 ]]; then + echo "Error: GitHub PR close failed (gh exit $gh_rc)" >&2 + exit 1 fi - gh pr close "$PR_NUMBER" echo "Closed GitHub PR #$PR_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then if [[ -n "$COMMENT" ]]; then diff --git a/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh new file mode 100755 index 00000000..31c68271 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-reopen.sh (R1/R4, 2026-08-28). +# +# R4: usage errors print to STDERR and exit 2, distinct from provider, +# credential, and verification failures (exit 1). R1: -b/--body is the +# canonical comment flag; -c/--comment remains a compatible alias. +# The comment is OPTIONAL here (an issue may close without one), so unlike +# issue-comment there is no missing-comment arm. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-reopen-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <> "$PROBE_LOG" +exit 0 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-reopen.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-reopen.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr + grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-reopen.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "unknown option" "unknown option names itself on stderr" + +# 3. Missing required issue number: rc 2, stderr. +expect_rc 2 "missing -i exits 2" +expect_stderr "issue number is required" "missing -i message on stderr" + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -i -b -c --issue --body --comment; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b -c; do + rc=0 + run_wrapper_sandboxed -i 5 "$flag" "closing note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Sandbox arms may issue DETECTION reads only (tea login list via the +# stub); no gh/curl write or read may occur. +if grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" | grep -q .; then + echo "FAIL: a sandbox arm performed a non-detection provider request:" >&2 + grep -Ev '^(gh|tea|curl) login list' "$PROBE_LOG" >&2 + exit 1 +fi +if grep -qE '^(gh|curl)' "$PROBE_LOG"; then + echo "FAIL: gh or curl was invoked during a sandbox arm:" >&2 + grep -E '^(gh|curl)' "$PROBE_LOG" >&2 + exit 1 +fi + +echo "issue-reopen.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh new file mode 100755 index 00000000..610d0f33 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-close.sh (R1/R4, 2026-08-28). +# +# R4: usage errors print to STDERR and exit 2, distinct from provider, +# credential, and verification failures (exit 1). R1: -b/--body is the +# canonical comment flag; -c/--comment remains a compatible alias. +# The comment is OPTIONAL here (an issue may close without one), so unlike +# issue-comment there is no missing-comment arm. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-close-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/pr-close.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/pr-close.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr + grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: pr-close.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "unknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -n exits 2" +expect_stderr "PR number is required" "missing -n message on stderr" + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -n -b -c --number --body --comment; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b -c; do + rc=0 + run_wrapper_sandboxed -n 5 "$flag" "closing note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "pr-close.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 15980f6d..a5bb877e 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From 31a447a46faaaaecebce9684e7848878cddc29fb Mon Sep 17 00:00:00 2001 From: marcie Date: Fri, 28 Aug 2026 17:57:29 -0500 Subject: [PATCH 03/17] framework tools/git: pr-review R1/R4 conversion (P1) -b/--body canonical review comment flag with -c/--comment alias; usage errors stderr + exit 2 (unknown option, missing -n/-a, value-less flags including -a/-l/-r/-H, and the semantic check: request-changes without a comment). Existing pr-review suites still green. Suite enrolled in framework-shell CI (enumeration guard OK); mirrored to the brain tree. --- .../mosaic/framework/tools/git/pr-review.sh | 41 ++++-- .../git/test-pr-review-usage-contract.sh | 130 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 159 insertions(+), 14 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index b131f385..b36a6a66 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -43,60 +43,77 @@ LOGIN_OVERRIDE="" REPO_OVERRIDE="" HOST_OVERRIDE="" +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1), so a +# caller or stop gate can tell an invocation defect from a delivery blocker. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: pr-review.sh -n -a [-b ] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -n|--number) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" PR_NUMBER="$2" shift 2 ;; -a|--action) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ACTION="$2" shift 2 ;; - -c|--comment) + -b|--body|-c|--comment) + # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays + # a backward-compatible alias. + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" COMMENT="$2" shift 2 ;; -l|--login) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LOGIN_OVERRIDE="$2" shift 2 ;; -r|--repo) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" REPO_OVERRIDE="$2" shift 2 ;; -H|--host) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" HOST_OVERRIDE="$2" shift 2 ;; -h|--help) - echo "Usage: pr-review.sh -n -a [-c ] [--login ] [-r owner/repo] [-H host]" + echo "Usage: pr-review.sh -n -a [-b ] [--login ] [-r owner/repo] [-H host]" echo "" echo "Options:" echo " -n, --number PR number (required)" echo " -a, --action Review action: approve, request-changes, comment (required)" - echo " -c, --comment Review comment (required for request-changes)" + echo " -b, --body Review comment (required for request-changes; canonical)" + echo " -c, --comment Alias for --body" echo " -l, --login Override the detected Gitea tea login (approve/request-changes only)" echo " -r, --repo Explicit owner/repo slug (skips git-remote slug inference)" echo " -H, --host Explicit Gitea host (skips remote-host inference)" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$PR_NUMBER" ]]; then - echo "Error: PR number is required (-n)" - exit 1 + usage_error "PR number is required (-n/--number)" fi if [[ -z "$ACTION" ]]; then - echo "Error: Action is required (-a): approve, request-changes, comment" - exit 1 + usage_error "Action is required (-a/--action): approve, request-changes, comment" fi if [[ -n "$REPO_OVERRIDE" ]]; then @@ -684,8 +701,7 @@ if [[ "$PLATFORM" == "github" ]]; then ;; request-changes) if [[ -z "$COMMENT" ]]; then - echo "Error: Comment required for request-changes" - exit 1 + usage_error "comment required for request-changes (-b/--body)" fi gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT" echo "Requested changes on GitHub PR #$PR_NUMBER" @@ -738,8 +754,7 @@ elif [[ "$PLATFORM" == "gitea" ]]; then ;; request-changes) if [[ -z "$COMMENT" ]]; then - echo "Error: Comment required for request-changes" - exit 1 + usage_error "comment required for request-changes (-b/--body)" fi # Best-effort host for credential resolution only (gitea_resolve_api_for_login # below re-derives the real host from HOST_OVERRIDE/remote independently and diff --git a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh new file mode 100755 index 00000000..518db7c6 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-review.sh (R1/R4, 2026-08-28). +# +# R4: usage errors print to STDERR and exit 2, distinct from provider, +# credential, and verification failures (exit 1). R1: -b/--body is the +# canonical comment flag; -c/--comment remains a compatible alias. +# Required: -n AND -a. The comment is required only for the +# request-changes action (semantic usage check, also rc 2). +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-review-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/pr-review.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/pr-review.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr + grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: pr-review.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "unknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -n exits 2" +expect_stderr "PR number is required" "missing -n message on stderr" +expect_rc 2 "missing -a exits 2" -n 5 +expect_stderr "Action is required" "missing -a message on stderr" +expect_rc 2 "request-changes without comment exits 2" -n 5 -a request-changes +expect_stderr "comment required for request-changes" "request-changes message on stderr" + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -n -a -b -c -l -r --number --action --body --comment --login --repo; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b -c; do + rc=0 + run_wrapper_sandboxed -n 5 -a comment "$flag" "review note" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "pr-review.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index a5bb877e..33f48d8d 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From dff486b43a8f32f78301a6d02f227ff5b4f2fd20 Mon Sep 17 00:00:00 2001 From: marcie Date: Fri, 28 Aug 2026 17:59:18 -0500 Subject: [PATCH 04/17] framework tools/git: issue-edit R1/R4 conversion (P1) issue-edit already used long-flag-first parsing; this adds the rc=2 usage-error contract (unknown option, missing -i, value-less flags across all five options), the exit-code line in help, and the enrolled usage-contract suite (population 78). Mirrored to the brain tree. --- .../mosaic/framework/tools/git/issue-edit.sh | 22 ++- .../git/test-issue-edit-usage-contract.sh | 125 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 144 insertions(+), 5 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index 865af4fc..94ae9d9d 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -14,25 +14,39 @@ BODY="" LABELS="" MILESTONE="" +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1), so a +# caller or stop gate can tell an invocation defect from a delivery blocker. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: issue-edit.sh -i [-t ] [-b <body>] [-l <labels>] [-m <milestone>] (see --help)" >&2 + exit 2 +} + while [[ $# -gt 0 ]]; do case $1 in -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE_NUMBER="$2" shift 2 ;; -t|--title) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" TITLE="$2" shift 2 ;; -b|--body) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" BODY="$2" shift 2 ;; -l|--labels) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LABELS="$2" shift 2 ;; -m|--milestone) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" MILESTONE="$2" shift 2 ;; @@ -46,18 +60,18 @@ while [[ $# -gt 0 ]]; do echo " -l, --labels Labels (comma-separated, replaces existing)" echo " -m, --milestone Milestone name" echo " -h, --help Show this help" + echo "" + echo "Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential/verification failure." exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$ISSUE_NUMBER" ]]; then - echo "Error: Issue number is required (-i)" - exit 1 + usage_error "issue number is required (-i/--issue)" fi detect_platform >/dev/null diff --git a/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh new file mode 100755 index 00000000..c2ad83fb --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-edit.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-edit-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-edit.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-edit.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-edit.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "unknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -i exits 2" +expect_stderr "issue number is required" "missing -i message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -i -t -b -l -m --issue --title --body --labels --milestone; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b; do + rc=0 + run_wrapper_sandboxed -i 5 "$flag" "value" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "issue-edit.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 33f48d8d..2c01eaa5 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From c43b1a129313966378ac0a480add62cb8863deac Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:01:12 -0500 Subject: [PATCH 05/17] framework tools/git: issue-create R1/R4 conversion + template bug fix (P1) - issue-create: usage() default exit is now 2 (usage-error contract); unknown option, missing title, and value-less -t/-b/-l/-m land there with stderr messages; exit-code line added to help. Interactive -i/--interactive unchanged (boolean, no value arm). Existing body-safety and interactive-auth suites still green. - Template bug fixed across ALL seven usage-contract suites: expect_stderr referenced an unbound $desc (should be $2); latent until issue-create's capitalized 'Unknown option' missed the case-sensitive grep and fired it. All seven re-verified green. - Suite enrolled (population 79); mirrored to the brain tree. --- .../framework/tools/git/issue-create.sh | 15 ++- .../git/test-issue-close-usage-contract.sh | 2 +- .../git/test-issue-comment-usage-contract.sh | 2 +- .../git/test-issue-create-usage-contract.sh | 125 ++++++++++++++++++ .../git/test-issue-edit-usage-contract.sh | 2 +- .../git/test-issue-reopen-usage-contract.sh | 2 +- .../tools/git/test-pr-close-usage-contract.sh | 2 +- .../tools/git/test-pr-review-gitea-comment.sh | 0 .../git/test-pr-review-usage-contract.sh | 2 +- packages/mosaic/package.json | 2 +- 10 files changed, 146 insertions(+), 8 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh mode change 100644 => 100755 packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index d9c0bc24..1cb378b4 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -74,26 +74,39 @@ Examples: $(basename "$0") -t "Fix login bug" -l "bug,priority-high" $(basename "$0") -t "Add dark mode" -b "Implement theme switching" -m "0.2.0" $(basename "$0") -i + +Exit codes: 0 success; 2 usage error (stderr); 1 provider/credential failure. EOF - exit "${1:-1}" + exit "${1:-2}" } # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage +} + while [[ $# -gt 0 ]]; do case $1 in -t|--title) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" TITLE="$2" shift 2 ;; -b|--body) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" BODY="$2" shift 2 ;; -l|--labels) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LABELS="$2" shift 2 ;; -m|--milestone) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" MILESTONE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh index 1a58618e..9dfba9f0 100755 --- a/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh @@ -75,7 +75,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. diff --git a/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh index a1be441a..e525357a 100755 --- a/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh @@ -97,7 +97,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage on stdout. diff --git a/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh new file mode 100755 index 00000000..850487a5 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-create.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-create.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-create.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-create.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -t exits 2" +expect_stderr "Title is required" "missing -t message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -t -b -l -m --title --body --labels --milestone; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b; do + rc=0 + run_wrapper_sandboxed -t "smoke" "$flag" "value" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "issue-create.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh index c2ad83fb..48334bd5 100755 --- a/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh @@ -76,7 +76,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. diff --git a/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh index 31c68271..f60b004d 100755 --- a/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh @@ -75,7 +75,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. diff --git a/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh index 610d0f33..734bb150 100755 --- a/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh @@ -78,7 +78,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh old mode 100644 new mode 100755 diff --git a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh index 518db7c6..8841553c 100755 --- a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh @@ -78,7 +78,7 @@ expect_rc() { # expect_rc <want> <desc> <args...> } expect_stderr() { # expect_stderr <pattern> <desc> - grep -q "$1" "$ERR_FILE" || fail "$desc: stderr missing '$1'" + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" } # 1. Help exits 0 and prints usage. diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 2c01eaa5..425a2e15 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From b8b396d9f5d010a1ee55604637c37ed582d06b48 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:03:20 -0500 Subject: [PATCH 06/17] framework tools/git: pr-edit R1/R4 conversion (P1) usage() default exit 2; semantic invocation checks (mutual exclusion, integer/format validation, missing edit option, --login-only-for-Gitea) exit 2; value guards on all value-taking options; existing pr-edit suite still green; usage-contract suite enrolled (population 80); mirrored to the brain tree. --- .../mosaic/framework/tools/git/pr-edit.sh | 37 +++-- .../tools/git/test-pr-edit-usage-contract.sh | 133 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 156 insertions(+), 16 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/pr-edit.sh b/packages/mosaic/framework/tools/git/pr-edit.sh index 47a12d8e..f5db9fb4 100755 --- a/packages/mosaic/framework/tools/git/pr-edit.sh +++ b/packages/mosaic/framework/tools/git/pr-edit.sh @@ -50,38 +50,45 @@ Options: -H, --host HOST Explicit Gitea host (required with --repo off-host) -h, --help Show this help message EOF - exit "${1:-1}" + exit "${1:-2}" +} + +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage } while [[ $# -gt 0 ]]; do case "$1" in - -n|--number) PR_NUMBER="${2:-}"; shift 2 ;; - -t|--title) TITLE="${2:-}"; shift 2 ;; - -b|--body) BODY="${2:-}"; shift 2 ;; - -B|--base) BASE_BRANCH="${2:-}"; shift 2 ;; + -n|--number) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; PR_NUMBER="${2:-}"; shift 2 ;; + -t|--title) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; TITLE="${2:-}"; shift 2 ;; + -b|--body) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; BODY="${2:-}"; shift 2 ;; + -B|--base) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; BASE_BRANCH="${2:-}"; shift 2 ;; --draft) - [[ "$DRAFT_MODE" != "ready" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 1; } + [[ "$DRAFT_MODE" != "ready" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="draft"; shift ;; --ready) - [[ "$DRAFT_MODE" != "draft" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 1; } + [[ "$DRAFT_MODE" != "draft" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="ready"; shift ;; - -l|--login) LOGIN_OVERRIDE="${2:-}"; shift 2 ;; - -r|--repo) REPO_OVERRIDE="${2:-}"; shift 2 ;; - -H|--host) HOST_OVERRIDE="${2:-}"; shift 2 ;; + -l|--login) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LOGIN_OVERRIDE="${2:-}"; shift 2 ;; + -r|--repo) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; REPO_OVERRIDE="${2:-}"; shift 2 ;; + -H|--host) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; HOST_OVERRIDE="${2:-}"; shift 2 ;; -h|--help) usage 0 ;; *) echo "Unknown option: $1" >&2; usage ;; esac done -[[ -n "$PR_NUMBER" ]] || { echo "Error: Pull request number is required (-n)" >&2; exit 1; } -[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "Error: Pull request number must be a positive integer" >&2; exit 1; } +[[ -n "$PR_NUMBER" ]] || { echo "Error: Pull request number is required (-n)" >&2; exit 2; } +[[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "Error: Pull request number must be a positive integer" >&2; exit 2; } if [[ -z "$TITLE" && -z "$BODY" && -z "$BASE_BRANCH" && -z "$DRAFT_MODE" ]]; then echo "Error: At least one edit option is required" >&2 - exit 1 + exit 2 fi [[ -z "$REPO_OVERRIDE" || "$REPO_OVERRIDE" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || { echo "Error: --repo must be OWNER/REPO" >&2 - exit 1 + exit 2 } if [[ -n "$HOST_OVERRIDE" || -n "$REPO_OVERRIDE" ]]; then @@ -92,7 +99,7 @@ fi case "$PLATFORM" in github) - [[ -z "$LOGIN_OVERRIDE" ]] || { echo "Error: --login is only valid for Gitea" >&2; exit 1; } + [[ -z "$LOGIN_OVERRIDE" ]] || { echo "Error: --login is only valid for Gitea" >&2; exit 2; } if [[ -n "$TITLE" || -n "$BODY" || -n "$BASE_BRANCH" ]]; then CMD=(gh pr edit "$PR_NUMBER") [[ -n "$TITLE" ]] && CMD+=(--title "$TITLE") diff --git a/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh new file mode 100755 index 00000000..e816f69d --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-edit.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-edit-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/pr-edit.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/pr-edit.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: pr-edit.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -n exits 2" +expect_stderr "Pull request number is required" "missing -n message on stderr" +expect_rc 2 "no edit option exits 2" -n 5 +expect_stderr "At least one edit option is required" "no-edit-option message on stderr" +expect_rc 2 "non-integer PR number exits 2" -n abc -t x +expect_stderr "positive integer" "integer check on stderr" +expect_rc 2 "mutually exclusive draft/ready exits 2" -n 5 --draft --ready +expect_stderr "mutually exclusive" "mutual exclusion on stderr" +expect_rc 2 "bad repo format exits 2" -n 5 -t x -r not-a-slug +expect_stderr "OWNER/REPO" "repo format on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -n -t -b -B -l -r -H --number --title --body --base --login --repo --host; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b; do + rc=0 + run_wrapper_sandboxed -n 5 "$flag" "value" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "pr-edit.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 425a2e15..3474ee79 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From cf971f93f89c401bff01338dbfb25ea212194f50 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:05:17 -0500 Subject: [PATCH 07/17] framework tools/git: pr-create R1/R4 conversion (P1) usage() default exit 2 (usage-error contract); value guards on all seven value-taking options; -d/--draft stays boolean. Existing fallback-default-base suite green (29/29). Usage-contract suite enrolled (population 81); mirrored to the brain tree. --- .../mosaic/framework/tools/git/pr-create.sh | 16 ++- .../git/test-pr-create-usage-contract.sh | 125 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index 31a8dfa9..a5bf6c7b 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -135,37 +135,51 @@ Examples: $(basename "$0") -i 42 -b "Implements the feature described in #42" $(basename "$0") -t "WIP: New feature" --draft EOF - exit "${1:-1}" + exit "${1:-2}" +} + +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -t|--title) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" TITLE="$2" shift 2 ;; -b|--body) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" BODY="$2" shift 2 ;; -B|--base) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" BASE_BRANCH="$2" shift 2 ;; -H|--head) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" HEAD_BRANCH="$2" shift 2 ;; -l|--labels) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LABELS="$2" shift 2 ;; -m|--milestone) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" MILESTONE="$2" shift 2 ;; -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh new file mode 100755 index 00000000..a7d6c952 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for pr-create.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-create-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/pr-create.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/pr-create.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: pr-create.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -t exits 2" +expect_stderr "Title is required" "missing -t message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -t -b -B -H -l -m -i --title --body --base --head --labels --milestone --issue; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -b; do + rc=0 + run_wrapper_sandboxed -t "smoke" "$flag" "value" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "pr-create.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 3474ee79..78d2a85a 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From 0467d28191dd368df9d326fa2b3576fdce62dfd2 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:06:42 -0500 Subject: [PATCH 08/17] framework tools/git: issue-assign R1/R4 conversion (P1) usage() default exit 2; value guards on -i/-a/-l/-m; -r stays boolean. Suite enrolled (population 82); mirrored to the brain tree. --- .../framework/tools/git/issue-assign.sh | 13 +- .../git/test-issue-assign-usage-contract.sh | 125 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index 48d5c04b..a899eec0 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -33,25 +33,36 @@ Examples: $(basename "$0") -i 42 -l "in-progress" -m "0.2.0" $(basename "$0") -i 42 -a @me EOF - exit "${1:-1}" + exit "${1:-2}" +} + +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE="$2" shift 2 ;; -a|--assignee) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ASSIGNEE="$2" shift 2 ;; -l|--labels) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LABELS="$2" shift 2 ;; -m|--milestone) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" MILESTONE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh new file mode 100755 index 00000000..c7e537a6 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-assign.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-assign-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-assign.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-assign.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-assign.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -i exits 2" +expect_stderr "Issue number is required" "missing -i message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -i -a -l -m --issue --assignee --labels --milestone; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -a; do + rc=0 + run_wrapper_sandboxed -i 5 "$flag" "value" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "issue-assign.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 78d2a85a..9339f310 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From bb23cb6febc0d49345cb0b5f31b582552924aa18 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:08:15 -0500 Subject: [PATCH 09/17] framework tools/git: milestone-close, milestone-list, issue-view R1/R4 conversions (P1) usage_error contract (stderr + exit 2) with value guards on every value-taking option and the missing-required checks (milestone-close -t, issue-view -i). Three suites enrolled (population 85); green in both trees; mirrored to the brain tree. --- .../mosaic/framework/tools/git/issue-view.sh | 15 ++- .../framework/tools/git/milestone-close.sh | 15 ++- .../framework/tools/git/milestone-list.sh | 12 +- .../git/test-issue-view-usage-contract.sh | 125 ++++++++++++++++++ .../test-milestone-close-usage-contract.sh | 125 ++++++++++++++++++ .../git/test-milestone-list-usage-contract.sh | 124 +++++++++++++++++ packages/mosaic/package.json | 2 +- 7 files changed, 407 insertions(+), 11 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-view.sh b/packages/mosaic/framework/tools/git/issue-view.sh index da15bc10..1de25ad6 100755 --- a/packages/mosaic/framework/tools/git/issue-view.sh +++ b/packages/mosaic/framework/tools/git/issue-view.sh @@ -8,6 +8,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: issue-view.sh -i <issue_number> (see --help)" >&2 + exit 2 +} + ISSUE_NUMBER="" # get_remote_host and get_gitea_token are provided by detect-platform.sh @@ -74,6 +82,7 @@ if comments: while [[ $# -gt 0 ]]; do case $1 in -i|--issue) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ISSUE_NUMBER="$2" shift 2 ;; @@ -88,15 +97,13 @@ while [[ $# -gt 0 ]]; do exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$ISSUE_NUMBER" ]]; then - echo "Error: Issue number is required (-i)" - exit 1 + usage_error "Issue number is required" fi detect_platform >/dev/null diff --git a/packages/mosaic/framework/tools/git/milestone-close.sh b/packages/mosaic/framework/tools/git/milestone-close.sh index f57e6cc3..fc541db9 100755 --- a/packages/mosaic/framework/tools/git/milestone-close.sh +++ b/packages/mosaic/framework/tools/git/milestone-close.sh @@ -8,11 +8,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: milestone-close.sh -t <title> (see --help)" >&2 + exit 2 +} + TITLE="" while [[ $# -gt 0 ]]; do case $1 in -t|--title) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" TITLE="$2" shift 2 ;; @@ -25,15 +34,13 @@ while [[ $# -gt 0 ]]; do exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done if [[ -z "$TITLE" ]]; then - echo "Error: Milestone title is required (-t)" - exit 1 + usage_error "Milestone title is required" fi detect_platform >/dev/null diff --git a/packages/mosaic/framework/tools/git/milestone-list.sh b/packages/mosaic/framework/tools/git/milestone-list.sh index 3b46c3d5..01f6b761 100755 --- a/packages/mosaic/framework/tools/git/milestone-list.sh +++ b/packages/mosaic/framework/tools/git/milestone-list.sh @@ -8,11 +8,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + echo "Usage: milestone-list.sh [-s <state>] (see --help)" >&2 + exit 2 +} + STATE="open" while [[ $# -gt 0 ]]; do case $1 in -s|--state) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" STATE="$2" shift 2 ;; @@ -25,8 +34,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; *) - echo "Unknown option: $1" - exit 1 + usage_error "unknown option: $1" ;; esac done diff --git a/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh new file mode 100755 index 00000000..09170fb9 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-view.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-view-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-view.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-view.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-view.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -i exits 2" +expect_stderr "Issue number is required" "missing -i message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -i --issue; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -i; do + rc=0 + run_wrapper_sandboxed -i 5 >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "issue-view.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh new file mode 100755 index 00000000..bd674910 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for milestone-close.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/milestone-close-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/milestone-close.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/milestone-close.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: milestone-close.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -t exits 2" +expect_stderr "Milestone title is required" "missing -t message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -t --title; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -t; do + rc=0 + run_wrapper_sandboxed -t "smoke" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "milestone-close.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh new file mode 100755 index 00000000..d6865b81 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Usage-error contract for milestone-list.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/milestone-list-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/milestone-list.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/milestone-list.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: milestone-list.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. + + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -s --state; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -s; do + rc=0 + run_wrapper_sandboxed -s open >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "milestone-list.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 9339f310..91aa936c 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From c8b4af59c34b917e30140de9ae92e2f37074a0ea Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:10:27 -0500 Subject: [PATCH 10/17] framework tools/git: issue-list, milestone-create, lane-brief R1/R4 conversions (P1 complete) - issue-list + milestone-create: usage() default exit 2, value guards, missing-title via usage_error. - lane-brief: getopts replaced with a while/case parser carrying R2 long-flag aliases (--repo --milestone --label/--labels --login --limit) beside the short forms; login-resolution failure reclassified from exit 2 to exit 1 (credential class, not an invocation defect). - Three suites enrolled (population 88); lane-brief pr-linkage suite still ALL PASS; green in both trees; mirrored to the brain tree. With this, every agent-facing wrapper in tools/git carries the R1/R4 usage-error contract (22/22 non-guard wrappers; guards keep their own contracts). --- .../mosaic/framework/tools/git/issue-list.sh | 15 ++- .../mosaic/framework/tools/git/lane-brief.sh | 29 ++-- .../framework/tools/git/milestone-create.sh | 15 ++- .../git/test-issue-list-usage-contract.sh | 123 +++++++++++++++++ .../git/test-lane-brief-usage-contract.sh | 125 ++++++++++++++++++ .../test-milestone-create-usage-contract.sh | 125 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 7 files changed, 418 insertions(+), 16 deletions(-) create mode 100755 packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh create mode 100755 packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh diff --git a/packages/mosaic/framework/tools/git/issue-list.sh b/packages/mosaic/framework/tools/git/issue-list.sh index 4e59d3d9..2c905d49 100755 --- a/packages/mosaic/framework/tools/git/issue-list.sh +++ b/packages/mosaic/framework/tools/git/issue-list.sh @@ -36,33 +36,46 @@ Examples: $(basename "$0") -m "0.2.0" # Issues in milestone 0.2.0 $(basename "$0") --repo ddk/ai-bma # List issues from anywhere EOF - exit "${1:-1}" + exit "${1:-2}" } # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage +} + while [[ $# -gt 0 ]]; do case $1 in -s|--state) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" STATE="$2" shift 2 ;; -l|--label) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LABEL="$2" shift 2 ;; -m|--milestone) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" MILESTONE="$2" shift 2 ;; -a|--assignee) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" ASSIGNEE="$2" shift 2 ;; -n|--limit) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" LIMIT="$2" shift 2 ;; -r|--repo) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" REPO_OVERRIDE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/lane-brief.sh b/packages/mosaic/framework/tools/git/lane-brief.sh index 5a83fbe9..3201ab3d 100755 --- a/packages/mosaic/framework/tools/git/lane-brief.sh +++ b/packages/mosaic/framework/tools/git/lane-brief.sh @@ -28,18 +28,25 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" REPO="" MILESTONE="" LABEL="" LOGIN="" LIMIT=100 -while getopts "r:m:l:L:n:h" opt; do - case "$opt" in - r) REPO="$OPTARG" ;; - m) MILESTONE="$OPTARG" ;; - l) LABEL="$OPTARG" ;; - L) LOGIN="$OPTARG" ;; - n) LIMIT="$OPTARG" ;; - h) grep '^#' "$0" | sed 's/^# \?//'; exit 0 ;; - *) echo "see -h" >&2; exit 2 ;; +# R2 (2026-08-28): long-flag aliases with the same usage-error contract the +# wrapper family shares (rc 2, stderr). getopts could not take long flags. +usage_error() { + echo "Error: $*" >&2 + echo "Usage: lane-brief.sh -r <owner/repo> [-m milestone] [-l label] [-L login] [-n limit]" >&2 + exit 2 +} +while [[ $# -gt 0 ]]; do + case "$1" in + -r|--repo) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; REPO="$2"; shift 2 ;; + -m|--milestone) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; MILESTONE="$2"; shift 2 ;; + -l|--label|--labels) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LABEL="$2"; shift 2 ;; + -L|--login) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LOGIN="$2"; shift 2 ;; + -n|--limit) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LIMIT="$2"; shift 2 ;; + -h|--help) grep '^#' "$0" | sed 's/^# \?//'; exit 0 ;; + *) usage_error "unknown option: $1" ;; esac done -[[ -n "$REPO" ]] || { echo "FATAL: -r <owner/repo> required" >&2; exit 2; } +[[ -n "$REPO" ]] || usage_error "-r/--repo <owner/repo> required" # Resolve login: explicit -L, then $GITEA_LOGIN, then owner inference, then the # shared default-login resolver. Owner inference comes before the shared fallback @@ -72,7 +79,7 @@ if [[ -z "$LOGIN" ]]; then fi fi fi -[[ -n "$LOGIN" ]] || { echo "FATAL: could not resolve a Gitea login for $REPO (pass -L or set GITEA_LOGIN)" >&2; exit 2; } +[[ -n "$LOGIN" ]] || { echo "FATAL: could not resolve a Gitea login for $REPO (pass -L or set GITEA_LOGIN)" >&2; exit 1; } command -v tea >/dev/null || { echo "FATAL: tea not found" >&2; exit 1; } command -v jq >/dev/null || { echo "FATAL: jq not found" >&2; exit 1; } diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index 72d8f299..b063e1fe 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -37,21 +37,31 @@ Examples: $(basename "$0") -t "0.0.1" -d "Pre-MVP Foundation Sprint" $(basename "$0") -t "0.1.0" -d "MVP Release" --due "2025-03-01" EOF - exit "${1:-1}" + exit "${1:-2}" } # Parse arguments +# Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, +# distinct from provider, credential, and verification failures (exit 1). +usage_error() { + echo "Error: $*" >&2 + usage +} + while [[ $# -gt 0 ]]; do case $1 in -t|--title) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" TITLE="$2" shift 2 ;; -d|--desc) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" DESCRIPTION="$2" shift 2 ;; --due) + [[ $# -ge 2 ]] || usage_error "option $1 requires a value" DUE_DATE="$2" shift 2 ;; @@ -92,8 +102,7 @@ if [[ "$LIST_ONLY" == true ]]; then fi if [[ -z "$TITLE" ]]; then - echo "Error: Title is required (-t) for creating milestones" >&2 - usage + usage_error "Title is required (-t) for creating milestones" fi case "$PLATFORM" in diff --git a/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh new file mode 100755 index 00000000..88870bbd --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Usage-error contract for issue-list.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-list-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/issue-list.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-list.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: issue-list.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -s -l -m -a -n -r --state --label --milestone --assignee --limit --repo; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -s; do + rc=0 + run_wrapper_sandboxed -s open >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "issue-list.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh b/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh new file mode 100755 index 00000000..c832abec --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for lane-brief.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/lane-brief-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/lane-brief.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/lane-brief.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "owner/repo" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -r exits 2" +expect_stderr "required" "missing -r message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -r -m -l -L -n --repo --milestone --label --login --limit; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -r; do + rc=0 + run_wrapper_sandboxed -r owner/repo >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "lane-brief.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh new file mode 100755 index 00000000..2e08587d --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# Usage-error contract for milestone-create.sh (R4, 2026-08-28). +# +# issue-edit already uses long-flag-first parsing (-i/--issue, -t/--title, +# -b/--body, -l/--labels, -m/--milestone); this adds the rc=2 usage-error +# contract, value checks, and the no-provider-contact proof. Required: -i. +# +# Arms: +# 1. --help and -h exit 0 and print usage. +# 2. Unknown option exits 2 with the message on stderr. +# 3. Missing required -i exits 2 (stderr). +# 4. A value-less flag (-i -b -c and long forms) exits 2 (stderr). +# 5. -b and -c both pass parsing (sandboxed runner: the run then fails +# at credential resolution, nonzero and NOT 2) — no real token is +# ever read and no provider is contacted. +# 6. No arm performs any provider request (PATH shims record every +# invocation; the probe log must stay empty). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/milestone-create-usage}" +BIN_DIR="$WORK_DIR/bin" +PROBE_LOG="$WORK_DIR/provider-probes.log" +OUT_FILE="$WORK_DIR/out.log" +ERR_FILE="$WORK_DIR/err.log" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$BIN_DIR" +: > "$PROBE_LOG" + +# Unlike the issue suites, these stubs FAIL (exit 99): pr-close has an +# API fallback that treats a successful curl as a closed PR, so exit-0 +# stubs would let the sandbox arms "succeed" (measured 2026-08-28). +for tool in gh tea curl; do + cat > "$BIN_DIR/$tool" <<STUB +#!/usr/bin/env bash +echo "$tool \$*" >> "$PROBE_LOG" +exit 99 +STUB + chmod +x "$BIN_DIR/$tool" +done + +run_wrapper() { + ( cd "$WORK_DIR" && PATH="$BIN_DIR:$PATH" "$SCRIPT_DIR/milestone-create.sh" "$@" ) +} + +# Hermetic variant: neutralizes every identity/credential source the wrapper +# consults so parse-acceptance arms fail at credential resolution in ANY cwd +# repo (see test-issue-comment-usage-contract.sh for the measured incident). +run_wrapper_sandboxed() { + mkdir -p "$WORK_DIR/home" "$WORK_DIR/xdg" + ( + cd "$WORK_DIR" + PATH="$BIN_DIR:$PATH" HOME="$WORK_DIR/home" XDG_CONFIG_HOME="$WORK_DIR/xdg" \ + MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/milestone-create.sh" "$@" + ) +} + +fail() { + echo "FAIL: $*" >&2 + echo "--- stderr ---" >&2 + cat "$ERR_FILE" >&2 + exit 1 +} + +expect_rc() { # expect_rc <want> <desc> <args...> + local want="$1" desc="$2" rc=0 + shift 2 + run_wrapper "$@" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -eq "$want" ]] || fail "$desc: rc=$rc, want $want" +} + +expect_stderr() { # expect_stderr <pattern> <desc> + grep -q "$1" "$ERR_FILE" || fail "$2: stderr missing '$1'" +} + +# 1. Help exits 0 and prints usage. +expect_rc 0 "--help exits 0" --help +grep -q "Usage: milestone-create.sh" "$OUT_FILE" || fail "--help did not print usage" +expect_rc 0 "-h exits 0" -h + +# 2. Unknown option: rc 2, stderr. +expect_rc 2 "unknown option exits 2" --bogus +expect_stderr "[Uu]nknown option" "unknown option names itself on stderr" + +# 3. Missing required PR number: rc 2, stderr. +expect_rc 2 "missing -t exits 2" +expect_stderr "Title is required" "missing -t message on stderr" + + +# 4. Value-less flags: rc 2 with "requires a value" on stderr. +for flag in -t -d --due --title --desc; do + expect_rc 2 "value-less $flag exits 2" "$flag" + expect_stderr "requires a value" "value-less $flag message on stderr" +done + +# 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: a parser-failure arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi + +# 5. Alias acceptance under the sandbox: both -b and -c carry a value past +# parsing; the run fails at credential resolution nonzero and NOT 2. +for flag in -t; do + rc=0 + run_wrapper_sandboxed -t "smoke" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? + [[ "$rc" -ne 0 ]] || fail "$flag arm unexpectedly succeeded in the sandbox" + [[ "$rc" -ne 2 ]] || fail "$flag arm misclassified credential failure as a usage error" +done + +# 6. Post-sandbox provider assertions are intentionally NOT applied here: +# pr-close's gitea path attempts a tea WRITE (tea pr comment) when a +# comment parses, then falls back to the API. Hermeticity for this +# wrapper comes from the FAILING stubs (exit 99), not from non-contact — +# the arm above proves only parse acceptance and non-usage classification. +# Parser-failure arms (1-4) remain zero-contact (asserted at 4b). + +echo "milestone-create.sh usage-contract regression passed (R1/R4)" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 91aa936c..b047a4e8 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-repo-decl-consumption.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-issue-comment-usage-contract.sh && bash framework/tools/git/test-issue-comment-readback.sh && bash framework/tools/git/test-issue-close-usage-contract.sh && bash framework/tools/git/test-issue-reopen-usage-contract.sh && bash framework/tools/git/test-pr-close-usage-contract.sh && bash framework/tools/git/test-pr-review-usage-contract.sh && bash framework/tools/git/test-issue-edit-usage-contract.sh && bash framework/tools/git/test-issue-create-usage-contract.sh && bash framework/tools/git/test-pr-edit-usage-contract.sh && bash framework/tools/git/test-pr-create-usage-contract.sh && bash framework/tools/git/test-issue-assign-usage-contract.sh && bash framework/tools/git/test-milestone-close-usage-contract.sh && bash framework/tools/git/test-milestone-list-usage-contract.sh && bash framework/tools/git/test-issue-view-usage-contract.sh && bash framework/tools/git/test-issue-list-usage-contract.sh && bash framework/tools/git/test-milestone-create-usage-contract.sh && bash framework/tools/git/test-lane-brief-usage-contract.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*", -- 2.54.0 From e15e7fe8229b1c553432a4a955b68441c5c5bfa3 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:12:32 -0500 Subject: [PATCH 11/17] framework tools/git: restore exec bit on test-lane-brief-pr-linkage.sh (stash cycle side effect) --- packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh diff --git a/packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh b/packages/mosaic/framework/tools/git/test-lane-brief-pr-linkage.sh old mode 100644 new mode 100755 -- 2.54.0 From da4a5b2f1910a1dddabcdce8009591a5926fd5cb Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:14:57 -0500 Subject: [PATCH 12/17] framework tools/git: pr-review validates --action before provider contact (codex blocker on PR #1464) An unsupported --action previously reached platform detection (and could touch the provider) before failing with a provider-class status; comment-without-body exited 1 mid-switch. Both now fail fast with usage_error (rc 2, stderr) immediately after argument parsing. Test arms added including the zero-provider-contact assertion for invalid-action runs. --- packages/mosaic/framework/tools/git/pr-review.sh | 14 ++++++++++---- .../tools/git/test-pr-review-usage-contract.sh | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index b36a6a66..0eb717ec 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -116,6 +116,14 @@ if [[ -z "$ACTION" ]]; then usage_error "Action is required (-a/--action): approve, request-changes, comment" fi +# Validate the action BEFORE any provider contact (codex review of PR #1464: +# an unsupported --action previously reached platform detection and could +# touch the provider before failing with a provider-class status). +case "$ACTION" in + approve|request-changes|comment) ;; + *) usage_error "unknown action '$ACTION': approve, request-changes, comment" ;; +esac + if [[ -n "$REPO_OVERRIDE" ]]; then # An explicit --repo is the whole point of a reviewer worktree whose origin # is nonstandard or missing (#867 convention, mirrored from pr-view.sh / @@ -715,8 +723,7 @@ if [[ "$PLATFORM" == "github" ]]; then echo "Added review comment to GitHub PR #$PR_NUMBER" ;; *) - echo "Error: Unknown action: $ACTION" - exit 1 + usage_error "unknown action: $ACTION" ;; esac elif [[ "$PLATFORM" == "gitea" ]]; then @@ -809,8 +816,7 @@ elif [[ "$PLATFORM" == "gitea" ]]; then echo "Added and verified comment on Gitea PR #$PR_NUMBER (comment ID $comment_id)" ;; *) - echo "Error: Unknown action: $ACTION" - exit 1 + usage_error "unknown action: $ACTION" ;; esac else diff --git a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh index 8841553c..1c4f8d69 100755 --- a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh @@ -97,6 +97,15 @@ expect_rc 2 "missing -a exits 2" -n 5 expect_stderr "Action is required" "missing -a message on stderr" expect_rc 2 "request-changes without comment exits 2" -n 5 -a request-changes expect_stderr "comment required for request-changes" "request-changes message on stderr" +expect_rc 2 "invalid action exits 2 pre-detection" -n 5 -a bogus +expect_stderr "unknown action" "invalid action message on stderr" +# Invalid-action arms must not contact any provider (validation precedes +# detect_platform): probe log empty at this point. +if [[ -s "$PROBE_LOG" ]]; then + echo "FAIL: an invalid-action arm contacted a provider:" >&2 + cat "$PROBE_LOG" >&2 + exit 1 +fi # 4. Value-less flags: rc 2 with "requires a value" on stderr. for flag in -n -a -b -c -l -r --number --action --body --comment --login --repo; do -- 2.54.0 From ca91586222d8d4e73d2de0a47fcedc30aae64dd3 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:17:30 -0500 Subject: [PATCH 13/17] framework tools/git: pr-review body-required actions fail fast (codex suggestion on PR #1464) request-changes and comment both validate their required body immediately after argument parsing (rc 2, stderr, zero provider contact). The repo-host-override suite's action-message expectation updated to the new wording; suite green. --- packages/mosaic/framework/tools/git/pr-review.sh | 7 +++++++ .../tools/git/test-pr-review-repo-host-override.sh | 2 +- .../framework/tools/git/test-pr-review-usage-contract.sh | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 0eb717ec..c5e9028b 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -124,6 +124,13 @@ case "$ACTION" in *) usage_error "unknown action '$ACTION': approve, request-changes, comment" ;; esac +# Body-required actions fail fast too (codex follow-up on PR #1464): +# request-changes and comment both require a body; validate before any +# provider contact. +if [[ ( "$ACTION" == "request-changes" || "$ACTION" == "comment" ) && -z "$COMMENT" ]]; then + usage_error "comment required for $ACTION (-b/--body)" +fi + if [[ -n "$REPO_OVERRIDE" ]]; then # An explicit --repo is the whole point of a reviewer worktree whose origin # is nonstandard or missing (#867 convention, mirrored from pr-view.sh / diff --git a/packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh b/packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh index f5699ce4..789fa410 100755 --- a/packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-repo-host-override.sh @@ -218,7 +218,7 @@ if grep -q 'Unknown option' "$OUTPUT_FILE"; then cat "$OUTPUT_FILE" >&2 exit 1 fi -grep -q 'Unknown action: bogus-action' "$OUTPUT_FILE" +grep -q "unknown action 'bogus-action'" "$OUTPUT_FILE" # --- Case 2: -h/--help documents both overrides. HELP_TEXT="$("$SCRIPT_DIR/pr-review.sh" -h)" diff --git a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh index 1c4f8d69..a66b2075 100755 --- a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh @@ -97,6 +97,8 @@ expect_rc 2 "missing -a exits 2" -n 5 expect_stderr "Action is required" "missing -a message on stderr" expect_rc 2 "request-changes without comment exits 2" -n 5 -a request-changes expect_stderr "comment required for request-changes" "request-changes message on stderr" +expect_rc 2 "comment without body exits 2 pre-detection" -n 5 -a comment +expect_stderr "comment required for comment" "comment-without-body message on stderr" expect_rc 2 "invalid action exits 2 pre-detection" -n 5 -a bogus expect_stderr "unknown action" "invalid action message on stderr" # Invalid-action arms must not contact any provider (validation precedes -- 2.54.0 From d326e6572a31776acb792ba3bf7b04a680291df2 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:26:10 -0500 Subject: [PATCH 14/17] framework tools/git: strict value guards + provider-failure normalization family-wide (codex blockers on PR #1464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Value guards now reject option-like values: --anything always, and single-dash flag shapes (-h, -i). Previously -b --help consumed --help as the body and performed the write (codex example: issue-close -i --help proceeding to 'Closed GitHub issue #--help'). Multi-char dash-leading text (-start of a list) stays a legal value. - Every remaining direct provider exec (gh/tea/CMD arrays across issue-create/edit/assign/list/view, milestone-*, pr-create/edit, pr-close, issue-close/reopen) wrapped with rc capture and normalized to exit 1 with a stderr message — provider exit 2 no longer collides with the reserved usage-error status. - All 16 usage-contract suites gained option-like and short-flag arms (16/16 green). Existing suites re-verified; test-pr-edit and test-issue-create-interactive-auth fail identically with these changes stashed (environment-coupled, not regressions; documented). --- .../framework/tools/git/issue-assign.sh | 28 +++++++++++++------ .../mosaic/framework/tools/git/issue-close.sh | 8 ++++-- .../framework/tools/git/issue-comment.sh | 6 ++-- .../framework/tools/git/issue-create.sh | 12 ++++---- .../mosaic/framework/tools/git/issue-edit.sh | 18 +++++++----- .../mosaic/framework/tools/git/issue-list.sh | 20 +++++++------ .../framework/tools/git/issue-reopen.sh | 12 +++++--- .../mosaic/framework/tools/git/issue-view.sh | 6 ++-- .../mosaic/framework/tools/git/lane-brief.sh | 10 +++---- .../framework/tools/git/milestone-close.sh | 10 +++++-- .../framework/tools/git/milestone-create.sh | 22 ++++++++++----- .../framework/tools/git/milestone-list.sh | 10 +++++-- .../mosaic/framework/tools/git/pr-close.sh | 12 +++++--- .../mosaic/framework/tools/git/pr-create.sh | 18 ++++++------ .../mosaic/framework/tools/git/pr-edit.sh | 26 ++++++++++------- .../mosaic/framework/tools/git/pr-review.sh | 24 ++++++++++------ .../git/test-issue-assign-usage-contract.sh | 7 +++++ .../git/test-issue-close-usage-contract.sh | 7 +++++ .../git/test-issue-comment-usage-contract.sh | 7 +++++ .../git/test-issue-create-usage-contract.sh | 7 +++++ .../git/test-issue-edit-usage-contract.sh | 7 +++++ .../git/test-issue-list-usage-contract.sh | 7 +++++ .../git/test-issue-reopen-usage-contract.sh | 7 +++++ .../git/test-issue-view-usage-contract.sh | 7 +++++ .../git/test-lane-brief-usage-contract.sh | 7 +++++ .../test-milestone-close-usage-contract.sh | 7 +++++ .../test-milestone-create-usage-contract.sh | 7 +++++ .../git/test-milestone-list-usage-contract.sh | 7 +++++ .../tools/git/test-pr-close-usage-contract.sh | 7 +++++ .../git/test-pr-create-usage-contract.sh | 7 +++++ .../tools/git/test-pr-edit-usage-contract.sh | 7 +++++ .../git/test-pr-review-usage-contract.sh | 7 +++++ 32 files changed, 264 insertions(+), 90 deletions(-) diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index a899eec0..c1c26c0c 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -47,22 +47,22 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE="$2" shift 2 ;; -a|--assignee) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ASSIGNEE="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" MILESTONE="$2" shift 2 ;; @@ -90,20 +90,28 @@ PLATFORM=$(detect_platform) case "$PLATFORM" in github) if [[ -n "$ASSIGNEE" ]]; then - gh issue edit "$ISSUE" --add-assignee "$ASSIGNEE" + prov_rc=0 + gh issue edit "$ISSUE" --add-assignee "$ASSIGNEE" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi if [[ "$REMOVE_ASSIGNEE" == true ]]; then # Get current assignees and remove them CURRENT=$(gh issue view "$ISSUE" --json assignees -q '.assignees[].login' 2>/dev/null | tr '\n' ',') if [[ -n "$CURRENT" ]]; then - gh issue edit "$ISSUE" --remove-assignee "${CURRENT%,}" + prov_rc=0 + gh issue edit "$ISSUE" --remove-assignee "${CURRENT%,}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi fi if [[ -n "$LABELS" ]]; then - gh issue edit "$ISSUE" --add-label "$LABELS" + prov_rc=0 + gh issue edit "$ISSUE" --add-label "$LABELS" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi if [[ -n "$MILESTONE" ]]; then - gh issue edit "$ISSUE" --milestone "$MILESTONE" + prov_rc=0 + gh issue edit "$ISSUE" --milestone "$MILESTONE" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi echo "Issue #$ISSUE updated successfully" ;; @@ -142,7 +150,9 @@ case "$PLATFORM" in fi if [[ "$NEEDS_EDIT" == true ]]; then - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Issue #$ISSUE updated successfully" else echo "No changes specified" diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index 58850292..7109247c 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -25,14 +25,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" COMMENT="$2" shift 2 ;; @@ -134,7 +134,9 @@ elif [[ "$PLATFORM" == "gitea" ]]; then exit 1 } fi - tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "$GITEA_LOGIN_NAME" + prov_rc=0 + tea issue close "$ISSUE_NUMBER" --repo "$OWNER/$REPO" --login "$GITEA_LOGIN_NAME" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } else echo "No tea login configured for $(get_remote_host); using authenticated Gitea API fallback." >&2 if [[ -n "$COMMENT" ]]; then diff --git a/packages/mosaic/framework/tools/git/issue-comment.sh b/packages/mosaic/framework/tools/git/issue-comment.sh index e5858b35..93231c99 100755 --- a/packages/mosaic/framework/tools/git/issue-comment.sh +++ b/packages/mosaic/framework/tools/git/issue-comment.sh @@ -46,7 +46,7 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE_NUMBER="$2" shift 2 ;; @@ -54,12 +54,12 @@ while [[ $# -gt 0 ]]; do # R1 (2026-08-28): --body is the canonical flag, matching # issue-create/issue-edit/pr-create/pr-edit; -c/--comment stays a # backward-compatible alias. - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" COMMENT="$2" shift 2 ;; -l|--login) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LOGIN_OVERRIDE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index 1cb378b4..3e42e14a 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -91,22 +91,22 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" BODY="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" MILESTONE="$2" shift 2 ;; @@ -144,7 +144,9 @@ case "$PLATFORM" in [[ -n "$BODY" ]] && CMD+=(--body "$BODY") [[ -n "$LABELS" ]] && CMD+=(--label "$LABELS") [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; gitea) if command -v tea >/dev/null 2>&1; then diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index 94ae9d9d..f40a2f34 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -26,27 +26,27 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE_NUMBER="$2" shift 2 ;; -t|--title) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" BODY="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" MILESTONE="$2" shift 2 ;; @@ -82,7 +82,9 @@ if [[ "$PLATFORM" == "github" ]]; then [[ -n "$BODY" ]] && CMD+=(--body "$BODY") [[ -n "$LABELS" ]] && CMD+=(--add-label "$LABELS") [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Updated GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then REPO_SLUG=$(get_repo_slug) || { @@ -98,7 +100,9 @@ elif [[ "$PLATFORM" == "gitea" ]]; then [[ -n "$BODY" ]] && CMD+=(--description "$BODY") [[ -n "$LABELS" ]] && CMD+=(--add-labels "$LABELS") [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Updated Gitea issue #$ISSUE_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/issue-list.sh b/packages/mosaic/framework/tools/git/issue-list.sh index 2c905d49..534c52bb 100755 --- a/packages/mosaic/framework/tools/git/issue-list.sh +++ b/packages/mosaic/framework/tools/git/issue-list.sh @@ -50,32 +50,32 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -s|--state) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" STATE="$2" shift 2 ;; -l|--label) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LABEL="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" MILESTONE="$2" shift 2 ;; -a|--assignee) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ASSIGNEE="$2" shift 2 ;; -n|--limit) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LIMIT="$2" shift 2 ;; -r|--repo) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" REPO_OVERRIDE="$2" shift 2 ;; @@ -108,7 +108,9 @@ case "$PLATFORM" in [[ -n "$LABEL" ]] && CMD+=(--label "$LABEL") [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") [[ -n "$ASSIGNEE" ]] && CMD+=(--assignee "$ASSIGNEE") - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; gitea) if [[ -n "$REPO_OVERRIDE" ]]; then @@ -127,7 +129,9 @@ case "$PLATFORM" in [[ -n "$MILESTONE" ]] && CMD+=(--milestones "$MILESTONE") # Note: tea may not support assignee filter directly in all versions. [[ -n "$ASSIGNEE" ]] && echo "Note: Assignee filtering may require manual review for Gitea" >&2 - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; *) echo "Error: Could not detect git platform" >&2 diff --git a/packages/mosaic/framework/tools/git/issue-reopen.sh b/packages/mosaic/framework/tools/git/issue-reopen.sh index b686f370..58e80382 100755 --- a/packages/mosaic/framework/tools/git/issue-reopen.sh +++ b/packages/mosaic/framework/tools/git/issue-reopen.sh @@ -24,14 +24,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" COMMENT="$2" shift 2 ;; @@ -116,9 +116,13 @@ elif [[ "$PLATFORM" == "gitea" ]]; then REPO_ARGS=$(get_gitea_repo_args || true) if [[ -n "$REPO_ARGS" ]]; then if [[ -n "$COMMENT" ]]; then - tea issue comment "$ISSUE_NUMBER" "$COMMENT" $REPO_ARGS + prov_rc=0 + tea issue comment "$ISSUE_NUMBER" "$COMMENT" $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi - tea issue reopen "$ISSUE_NUMBER" $REPO_ARGS + prov_rc=0 + tea issue reopen "$ISSUE_NUMBER" $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } else echo "No tea login configured for $(get_remote_host); using authenticated Gitea API fallback." >&2 if [[ -n "$COMMENT" ]]; then diff --git a/packages/mosaic/framework/tools/git/issue-view.sh b/packages/mosaic/framework/tools/git/issue-view.sh index 1de25ad6..4d45ce87 100755 --- a/packages/mosaic/framework/tools/git/issue-view.sh +++ b/packages/mosaic/framework/tools/git/issue-view.sh @@ -82,7 +82,7 @@ if comments: while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE_NUMBER="$2" shift 2 ;; @@ -109,7 +109,9 @@ fi detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then - gh issue view "$ISSUE_NUMBER" + prov_rc=0 + gh issue view "$ISSUE_NUMBER" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } elif [[ "$PLATFORM" == "gitea" ]]; then if command -v tea >/dev/null 2>&1; then # --comments is what makes tea print the comment bodies (#1357 F3). diff --git a/packages/mosaic/framework/tools/git/lane-brief.sh b/packages/mosaic/framework/tools/git/lane-brief.sh index 3201ab3d..5d1df846 100755 --- a/packages/mosaic/framework/tools/git/lane-brief.sh +++ b/packages/mosaic/framework/tools/git/lane-brief.sh @@ -37,11 +37,11 @@ usage_error() { } while [[ $# -gt 0 ]]; do case "$1" in - -r|--repo) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; REPO="$2"; shift 2 ;; - -m|--milestone) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; MILESTONE="$2"; shift 2 ;; - -l|--label|--labels) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LABEL="$2"; shift 2 ;; - -L|--login) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LOGIN="$2"; shift 2 ;; - -n|--limit) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LIMIT="$2"; shift 2 ;; + -r|--repo) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; REPO="$2"; shift 2 ;; + -m|--milestone) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; MILESTONE="$2"; shift 2 ;; + -l|--label|--labels) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LABEL="$2"; shift 2 ;; + -L|--login) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LOGIN="$2"; shift 2 ;; + -n|--limit) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LIMIT="$2"; shift 2 ;; -h|--help) grep '^#' "$0" | sed 's/^# \?//'; exit 0 ;; *) usage_error "unknown option: $1" ;; esac diff --git a/packages/mosaic/framework/tools/git/milestone-close.sh b/packages/mosaic/framework/tools/git/milestone-close.sh index fc541db9..92162bb8 100755 --- a/packages/mosaic/framework/tools/git/milestone-close.sh +++ b/packages/mosaic/framework/tools/git/milestone-close.sh @@ -21,7 +21,7 @@ TITLE="" while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" TITLE="$2" shift 2 ;; @@ -46,14 +46,18 @@ fi detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then - gh api -X PATCH "/repos/{owner}/{repo}/milestones/$(gh api "/repos/{owner}/{repo}/milestones" --jq ".[] | select(.title==\"$TITLE\") | .number")" -f state=closed + prov_rc=0 + gh api -X PATCH "/repos/{owner}/{repo}/milestones/$(gh api "/repos/{owner}/{repo}/milestones" --jq ".[] | select(.title==\"$TITLE\") | .number")" -f state=closed || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Closed GitHub milestone: $TITLE" elif [[ "$PLATFORM" == "gitea" ]]; then REPO_ARGS=$(get_gitea_repo_args) || { echo "Error: Could not resolve Gitea repo/login for remote host" >&2 exit 1 } - tea milestone close "$TITLE" $REPO_ARGS + prov_rc=0 + tea milestone close "$TITLE" $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Closed Gitea milestone: $TITLE" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index b063e1fe..35361c0f 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -51,17 +51,17 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" TITLE="$2" shift 2 ;; -d|--desc) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" DESCRIPTION="$2" shift 2 ;; --due) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" DUE_DATE="$2" shift 2 ;; @@ -84,14 +84,18 @@ PLATFORM=$(detect_platform) if [[ "$LIST_ONLY" == true ]]; then case "$PLATFORM" in github) - gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)\t\(.title)\t\(.state)\t\(.open_issues)/\(.closed_issues) issues"' + prov_rc=0 + gh api repos/:owner/:repo/milestones --jq '.[] | "\(.number)\t\(.title)\t\(.state)\t\(.open_issues)/\(.closed_issues) issues"' || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; gitea) REPO_ARGS=$(get_gitea_repo_args) || { echo "Error: Could not resolve Gitea repo/login for remote host" >&2 exit 1 } - tea milestones list $REPO_ARGS + prov_rc=0 + tea milestones list $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; *) echo "Error: Could not detect git platform" >&2 @@ -118,7 +122,9 @@ case "$PLATFORM" in + (if $d != "" then {"description": $d} else {} end) + (if $due != "" then {"due_on": ($due + "T00:00:00Z")} else {} end)') - gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD" + prov_rc=0 + gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Milestone '$TITLE' created successfully" ;; gitea) @@ -129,7 +135,9 @@ case "$PLATFORM" in CMD=(tea milestones create --title "$TITLE") [[ -n "$DESCRIPTION" ]] && CMD+=(--description "$DESCRIPTION") [[ -n "$DUE_DATE" ]] && CMD+=(--deadline "$DUE_DATE") - "${CMD[@]}" $REPO_ARGS + prov_rc=0 + "${CMD[@]}" $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Milestone '$TITLE' created successfully" ;; *) diff --git a/packages/mosaic/framework/tools/git/milestone-list.sh b/packages/mosaic/framework/tools/git/milestone-list.sh index 01f6b761..64beff8e 100755 --- a/packages/mosaic/framework/tools/git/milestone-list.sh +++ b/packages/mosaic/framework/tools/git/milestone-list.sh @@ -21,7 +21,7 @@ STATE="open" while [[ $# -gt 0 ]]; do case $1 in -s|--state) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" STATE="$2" shift 2 ;; @@ -42,13 +42,17 @@ done detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then - gh api "/repos/{owner}/{repo}/milestones?state=$STATE" --jq '.[] | "\(.title) (\(.state)) - \(.open_issues) open, \(.closed_issues) closed"' + prov_rc=0 + gh api "/repos/{owner}/{repo}/milestones?state=$STATE" --jq '.[] | "\(.title) (\(.state)) - \(.open_issues) open, \(.closed_issues) closed"' || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } elif [[ "$PLATFORM" == "gitea" ]]; then REPO_ARGS=$(get_gitea_repo_args) || { echo "Error: Could not resolve Gitea repo/login for remote host" >&2 exit 1 } - tea milestone list $REPO_ARGS + prov_rc=0 + tea milestone list $REPO_ARGS || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } else echo "Error: Unknown platform" exit 1 diff --git a/packages/mosaic/framework/tools/git/pr-close.sh b/packages/mosaic/framework/tools/git/pr-close.sh index e2a75c14..f05a803f 100755 --- a/packages/mosaic/framework/tools/git/pr-close.sh +++ b/packages/mosaic/framework/tools/git/pr-close.sh @@ -24,14 +24,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -n|--number) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" PR_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" COMMENT="$2" shift 2 ;; @@ -79,9 +79,13 @@ if [[ "$PLATFORM" == "github" ]]; then echo "Closed GitHub PR #$PR_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then if [[ -n "$COMMENT" ]]; then - tea pr comment "$PR_NUMBER" "$COMMENT" $(get_gitea_repo_args) + prov_rc=0 + tea pr comment "$PR_NUMBER" "$COMMENT" $(get_gitea_repo_args) || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi - tea pr close "$PR_NUMBER" $(get_gitea_repo_args) + prov_rc=0 + tea pr close "$PR_NUMBER" $(get_gitea_repo_args) || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } echo "Closed Gitea PR #$PR_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index a5bf6c7b..6f599c0f 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -149,37 +149,37 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" BODY="$2" shift 2 ;; -B|--base) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" BASE_BRANCH="$2" shift 2 ;; -H|--head) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" HEAD_BRANCH="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" MILESTONE="$2" shift 2 ;; -i|--issue) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ISSUE="$2" shift 2 ;; @@ -280,7 +280,9 @@ case "$PLATFORM" in [[ -n "$LABELS" ]] && CMD+=(--label "$LABELS") [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") [[ "$DRAFT" == true ]] && CMD+=(--draft) - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } ;; gitea) # tea pull create syntax. Always pass --repo because tea repo inference diff --git a/packages/mosaic/framework/tools/git/pr-edit.sh b/packages/mosaic/framework/tools/git/pr-edit.sh index f5db9fb4..ac167865 100755 --- a/packages/mosaic/framework/tools/git/pr-edit.sh +++ b/packages/mosaic/framework/tools/git/pr-edit.sh @@ -62,19 +62,19 @@ usage_error() { while [[ $# -gt 0 ]]; do case "$1" in - -n|--number) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; PR_NUMBER="${2:-}"; shift 2 ;; - -t|--title) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; TITLE="${2:-}"; shift 2 ;; - -b|--body) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; BODY="${2:-}"; shift 2 ;; - -B|--base) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; BASE_BRANCH="${2:-}"; shift 2 ;; + -n|--number) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; PR_NUMBER="${2:-}"; shift 2 ;; + -t|--title) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; TITLE="${2:-}"; shift 2 ;; + -b|--body) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; BODY="${2:-}"; shift 2 ;; + -B|--base) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; BASE_BRANCH="${2:-}"; shift 2 ;; --draft) [[ "$DRAFT_MODE" != "ready" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="draft"; shift ;; --ready) [[ "$DRAFT_MODE" != "draft" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="ready"; shift ;; - -l|--login) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; LOGIN_OVERRIDE="${2:-}"; shift 2 ;; - -r|--repo) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; REPO_OVERRIDE="${2:-}"; shift 2 ;; - -H|--host) [[ $# -ge 2 ]] || usage_error "option $1 requires a value"; HOST_OVERRIDE="${2:-}"; shift 2 ;; + -l|--login) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LOGIN_OVERRIDE="${2:-}"; shift 2 ;; + -r|--repo) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; REPO_OVERRIDE="${2:-}"; shift 2 ;; + -H|--host) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; HOST_OVERRIDE="${2:-}"; shift 2 ;; -h|--help) usage 0 ;; *) echo "Unknown option: $1" >&2; usage ;; esac @@ -105,12 +105,18 @@ case "$PLATFORM" in [[ -n "$TITLE" ]] && CMD+=(--title "$TITLE") [[ -n "$BODY" ]] && CMD+=(--body "$BODY") [[ -n "$BASE_BRANCH" ]] && CMD+=(--base "$BASE_BRANCH") - "${CMD[@]}" + prov_rc=0 + "${CMD[@]}" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi if [[ "$DRAFT_MODE" == "draft" ]]; then - gh pr ready "$PR_NUMBER" --undo + prov_rc=0 + gh pr ready "$PR_NUMBER" --undo || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } elif [[ "$DRAFT_MODE" == "ready" ]]; then - gh pr ready "$PR_NUMBER" + prov_rc=0 + gh pr ready "$PR_NUMBER" || prov_rc=$? + [[ "$prov_rc" -eq 0 ]] || { echo "Error: provider command failed (exit ${prov_rc}; provider failure, not a usage error)" >&2; exit 1; } fi ;; gitea) diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index c5e9028b..716e3fe7 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -55,34 +55,34 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -n|--number) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" PR_NUMBER="$2" shift 2 ;; -a|--action) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" ACTION="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" COMMENT="$2" shift 2 ;; -l|--login) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" LOGIN_OVERRIDE="$2" shift 2 ;; -r|--repo) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" REPO_OVERRIDE="$2" shift 2 ;; -H|--host) - [[ $# -ge 2 ]] || usage_error "option $1 requires a value" + [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" HOST_OVERRIDE="$2" shift 2 ;; @@ -711,14 +711,18 @@ PY if [[ "$PLATFORM" == "github" ]]; then case $ACTION in approve) - gh pr review "$PR_NUMBER" --approve ${COMMENT:+--body "$COMMENT"} + gh_rc=0 + gh pr review "$PR_NUMBER" --approve ${COMMENT:+--body "$COMMENT"} || gh_rc=$? + [[ "$gh_rc" -eq 0 ]] || { echo "Error: GitHub approve failed (gh exit $gh_rc; provider failure, not a usage error)" >&2; exit 1; } echo "Approved GitHub PR #$PR_NUMBER" ;; request-changes) if [[ -z "$COMMENT" ]]; then usage_error "comment required for request-changes (-b/--body)" fi - gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT" + gh_rc=0 + gh pr review "$PR_NUMBER" --request-changes --body "$COMMENT" || gh_rc=$? + [[ "$gh_rc" -eq 0 ]] || { echo "Error: GitHub request-changes failed (gh exit $gh_rc; provider failure, not a usage error)" >&2; exit 1; } echo "Requested changes on GitHub PR #$PR_NUMBER" ;; comment) @@ -726,7 +730,9 @@ if [[ "$PLATFORM" == "github" ]]; then echo "Error: Comment required" exit 1 fi - gh pr review "$PR_NUMBER" --comment --body "$COMMENT" + gh_rc=0 + gh pr review "$PR_NUMBER" --comment --body "$COMMENT" || gh_rc=$? + [[ "$gh_rc" -eq 0 ]] || { echo "Error: GitHub review comment failed (gh exit $gh_rc; provider failure, not a usage error)" >&2; exit 1; } echo "Added review comment to GitHub PR #$PR_NUMBER" ;; *) diff --git a/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh index c7e537a6..8eb35e0a 100755 --- a/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -i -a -l -m --issue --assignee --labels --milestone; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i 5 -a --help +expect_rc 2 "short flag value rejected" -i 5 -a -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh index 9dfba9f0..b0ef6ae4 100755 --- a/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-close-usage-contract.sh @@ -97,6 +97,13 @@ for flag in -i -b -c --issue --body --comment; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i 5 -b --help +expect_rc 2 "short flag value rejected" -i 5 -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh index e525357a..d80a89a4 100755 --- a/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-comment-usage-contract.sh @@ -124,6 +124,13 @@ for flag in -i -b -c -l --issue --body --comment --login; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i 5 -b --help +expect_rc 2 "short flag value rejected" -i 5 -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 6. Alias acceptance at parse level: both -b and -c carry a value past # parsing; the wrapper then fails at platform detection (not a git repo) # nonzero but NOT as a usage error (rc must not be 2). diff --git a/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh index 850487a5..1cdbfc7a 100755 --- a/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-create-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -t -b -l -m --title --body --labels --milestone; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -t smoke -b --help +expect_rc 2 "short flag value rejected" -t smoke -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh index 48334bd5..0f939f9d 100755 --- a/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-edit-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -i -t -b -l -m --issue --title --body --labels --milestone; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i 5 -b --help +expect_rc 2 "short flag value rejected" -i 5 -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh index 88870bbd..75aaae46 100755 --- a/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-list-usage-contract.sh @@ -97,6 +97,13 @@ for flag in -s -l -m -a -n -r --state --label --milestone --assignee --limit --r expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -s --help +expect_rc 2 "short flag value rejected" -s -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh index f60b004d..ff7e7343 100755 --- a/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-reopen-usage-contract.sh @@ -97,6 +97,13 @@ for flag in -i -b -c --issue --body --comment; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i 5 -b --help +expect_rc 2 "short flag value rejected" -i 5 -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh index 09170fb9..d2ff9182 100755 --- a/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-view-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -i --issue; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -i --help +expect_rc 2 "short flag value rejected" -i -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh b/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh index c832abec..64c08ebc 100755 --- a/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-lane-brief-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -r -m -l -L -n --repo --milestone --label --login --limit; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -r owner/repo -m --help +expect_rc 2 "short flag value rejected" -r owner/repo -m -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh index bd674910..c7f38771 100755 --- a/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-milestone-close-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -t --title; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -t --help --help +expect_rc 2 "short flag value rejected" -t --help -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh index 2e08587d..e267f9dc 100755 --- a/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-milestone-create-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -t -d --due --title --desc; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -t smoke -d --help +expect_rc 2 "short flag value rejected" -t smoke -d -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh index d6865b81..64b894f4 100755 --- a/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh @@ -98,6 +98,13 @@ for flag in -s --state; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -s --help +expect_rc 2 "short flag value rejected" -s -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh index 734bb150..6d380e59 100755 --- a/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-close-usage-contract.sh @@ -100,6 +100,13 @@ for flag in -n -b -c --number --body --comment; do expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -n 5 -b --help +expect_rc 2 "short flag value rejected" -n 5 -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh index a7d6c952..ad23472c 100755 --- a/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-create-usage-contract.sh @@ -99,6 +99,13 @@ for flag in -t -b -B -H -l -m -i --title --body --base --head --labels --milesto expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -t smoke -b --help +expect_rc 2 "short flag value rejected" -t smoke -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh index e816f69d..71448ca9 100755 --- a/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-edit-usage-contract.sh @@ -107,6 +107,13 @@ for flag in -n -t -b -B -l -r -H --number --title --body --base --login --repo - expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -n 5 -t smoke -b --help +expect_rc 2 "short flag value rejected" -n 5 -t smoke -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 diff --git a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh index a66b2075..13dbf50b 100755 --- a/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-usage-contract.sh @@ -115,6 +115,13 @@ for flag in -n -a -b -c -l -r --number --action --body --comment --login --repo; expect_stderr "requires a value" "value-less $flag message on stderr" done +# 4a. An option-like value is a MISSING value, not a value (codex PR #1464: +# -b --help previously consumed --help as the body and performed the write). +expect_rc 2 "option-like value rejected" -n 5 -a comment -b --help +expect_rc 2 "short flag value rejected" -n 5 -a comment -b -h +expect_stderr "requires a value" "short flag value message on stderr" +expect_stderr "requires a value" "option-like value message on stderr" + # 4b. Parser-failure arms (1-4) must have performed ZERO provider contact. if [[ -s "$PROBE_LOG" ]]; then echo "FAIL: a parser-failure arm contacted a provider:" >&2 -- 2.54.0 From 90f982afaa448b005fade4b5473ba79c9e68f574 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:28:28 -0500 Subject: [PATCH 15/17] framework tools/git: guards reject all option-shaped values; usage_error routes usage to stderr (codex round on PR #1464) - Option-like value = any token starting with '-' followed by an alphanumeric (-h, -ab, --help); multi-character short clusters were still accepted (codex blocker). Bare '-' is reserved (future stdin). - usage_error now prints the usage text to stderr as well (codex should-fix): usage output belonged to stdout only on the help path. - 16/16 usage-contract suites green. --- .../mosaic/framework/tools/git/issue-assign.sh | 10 +++++----- .../mosaic/framework/tools/git/issue-close.sh | 4 ++-- .../mosaic/framework/tools/git/issue-comment.sh | 6 +++--- .../mosaic/framework/tools/git/issue-create.sh | 10 +++++----- .../mosaic/framework/tools/git/issue-edit.sh | 10 +++++----- .../mosaic/framework/tools/git/issue-list.sh | 14 +++++++------- .../mosaic/framework/tools/git/issue-reopen.sh | 4 ++-- .../mosaic/framework/tools/git/issue-view.sh | 2 +- .../mosaic/framework/tools/git/lane-brief.sh | 10 +++++----- .../framework/tools/git/milestone-close.sh | 2 +- .../framework/tools/git/milestone-create.sh | 8 ++++---- .../mosaic/framework/tools/git/milestone-list.sh | 2 +- packages/mosaic/framework/tools/git/pr-close.sh | 4 ++-- packages/mosaic/framework/tools/git/pr-create.sh | 16 ++++++++-------- packages/mosaic/framework/tools/git/pr-edit.sh | 16 ++++++++-------- packages/mosaic/framework/tools/git/pr-review.sh | 12 ++++++------ 16 files changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index c1c26c0c..b7192005 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -40,29 +40,29 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE="$2" shift 2 ;; -a|--assignee) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ASSIGNEE="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" MILESTONE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index 7109247c..78d92384 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -25,14 +25,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" COMMENT="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-comment.sh b/packages/mosaic/framework/tools/git/issue-comment.sh index 93231c99..d5b74028 100755 --- a/packages/mosaic/framework/tools/git/issue-comment.sh +++ b/packages/mosaic/framework/tools/git/issue-comment.sh @@ -46,7 +46,7 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE_NUMBER="$2" shift 2 ;; @@ -54,12 +54,12 @@ while [[ $# -gt 0 ]]; do # R1 (2026-08-28): --body is the canonical flag, matching # issue-create/issue-edit/pr-create/pr-edit; -c/--comment stays a # backward-compatible alias. - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" COMMENT="$2" shift 2 ;; -l|--login) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LOGIN_OVERRIDE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index 3e42e14a..e0b061a6 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -85,28 +85,28 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" BODY="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" MILESTONE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index f40a2f34..20465de9 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -26,27 +26,27 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE_NUMBER="$2" shift 2 ;; -t|--title) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" BODY="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" MILESTONE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-list.sh b/packages/mosaic/framework/tools/git/issue-list.sh index 534c52bb..54ca1ce8 100755 --- a/packages/mosaic/framework/tools/git/issue-list.sh +++ b/packages/mosaic/framework/tools/git/issue-list.sh @@ -44,38 +44,38 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } while [[ $# -gt 0 ]]; do case $1 in -s|--state) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" STATE="$2" shift 2 ;; -l|--label) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABEL="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" MILESTONE="$2" shift 2 ;; -a|--assignee) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ASSIGNEE="$2" shift 2 ;; -n|--limit) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LIMIT="$2" shift 2 ;; -r|--repo) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" REPO_OVERRIDE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-reopen.sh b/packages/mosaic/framework/tools/git/issue-reopen.sh index 58e80382..d1011035 100755 --- a/packages/mosaic/framework/tools/git/issue-reopen.sh +++ b/packages/mosaic/framework/tools/git/issue-reopen.sh @@ -24,14 +24,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" COMMENT="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/issue-view.sh b/packages/mosaic/framework/tools/git/issue-view.sh index 4d45ce87..dd0364f5 100755 --- a/packages/mosaic/framework/tools/git/issue-view.sh +++ b/packages/mosaic/framework/tools/git/issue-view.sh @@ -82,7 +82,7 @@ if comments: while [[ $# -gt 0 ]]; do case $1 in -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE_NUMBER="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/lane-brief.sh b/packages/mosaic/framework/tools/git/lane-brief.sh index 5d1df846..c8a07180 100755 --- a/packages/mosaic/framework/tools/git/lane-brief.sh +++ b/packages/mosaic/framework/tools/git/lane-brief.sh @@ -37,11 +37,11 @@ usage_error() { } while [[ $# -gt 0 ]]; do case "$1" in - -r|--repo) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; REPO="$2"; shift 2 ;; - -m|--milestone) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; MILESTONE="$2"; shift 2 ;; - -l|--label|--labels) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LABEL="$2"; shift 2 ;; - -L|--login) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LOGIN="$2"; shift 2 ;; - -n|--limit) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LIMIT="$2"; shift 2 ;; + -r|--repo) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; REPO="$2"; shift 2 ;; + -m|--milestone) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; MILESTONE="$2"; shift 2 ;; + -l|--label|--labels) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; LABEL="$2"; shift 2 ;; + -L|--login) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; LOGIN="$2"; shift 2 ;; + -n|--limit) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; LIMIT="$2"; shift 2 ;; -h|--help) grep '^#' "$0" | sed 's/^# \?//'; exit 0 ;; *) usage_error "unknown option: $1" ;; esac diff --git a/packages/mosaic/framework/tools/git/milestone-close.sh b/packages/mosaic/framework/tools/git/milestone-close.sh index 92162bb8..9f31b542 100755 --- a/packages/mosaic/framework/tools/git/milestone-close.sh +++ b/packages/mosaic/framework/tools/git/milestone-close.sh @@ -21,7 +21,7 @@ TITLE="" while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" TITLE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index 35361c0f..67202fb2 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -45,23 +45,23 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" TITLE="$2" shift 2 ;; -d|--desc) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" DESCRIPTION="$2" shift 2 ;; --due) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" DUE_DATE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/milestone-list.sh b/packages/mosaic/framework/tools/git/milestone-list.sh index 64beff8e..2704cc9d 100755 --- a/packages/mosaic/framework/tools/git/milestone-list.sh +++ b/packages/mosaic/framework/tools/git/milestone-list.sh @@ -21,7 +21,7 @@ STATE="open" while [[ $# -gt 0 ]]; do case $1 in -s|--state) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" STATE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/pr-close.sh b/packages/mosaic/framework/tools/git/pr-close.sh index f05a803f..9a9dbdf7 100755 --- a/packages/mosaic/framework/tools/git/pr-close.sh +++ b/packages/mosaic/framework/tools/git/pr-close.sh @@ -24,14 +24,14 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -n|--number) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" PR_NUMBER="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" COMMENT="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index 6f599c0f..2226b091 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -142,44 +142,44 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -t|--title) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" TITLE="$2" shift 2 ;; -b|--body) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" BODY="$2" shift 2 ;; -B|--base) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" BASE_BRANCH="$2" shift 2 ;; -H|--head) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" HEAD_BRANCH="$2" shift 2 ;; -l|--labels) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" shift 2 ;; -m|--milestone) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" MILESTONE="$2" shift 2 ;; -i|--issue) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ISSUE="$2" shift 2 ;; diff --git a/packages/mosaic/framework/tools/git/pr-edit.sh b/packages/mosaic/framework/tools/git/pr-edit.sh index ac167865..db520f7d 100755 --- a/packages/mosaic/framework/tools/git/pr-edit.sh +++ b/packages/mosaic/framework/tools/git/pr-edit.sh @@ -57,24 +57,24 @@ EOF # distinct from provider, credential, and verification failures (exit 1). usage_error() { echo "Error: $*" >&2 - usage + usage >&2 } while [[ $# -gt 0 ]]; do case "$1" in - -n|--number) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; PR_NUMBER="${2:-}"; shift 2 ;; - -t|--title) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; TITLE="${2:-}"; shift 2 ;; - -b|--body) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; BODY="${2:-}"; shift 2 ;; - -B|--base) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; BASE_BRANCH="${2:-}"; shift 2 ;; + -n|--number) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; PR_NUMBER="${2:-}"; shift 2 ;; + -t|--title) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; TITLE="${2:-}"; shift 2 ;; + -b|--body) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; BODY="${2:-}"; shift 2 ;; + -B|--base) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; BASE_BRANCH="${2:-}"; shift 2 ;; --draft) [[ "$DRAFT_MODE" != "ready" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="draft"; shift ;; --ready) [[ "$DRAFT_MODE" != "draft" ]] || { echo "Error: --draft and --ready are mutually exclusive" >&2; exit 2; } DRAFT_MODE="ready"; shift ;; - -l|--login) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; LOGIN_OVERRIDE="${2:-}"; shift 2 ;; - -r|--repo) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; REPO_OVERRIDE="${2:-}"; shift 2 ;; - -H|--host) [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)"; HOST_OVERRIDE="${2:-}"; shift 2 ;; + -l|--login) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; LOGIN_OVERRIDE="${2:-}"; shift 2 ;; + -r|--repo) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; REPO_OVERRIDE="${2:-}"; shift 2 ;; + -H|--host) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)"; HOST_OVERRIDE="${2:-}"; shift 2 ;; -h|--help) usage 0 ;; *) echo "Unknown option: $1" >&2; usage ;; esac diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 716e3fe7..e468222d 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -55,34 +55,34 @@ usage_error() { while [[ $# -gt 0 ]]; do case $1 in -n|--number) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" PR_NUMBER="$2" shift 2 ;; -a|--action) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" ACTION="$2" shift 2 ;; -b|--body|-c|--comment) # R1 (2026-08-28): --body is the canonical flag; -c/--comment stays # a backward-compatible alias. - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" COMMENT="$2" shift 2 ;; -l|--login) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LOGIN_OVERRIDE="$2" shift 2 ;; -r|--repo) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" REPO_OVERRIDE="$2" shift 2 ;; -H|--host) - [[ $# -ge 2 && "$2" != --* && ! "$2" =~ ^-[a-zA-Z]$ ]] || usage_error "option $1 requires a value (option-like values are rejected)" + [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" HOST_OVERRIDE="$2" shift 2 ;; -- 2.54.0 From 7db25b8820f3996595406c519ef32de307106c89 Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:31:18 -0500 Subject: [PATCH 16/17] framework tools/git: lookup-pipeline pipefail + provider-normalization test arm (codex round on PR #1464) - issue-assign + milestone-create: set -o pipefail; the remove-assignee lookup pipeline now fails loud on provider failure instead of reading an empty result as a silent no-assignees skip (codex blocker). A successful lookup with zero assignees still skips the edit. - test-issue-assign-usage-contract: 6b arm proves provider-exit normalization end to end (gh stub exiting 2 on issue edit -> wrapper exit 1 with the normalized stderr message; codex should-fix). - Battery: 23/23 suites green. --- .../framework/tools/git/issue-assign.sh | 10 ++++++++- .../framework/tools/git/milestone-create.sh | 1 + .../git/test-issue-assign-usage-contract.sh | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index b7192005..1dad6987 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -3,6 +3,7 @@ # Usage: issue-assign.sh -i ISSUE_NUMBER [-a assignee] [-l labels] [-m milestone] set -e +set -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" @@ -96,7 +97,14 @@ case "$PLATFORM" in fi if [[ "$REMOVE_ASSIGNEE" == true ]]; then # Get current assignees and remove them - CURRENT=$(gh issue view "$ISSUE" --json assignees -q '.assignees[].login' 2>/dev/null | tr '\n' ',') + # pipefail preserves the provider status through the pipeline; + # a FAILED lookup exits here instead of reading as a silent + # no-assignees skip (codex PR #1464). A successful lookup with + # zero assignees still skips the edit below. + CURRENT=$(gh issue view "$ISSUE" --json assignees -q '.assignees[].login' 2>/dev/null | tr '\n' ',') || { + echo "Error: could not read current assignees (provider lookup failed)" >&2 + exit 1 + } if [[ -n "$CURRENT" ]]; then prov_rc=0 gh issue edit "$ISSUE" --remove-assignee "${CURRENT%,}" || prov_rc=$? diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index 67202fb2..c21acfce 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -3,6 +3,7 @@ # Usage: milestone-create.sh -t "Title" [-d "Description"] [--due "YYYY-MM-DD"] set -e +set -o pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/detect-platform.sh" diff --git a/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh index 8eb35e0a..5c73537c 100755 --- a/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-issue-assign-usage-contract.sh @@ -129,4 +129,26 @@ done # the arm above proves only parse acceptance and non-usage classification. # Parser-failure arms (1-4) remain zero-contact (asserted at 4b). +# 6b. Provider-exit normalization (codex PR #1464): a provider stub exiting +# 2 (its own usage-error status) must surface as wrapper exit 1, never 2. +GH_REPO="$WORK_DIR/repo-gh" +mkdir -p "$GH_REPO" +git -C "$GH_REPO" init -q +git -C "$GH_REPO" remote add origin https://github.com/acme/widgets.git +cat > "$BIN_DIR/gh" <<GHSTUB +#!/usr/bin/env bash +echo "gh \$*" >> "$PROBE_LOG" +if [[ "\$1 \$2" == "issue edit" ]]; then exit 2; fi +exit 0 +GHSTUB +chmod +x "$BIN_DIR/gh" +rc=0 +( + cd "$GH_REPO" + PATH="$BIN_DIR:$PATH" MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/issue-assign.sh" -i 5 -a someone >"$OUT_FILE" 2>"$ERR_FILE" +) || rc=$? +[[ "$rc" -eq 1 ]] || fail "GitHub path: gh exit 2 must normalize to wrapper exit 1 (got $rc)" +grep -q "provider" "$ERR_FILE" || fail "GitHub path: normalized provider error missing from stderr" + echo "issue-assign.sh usage-contract regression passed (R1/R4)" -- 2.54.0 From 34e56f242eb5b1f6d2fb5f0f707ec85c014dbfcc Mon Sep 17 00:00:00 2001 From: marcie <marcie@fleet.mosaicstack.dev> Date: Fri, 28 Aug 2026 18:33:32 -0500 Subject: [PATCH 17/17] framework tools/git: provider-normalization arms on issue-close and milestone-list (codex should-fix) gh stub exiting 2 on the close/api path -> wrapper exit 1 with the normalized stderr message; mirrors the issue-assign 6b arm. All touched suites green; brain mirror updated. --- .../git/test-milestone-list-usage-contract.sh | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh index 64b894f4..e2456405 100755 --- a/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh +++ b/packages/mosaic/framework/tools/git/test-milestone-list-usage-contract.sh @@ -128,4 +128,26 @@ done # the arm above proves only parse acceptance and non-usage classification. # Parser-failure arms (1-4) remain zero-contact (asserted at 4b). +# 6b. Provider-exit normalization (codex PR #1464): a provider stub exiting +# 2 (its own usage-error status) must surface as wrapper exit 1, never 2. +GH_REPO="$WORK_DIR/repo-gh" +mkdir -p "$GH_REPO" +git -C "$GH_REPO" init -q +git -C "$GH_REPO" remote add origin https://github.com/acme/widgets.git +cat > "$BIN_DIR/gh" <<GHSTUB +#!/usr/bin/env bash +echo "gh \$*" >> "$PROBE_LOG" +if [[ "\$1" == "api" ]]; then exit 2; fi +exit 0 +GHSTUB +chmod +x "$BIN_DIR/gh" +rc=0 +( + cd "$GH_REPO" + PATH="$BIN_DIR:$PATH" MOSAIC_GIT_IDENTITY="" MOSAIC_BRAIN_HOME="" \ + "$SCRIPT_DIR/milestone-list.sh" >"$OUT_FILE" 2>"$ERR_FILE" +) || rc=$? +[[ "$rc" -eq 1 ]] || fail "GitHub path: gh exit 2 must normalize to wrapper exit 1 (got $rc)" +grep -q "provider" "$ERR_FILE" || fail "GitHub path: normalized provider error missing from stderr" + echo "milestone-list.sh usage-contract regression passed (R1/R4)" -- 2.54.0