From 39051e48ef8ddcef595505b32abb3334108b8963 Mon Sep 17 00:00:00 2001 From: marcie Date: Fri, 28 Aug 2026 21:40:11 -0500 Subject: [PATCH] framework tools/git: R3 --body-file across all nine body/comment carriers --body-file and --body-file - (stdin) load the body/comment from a file, killing the fragile shell-quoting class for long Markdown (Jason remediation R3). Contract: mutually exclusive with --body (rc 2), unreadable file rc 2 naming the path, resolution runs AFTER parsing and BEFORE the required-value checks. Full arm set on the issue-comment suite (file, stdin, exclusive, missing); all nine carrier suites and the readback suite green. Mirrored to brain. --- .../mosaic/framework/tools/git/issue-close.sh | 20 ++++++++++++++++ .../framework/tools/git/issue-comment.sh | 20 ++++++++++++++++ .../framework/tools/git/issue-create.sh | 20 ++++++++++++++++ .../mosaic/framework/tools/git/issue-edit.sh | 20 ++++++++++++++++ .../framework/tools/git/issue-reopen.sh | 20 ++++++++++++++++ .../mosaic/framework/tools/git/pr-close.sh | 20 ++++++++++++++++ .../mosaic/framework/tools/git/pr-create.sh | 20 ++++++++++++++++ .../mosaic/framework/tools/git/pr-edit.sh | 15 ++++++++++++ .../mosaic/framework/tools/git/pr-review.sh | 20 ++++++++++++++++ .../git/test-issue-comment-usage-contract.sh | 24 +++++++++++++++++++ 10 files changed, 199 insertions(+) diff --git a/packages/mosaic/framework/tools/git/issue-close.sh b/packages/mosaic/framework/tools/git/issue-close.sh index 78d92384..d0de865f 100755 --- a/packages/mosaic/framework/tools/git/issue-close.sh +++ b/packages/mosaic/framework/tools/git/issue-close.sh @@ -12,6 +12,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments ISSUE_NUMBER="" COMMENT="" +BODY_FILE="" # 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 @@ -36,6 +37,12 @@ while [[ $# -gt 0 ]]; do COMMENT="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$2" + shift 2 + ;; -h|--help) echo "Usage: issue-close.sh -i [-b ]" echo "" @@ -54,6 +61,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into COMMENT (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$COMMENT" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + COMMENT=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + COMMENT=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$ISSUE_NUMBER" ]]; then usage_error "issue number is required (-i/--issue)" fi diff --git a/packages/mosaic/framework/tools/git/issue-comment.sh b/packages/mosaic/framework/tools/git/issue-comment.sh index d5b74028..50dd34fc 100755 --- a/packages/mosaic/framework/tools/git/issue-comment.sh +++ b/packages/mosaic/framework/tools/git/issue-comment.sh @@ -31,6 +31,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments ISSUE_NUMBER="" COMMENT="" +BODY_FILE="" LOGIN_OVERRIDE="" # Usage-error contract (R4, 2026-08-28): usage errors print to STDERR and exit 2, @@ -58,6 +59,12 @@ while [[ $# -gt 0 ]]; do COMMENT="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$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" @@ -82,6 +89,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into COMMENT (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$COMMENT" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + COMMENT=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + COMMENT=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$ISSUE_NUMBER" ]]; then usage_error "issue number is required (-i/--issue)" fi diff --git a/packages/mosaic/framework/tools/git/issue-create.sh b/packages/mosaic/framework/tools/git/issue-create.sh index e0b061a6..b93292a5 100755 --- a/packages/mosaic/framework/tools/git/issue-create.sh +++ b/packages/mosaic/framework/tools/git/issue-create.sh @@ -10,6 +10,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Default values TITLE="" BODY="" +BODY_FILE="" LABELS="" MILESTONE="" INTERACTIVE=false @@ -100,6 +101,12 @@ while [[ $# -gt 0 ]]; do BODY="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$2" + shift 2 + ;; -l|--labels) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" @@ -124,6 +131,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into BODY (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$BODY" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + BODY=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + BODY=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ "$INTERACTIVE" == true ]]; then [[ -n "$TITLE" ]] || read -r -p "Issue title: " TITLE [[ -n "$BODY" ]] || read -r -p "Issue body (optional): " BODY || true diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index 20465de9..3c035cde 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/detect-platform.sh" ISSUE_NUMBER="" TITLE="" BODY="" +BODY_FILE="" LABELS="" MILESTONE="" @@ -40,6 +41,12 @@ while [[ $# -gt 0 ]]; do BODY="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$2" + shift 2 + ;; -l|--labels) [[ $# -ge 2 && "$2" != - && "$2" != --* && ! "$2" =~ ^-[[:alnum:]] ]] || usage_error "option $1 requires a value (option-like values are rejected; bare - is reserved)" LABELS="$2" @@ -70,6 +77,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into BODY (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$BODY" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + BODY=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + BODY=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$ISSUE_NUMBER" ]]; then usage_error "issue number is required (-i/--issue)" fi diff --git a/packages/mosaic/framework/tools/git/issue-reopen.sh b/packages/mosaic/framework/tools/git/issue-reopen.sh index d1011035..ee82d4ae 100755 --- a/packages/mosaic/framework/tools/git/issue-reopen.sh +++ b/packages/mosaic/framework/tools/git/issue-reopen.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments ISSUE_NUMBER="" COMMENT="" +BODY_FILE="" # 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 @@ -35,6 +36,12 @@ while [[ $# -gt 0 ]]; do COMMENT="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$2" + shift 2 + ;; -h|--help) echo "Usage: issue-reopen.sh -i [-b ]" echo "" @@ -53,6 +60,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into COMMENT (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$COMMENT" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + COMMENT=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + COMMENT=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$ISSUE_NUMBER" ]]; then usage_error "issue number is required (-i/--issue)" fi diff --git a/packages/mosaic/framework/tools/git/pr-close.sh b/packages/mosaic/framework/tools/git/pr-close.sh index 9a9dbdf7..0f2ccb73 100755 --- a/packages/mosaic/framework/tools/git/pr-close.sh +++ b/packages/mosaic/framework/tools/git/pr-close.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Parse arguments PR_NUMBER="" COMMENT="" +BODY_FILE="" # 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 @@ -35,6 +36,12 @@ while [[ $# -gt 0 ]]; do COMMENT="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$2" + shift 2 + ;; -h|--help) echo "Usage: pr-close.sh -n [-b ]" echo "" @@ -53,6 +60,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into COMMENT (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$COMMENT" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + COMMENT=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + COMMENT=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$PR_NUMBER" ]]; then usage_error "PR number is required (-n/--number)" fi diff --git a/packages/mosaic/framework/tools/git/pr-create.sh b/packages/mosaic/framework/tools/git/pr-create.sh index 2226b091..4cd29e5c 100755 --- a/packages/mosaic/framework/tools/git/pr-create.sh +++ b/packages/mosaic/framework/tools/git/pr-create.sh @@ -10,6 +10,7 @@ source "$SCRIPT_DIR/detect-platform.sh" # Default values TITLE="" BODY="" +BODY_FILE="" BASE_BRANCH="" HEAD_BRANCH="" LABELS="" @@ -158,6 +159,12 @@ while [[ $# -gt 0 ]]; do BODY="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$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" @@ -197,6 +204,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into BODY (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$BODY" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + BODY=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + BODY=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + # If no title but issue provided, generate title if [[ -z "$TITLE" ]] && [[ -n "$ISSUE" ]]; then TITLE="Fixes #$ISSUE" diff --git a/packages/mosaic/framework/tools/git/pr-edit.sh b/packages/mosaic/framework/tools/git/pr-edit.sh index db520f7d..ad05aac0 100755 --- a/packages/mosaic/framework/tools/git/pr-edit.sh +++ b/packages/mosaic/framework/tools/git/pr-edit.sh @@ -11,6 +11,7 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" TITLE="" BODY="" +BODY_FILE="" BASE_BRANCH="" DRAFT_MODE="" LOGIN_OVERRIDE="" @@ -65,6 +66,7 @@ while [[ $# -gt 0 ]]; do -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 ;; + --body-file) [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)"; BODY_FILE="$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; } @@ -80,6 +82,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into BODY (file or stdin '-'); +# exclusive with an explicit --body value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$BODY" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + BODY=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + BODY=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + [[ -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 diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index e468222d..f230a96b 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -39,6 +39,7 @@ source "$SCRIPT_DIR/detect-platform.sh" PR_NUMBER="" ACTION="" COMMENT="" +BODY_FILE="" LOGIN_OVERRIDE="" REPO_OVERRIDE="" HOST_OVERRIDE="" @@ -71,6 +72,12 @@ while [[ $# -gt 0 ]]; do COMMENT="$2" shift 2 ;; + --body-file) + # R3: body from file (or '-' = stdin); mutually exclusive with --body. + [[ $# -ge 2 && "$2" != --* ]] || usage_error "option $1 requires a path (or - for stdin)" + BODY_FILE="$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" @@ -108,6 +115,19 @@ while [[ $# -gt 0 ]]; do esac done +# R3 (2026-08-29): resolve --body-file into COMMENT (file or stdin '-'); +# exclusive with an explicit --body/--comment value. +if [[ -n "$BODY_FILE" ]]; then + [[ -z "$COMMENT" ]] || usage_error "--body-file and --body are mutually exclusive" + if [[ "$BODY_FILE" == "-" ]]; then + COMMENT=$(cat) || usage_error "could not read body from stdin" + else + [[ -r "$BODY_FILE" ]] || usage_error "body file not readable: $BODY_FILE" + COMMENT=$(cat "$BODY_FILE") || usage_error "could not read body file: $BODY_FILE" + fi +fi + + if [[ -z "$PR_NUMBER" ]]; then usage_error "PR number is required (-n/--number)" fi 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 d80a89a4..498d162b 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 @@ -161,6 +161,30 @@ rc=0 grep -q "GitHub comment write failed" "$ERR_FILE" || fail "GitHub path: normalized error missing from stderr" grep -q "^gh issue comment" "$PROBE_LOG" || fail "GitHub path: gh write was not invoked" +# R3 body-file arms (2026-08-29): --body-file and '-' (stdin). +BF_FILE="$WORK_DIR/body.md" +printf 'line one\nline two\n' > "$BF_FILE" + +# File loads the body: parse acceptance then credential-class failure +# (sandboxed runner: rc nonzero and NOT 2). +rc=0 +run_wrapper_sandboxed -i 5 --body-file "$BF_FILE" >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? +[[ "$rc" -ne 0 ]] || fail "body-file arm unexpectedly succeeded in the sandbox" +[[ "$rc" -ne 2 ]] || fail "body-file arm misclassified credential failure as a usage error" + +# Stdin form loads the body the same way. +rc=0 +printf 'from stdin' | run_wrapper_sandboxed -i 5 --body-file - >"$OUT_FILE" 2>"$ERR_FILE" || rc=$? +[[ "$rc" -ne 0 && "$rc" -ne 2 ]] || fail "body-file stdin arm rc=$rc (want nonzero, not 2)" + +# Mutually exclusive with --body: rc 2. +expect_rc 2 "body-file + body exclusive" -i 5 --body-file "$BF_FILE" -b explicit +expect_stderr "mutually exclusive" "exclusivity message on stderr" + +# Missing file: rc 2 naming the path. +expect_rc 2 "missing body file" -i 5 --body-file "$WORK_DIR/nope.md" +expect_stderr "not readable" "missing-file message on stderr" + # 7. No provider contact from any usage-error arm (arm 6b's deliberate gh # invocation is the only permitted entry in the probe log). if grep -v '^gh issue comment' "$PROBE_LOG" | grep -q .; then