From 4d72461ce43a4488b64aba90ffe488a945a6f8d3 Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:12:21 +0000 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20implement=20issue=20#2129=20?= =?UTF-8?q?=E2=80=94=20relay=20suppressed-span=20breadcrumb=20from=20the?= =?UTF-8?q?=20docgate=20read=20helper=20(checkpoint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issue-2129-docgate-suppressed-span.md | 7 +++ docs/internal/implement-skill.md | 9 ++-- lib/test/modules/coverage-map.json | 4 ++ lib/test/run.sh | 23 +++++--- scripts/read-doc-needed-deliverables.sh | 52 +++++++++++++++++-- 5 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 .changeset/issue-2129-docgate-suppressed-span.md diff --git a/.changeset/issue-2129-docgate-suppressed-span.md b/.changeset/issue-2129-docgate-suppressed-span.md new file mode 100644 index 0000000000..68b0b046e2 --- /dev/null +++ b/.changeset/issue-2129-docgate-suppressed-span.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +Phase 4.1 documentation gate records only run-specific workpad facts (issue #2129). + +`scripts/read-doc-needed-deliverables.sh` now captures the extractor's stderr, forwards it unchanged to its own stderr, and relays the first suppressed Documentation Needed span onto stdout as a self-identifying `docgate-suppressed: ` line (the span's text with the breadcrumb's surrounding backticks removed). Phase 4.1 Stage 1 records a workpad note naming that span only when such a line is present — delivered through `--note-file`, never a double-quoted shell argument — replacing the fixed, always-false once-per-run disclosure sentence. The deferred documentation-AC discharge now ticks a criterion that names a check command only after the orchestrator has itself run that command (or the covering run the coverage map names) over the landed docs and quoted the result line, never on a subagent's report or a gate that has not yet run. diff --git a/docs/internal/implement-skill.md b/docs/internal/implement-skill.md index cad43948d4..d81d6f80f7 100644 --- a/docs/internal/implement-skill.md +++ b/docs/internal/implement-skill.md @@ -1773,8 +1773,9 @@ tracked file (`` `docs/a.md docs/b.md` ``, `` `docs/a.md LICENSE` ``). Any other `(`, `:`, `*`, or any non-path character (a grant `` `Bash(x.sh:*)` ``), or a bare command word like `` `bash lib/test/run.sh` `` (`bash` is extensionless and not an in-tree file) — is a command/grant literal: it contributes no tokens, and a **one-time stderr breadcrumb** names the first suppressed span -(disclosed by Phase 4.1 as ephemeral on the cloud tier — the gate does not capture that stderr, so a -suppressed span leaves no run-record trace there; see the phase file's cloud-tier residual note). (2) Outside +(the read boundary `read-doc-needed-deliverables.sh` captures that stderr, forwards it unchanged, and +relays the span onto stdout as a `docgate-suppressed: ` line that Phase 4.1 Stage 1 records on the +workpad — see that helper's contract below). (2) Outside spans, a `Word(...)` **call group** (a word immediately followed by a parenthesized group, e.g. an un-backticked `Bash(lib/test/run.sh:*)`) contributes no tokens. (3) A **fenced code block** — opened and closed by a line whose first non-whitespace characters are three-plus backticks or three-plus tildes (the @@ -1800,7 +1801,9 @@ Stage B emits. `scripts/read-doc-needed-deliverables.sh ` owns the read both stages perform — the `gh issue view` fetch, its scratch file, the invocation of `extract-doc-needed-paths.sh` over it, and -a retry on each. It prints an **outcome token** on a `docgate-outcome: ` line and, on success with +a retry on each. It prints an **outcome token** on a `docgate-outcome: ` line; on a success token, one +`docgate-suppressed: ` line when the extractor suppressed a span (the first suppressed span, its +surrounding backticks removed, which Phase 4.1 Stage 1 records on the workpad); and, on success with paths, one `docgate-path: ` line per deliverable. **That helper's own header is the canonical statement of its token vocabulary and the exit status paired with each; read it there rather than from a copy.** Each token has its own status, and the success statuses are disjoint from the failure diff --git a/lib/test/modules/coverage-map.json b/lib/test/modules/coverage-map.json index cb53e10f04..74758019e8 100644 --- a/lib/test/modules/coverage-map.json +++ b/lib/test/modules/coverage-map.json @@ -1816,6 +1816,10 @@ "note": "the internal-identifier denylist class in lint-shipped-pruned-path.py: the module-constant comparand-shape pin (non-empty, covers the required floor), all three members fire, HTML/empty-reason marker discrimination, filename-boundary substring safety, and the fence-conditional marker pair", "owner": "unmodularized" }, + "2129": { + "note": "", + "owner": "unmodularized" + }, "216": { "note": "", "owner": "review-dirty-tree" diff --git a/lib/test/run.sh b/lib/test/run.sh index 3ae819839c..e41c885f82 100755 --- a/lib/test/run.sh +++ b/lib/test/run.sh @@ -11363,14 +11363,25 @@ assert_eq "#1554 arm order: an extractor succeeding on its SECOND attempt yields "$(printf 'docgate-outcome: deliverables\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ "$(rdnd_lines "$rdnd_dir/body-paths.md" 0 "$rdnd_dir/flaky-extractor")" # Adversarial input: the block carries a command span and a grant literal, which -# the extractor suppresses. Two things are asserted at once, because rdnd_run -# merges stderr: the literals are not phantom deliverables, AND the extractor's +# the extractor suppresses. Three things are asserted here, because rdnd_run +# merges stderr: the literals are not phantom deliverables, the extractor's # `suppressed a span` stderr breadcrumb — emitted on exactly this body — does not -# displace the outcome line or masquerade as a deliverable path. That is the whole -# reason the stdout shape is prefixed rather than positional. -assert_eq "#1554 adversarial input: a command span and a grant literal in the block are not deliverables, and the extractor's stderr breadcrumb does not corrupt the outcome" \ - "$(printf 'docgate-outcome: deliverables\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ +# displace the outcome line or masquerade as a deliverable path, AND (issue #2129) +# the helper now relays the first suppressed span onto stdout as a self-identifying +# `docgate-suppressed: ` line after the outcome line, so Phase 4.1 can record the +# real span rather than a scripted once-per-run boilerplate note. The suppressed +# value is the FIRST suppressed span (`bash lib/test/run.sh`) with the breadcrumb's +# surrounding backticks removed. +assert_eq "#1554/#2129 adversarial input: literals are not deliverables and the first suppressed span is relayed as a docgate-suppressed line" \ + "$(printf 'docgate-outcome: deliverables\ndocgate-suppressed: bash lib/test/run.sh\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ "$(rdnd_lines "$rdnd_dir/body-adversarial.md")" +# #2129: the helper captures the extractor's stderr to a scratch file but FORWARDS +# it unchanged to its own stderr, so the merged tool result the caller reads still +# carries the `suppressed a span` breadcrumb. RED against a helper that captures the +# extractor's stderr without forwarding it (the span would then reach stdout but the +# breadcrumb would vanish from the stream). +assert_eq "#2129 adversarial input: the helper relays the extractor's 'suppressed a span' breadcrumb through the merged stream" \ + "1" "$(rdnd_run "$rdnd_dir/body-adversarial.md" | grep -c 'extract-doc-needed-paths.sh: suppressed a span')" # Stale-capture isolation (what "idempotent" has to mean here to be worth testing): # seed the scratch body file with a DIFFERENT body, then fail both read attempts. # A helper that extracted from whatever was already on disk would report that stale diff --git a/scripts/read-doc-needed-deliverables.sh b/scripts/read-doc-needed-deliverables.sh index 99d885a17c..912ba30d21 100755 --- a/scripts/read-doc-needed-deliverables.sh +++ b/scripts/read-doc-needed-deliverables.sh @@ -37,6 +37,10 @@ # STDOUT SHAPE — each line is SELF-IDENTIFYING BY PREFIX, never by position: # # docgate-outcome: exactly one, on every non-usage exit +# docgate-suppressed: at most one, after the outcome line and only on a +# success token — the FIRST span the extractor +# suppressed, with the breadcrumb's surrounding +# backticks removed (issue #2129) # docgate-path: zero or more, one per deliverable, after the # outcome line and only on `deliverables` # @@ -45,7 +49,12 @@ # extractor — and the extractor emits a `suppressed a span` breadcrumb on stderr for # exactly the adversarial bodies this gate exists to handle. A positional "line 1 is # the token" contract would read that breadcrumb as the token on a read that -# succeeded, and would read an interleaved stderr line as a deliverable path. +# succeeded, and would read an interleaved stderr line as a deliverable path. This +# helper captures the extractor's stderr to a scratch file, forwards it UNCHANGED to +# its own stderr (so the merged stream still carries every breadcrumb), and relays +# the first `suppressed a span` breadcrumb's span onto stdout as the self-identifying +# `docgate-suppressed: ` line above, so Phase 4.1 records a real span rather than a +# scripted once-per-run note (issue #2129). # # Failing the read means the deliverable list is UNKNOWN, never empty: a caller # that treats a failure token as `no-deliverables` waves the gate through exactly @@ -117,18 +126,55 @@ if ! "$DEVFLOW_GH" issue view "$ISSUE" --json body --jq '.body' > "$BODY_FILE" \ exit 11 fi -if ! DOC_NEEDED_PATHS="$("$EXTRACTOR" < "$BODY_FILE")" \ - && ! DOC_NEEDED_PATHS="$("$EXTRACTOR" < "$BODY_FILE")"; then +# Capture the extractor's stderr to a scratch file so its `suppressed a span` +# breadcrumb can be parsed (issue #2129) — the breadcrumb is the only channel that +# names the span. The file is TRUNCATED (not appended) on each attempt, so a retry +# overwrites the prior attempt's stderr rather than accumulating it. +EXTRACTOR_ERR="$SCRATCH/devflow-docgate-extractor-err-$ISSUE.txt" +rm -f "$EXTRACTOR_ERR" + +# _rdnd_relay_extractor_stderr — forward the captured extractor stderr UNCHANGED to +# this helper's own stderr (so the merged tool result still carries every +# breadcrumb the caller relied on), and set SUPPRESSED_SPAN to the FIRST +# `suppressed a span` breadcrumb's span text with the breadcrumb's surrounding +# backticks removed. Bash builtins only (`case`, `while IFS= read -r`, `${var#…}`/ +# `${var%…}`): the value decides an emitted stdout line, so it must not depend on a +# tool lib/preflight.sh does not guarantee. +SUPPRESSED_SPAN="" +_rdnd_relay_extractor_stderr() { + [ -f "$EXTRACTOR_ERR" ] || return 0 + local _line _span + while IFS= read -r _line; do + printf '%s\n' "$_line" >&2 + case "$_line" in + *"suppressed a span"*) + if [ -z "$SUPPRESSED_SPAN" ]; then + _span="${_line#*\`}" # drop up to and including the opening backtick + _span="${_span%\`}" # drop the trailing backtick + SUPPRESSED_SPAN="$_span" + fi + ;; + esac + done < "$EXTRACTOR_ERR" +} + +if ! DOC_NEEDED_PATHS="$("$EXTRACTOR" < "$BODY_FILE" 2>"$EXTRACTOR_ERR")" \ + && ! DOC_NEEDED_PATHS="$("$EXTRACTOR" < "$BODY_FILE" 2>"$EXTRACTOR_ERR")"; then + _rdnd_relay_extractor_stderr printf 'docgate-outcome: %s\n' extract-failed exit 12 fi +_rdnd_relay_extractor_stderr + if [ -z "$DOC_NEEDED_PATHS" ]; then printf 'docgate-outcome: %s\n' no-deliverables + [ -n "$SUPPRESSED_SPAN" ] && printf 'docgate-suppressed: %s\n' "$SUPPRESSED_SPAN" exit 10 fi printf 'docgate-outcome: %s\n' deliverables +[ -n "$SUPPRESSED_SPAN" ] && printf 'docgate-suppressed: %s\n' "$SUPPRESSED_SPAN" # Read line-wise rather than word-splitting, so a path carrying whitespace stays # one deliverable instead of becoming several. printf '%s\n' "$DOC_NEEDED_PATHS" | while IFS= read -r _rdnd_path; do From ac6b6d1e9342f7169e31294cbfc64cbc687c7065 Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:15:00 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20implement=20issue=20#2129=20?= =?UTF-8?q?=E2=80=94=20record=20run-specific=20docgate-suppressed=20span?= =?UTF-8?q?=20note=20and=20check-command=20doc-AC=20discharge=20in=20Phase?= =?UTF-8?q?=204.1=20(checkpoint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- skills/implement/phases/phase-4-documentation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/implement/phases/phase-4-documentation.md b/skills/implement/phases/phase-4-documentation.md index 757e9786c7..53224d3cf9 100644 --- a/skills/implement/phases/phase-4-documentation.md +++ b/skills/implement/phases/phase-4-documentation.md @@ -86,7 +86,7 @@ Shared read contract (both stages). The helper owns the scratch file and fails c "${CLAUDE_SKILL_DIR:-}"/../../scripts/read-doc-needed-deliverables.sh $ISSUE_NUMBER ``` -Span-suppression breadcrumb disclosure (once per run). The extractor emits a one-time `suppressed a span` breadcrumb on stderr this gate does not capture. Record the residual once by Stage 1: `workpad.py update $ISSUE_NUMBER --note "Phase 4.1: extractor span-suppression breadcrumbs are not durably observable on the cloud tier (stderr not captured); a suppressed command/grant literal in the Documentation Needed block leaves no run-record trace"`. +Span-suppression note (only when the extractor suppressed a span). When Stage 1's tool result holds a `docgate-suppressed: ` line — the read boundary's relay that the extractor suppressed a span — take its value as the span text, write `Phase 4.1: extractor suppressed a Documentation Needed span: ` (that value substituted, cut to fit the 2048-byte note budget) with the Write tool to `.prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt`, then record it with `workpad.py update $ISSUE_NUMBER --note-file .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt` — never a double-quoted `--note "…"` argument, since the span is third-party text. No such line, no note; Stage 2 records nothing. If the helper reports `no-deliverables` but the issue body still contains a Documentation Needed section in either accepted form — the bold-bullet `**Documentation Needed**` form or a `### Documentation Needed` heading (`gh issue view $ISSUE_NUMBER --json body --jq '.body' | grep -qE '\*\*Documentation Needed\*\*|^###[[:space:]]+\*{0,2}Documentation Needed'`) — record a workpad note (`workpad.py update $ISSUE_NUMBER --note "Phase 4.1: Documentation Needed section present but the extractor found no file paths; the deliverable cross-check is skipped this run"`). @@ -174,7 +174,7 @@ Otherwise apply the configured docs labels with one call — the helper resolves Then tick the Documentation phase in the workpad: `workpad.py update $ISSUE_NUMBER --tick-progress "Documentation"`. -Discharge every 3.4-deferred documentation AC (mandatory, before §4.3). Phase 3.4's *Documentation-AC deferral* rule leaves any acceptance criterion whose satisfaction is a Phase-4.1-owned `docs/…` edit unticked at the gate, recording it in a workpad note of the form `3.4: doc-AC deferred to Phase 4.1: {AC text}`. For each such deferred doc-AC confirm the docs the criterion required actually landed in this run's diff, then tick it by its 1-based position, citing the deferral note — `workpad.py update $ISSUE_NUMBER --tick-ac-n {N} --note "Phase 4.1 discharged 3.4-deferred doc-AC: {AC text} — docs authored by the prflow:docs pass"` (consume the tick call's outcome line per the failure-isolation contract; a `remedy=retick-named-rows` or `remedy=retick-and-reset-status` means the index did not resolve — re-resolve and re-tick). This tick must happen before §4.3's terminal `--status Complete` write, which `_terminal_complete_gate` hard-fails while any non-post-merge Acceptance Criteria row is still `- [ ]`. If a deferred doc-AC genuinely cannot be discharged (the docs pass could not author it and the content cannot be derived), do not tick it and do not finalize Complete: take the existing Blocked path (`workpad.py update $ISSUE_NUMBER --status Blocked --reflection-kind blocked --reflection "Phase 4.1: 3.4-deferred doc-AC could not be discharged: {AC text}"`), emit the 👎 outcome reaction, and stop. +Discharge every 3.4-deferred documentation AC (mandatory, before §4.3). Phase 3.4's *Documentation-AC deferral* rule leaves any acceptance criterion whose satisfaction is a Phase-4.1-owned `docs/…` edit unticked at the gate, recording it in a workpad note of the form `3.4: doc-AC deferred to Phase 4.1: {AC text}`. For each such deferred doc-AC confirm the docs the criterion required actually landed in this run's diff, then tick it by its 1-based position, citing the deferral note — `workpad.py update $ISSUE_NUMBER --tick-ac-n {N} --note "Phase 4.1 discharged 3.4-deferred doc-AC: {AC text} — docs authored by the prflow:docs pass"` (consume the tick call's outcome line per the failure-isolation contract; a `remedy=retick-named-rows` or `remedy=retick-and-reset-status` means the index did not resolve — re-resolve and re-tick). When a deferred criterion instead names a check command, discharge it only after the docs commit by running that command yourself, in your own tool call over the landed docs — when the tier does not grant it, the covering run `lib/test/modules/coverage-map.json` names for that unit — and quote the result line of the command you actually ran in the tick note; a subagent's report that it ran the command does not discharge the criterion, and the tick note names no result of a gate that has not run yet. When the tier refuses both the named command and its covering run, leave the criterion unticked and take this paragraph's existing Blocked arm below, its reflection naming `prflow_implement.allowed_tools` as the remedy. This tick must happen before §4.3's terminal `--status Complete` write, which `_terminal_complete_gate` hard-fails while any non-post-merge Acceptance Criteria row is still `- [ ]`. If a deferred doc-AC genuinely cannot be discharged (the docs pass could not author it and the content cannot be derived), do not tick it and do not finalize Complete: take the existing Blocked path (`workpad.py update $ISSUE_NUMBER --status Blocked --reflection-kind blocked --reflection "Phase 4.1: 3.4-deferred doc-AC could not be discharged: {AC text}"`), emit the 👎 outcome reaction, and stop. Resume directly at §4.2 (after the Phase 4.1 documentation subagent returns and its docs are committed). The docs subagent is an Agent-tool dispatch whose return enters this context as a report only, so proceed to §4.2 directly — do not re-dispatch the §4.1 docs subagent, and do not re-read this phase file. The prompt-extension re-load still fires at this boundary (SKILL.md's re-load trigger). From 9f3180b04eeab47caf21a63ca2643f3a93c2cbea Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:26:06 +0000 Subject: [PATCH 3/7] fix: add pruned-path-ok marker for the coverage-map reference in phase-4-documentation.md (issue #2129) --- skills/implement/phases/phase-4-documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/implement/phases/phase-4-documentation.md b/skills/implement/phases/phase-4-documentation.md index 53224d3cf9..95232cc86e 100644 --- a/skills/implement/phases/phase-4-documentation.md +++ b/skills/implement/phases/phase-4-documentation.md @@ -174,7 +174,7 @@ Otherwise apply the configured docs labels with one call — the helper resolves Then tick the Documentation phase in the workpad: `workpad.py update $ISSUE_NUMBER --tick-progress "Documentation"`. -Discharge every 3.4-deferred documentation AC (mandatory, before §4.3). Phase 3.4's *Documentation-AC deferral* rule leaves any acceptance criterion whose satisfaction is a Phase-4.1-owned `docs/…` edit unticked at the gate, recording it in a workpad note of the form `3.4: doc-AC deferred to Phase 4.1: {AC text}`. For each such deferred doc-AC confirm the docs the criterion required actually landed in this run's diff, then tick it by its 1-based position, citing the deferral note — `workpad.py update $ISSUE_NUMBER --tick-ac-n {N} --note "Phase 4.1 discharged 3.4-deferred doc-AC: {AC text} — docs authored by the prflow:docs pass"` (consume the tick call's outcome line per the failure-isolation contract; a `remedy=retick-named-rows` or `remedy=retick-and-reset-status` means the index did not resolve — re-resolve and re-tick). When a deferred criterion instead names a check command, discharge it only after the docs commit by running that command yourself, in your own tool call over the landed docs — when the tier does not grant it, the covering run `lib/test/modules/coverage-map.json` names for that unit — and quote the result line of the command you actually ran in the tick note; a subagent's report that it ran the command does not discharge the criterion, and the tick note names no result of a gate that has not run yet. When the tier refuses both the named command and its covering run, leave the criterion unticked and take this paragraph's existing Blocked arm below, its reflection naming `prflow_implement.allowed_tools` as the remedy. This tick must happen before §4.3's terminal `--status Complete` write, which `_terminal_complete_gate` hard-fails while any non-post-merge Acceptance Criteria row is still `- [ ]`. If a deferred doc-AC genuinely cannot be discharged (the docs pass could not author it and the content cannot be derived), do not tick it and do not finalize Complete: take the existing Blocked path (`workpad.py update $ISSUE_NUMBER --status Blocked --reflection-kind blocked --reflection "Phase 4.1: 3.4-deferred doc-AC could not be discharged: {AC text}"`), emit the 👎 outcome reaction, and stop. +Discharge every 3.4-deferred documentation AC (mandatory, before §4.3). Phase 3.4's *Documentation-AC deferral* rule leaves any acceptance criterion whose satisfaction is a Phase-4.1-owned `docs/…` edit unticked at the gate, recording it in a workpad note of the form `3.4: doc-AC deferred to Phase 4.1: {AC text}`. For each such deferred doc-AC confirm the docs the criterion required actually landed in this run's diff, then tick it by its 1-based position, citing the deferral note — `workpad.py update $ISSUE_NUMBER --tick-ac-n {N} --note "Phase 4.1 discharged 3.4-deferred doc-AC: {AC text} — docs authored by the prflow:docs pass"` (consume the tick call's outcome line per the failure-isolation contract; a `remedy=retick-named-rows` or `remedy=retick-and-reset-status` means the index did not resolve — re-resolve and re-tick). When a deferred criterion instead names a check command, discharge it only after the docs commit by running that command yourself, in your own tool call over the landed docs — when the tier does not grant it, the covering run `lib/test/modules/coverage-map.json` names for that unit — and quote the result line of the command you actually ran in the tick note; a subagent's report that it ran the command does not discharge the criterion, and the tick note names no result of a gate that has not run yet. When the tier refuses both the named command and its covering run, leave the criterion unticked and take this paragraph's existing Blocked arm below, its reflection naming `prflow_implement.allowed_tools` as the remedy. This tick must happen before §4.3's terminal `--status Complete` write, which `_terminal_complete_gate` hard-fails while any non-post-merge Acceptance Criteria row is still `- [ ]`. If a deferred doc-AC genuinely cannot be discharged (the docs pass could not author it and the content cannot be derived), do not tick it and do not finalize Complete: take the existing Blocked path (`workpad.py update $ISSUE_NUMBER --status Blocked --reflection-kind blocked --reflection "Phase 4.1: 3.4-deferred doc-AC could not be discharged: {AC text}"`), emit the 👎 outcome reaction, and stop. Resume directly at §4.2 (after the Phase 4.1 documentation subagent returns and its docs are committed). The docs subagent is an Agent-tool dispatch whose return enters this context as a report only, so proceed to §4.2 directly — do not re-dispatch the §4.1 docs subagent, and do not re-read this phase file. The prompt-extension re-load still fires at this boundary (SKILL.md's re-load trigger). From 6e72519a7e140857ee07a546f742188f9a54f97a Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 03:54:01 +0000 Subject: [PATCH 4/7] fix: cover the no-deliverables docgate-suppressed emit site for issue #2129 (review finding) --- lib/test/run.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/test/run.sh b/lib/test/run.sh index e41c885f82..04fabec358 100755 --- a/lib/test/run.sh +++ b/lib/test/run.sh @@ -11289,6 +11289,13 @@ printf '%s\n' "## Implementation Notes" "" \ printf '%s\n' "## Implementation Notes" "" \ "- **Documentation Needed** — update \`docs/internal/implement-skill.md\`; verify with \`bash lib/test/run.sh\` and grant \`Bash(scripts/x.sh:*)\`." \ > "$rdnd_dir/body-adversarial.md" +# #2129: a Documentation Needed block whose only span is a suppressed command +# literal yields NO deliverable — the `no-deliverables` outcome — yet still names a +# suppressed span. Exercises the docgate-suppressed emit site on the no-deliverables +# branch, a distinct site from the deliverables branch the adversarial body covers. +printf '%s\n' "## Implementation Notes" "" \ + "- **Documentation Needed** — verify with \`bash lib/test/run.sh\`." \ + > "$rdnd_dir/body-suppressed-only.md" # An extractor stub that fails only its FIRST call, so the extractor retry has the # same both-orderings coverage the gh retry does. @@ -11382,6 +11389,12 @@ assert_eq "#1554/#2129 adversarial input: literals are not deliverables and the # breadcrumb would vanish from the stream). assert_eq "#2129 adversarial input: the helper relays the extractor's 'suppressed a span' breadcrumb through the merged stream" \ "1" "$(rdnd_run "$rdnd_dir/body-adversarial.md" | grep -c 'extract-doc-needed-paths.sh: suppressed a span')" +# #2129: the docgate-suppressed line is emitted on the no-deliverables branch too, +# not only the deliverables branch — a body whose one span is a suppressed command +# literal reports no-deliverables (exit 10) AND names the suppressed span. +assert_eq "#2129 no-deliverables input: a suppressed span with no real deliverable still relays the docgate-suppressed line" \ + "$(printf 'docgate-outcome: no-deliverables\ndocgate-suppressed: bash lib/test/run.sh\nrc=10')" \ + "$(rdnd_lines "$rdnd_dir/body-suppressed-only.md")" # Stale-capture isolation (what "idempotent" has to mean here to be worth testing): # seed the scratch body file with a DIFFERENT body, then fail both read attempts. # A helper that extracted from whatever was already on disk would report that stale From a6ca9180a058315ea2418582b13423f60a15359a Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:04:09 +0000 Subject: [PATCH 5/7] fix: dedupe 'suppressed a span' phrase to a single occurrence in phase-4-documentation.md (issue #2129 AC5) --- skills/implement/phases/phase-4-documentation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/implement/phases/phase-4-documentation.md b/skills/implement/phases/phase-4-documentation.md index 95232cc86e..611fadbada 100644 --- a/skills/implement/phases/phase-4-documentation.md +++ b/skills/implement/phases/phase-4-documentation.md @@ -86,7 +86,7 @@ Shared read contract (both stages). The helper owns the scratch file and fails c "${CLAUDE_SKILL_DIR:-}"/../../scripts/read-doc-needed-deliverables.sh $ISSUE_NUMBER ``` -Span-suppression note (only when the extractor suppressed a span). When Stage 1's tool result holds a `docgate-suppressed: ` line — the read boundary's relay that the extractor suppressed a span — take its value as the span text, write `Phase 4.1: extractor suppressed a Documentation Needed span: ` (that value substituted, cut to fit the 2048-byte note budget) with the Write tool to `.prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt`, then record it with `workpad.py update $ISSUE_NUMBER --note-file .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt` — never a double-quoted `--note "…"` argument, since the span is third-party text. No such line, no note; Stage 2 records nothing. +Span-suppression note (only when the extractor dropped a span). When Stage 1's tool result holds a `docgate-suppressed: ` line — the read boundary's relay that the extractor suppressed a span — take its value as the span text, write `Phase 4.1: extractor suppressed a Documentation Needed span: ` (that value substituted, cut to fit the 2048-byte note budget) with the Write tool to `.prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt`, then record it with `workpad.py update $ISSUE_NUMBER --note-file .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt` — never a double-quoted `--note "…"` argument, since the span is third-party text. No such line, no note; Stage 2 records nothing. If the helper reports `no-deliverables` but the issue body still contains a Documentation Needed section in either accepted form — the bold-bullet `**Documentation Needed**` form or a `### Documentation Needed` heading (`gh issue view $ISSUE_NUMBER --json body --jq '.body' | grep -qE '\*\*Documentation Needed\*\*|^###[[:space:]]+\*{0,2}Documentation Needed'`) — record a workpad note (`workpad.py update $ISSUE_NUMBER --note "Phase 4.1: Documentation Needed section present but the extractor found no file paths; the deliverable cross-check is skipped this run"`). From ec6e53f18a7ee66df300f1c9c83e0cbd80e7040b Mon Sep 17 00:00:00 2001 From: Daniel Radman <6251388+The01Geek@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:20:14 +0000 Subject: [PATCH 6/7] docs: describe the check-command deferred-doc-AC tick rule for issue #2129 --- docs/internal/implement-skill.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/internal/implement-skill.md b/docs/internal/implement-skill.md index d81d6f80f7..5e71f9eb8e 100644 --- a/docs/internal/implement-skill.md +++ b/docs/internal/implement-skill.md @@ -1949,7 +1949,14 @@ Phase 3.4 gate deferred as Phase-4.1-owned (a `docs/…` deliverable, recorded i Phase 4.1: {AC text}` workpad note — see the Phase 3.4 gate above) is this phase's obligation to close. Once the docs pass has run and its changes are committed, for **each** such deferred doc-AC the orchestrator confirms the required docs actually landed in this run's diff (Stage 2 already verified the -named deliverable paths) and ticks the criterion by its 1-based position, citing the deferral note. This +named deliverable paths) and ticks the criterion by its 1-based position, citing the deferral note. When a +deferred criterion instead **names a check command**, the orchestrator discharges it only after the docs +commit by running that command itself, in its own tool call over the landed docs — and when the tier does +not grant the command, the covering run `lib/test/modules/coverage-map.json` names for that unit — then +quotes the result line of the command it actually ran in the tick note; a subagent's report that it ran the +command does not discharge the criterion, and a tick note names no result of a gate that has not yet run. +When the tier refuses both the named command and its covering run, the criterion is left unticked and takes +the `Blocked` arm below, its reflection naming `prflow_implement.allowed_tools` as the remedy. This tick **must** happen before §4.3's terminal `--status Complete` write, because `workpad.py`'s terminal Complete gate hard-fails a Complete write while any non-post-merge acceptance-criteria row is still unticked — a doc-AC left unticked would abort the finalize. A deferred doc-AC that genuinely cannot be From 1f318a3feb5ffc72926ad445eea8f1735ec159c5 Mon Sep 17 00:00:00 2001 From: The01Geek Date: Fri, 28 Aug 2026 23:37:24 -0600 Subject: [PATCH 7/7] test: pin the per-attempt stderr truncation of the docgate extractor capture (review of #2137) Adds a suppress-flaky-extractor stub whose failing attempts emit the `suppressed a span` breadcrumb, and two run.sh assertions: a clean retry relays no phantom docgate-suppressed line (RED under `2>>`), and the extract-failed token forwards the breadcrumb but relays no span (RED with an emit on the failure arm). Trims the #2129 run.sh comment blocks to the prevention-only standard and names the breadcrumb/parse backtick coupling at both the producer and the consumer. Co-Authored-By: Claude Fable 5 --- lib/test/run.sh | 57 +++++++++++++++---------- scripts/extract-doc-needed-paths.sh | 2 + scripts/read-doc-needed-deliverables.sh | 6 ++- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/test/run.sh b/lib/test/run.sh index 04fabec358..8144013f08 100755 --- a/lib/test/run.sh +++ b/lib/test/run.sh @@ -11289,10 +11289,8 @@ printf '%s\n' "## Implementation Notes" "" \ printf '%s\n' "## Implementation Notes" "" \ "- **Documentation Needed** — update \`docs/internal/implement-skill.md\`; verify with \`bash lib/test/run.sh\` and grant \`Bash(scripts/x.sh:*)\`." \ > "$rdnd_dir/body-adversarial.md" -# #2129: a Documentation Needed block whose only span is a suppressed command -# literal yields NO deliverable — the `no-deliverables` outcome — yet still names a -# suppressed span. Exercises the docgate-suppressed emit site on the no-deliverables -# branch, a distinct site from the deliverables branch the adversarial body covers. +# #2129: keep this body's one span a suppressed literal with NO real path — it must +# reach the no-deliverables emit site, not the deliverables one the adversarial body covers. printf '%s\n' "## Implementation Notes" "" \ "- **Documentation Needed** — verify with \`bash lib/test/run.sh\`." \ > "$rdnd_dir/body-suppressed-only.md" @@ -11309,6 +11307,20 @@ fi exec "$RDND_REAL_EXTRACTOR" RDND_FLAKY_STUB chmod +x "$rdnd_dir/flaky-extractor" +# #2129: a stub whose FAILING attempts each emit the `suppressed a span` breadcrumb, then +# succeeds via the real extractor — do not fold it into flaky-extractor, whose failure +# prints no breadcrumb and so cannot tell a truncated stderr capture from an appended one. +cat > "$rdnd_dir/suppress-flaky-extractor" <<'RDND_SUPPRESS_FLAKY_STUB' +#!/usr/bin/env bash +n=$(cat "$RDND_EXTRACT_COUNT_FILE" 2>/dev/null || echo 0); n=$((n + 1)); echo "$n" > "$RDND_EXTRACT_COUNT_FILE" +if [ "$n" -le "${RDND_SUPPRESS_FAIL_TIMES:-1}" ]; then + echo "extract-doc-needed-paths.sh: suppressed a span (a command/grant/skill literal, or a path mixed with non-path tokens — not a set of bare-path deliverables, so no tokens emitted): \`phantom-span\`" >&2 + echo "extract-doc-needed-paths.sh: token scan error" >&2 + exit 3 +fi +exec "$RDND_REAL_EXTRACTOR" +RDND_SUPPRESS_FLAKY_STUB +chmod +x "$rdnd_dir/suppress-flaky-extractor" # rdnd_run BODY_FILE FAIL_TIMES [EXTRACTOR] -> prints the helper's stdout, then a # final line `rc=`, so one capture carries both halves of the contract. @@ -11369,32 +11381,33 @@ assert_eq "#1554 token vocabulary: an extractor failing BOTH attempts prints \`e assert_eq "#1554 arm order: an extractor succeeding on its SECOND attempt yields the success token" \ "$(printf 'docgate-outcome: deliverables\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ "$(rdnd_lines "$rdnd_dir/body-paths.md" 0 "$rdnd_dir/flaky-extractor")" -# Adversarial input: the block carries a command span and a grant literal, which -# the extractor suppresses. Three things are asserted here, because rdnd_run -# merges stderr: the literals are not phantom deliverables, the extractor's -# `suppressed a span` stderr breadcrumb — emitted on exactly this body — does not -# displace the outcome line or masquerade as a deliverable path, AND (issue #2129) -# the helper now relays the first suppressed span onto stdout as a self-identifying -# `docgate-suppressed: ` line after the outcome line, so Phase 4.1 can record the -# real span rather than a scripted once-per-run boilerplate note. The suppressed -# value is the FIRST suppressed span (`bash lib/test/run.sh`) with the breadcrumb's -# surrounding backticks removed. +# Adversarial input (command span + grant literal, both suppressed): the exact line +# set is load-bearing — the merged breadcrumb must not displace the outcome line or +# read as a path, and the relayed span is backtick-stripped (#1554, #2129). assert_eq "#1554/#2129 adversarial input: literals are not deliverables and the first suppressed span is relayed as a docgate-suppressed line" \ "$(printf 'docgate-outcome: deliverables\ndocgate-suppressed: bash lib/test/run.sh\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ "$(rdnd_lines "$rdnd_dir/body-adversarial.md")" -# #2129: the helper captures the extractor's stderr to a scratch file but FORWARDS -# it unchanged to its own stderr, so the merged tool result the caller reads still -# carries the `suppressed a span` breadcrumb. RED against a helper that captures the -# extractor's stderr without forwarding it (the span would then reach stdout but the -# breadcrumb would vanish from the stream). +# #2129: capturing the extractor's stderr to a file must not swallow it — a helper +# that captures without forwarding drops the breadcrumb from the merged tool result. assert_eq "#2129 adversarial input: the helper relays the extractor's 'suppressed a span' breadcrumb through the merged stream" \ "1" "$(rdnd_run "$rdnd_dir/body-adversarial.md" | grep -c 'extract-doc-needed-paths.sh: suppressed a span')" -# #2129: the docgate-suppressed line is emitted on the no-deliverables branch too, -# not only the deliverables branch — a body whose one span is a suppressed command -# literal reports no-deliverables (exit 10) AND names the suppressed span. +# #2129: the docgate-suppressed emit lives on BOTH success branches; removing it from +# the no-deliverables one loses the span exactly when the block held nothing else. assert_eq "#2129 no-deliverables input: a suppressed span with no real deliverable still relays the docgate-suppressed line" \ "$(printf 'docgate-outcome: no-deliverables\ndocgate-suppressed: bash lib/test/run.sh\nrc=10')" \ "$(rdnd_lines "$rdnd_dir/body-suppressed-only.md")" +# #2129: the extractor's stderr capture is TRUNCATED per attempt (`2>` after `rm -f`); +# `2>>` or a kept prior capture relays a failed attempt's span as a phantom +# docgate-suppressed line on the clean retry. +assert_eq "#2129 retry truncation: a failed attempt's suppressed-span breadcrumb never becomes a docgate-suppressed line on the clean retry" \ + "$(printf 'docgate-outcome: deliverables\ndocgate-path: docs/internal/implement-skill.md\nrc=0')" \ + "$(rdnd_lines "$rdnd_dir/body-paths.md" 0 "$rdnd_dir/suppress-flaky-extractor")" +# #2129: on the extract-failed token the span is NOT relayed, though the breadcrumb +# still reaches the merged stream — an emit added to the failure arm would report a +# span for a read the caller must treat as UNKNOWN. +assert_eq "#2129 extract-failed: a suppressed-span breadcrumb on both failing attempts yields the failure token, the forwarded breadcrumb, and no docgate-suppressed line" \ + "$(printf 'docgate-outcome: extract-failed\nrc=12\nbreadcrumbs=1')" \ + "$(RDND_SUPPRESS_FAIL_TIMES=2 rdnd_run "$rdnd_dir/body-paths.md" 0 "$rdnd_dir/suppress-flaky-extractor" | { _m="$(cat)"; printf '%s\n' "$_m" | grep -E '^(docgate-|rc=)'; printf 'breadcrumbs=%s\n' "$(printf '%s\n' "$_m" | grep -c 'suppressed a span')"; })" # Stale-capture isolation (what "idempotent" has to mean here to be worth testing): # seed the scratch body file with a DIFFERENT body, then fail both read attempts. # A helper that extracted from whatever was already on disk would report that stale diff --git a/scripts/extract-doc-needed-paths.sh b/scripts/extract-doc-needed-paths.sh index 7ac3e94f1b..4687352ae9 100755 --- a/scripts/extract-doc-needed-paths.sh +++ b/scripts/extract-doc-needed-paths.sh @@ -509,6 +509,8 @@ span_token_ok() { # all-output-channels-honesty guard) — it names both possibilities. suppress_span() { if [ "$span_warned" -eq 0 ]; then + # Keep the span the line's ONLY backticked text: read-doc-needed-deliverables.sh + # relays what sits between the first and last backtick (issue #2129). printf '%s\n' "extract-doc-needed-paths.sh: suppressed a span (a command/grant/skill literal, or a path mixed with non-path tokens — not a set of bare-path deliverables, so no tokens emitted): \`$1\`" >&2 span_warned=1 fi diff --git a/scripts/read-doc-needed-deliverables.sh b/scripts/read-doc-needed-deliverables.sh index 912ba30d21..40cf7b2e02 100755 --- a/scripts/read-doc-needed-deliverables.sh +++ b/scripts/read-doc-needed-deliverables.sh @@ -149,8 +149,10 @@ _rdnd_relay_extractor_stderr() { case "$_line" in *"suppressed a span"*) if [ -z "$SUPPRESSED_SPAN" ]; then - _span="${_line#*\`}" # drop up to and including the opening backtick - _span="${_span%\`}" # drop the trailing backtick + # Parses suppress_span() in extract-doc-needed-paths.sh: first backtick to + # last, so a backtick added to that breadcrumb's text relays the wrong span. + _span="${_line#*\`}" + _span="${_span%\`}" SUPPRESSED_SPAN="$_span" fi ;;