Skip to content

fix(security): keep a quoted newline inside its word on the push path - #15438

Open
Maqix wants to merge 1 commit into
kirodotdev:mainfrom
Maqix:fix/push-floor-quoted-newline
Open

Maqix wants to merge 1 commit into
kirodotdev:mainfrom
Maqix:fix/push-floor-quoted-newline

Conversation

@Maqix

@Maqix Maqix commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: A literal feature-branch push in the same shell call as a multi-line git commit message is allowed when the call has exactly the shape the reported #12631 commands use: segments cd <dir> or git add/commit/push, flags only commit -m/-q and push -u/-q, and a literal single- or double-quoted message. Every other shape (add -A, commit -am/-s, --set-upstream, cd ~/r, a $ in a double-quoted body, a redirect) is refused as on main, with the "push on its own line" hint where that retry passes; see the event replay under Backwards compatibility.

git commit -m '<subject>⏎⏎<body>'; git push -u fork fix/x is refused with rule=git-publish-target-unverifiable, and the reason says "shell substitution or expansion fuses text into the push target" even though nothing does. I hit this today on current main. @bolichen97 traced the mechanism on #12631: _split_push_command_segments ends a segment at every newline, including one inside quotes. The next segment's quote walk restarts unquoted, so the message's closing quote is read as an opener, and that quote swallows the following && / ;. The push segment becomes one unparseable word and lands on the ungated sentinel.

Why it matters

Multi-line commit messages are the normal way agents write commits. Every such commit chained to a push is refused with a reason that points at a defect the command doesn't have, so the operator or agent can't tell which token to change. The same misreading also hides the real reason for a protected push: git commit -m "a⏎⏎b" && git push origin main is refused as target-unverifiable rather than push-protected-branch-name.

Not a goal

This allowlist is the base those follow-ups extend, not a one-off: #20847's fix (PR #20854, stacked on this one) widens _inert_segment by one prefix and adds its own oracle rows, and any further shape goes the same way, one allowlist entry plus a real-bash oracle row.

These are why the PR uses Refs #12631 rather than a closing keyword.

Backwards compatibility

Reader: argv_floor_git.py:_git_publish_floor_tags is the splitter's only production caller. The splitter change is load-bearing: with only keep_quoted_newlines off, the reporter's command and event 7 are refused (test_the_splitter_keeping_a_quoted_newline_is_what_allows_the_reported_command).

Compatible: the gate moves in the allow direction only, for a literal unprotected push chained after a git commit whose shape fits the allowlist under What changed. An opener inside a plain single quote (git commit -m 'fix `foo` ...') is literal to bash and keeps the trust. Every other shape reads exactly as on main (see Tests).

Still refused, as on main plus the new hint (one retry with the push on its own line): commit -s/--signoff, --no-verify, --amend, --no-edit; cd ~/r or an env prefix; a double-quoted body containing `, $ or \; any $'…' or $"…" body; a heredoc or $(…) message; git tag, echo, printf or gh as the owner; git -c settings; commit --message, --quiet, -a/--all; add -A/--all; push -f/--tags, --force-with-lease, --set-upstream, --quiet. _INERT_FLAGS is not widened without a security-owner review; the real-bash oracle test and the one-oracle-row-per-entry rule (both enforced by tests, security.md) are the bar for any widening.

#12631 events replayed with a multi-line message (issue_events_probe, head vs main):

  • Now pass: the reporter's git commit -m '<ml>'; git push -u fork fix/x, and event 7 (cd /r && git add f && git commit -m '<ml>' or "<ml>" && git push origin docs/…).
  • Still need the own-line retry: event 7 with -s, cd ~/r or $ in a double-quoted body; event 16 (git push -q origin docs/s 2>/dev/null; gh pr create …: the redirect keeps the sentinel; it passes with the push on its own line).
  • Correctly denied: event 5 (pushes main). Events 9 and 10 (env prefix, | tail, no multi-line text) already pass on main.

The one visible change for a protected push is a more accurate rule id (push-protected-branch-name instead of target-unverifiable).

What changed (motivation → approach → change)

  • Symptom: a literal feature push is refused as target-unverifiable.
  • Cause: a quoted newline splits the segment and desynchronises the quote state.
  • Change, in _split_push_command_segments (shell_normalizer_walk.py): a newline that _iter_shell_chars reports as inside quotes (and not escaped) stays in its word and does not end the segment. Unquoted and escaped newlines still split, as on main.
    • Trust is limited to commands the quote walk models. A heredoc, a $(/${/backtick/<(/>( body, a real # comment, or any backslash-newline outside a plain single quote keeps the split at every newline (_QUOTE_WALK_UNMODELLED_RE). The list only narrows allows; no denial depends on it.
    • A merged segment that does not parse as a push is re-read line by line, as on main, so quoted text that only mentions a push reads as before.

Also changed:

  • _git_publish_floor_tags, the per-line backstop and the one deny authority: when the command has a newline, main's per-line reading also runs on every push line. Its ungated sentinel is dropped only where the whole-text walk enters the line inside a quote that closes before the push and ends its word; the code after that close is judged in full.
  • Why the backstop can't rely on the merged segments: they come from the same quote walk a phantom quote fools. In trap 'x⏎git push origin $b #' EXIT git push origin f (Opus 5.5 review) the walk keeps the $b push inside a quoted word, so the merged segments show only the feature push; only the per-line reading sees git push origin $b. That row is in the real-bash oracle corpus. The same row is why the root-cause alternative, carrying the cumulative quote state into each per-line segment instead of restarting it unquoted, was rejected: every reading would then inherit the phantom quote and none would see the $b push.
  • The exemption needs the quoted word to be inert data (shell_normalizer_walk._quoted_word_is_inert), one closed allowlist checked whole:
    • the owner is git commit (no git -c setting), the owner git-publish-* floor refuses literal git push [flags] origin feature-branch #12631 names; echo, printf, git tag and gh read as on main;
    • the word follows a space or tab and doesn't open with -; the word is '…' or "…": a $'…' or $"…" word is decoded or translated by bash and no reported event needs one, so it is never data (First Principles item 5); a double-quoted body holds no $, backtick or \, so no expansion ("$1⏎" with -- -em) reaches bash as an option. Only a literal body is data;
    • with every other quoted word masked, only word characters, spaces, tabs, newlines and . / : = @ % + , - ; & remain, and no masked word is empty or opens with - (bash dequotes '-e' and ''-e);
    • every segment is cd <dir> or git add/commit/push with only allowlisted lowercase flags (commit -m/-q, push -u/-q, no add flag: only the flags the replayed git-publish-* floor refuses literal git push [flags] origin feature-branch #12631 events use, First Principles item 6). Only -m takes a value, a commit must carry its message, and option scanning stops at -- (unless that -- is the value of -m), so no git config, -e/--edit, --amend, --author/--date or pathspec named --no-edit can open an editor on the text;
    • why cd <dir> cannot change which push bash runs (Design review of f9313632): <dir> must be one plain word or a literal quoted word (no $, ~, \, backtick, glob or brace), so bash expands it to literal text and runs nothing; a function or alias named cd would need a definition, which is outside the allowlist; and the push's destination is the literal refspec word the floor judges, the same word main already judges after a single-line cd dir && git push origin feat. A directory's own git config (for example a remote.<name>.push mapping) is the same exposure main has for that single-line form, so the exemption adds none. cd . is the oracle row;
    • anything else (pipe, redirect, expansion, glob, brace, comment, definition, source, trap, eval, ~ path, env prefix) keeps the sentinel, as on main. A new bypass is closed by the allowlist, not a new clause. The allowlist is meant to stay this narrow (Design review of 3eb721db): add -A, commit -s, cd ~/r and a $(cat <<'EOF'…) message stay refused on purpose, and any widening is a maintainer's recorded decision, carrying a real-bash oracle row that a test enforces (security.md).
  • The kiro-cli shell title (GPT 6.1 review of 1b0852fc5): kiro-cli titles a shell call Running: <command>, and the deny tiers judge that title as well as the command (GateFacts.deny_targets), so a proof anchored at offset 0 left the live title refused for an allowed command (fails closed, but the fix missed the real call). _quoted_word_is_inert now reads through ONE exact leading running: (the text is lowercased there), the same rule denied_rules._exception_applies already applies. Its constant _SHELL_TITLE_PREFIX moved to shell_normalizer_walk.py (denied_rules imports it back through shell_normalizer; the walk cannot import denied_rules without a load cycle), so there is still one copy. running: running: …, running:git …, xrunning: …, running … and a prefix after a separator are not stripped and read as a command word.
  • The bar: the whole-text reading keeps every tag main's per-line reading gives a push line, with one exception. The ungated sentinel is the one relaxed deny: it is dropped only on a line the walk enters inside a quote, and only when _quoted_word_is_inert accepts that word. So the safety of that drop rests on _quoted_word_is_inert (with _inert_mask and _inert_segment) modelling bash correctly, which is why every allowlist entry needs a real-bash oracle row (Design review of f66ddd0ec). Each inert check exists because an Opus 5.5, GPT 6.1 or Design review showed such a command; test_every_per_line_deny_survives_the_whole_text_reading pins the bar and test_no_command_real_bash_runs_as_a_protected_push_is_allowed checks it against real bash.
  • Refusal text: a refused multi-line push in which some line starts inside a quote (_quoted_line_entries), and which clears the floor once each git command after an unquoted separator (with any env or assignment prefix) is moved onto its own line, judged over every nested shell payload as the floor judges it (deny_gate._own_line_fix_applies), says "the push shares a line with a multi-line quoted argument, so its destination branch cannot be read before the push runs. Put the push on its own line." in place of the substitution reason, which such a command does not have. Every other unverifiable push keeps the substitution reason: a separator inside the quoted message is message text and is not moved, a bash -c push that stays unverifiable keeps the hint out, and so does a walk that fails. Only the reason text depends on this; no verdict does. The hint alone does not fix git-publish-* floor refuses literal git push [flags] origin feature-branch #12631: git commit -m '<multi-line>' && git push origin feat is the shape agents emit by default, so a hint-only fix costs a retry turn on every commit. The exemption allows exactly that shape.
  • docs/system-specs/modules/security.md: when a quoted newline stays in its word, the per-line backstop as the one deny authority, the construct list as allow-narrowing only, and the oracle-row rule.
  • test/test_security_gate_liveness.py: the package line budget is re-pinned from 32,526 (current main, b9ff59ab5) to 32,730. The per-line inert-head reading lives in shell_normalizer_walk._inert_quoted_line_heads, so the floor's loop stays small, and the argv_floor facade names it in its TYPE_CHECKING import list (test_argv_floor_facade.py checks that list); moving it changed no verdict or floor tag on 17,616 generated multi-line commands.
  • Rebased onto 518b36d1, which splits the security facade and the shell reader under the 2,000-line cap: the splitter change and the quoted-newline helpers (_QUOTE_WALK_UNMODELLED_RE, _INERT_FLAGS, _quoted_line_entries, _quoted_word_is_inert, _inert_quoted_line_heads, _inert_mask, _inert_segment) moved with _split_push_command_segments into shell_normalizer_walk.py (1,510 lines), which shell_normalizer.py re-imports for argv_floor.py and deny_gate.py; _own_line_fix_applies and the hint moved with is_denied into deny_gate.py, exported on the facade like its neighbours. The tests that patch the construct list or _opens_comment patch shell_normalizer_walk, where their readers are. No code changed: is_denied and _git_publish_floor_tags give the same result as 12dc6bed3 on all 17,617 commands of the move probe corpus (0 differing).
  • Rebased onto 2798d6b3, which splits argv_floor.py into owner modules behind a facade: the per-line loop and the kept-newline re-split moved unchanged into argv_floor_git._git_publish_floor_tags (1,136 lines). Same move probe against 01f2d721: 17,617 commands, 0 differing.

Tests

In test/test_push_branch_gate_parser.py (where main moved this class), TestTokenizerContractBetweenTheLayers:

  • test_a_newline_inside_quotes_stays_in_its_word:
    • the segment split of a double-quoted multi-line message;
    • double-quoted && and single-quoted ; feature pushes after a multi-line message are allowed;
    • a multi-line message followed by a push to main, and an unquoted newline before a push to main, both report git-publish-push-protected-branch-name.
  • test_a_quote_the_walk_cannot_model_keeps_the_split_at_every_newline:
    • the comment-apostrophe bypass, a heredoc-body phantom quote, a heredoc inside a double-quoted command substitution with a " in its body, a backtick phantom quote, the three backslash-newline bypasses from the Opus and GPT reviews (inside double quotes, before an ANSI-C quote, and inside a heredoc opener), and the four comment phantoms hiding an ungated-only push from the d093e50 reviews ($'main', $b, ~, and $b behind a real multi-line X="...") all stay denied (each fails on the commit it was reported against, passes now);
    • a quoted message containing Fixes #1 stays whole.
    • Fails on the previous head of this PR, passes now.
  • test_quoted_text_that_quotes_a_push_line_is_judged_line_by_line: the Security Scope Review's newly refused rows (an echo, a printf, a PR comment body and a commit message that quote a feature push line) are allowed again, and a quoted push to main stays denied. Fails on f96e4f0, passes now. It also holds the editor and quoted-option rows ('-e', ''-e, "--edit") and the refusal-hint row, which fail on 3752abf6. The --author -m/--date -m amend rows and the dropped echo/git tag/gh pr owners followed by a feature push fail on 33f99449 and pass now.
  • test_the_unmodelled_construct_list_is_pinned_to_the_quote_walk: pins the _QUOTE_WALK_UNMODELLED_RE pattern and checks every listed construct, backslash-newline included, turns quoted-newline trust off, and that the same construct inside a plain single quote does not; a single-quoted message holding a backtick, chained to a feature push, is allowed and the same chain to main denied. An earlier revision also pinned a sha256 of _iter_shell_chars's source; it was removed on the First Principles review, since the per-line backstop and its test now guard a construct the list misses. d093e50 removed the list on the First Principles review, since the backstop denied every known phantom shape on its own; the Opus and GPT reviews then found that the backstop as written (targets only, every sentinel dropped) let a comment apostrophe hide $'main'/$b, which the list's # check had been covering. Both are restored and the backstop is strengthened, so either guard alone denies every case found. Separately, why both guards stay: the construct list decides where the merged (quote-aware) reading is used at all, so an allow rests only on a walk that models bash for that command; the per-line backstop is the deny guarantee if the list is ever incomplete. At this head, removing a construct from the list no longer lets its phantom case through, because the backstop denies it; that is the backstop's job, and test_a_phantom_quote_cannot_hide_a_push_even_without_the_construct_list pins it.
  • test_the_shell_title_gets_the_verdict_of_the_command_it_displays (GPT 6.1 review of 1b0852fc5): every allow row in _INERT_ALLOW_ROWS (the reported shapes and each oracle row's feature twin, 11 rows) with and without Running: ; the 11 prefixed cases fail on 1b0852fc5. test_the_shell_title_prefix_is_read_through_once_and_only_exactly: the prefixed oracle rows (protected pushes) stay denied, the prefixed form of every corpus command the bare form denies is denied, and the lookalikes above stay denied.

Existing suites:

  • Every test file referencing is_denied, shell_normalizer, _split_push_command_segments or _git_publish_floor_tags, plus the line-budget test: 8277 passed, 16 skipped. After the rebase onto af1eb61: the push-gate, line-budget, argv-floor and security test files, 5103 passed. After the rebase onto 518b36d1: the push-gate, line-budget, argv-floor, denied-commands, split patch-reach and facade test files, 1339 passed, and the substitution/paren/heredoc subset of test/test_security.py, 65 passed. After the rebase onto 2798d6b3: the same files, 1328 passed. On 9800507e1, with the split security suites and facade tests added: 1,847 passed, 1 skipped (2 local-only markdown_it import errors). test_a_phantom_quote_cannot_hide_a_push_even_without_the_construct_list empties the construct list and checks the three reviewer bypasses still deny a hidden push to main, a hidden bare push and a hidden $b push; it fails without the per-line backstop.
  • test_every_per_line_deny_survives_the_whole_text_reading (Design Review's differential): over this module's own multi-line push corpus plus generated head × push × tail variants (comment apostrophes, X=", trap ', heredocs, backslash-newlines, backticks × main/master/bare/$b/$'main'/~/${b}/feature targets), run with the construct list on and emptied, every tag a push line earns judged alone as a single-line command is present in the whole-text verdict; only the ungated sentinel may be absent, and only on a line the whole-text walk enters inside a quote. It fails when the per-line backstop is disabled.
  • test_no_command_real_bash_runs_as_a_protected_push_is_allowed (Design Review's "checked against real bash"): an oracle independent of _iter_shell_chars. Real bash parses and runs every command in the same corpus, read from a script file, with git a shell function (loaded through BASH_ENV) that only logs its argv (b=main, g=git; sh/bash re-enter the same bash; PATH is an empty directory; scratch cwd). Every command whose executed argv is a protected or bare push (over 100 of them, including the trap $'\x67it …' shape) must be denied, with the construct list on and emptied. It fails on a4c74349, where bash runs a main push the filter allowed. It is never skipped: Windows runs it under the shared Git Bash lookup (_find_posix_test_shell), since the function stubs need no shebang or symlink, and a missing bash fails the test on every platform (Design review of 33f99449, GPT 6.1 review of 30e81bdb). On Linux the function stubs catch exactly the same 194 commands as the earlier PATH stubs. On 3752abf6 it ran and passed (6.2 s) on Linux, in Backend Tests (3.12, 3). On 9f26005b it ran and passed on Windows in Backend Tests (Windows) (3), in 40.79 s against its per-command timeout=10. On 6c45e3c4 it passed on Windows again in Backend Tests (Windows) (3), in 38.80 s. On 9eaabe9f it passed on Windows in Backend Tests (Windows) (3), in 38.72 s.
  • Where CI runs the oracle and the per-line differential (test_every_per_line_deny_survives_the_whole_text_reading), Design review of 529a6daa8: both are plain tests in test/test_push_branch_gate_parser.py, with no mark or skip. ci.yml's changes filter counts every path outside website/, .github/ and scripts/ as backend, so any change to shell_normalizer_walk.py or argv_floor_git.py runs them in the eight Linux Backend Tests (3.12, 1-8) shards. The eight Backend Tests (Windows) shards run them under Git Bash on a fork head when a Windows path or Windows-branching line changes (or with the ci:windows label), and on main after every merge. On 529a6daa8 all 16 Linux and Windows shards ran; the one red, Windows (1), is the known test_ci_ipv6_routing collection timeout.
  • test_every_inert_allowlist_entry_has_a_real_bash_oracle_row (Design Review's "enforced by a test rather than by convention"): reads _INERT_FLAGS and fails on any flag with no row in _INERT_ORACLE_ROWS; the real-bash oracle requires every one of those rows to run a protected push that stays denied. Adding -n to git add fails it.
  • test_generated_mask_edge_words_never_make_a_dash_word_inert (Design Review's "oracle corpus generates mask-edge words systematically"): 17 mask-edge words (empty, dash-leading, glued, escaped quotes) x 3 commit-message bodies (one a disguised $1 push) x 3 positions (before, after, glued to the message), each followed by a feature push. Wherever the exemption allows one, every word bash passes to git commit is non-empty and only -m/-q lead with a dash, so no edge word can open the editor. Mutation check: letting quote marks or anything through the post-mask character class fails it (the existing suite missed the quote-mark weakening).
  • test_the_splitter_keeping_a_quoted_newline_is_what_allows_the_reported_command (Design Review's "a test shows a git-publish-* floor refuses literal git push [flags] origin feature-branch #12631 command whose verdict changes when only keep_quoted_newlines is turned off"): the reporter's git commit -m '<multi-line>'; git push -u fork fix/x and event 7 with a single- or double-quoted message are allowed on this head and refused when the splitter is forced to split inside quotes again. The per-line exemption alone does not allow them, so both halves decide and _split_push_command_segments cannot stay identical to main. A replay of 26 git-publish-* floor refuses literal git push [flags] origin feature-branch #12631-shaped commands found 15 whose verdict or reason changes with that one switch.

Manual verification

26-case replay through security.is_denied, 0 failures:

  • Allowed:
    • a multi-line double- or single-quoted message (including one containing Fixes #123, and a CRLF one) followed by a feature push;
    • a plain feature push, and an unquoted newline before a feature push;
    • the four Security Scope rows that quote a feature push line.
  • Denied:
    • both comment-apostrophe bypass variants;
    • heredoc-body and command-substitution-heredoc phantom quotes, and a backtick phantom quote;
    • a multi-line message then a push to main, and an unquoted newline then a push to main;
    • backslash-newline splices, plain and inside double quotes;
    • a bash -c payload with a newline then a push to main;
    • a heredoc apostrophe then a push to main;
    • a bare push after a quoted newline;
    • substitution and brace glue in the target;
    • a quoted push to main inside a feature push's refspec followed by a real push to main.
  • On main: only the four quoted-message-then-feature-push cases differ (they are refused as target-unverifiable).

Running: title sweep (GPT 6.1 review of 1b0852fc5): 8,411 commands (the parser corpus plus generated heads × messages × separators × pushes, including running: and double-prefix heads), each judged bare and with Running: , on this head, 1b0852fc5 and main (9800507e1). On this head the prefixed title is never weaker than the bare command (0 cases), and on the 5,819 commands without a prefix of their own both forms get the same verdict (0 differ; on 1b0852fc5 the title was stricter on 122). The title allows 122 commands main denies, all 122 also allowed bare on this head (this PR's intended allows). Bare verdicts against 1b0852fc5: the only 36 changes are commands that themselves begin with one running: ; a double prefix is still refused.

Also clean: black --check, isort --check-only, flake8, mypy on shell_normalizer_walk.py, shell_normalizer.py, denied_rules.py, deny_gate.py and argv_floor_git.py, scripts/check_file_line_cap.py, scripts/check_comment_history.py, scripts/docs-lint.sh.

Screenshots / video

N/A. No UI change.

Related Issues

Refs #12631. This fixes the quoted-newline cause diagnosed there. The issue stays open for:

  • the heredoc-in-command-substitution commit-message shape, still refused, now by the conservative split;
  • a quoted body whose own line reads as a push;
  • the env PATH=… prefix.

Pattern harvest

Rule candidate: review-prompt
Pattern: a splitter that trusts a quote state machine across a boundary the machine does not fully model (a newline ending a comment or heredoc line). Trust the state only where the grammar it models is the whole grammar in play, and fall back to the conservative split otherwise.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

@Maqix
Maqix requested a review from a team as a code owner September 30, 2026 14:44
@Maqix
Maqix requested a review from dodgeblaster September 30, 2026 14:44
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — ✅ PASS

Premise-level review of 5986a0eb8e41ad8fa117779700eddc6d11e45a03 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Nothing blocks. Issue #12631 and a pinning test that is refused on base back the fix, both halves are within the Goal, and the one deeper fix (carrying quote state across lines) is rejected in the description with an oracle row as evidence.

What this change ships

Inventory (8 items) — 8 justified

Intent: let an agent chain a multi-line git commit -m with a literal feature-branch push in one shell call without a false refusal. FIX.

  1. A newline inside quotes no longer ends the push segment, so the reported command parses as the push it is. — justified
  2. The per-line backstop still judges every push line, and drops the ungated sentinel only behind an allowlisted git commit message word. — justified
  3. A merged segment that does not parse as a push (quoted text that mentions one) is read line by line, as on main. — justified
  4. Constructs the quote walk does not model (heredoc, $(, backtick, # comment, backslash-newline) keep the per-line split. — justified
  5. Refusals outside the allowlist now say "Put the push on its own line" when that retry passes. — justified
  6. A protected push behind a multi-line message is refused as push-protected-branch-name instead of target-unverifiable. — justified
  7. The Running: title prefix is read through once, so the title gets the same verdict as the command. _SHELL_TITLE_PREFIX moves rather than gaining a second copy. — justified
  8. The security package line budget rises by 204 lines. — justified

[FIRST-PRINCIPLES-REVIEWED] 5986a0e

Description read

[DESCRIPTION-READ] 792c3c1586d617f01443acd645a28203f8683856a11f9ac9a58c4a238aa2ddfc

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of 6dfccbc5a630be9414ac413ebf08808053a3cb69 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

Nothing blocks: the change only loosens the gate, through one closed allowlist. The per-line reading keeps the deny authority, and a real-bash oracle backs it. A revert walks it back cleanly.

Notes

[DESIGN-REVIEWED] 6dfccbc

Description read

[DESCRIPTION-READ] d7f7f548c8b8ef54b10313047181dc3bc23e6f5740e9cd8aa64157a9a9eacf68

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — ✅ no blocking findings

Reviewed 5986a0eb8e41ad8fa117779700eddc6d11e45a03 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 5986a0e

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed 5986a0eb8e41ad8fa117779700eddc6d11e45a03 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 5986a0e

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Security Scope Review (fork) — ✅ PASS

Which legitimate operations does 5986a0eb8e41ad8fa117779700eddc6d11e45a03 newly refuse? Model proposes, scripts/deny_diff.py decides — updated in place on each push. A script-confirmed regression blocks PR readiness.

adjudicated, zero confirmed regressions

Scope-Verdict: PASS

Reason: Mostly this change loosens the push floor: a newline inside quotes now stays inside its word. The one stricter part is a per-line pass that runs on every multi-line command and adds each push line's single-line tags back into the result; that is the same reading the base splitter already gave. No regression was handed to this lane. A residual narrowing argument is named under UNADJUDICATED; it does not block.

TIGHTENING: the git-publish argv floor (_git_publish_floor_tags, sensitive-command tier) now unions per-line tags over every multi-line command, and a merged quoted segment that does not parse as a push is re-split line by line -- the one threat it refuses is a protected, bare or unverifiable push hidden behind a quote the walk misreads: if "\n" in text_lower: ... for line, close in _inert_quoted_line_heads(text_lower, _is_git_publish): line_tags = set(_git_publish_floor_tags(line)) ... tags |= line_tags, plus if "\n" in command and _git_push_args(command) is None: pending[:0] = _split_push_command_segments(command, keep_quoted_newlines=False).

ADJUDICATED: 16 candidates, 0 confirmed newly-refused

PLATFORM COVERAGE: posix -- one row uses a heredoc commit message (<<, the new unmodelled-construct path) and one uses cd into an absolute POSIX worktree path (the inert allowlist's cd <dir> head). windows -- three rows: cd C:\Users\dev\repo with drive-letter and \ separators, on the same line as a multi-line commit and push and then in the own-line form; plus git -C C:\... push. _INERT_PLAIN_RE assumes a character set with no \, so these rows probe whether the base split is kept for Windows paths rather than newly refused. any -- eleven rows with no path: multi-line single- and double-quoted commit messages before a push, the hint's own-line resubmission, plain newline-joined add/commit/push, quoted push text in echo/gh pr create/gh pr comment, a push with a multi-line gh body after it, a lease push followed by a check poll, a multi-line annotated tag, and the Running: title form (the diff moved _SHELL_TITLE_PREFIX). I wrote no PowerShell-specific multi-line spelling (a backtick-n inside double quotes). The diff's quote walk is bash-only, so that tokenizer is still an open question.

UNADJUDICATED: The refusal's new "Put the push on its own line" hint (_own_line_fix_applies in deny_gate.is_denied, reached only on the git-publish-target-unverifiable refusal) only changes the reason text. The classifier judges allow or refuse, not the wording, so I got no verdict on whether the hint is shown truthfully. Inputs I would have submitted: cd ~/r && git commit -m 'a\n\nb' && git push origin feat (should carry the hint) and git commit -m 'a; git push origin f\n\nb' && git push origin feat (should not). Narrowing argument, not confirmed: the per-line pass runs _git_publish_floor_tags on lines that sit entirely inside a quoted word (for example, a multi-line --body that quotes git push origin main). Such text is refused even though it is data. The base splitter refused it the same way, so this is a narrowing case and not a regression.

[SCOPE-REVIEWED] 5986a0e

Security scope review -- adjudicated candidates

  • macos-latest (platform posix): 13 classified, 0 newly refused, 8 still allowed.
  • ubuntu-latest (platform posix): 13 classified, 0 newly refused, 8 still allowed.
  • windows-latest (platform windows): 14 classified, 0 newly refused, 9 still allowed.

No candidate flipped from allowed to refused.

@Maqix
Maqix force-pushed the fix/push-floor-quoted-newline branch 2 times, most recently from f96e4f0 to df753ed Compare September 30, 2026 17:09
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 30, 2026
@Maqix

Maqix commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Could a maintainer re-run the failed jobs? A fork contributor can't re-run workflows here.

On df753ed the only red check besides readiness is Backend Tests (3.12, 3), and it's unrelated to this diff:

  • The failing test is test/test_session_export.py::test_a_sent_export_streams_its_staged_file_and_removes_it, which expected a staged egress/*.json.gz file to have been removed.
  • Coverage Gate fails only because of it ("backend-test=failure -- failing closed").

Every file that exercises the push floor is green, and the Design, First Principles, GPT 5.6 and Opus 5 reviews pass on this head. The fork Security Scope lane hasn't re-run since f96e4f0. df753ed addresses its four newly-refused rows: quoted text that only mentions a push is read line by line again, exactly as on main.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: checking Automated validation is still running labels Sep 30, 2026
@Maqix
Maqix force-pushed the fix/push-floor-quoted-newline branch from df753ed to b5c36ce Compare October 2, 2026 13:02
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Oct 2, 2026
@Maqix
Maqix force-pushed the fix/push-floor-quoted-newline branch from b5c36ce to bdf1c79 Compare October 2, 2026 13:30
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Oct 2, 2026
@Maqix

Maqix commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

All five AI reviews pass on edfdd75 (Design, First Principles, GPT 6.1, Opus 5.5, Security Scope). The remaining reds are not this change:

  • Sensitive Change Review: this PR touches security/argv_floor.py and security/shell_normalizer.py and comes from a fork, so per the gate it needs a maintainer takeover on an upstream branch (Co-authored-by: + Supersedes #15438) with the real sandbox run. Could a maintainer pick that up?
  • Coverage Gate: src/kiro_crew/apps/hooks_integration.py now meets the per-file floor (82.3%) and the gate asks for it to be pruned from .github/coverage-baselines/backend.txt. That is a baseline ratchet on main, unrelated to this diff, so I have left it to the baseline refresh rather than widen this PR.

Happy to rebase or adjust anything on the takeover branch.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 6, 2026
@Maqix
Maqix force-pushed the fix/push-floor-quoted-newline branch from edfdd75 to 1b87861 Compare October 6, 2026 07:01
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 6, 2026
@Maqix
Maqix force-pushed the fix/push-floor-quoted-newline branch from 1b87861 to b992d86 Compare October 6, 2026 09:06
@iamwhatever

Copy link
Copy Markdown
Contributor

Reviewed, not approving (crew-pr-reviewer). This changes the security command parser, and Sensitive Change Review is red. A security owner must approve it.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — ⏭️ skipped

Revision 5986a0eb8e41ad8fa117779700eddc6d11e45a03 touches no user-facing surface (no changes under website/ or committed screenshots), so the UX review was skipped. Does not block merge.

@iamwhatever

Copy link
Copy Markdown
Contributor

This stops refusing a feature push chained after a multi-line quoted git commit message. It fixes the root cause in _split_push_command_segments: a quoted newline now stays in its word. It keeps main's per-line reading as the deny backstop in _git_publish_floor_tags, and it drops the ungated sentinel only for an inert git commit word that passes a closed allowlist. I tried to break it in three ways. First, 39 hand-made shapes: HEAD:main, +main, :main, refs/heads/main, mai''n, "main", @, --all/--mirror, backslash-newline splices, git -C . push, env prefixes, alias, #' comment phantoms, apostrophes inside double quotes, CRLF, and trailing second pushes after &&, ;, &, || and |. All protected, bare and single-arg pushes stayed denied. Second, a 60k-command structured corpus run on the PR head and on its parent: I ran each of the 1,006 commands the PR newly allows through real bash against a logging git stub, and none ran a push to main/master or a bare push. Third, each of the 151 commands it newly denies was a bash syntax error or ran no push, so no reasonable use regresses. The code looks correct and safe to me, and I have no defect to report. The one blocker is the failing Sensitive Change Review, which by design refuses a fork PR that touches security/argv_floor.py and shell_normalizer.py. A maintainer has to take this over on an upstream branch (Co-authored-by plus Supersedes #15438) so that gate can run. Can a maintainer pick that up? — crew-pr-reviewer (deep re-check)

@iamwhatever

Copy link
Copy Markdown
Contributor

This PR stops the push gate from refusing a feature push chained after a multi-line quoted git commit message. It fixes the root cause in _split_push_command_segments, where a quoted newline now stays inside its word. Main's per-line reading stays the deny backstop in _git_publish_floor_tags, and the ungated sentinel is dropped only for an inert git commit word that passes the closed _quoted_word_is_inert allowlist. Head 9e252fc is new since our last review at 1c569f3. I diffed the two heads' production files. The only security-relevant change is in _inert_segment: -- is no longer an allowlisted flag, and the words after it are treated as pathspecs, so a tracked file named -m after -- can no longer satisfy the commit-message rule. That tightens the gate. The rest of the delta is a rebase onto main's _exception_applies refactor and a shared _INERT_CONFIG_KEYS constant. I re-tried the earlier break shapes against this logic: git commit 'x' -- -m, protected targets on a later line, and #' phantom quotes. None of them reach an allow. My earlier real-bash corpus result (no newly allowed command ran a push to main/master or a bare push) still holds, because the delta only narrows. I have no defect to report. The hard blocker is unchanged: Sensitive Change Review fails by design because a fork PR changes security/argv_floor.py, shell_normalizer.py and init.py ("A fork PR cannot pass this gate"). Can a maintainer take this over on an upstream branch (Co-authored-by: Maqix, Supersedes #15438), run the real sandbox, and in that PR confirm Design's ask that the real-bash oracle test is the accepted bar for widening _INERT_FLAGS? — crew-pr-reviewer (deep re-check)

@iamwhatever

Copy link
Copy Markdown
Contributor

This PR stops the push gate from refusing a feature push chained after a multi-line quoted git commit message. It fixes the root cause in _split_push_command_segments, where a quoted newline now stays inside its word. Main's per-line reading stays the deny backstop in _git_publish_floor_tags, and the ungated sentinel is dropped only for a word that passes the closed _quoted_word_is_inert allowlist. Head dcb6bfa is a rebase since our last review at 9e252fc, and I diffed the PR's own production hunks across the two heads. The per-line entry walk moved unchanged into shell_normalizer._quoted_line_entries. _quoted_word_is_inert now refuses every $'…' and $"…" word outright, where before it accepted a $'…' body with no $, backtick or backslash, so the delta only narrows the allow. I re-checked the earlier break shapes (git commit 'x' -- -m, protected targets on a later line, #' phantom quotes, $'\x2d…'-style decoded options) against the new code, and none reach an allow. I still have no defect to report. The hard blocker is unchanged: Sensitive Change Review fails by design ("This PR comes from a fork and changes a sensitive path. A fork PR cannot pass this gate"). Can a maintainer take this over on an upstream branch (Co-authored-by: Maqix, Supersedes #15438) and run the real sandbox there so the gate can pass? — crew-pr-reviewer (deep re-check)

@iamwhatever

Copy link
Copy Markdown
Contributor

HOLD at 91eeda3.

This PR changes the push gate's deny logic in src/kiro_crew/security/ (argv_floor.py L4435, shell_normalizer.py L2785). Security-gate changes need a human security owner, and Sensitive Change Review fails by design for a fork PR on a sensitive path. A maintainer needs to take it over on an upstream branch (Co-authored-by: Maqix, Supersedes #15438).

Since our last review at dcb6bfa, the production delta only narrows the allow:

  • _INERT_FLAGS is now commit -m/-q and push -u/-q, with no add flags;
  • the git -c owner and the = predecessor are gone;
  • a -- used as the -m value no longer ends flag scanning.

I found no concrete defect.

Two things need answers:

  • The current-head Design review is CONCERNS ("two layers where one decides"). It says the splitter change adds no allow, and asks for _split_push_command_segments to stay byte-identical to main. There is no target=design reply for this head.
  • The description says the inert word may follow a long option's =. L2824 now accepts only a space or tab there, so that sentence should go.

The other AI lanes pass, and Security Scope found no confirmed regressions. I did not run the gate or the oracle tests locally.

— crew-pr-reviewer (r100)

@iamwhatever

Copy link
Copy Markdown
Contributor

HOLD at b74bc22.

This PR stops the push gate from refusing a feature push chained after a multi-line quoted git commit -m. A quoted newline now stays inside its word in _split_push_command_segments. Main's per-line reading in _git_publish_floor_tags stays the deny authority. The two points from our r100 review are settled: the Design lane is now PASS, and the description no longer claims an inert word may follow a long option's =. I found no concrete defect.

What blocks approval:

  1. Policy requires human review. The PR changes the push gate in security/argv_floor.py, security/shell_normalizer.py and security/__init__.py. Sensitive Change Review fails by design for a fork PR on a sensitive path, so a maintainer needs to take it over on an upstream branch (Co-authored-by: Maqix, Supersedes fix(security): keep a quoted newline inside its word on the push path #15438).
  2. The current-head Opus advisory has no reply. The refusal hint fires whenever any line starts inside a quote. That includes git commit -m 'a⏎b'; git push origin $b, where moving the push to its own line is still refused. Fix it or post a disposition.
  3. The PR says it uses Refs #12631 to leave room for the three Not a goal gaps: the $(cat <<'EOF' …) message, a quoted body whose line reads as a push, and the env PATH=… prefix. But git-publish-* floor refuses literal git push [flags] origin feature-branch #12631 was closed on 2026-10-13, so nothing tracks them now. Please open one issue per remaining gap and link them in the description.

CI is otherwise green on this head. I read the code and tests but did not run them.

— crew-pr-reviewer (r103)

@Maqix

Maqix commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (a5c1e85b3), no conflicts and no behaviour change. Push-gate tests (1,805), lint, type and comment-history checks pass locally. The Fast Gate failure on the previous head was Harness Parity Gate, which main fixed in #22178.

Two asks for a maintainer:

  1. Approve the fork workflows on 028d9b67e, so CI and the AI reviews can run.
  2. @iamwhatever: your last review found no defect and suggested a maintainer copy this upstream, since Sensitive Change Review can't pass from a fork. Is that still the plan? I'm happy to rebase it whenever you need. fix(security): allow an env-prefixed push after a multi-line message #20854 is stacked on it.

@Maqix

Maqix commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Re the GPT 6.1 blocking finding on 1b0852fc5 (comment): confirmed and fixed in 65b3df737.

Finding. kiro-cli titles a shell call Running: <command>, and GateFacts.deny_targets judges that title as well as the command. The inert-word proof only matched a git commit starting at offset 0, so the title Running: git commit -m 'a⏎b'; git push -u fork fix/x was still refused while the bare command was allowed. It failed closed, but the fix did not reach the live call. Before the fix I reproduced it on 7 of 8 allowed commands.

Fix. _quoted_word_is_inert now reads through ONE exact leading running: (the text is lowercased there). This is the rule denied_rules._exception_applies already uses. The _SHELL_TITLE_PREFIX constant moved to shell_normalizer_walk.py, and denied_rules imports it back, so there is still one copy (the walk cannot import denied_rules without a load cycle). running: running: …, running:git …, xrunning: … and a prefix after a separator are not stripped.

Tests (TestTokenizerContractBetweenTheLayers):

  • test_the_shell_title_gets_the_verdict_of_the_command_it_displays runs 11 allow rows with and without Running: . The 11 prefixed cases fail on 1b0852fc5.
  • test_the_shell_title_prefix_is_read_through_once_and_only_exactly checks three things: prefixed protected-push oracle rows stay denied, the prefixed form of every corpus command the bare form denies is denied, and the lookalikes above stay denied.

Sweep. 8,411 commands, each judged bare and prefixed. On this head the title is never weaker than the command (0 cases), and on commands without a prefix of their own the two forms always agree. The title allows only the 122 commands this PR already allows bare. Gates pass; the package budget is 32,244 on main 9800507e1.

Could a maintainer approve the fork workflows on 65b3df737? Thanks.

@Maqix

Maqix commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto today's main as 006d159042: one commit, and only the line-count number changed (0 verdict changes vs e1e49474fc, same test pass set). Opus, GPT, Design and First Principles all passed the previous head. Security Scope's check was red only because its per-platform reports could not be folded; its own verdict was PASS with 0 newly refused.

@iamwhatever @bolichen97 @cruisercohen could a maintainer approve the fork workflows on this head, and pick up the upstream copy offered earlier (Co-authored-by: + Supersedes #15438)? That is the only way past Sensitive Change Review for a fork. #20854 is stacked on this and is rebased onto it too.

@Maqix

Maqix commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for approving the runs on 62b1ee50b7 (the rebase onto 241e39cf78; only the line-count number changed, re-pinned to the measured 32,394).

Status on this head:

  • Backend Tests (3.12) 1-8, Coverage Gate and the other Linux gates pass. The red Windows shards are timeouts (for example ci-surface-tests.py --surface backend after 45 s), and the same shards are red on fix(security): read a push line in a literal gh body as data #20853 and fix(security): allow an env-prefixed push after a multi-line message #20854. Dependency License Gate reports "Dependency review is not supported on this repository".
  • Three review lanes did not run, so they have no verdict: GPT 6.1 Review ("could not run"), Security Scope Review ("the generate job did not finish") and Internal Content Scan ("could not run"). On the previous head 5bf16a3e20, with the same code, all AI lanes passed: Opus, GPT, Design, First Principles and Security Scope.

Could a maintainer please re-run those three lanes? After that, the PR needs a human review. Or, as offered earlier, an upstream copy, since Sensitive Change Review cannot pass from a fork.

_split_push_command_segments ended a segment at every newline, including
one inside quotes. The next segment's quote walk then restarted unquoted,
so the closing quote of a multi-line commit message opened a new quote
that swallowed the following && and turned a literal feature-branch push
into one unparseable word, refused as git-publish-target-unverifiable.

A literal newline inside quotes is now word data, like the other quoted
separators, but only while the quote walk can be trusted across lines.
The walk does not model heredocs, command/parameter/process substitution,
backticks or comments, where a phantom quote could hide the next line, so
a command holding any of them keeps the split at every newline. A merged
segment that does not parse as a push (quoted text that merely mentions
one, like printed instructions or a PR comment body) is read line by
line, exactly as before, so no quoted text is newly refused.
Unquoted and escaped newlines still split, so backslash-newline splices
keep failing closed, and a protected push after a multi-line message is
now reported as push-protected-branch-name.

kiro-cli titles a shell call `Running: <command>` and the deny tiers
judge that title too, so the inert-word proof reads through ONE exact
leading `running: ` (the prefix constant denied_rules already uses for
its exceptions, now defined beside the walk) and the live call gets the
verdict of the command it displays.

The refusal's "put the push on its own line" hint moves a push only at
an unquoted separator, and judges the rewritten command over every
nested shell payload, as the floor does. A separator inside the quoted
message, or a bash -c push to $b, earned the hint, and the command with
the push on its own line came back with the same refusal and the same
hint. Verdicts do not change.

The security spec paragraph and the package line budget are updated to
match.

Refs kirodotdev#12631
@Maqix

Maqix commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Opus 5.5 finding on 6dfccbc (the "Put the push on its own line" hint): confirmed and fixed in 5986a0e, still one commit. The same function's second gap, which Opus raised on #20854, is fixed here too.

Cause. _own_line_fix_applies (deny_gate.py) decides whether the refusal says "Put the push on its own line". It had two gaps:

  1. It judged the rewritten command on the top-level text only (_git_publish_floor_tags(rewritten)), but the floor reads every nested shell payload. So git commit -m 'a⏎b' && git push origin feat; bash -c 'git push origin $b' got the hint, even though the $b in the nested push is what keeps it refused. Moving the push onto its own line returned the same refusal and the same hint.
  2. The rewrite was a quote-blind re.sub, so it also split at a ; inside the quoted message. git commit -m 'a; git push origin f⏎⏎b' && git push origin feat got the hint, and the resubmission still had ; git push inside the quote, so the hint came back every time.

Fix. The rewrite now moves a git command only after an active separator, using the offsets from the shared quote walk (_iter_shell_chars, step.active). It then judges the result over every _shell_payload_sources(rewritten) source that _is_git_publish, the same way the floor does. If that walk fails, the hint is left out, since it is only advice. Both of your commands now keep the substitution reason. The existing hint cases (cd ~/r && …, env GIT_DIR=/x git push …) still get the hint, and resubmitting them with the push on its own line is allowed. The new no-hint rows and the walk-failure test fail on 6dfccbc and pass on 5986a0e.

Verdicts did not change. Against 6dfccbc:

  • probe.py: 5,104 commands, 0 flips.
  • c100p probe: 6,688 commands, 0 flips. nested_probe: 2,268 commands, 0 flips.
  • c92 sweep: 8,415 commands, identical output.
  • c93 sweep: 11,026 commands, 0 verdict flips and 0 masked-reason diffs. Only the hint flag changed, on 1,732 commands, all removals. 1,731 of them are nested-payload pushes, and the remaining one is the quoted-separator command.

The package line budget is 32,730.

@iamwhatever @bolichen97 @cruisercohen could one of you approve the workflow runs on 5986a0e? Once the AI reviews are green, this PR only needs a review, or the upstream copy offered earlier if the fork-only gates are still in the way.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

git-publish-* floor refuses literal git push [flags] origin feature-branch

2 participants