You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As the PRFlow maintainer, I want the /prflow:implement Phase 4.1 documentation gate to record only real, run-specific facts on the workpad, so that workpads stop carrying a scripted boilerplate note and never tick a documentation-coupled acceptance criterion on evidence that does not yet exist.
Two things go wrong at the same gate today. First, every implement run writes the same fixed sentence to the workpad, and that sentence is false: it says the gate cannot see a warning that the gate can in fact see. Second, when a deferred acceptance criterion asks for a check to be run, the gate lets the run tick it while pointing at a later step as proof, before that later step has happened. Both fixes live in the same phase file.
Current Behavior
Triggering steps: run /prflow:implement on any issue whose acceptance criteria include a documentation-owned criterion that names a check command, and read the issue workpad after Phase 4.1. Observed on issue #2123 (cloud implement tier, workpad comment https://github.com/The01Geek/prflow/issues/2123#issuecomment-5458815030).
Observed result:
At 00:13:14 the workpad gained the fixed 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". The same sentence is scripted into the phase file, so every run writes it, including a run in which no span was suppressed.
At 00:15:32 the workpad ticked AC8 ("lib/test/lint-internal-docs.py reports no new violation") with a note saying the docs subagent reported 0 new violations and that this was "re-confirmed over the final committed tree by the Phase 4.3 whole-suite pass". The Phase 4.3 pass ran at 00:38:26, 23 minutes later. No command output for the lint appears anywhere on the workpad.
Expected result: a workpad note about a suppressed span appears only in a run where the extractor actually suppressed one, and names that span; a deferred criterion that names a check command is ticked only after that command has run in the same run over the landed docs, with the command's result quoted in the tick note and no reference to a step that has not run yet.
Environment: the cloud implement tier (GitHub Actions). The prose that causes both behaviors is tier-independent, so the local tier behaves the same way.
Desired Behavior
The helper that reads the Documentation Needed block, scripts/read-doc-needed-deliverables.sh, prints one extra self-identifying stdout line, docgate-suppressed: <span>, when the extractor suppressed a span, carrying the first suppressed 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, passing the text through a file rather than a shell argument, and the fixed once-per-run disclosure sentence is gone from the phase file.
A deferred documentation criterion that names a check command is ticked at Phase 4.1 only after the orchestrator has executed, in its own tool call over the landed docs, the command the criterion names — when the tier does not grant that command, the granted covering run that lib/test/modules/coverage-map.json names for it — and the tick note quotes the result line of the command actually run. A subagent's report that it ran the command does not discharge the criterion. A tick note names no result that has not been produced yet. When the tier refuses both forms, the criterion stays unticked and the run takes the paragraph's existing Blocked arm, with the reflection naming prflow_implement.allowed_tools as the remedy.
User Impact
The maintainer reading a workpad sees a suppressed-span note only when it carries a real fact, and sees a doc-criterion tick backed by evidence the run actually holds. The weekly retrospective's gate signals do not move, because they read workpad reflections, not notes.
Technical Context
Scope note: The files and details below are the known starting points, not the full list. Before implementing, trace the change through the codebase to find every affected call site, consumer, and layer — this issue maps the work, it does not bound it.
Verified: skills/implement/phases/phase-4-documentation.md says the extractor emits a "breadcrumb on stderr this gate does not capture" and scripts the fixed --note text quoted above into Stage 1.
Verified: scripts/read-doc-needed-deliverables.sh states the opposite in its header — the caller reads a "tool result that merges this command's stdout with the stderr of" the extractor — so the breadcrumb is visible to the agent in every run; it is only never recorded.
Verified: scripts/extract-doc-needed-paths.sh prints "suppressed a span (a command/grant/skill literal, or a path mixed with non-path tokens" to stderr at most once per run, from suppress_span(), and exposes the span on no other channel.
Verified: skills/implement/phases/phase-4-documentation.md discharges a deferred doc-AC on one piece of evidence — "confirm the docs the criterion required actually landed in this run's diff" — and says nothing about a criterion that names a check command.
Verified: docs/internal/implement-skill.md repeats the false premise — the breadcrumb is "disclosed by Phase 4.1 as ephemeral on the cloud tier" — and must be corrected with the change.
Verified: .prflow/prompt-extensions/implement.md requires a changeset for a "fix, feature, or breaking change to the engine surface", which includes skills/ and scripts/.
scripts/workpad.py has no structure that ties an acceptance-criterion tick to a recorded command result: --tick-ac-n flips a row, --note appends text under a 2048-byte budget, and the completion-evidence flags cover the whole-suite gate only. The evidence therefore lives in the tick note's text.
Architecture Alignment — the helper already owns the gate's stdout contract (self-identifying prefixes, never positions), so a third prefix is the same pattern; the phase file keeps routing on docgate-outcome: and reads the new line by prefix.
Dependencies — none new. The helper and workpad.py are already invoked by Phase 4.1 on both tiers, so no cloud grant changes. The helper and the phase file reach a consumer in the same vendored plugin fetch, so an old helper beside a new phase file cannot occur; a consumer that has not upgraded keeps today's behavior.
Data/Schema Considerations — none.
Cross-layer Impact — one shipped helper, one shipped phase file, its test pins, one internal doc page, one changeset.
Acceptance Criteria
The helper's stdout lines are observed through the #1554 fixture matrix in lib/test/run.sh: rdnd_lines keeps the docgate- lines, and rdnd_run is the unfiltered capture with stderr merged.
After the change the literal "not durably observable on the cloud tier" appears in no file under skills/.
After the change the phase file skills/implement/phases/phase-4-documentation.md names the phrase suppressed a span exactly once, in the sentence that tells Stage 1 to record a workpad note naming the span only when a docgate-suppressed: line is present; that sentence delivers the span text to the workpad through a file — the span text is written with the Write tool to a .prflow/tmp/ file and recorded with workpad.py update … --note-file <that file> — and the span text never appears inside a double-quoted --note "…" shell argument (an unquoted or backticked span in a double-quoted shell argument could execute); the extract-doc-needed-paths.sh: command and grant literals inside the Documentation Needed block are tokenized into phantom doc deliverables that Block implement runs #644 pin in lib/test/run.sh passes unchanged.
After the change the deferred doc-AC paragraph in skills/implement/phases/phase-4-documentation.md states that a deferred criterion naming a check command is ticked only after the orchestrator has run, in its own tool call after the docs commit, that command — when ungranted, the covering run lib/test/modules/coverage-map.json names for it — that a subagent's report that it ran the command does not discharge the criterion, that the tick note quotes the result line of the command actually run, that a tick note names no result of a gate that has not yet run, and that a tier refusal of both forms leaves the criterion unticked and takes the paragraph's existing Blocked arm with a reflection naming prflow_implement.allowed_tools as the remedy.
lib/test/lint-reference-size.py reports no violation for skills/implement/phases/phase-4-documentation.md after the change.
docs/internal/implement-skill.md no longer states that the gate does not capture the extractor's stderr, and states that the helper relays the first suppressed span as a docgate-suppressed: stdout line which Stage 1 records on the workpad.
Implementation Notes
Approach — In scripts/read-doc-needed-deliverables.sh, redirect the extractor's stderr into a scratch file beside the existing body capture under .prflow/tmp, forward that file to the helper's own stderr unchanged, and when it holds the suppressed a span phrase print one docgate-suppressed: <span> line after the docgate-outcome: line, carrying the span text from the breadcrumb with its surrounding backticks stripped. Both the phrase test and the forwarding use bash builtins (case, while IFS= read -r, ${var#prefix}), because a value that decides an emitted line must not depend on a tool lib/preflight.sh does not guarantee. In the phase file, replace the disclosure paragraph with one sentence: when Stage 1's tool result holds a docgate-suppressed: line, write the text Phase 4.1: extractor suppressed a Documentation Needed span: <span> with the Write tool to .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt and record it with workpad.py update $ISSUE_NUMBER --note-file .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt, cut to fit the 2048-byte note budget; Stage 2 records nothing. Amend the deferred doc-AC paragraph with the check-command rule stated in Desired Behavior, keeping its heading sentence byte-identical. Update the Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 adversarial exact-line expectation and the doc page. Add one .changeset/*.md file with bump: patch. Every edit to the phase file goes through the superpowers:writing-skills discipline, and the PR description carries the Writing-skills evidence: line.
Code Patterns — the helper's existing prefix contract and its # STDOUT SHAPE header table; the phase file's existing "read the token after that prefix" wording; the existing Blocked arm inside the deferred doc-AC paragraph, reused rather than duplicated.
Testing Strategy — Shell boundary, driven by the existing Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 fixture matrix in lib/test/run.sh (monolith shard). The one new case: the adversarial-body exact-line expectation gains the docgate-suppressed: line; it is RED against today's helper, which prints no such line, and GREEN after the change. A second new assertion reads rdnd_run (not rdnd_lines) on the adversarial body and checks the merged stream for suppressed a span; it is RED against a helper that captures the extractor's stderr without forwarding it. The clean fixtures already assert exact line sets, so absence of the new line needs no new assertion. The extractor's own stderr fixtures stay untouched. No prose-presence pin is added for the two amended paragraphs: the phase-file wording is agent-executed prose whose control is the review pass.
Documentation Needed — docs/internal/implement-skill.md: correct the "ephemeral on the cloud tier" sentence and describe the new stdout line and the check-command tick rule.
Problem Statement
As the PRFlow maintainer, I want the
/prflow:implementPhase 4.1 documentation gate to record only real, run-specific facts on the workpad, so that workpads stop carrying a scripted boilerplate note and never tick a documentation-coupled acceptance criterion on evidence that does not yet exist.Two things go wrong at the same gate today. First, every implement run writes the same fixed sentence to the workpad, and that sentence is false: it says the gate cannot see a warning that the gate can in fact see. Second, when a deferred acceptance criterion asks for a check to be run, the gate lets the run tick it while pointing at a later step as proof, before that later step has happened. Both fixes live in the same phase file.
Current Behavior
Triggering steps: run
/prflow:implementon any issue whose acceptance criteria include a documentation-owned criterion that names a check command, and read the issue workpad after Phase 4.1. Observed on issue #2123 (cloud implement tier, workpad commenthttps://github.com/The01Geek/prflow/issues/2123#issuecomment-5458815030).Observed result:
lib/test/lint-internal-docs.pyreports no new violation") with a note saying the docs subagent reported 0 new violations and that this was "re-confirmed over the final committed tree by the Phase 4.3 whole-suite pass". The Phase 4.3 pass ran at 00:38:26, 23 minutes later. No command output for the lint appears anywhere on the workpad.Expected result: a workpad note about a suppressed span appears only in a run where the extractor actually suppressed one, and names that span; a deferred criterion that names a check command is ticked only after that command has run in the same run over the landed docs, with the command's result quoted in the tick note and no reference to a step that has not run yet.
Environment: the cloud implement tier (GitHub Actions). The prose that causes both behaviors is tier-independent, so the local tier behaves the same way.
Desired Behavior
The helper that reads the Documentation Needed block,
scripts/read-doc-needed-deliverables.sh, prints one extra self-identifying stdout line,docgate-suppressed: <span>, when the extractor suppressed a span, carrying the first suppressed 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, passing the text through a file rather than a shell argument, and the fixed once-per-run disclosure sentence is gone from the phase file.A deferred documentation criterion that names a check command is ticked at Phase 4.1 only after the orchestrator has executed, in its own tool call over the landed docs, the command the criterion names — when the tier does not grant that command, the granted covering run that
lib/test/modules/coverage-map.jsonnames for it — and the tick note quotes the result line of the command actually run. A subagent's report that it ran the command does not discharge the criterion. A tick note names no result that has not been produced yet. When the tier refuses both forms, the criterion stays unticked and the run takes the paragraph's existing Blocked arm, with the reflection namingprflow_implement.allowed_toolsas the remedy.User Impact
The maintainer reading a workpad sees a suppressed-span note only when it carries a real fact, and sees a doc-criterion tick backed by evidence the run actually holds. The weekly retrospective's gate signals do not move, because they read workpad reflections, not notes.
Technical Context
skills/implement/phases/phase-4-documentation.md(the disclosure paragraph near line 89 and the deferred doc-AC discharge paragraph near line 177);scripts/read-doc-needed-deliverables.sh(the read boundary whose stdout the gate parses);scripts/extract-doc-needed-paths.sh(the only writer of the breadcrumb);lib/test/run.sh(the extract-doc-needed-paths.sh: command and grant literals inside the Documentation Needed block are tokenized into phantom doc deliverables that Block implement runs #644 and Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 pins);docs/internal/implement-skill.md(the owning page).skills/implement/phases/phase-4-documentation.mdsays the extractor emits a "breadcrumb on stderr this gate does not capture" and scripts the fixed--notetext quoted above into Stage 1.scripts/read-doc-needed-deliverables.shstates the opposite in its header — the caller reads a "tool result that merges this command's stdout with the stderr of" the extractor — so the breadcrumb is visible to the agent in every run; it is only never recorded.scripts/extract-doc-needed-paths.shprints "suppressed a span (a command/grant/skill literal, or a path mixed with non-path tokens" to stderr at most once per run, fromsuppress_span(), and exposes the span on no other channel.skills/implement/phases/phase-4-documentation.mddischarges a deferred doc-AC on one piece of evidence — "confirm the docs the criterion required actually landed in this run's diff" — and says nothing about a criterion that names a check command.skills/implement/phases/phase-3-ac-gate.mddefers only a documentation edit, one that "is left unticked at this gate, recorded in a workpad deferral note naming the AC"; on Grant three drift-detecting helpers to the cloud implement tier and mirror them into the matcher-probe EXTRAS list #2123 a check-command criterion was deferred anyway, so the Phase 4.1 paragraph must cope with that shape.lib/test/run.shpins that "P4_FILE quotes the extractor's actual breadcrumb phrase" exactly once, and its Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 fixture matrix asserts the exact stdout lines of the helper for an adversarial body.docs/internal/implement-skill.mdrepeats the false premise — the breadcrumb is "disclosed by Phase 4.1 as ephemeral on the cloud tier" — and must be corrected with the change..prflow/prompt-extensions/implement.mdrequires a changeset for a "fix, feature, or breaking change to the engine surface", which includesskills/andscripts/.scripts/workpad.pyhas no structure that ties an acceptance-criterion tick to a recorded command result:--tick-ac-nflips a row,--noteappends text under a 2048-byte budget, and the completion-evidence flags cover the whole-suite gate only. The evidence therefore lives in the tick note's text.docgate-outcome:and reads the new line by prefix.workpad.pyare already invoked by Phase 4.1 on both tiers, so no cloud grant changes. The helper and the phase file reach a consumer in the same vendored plugin fetch, so an old helper beside a new phase file cannot occur; a consumer that has not upgraded keeps today's behavior.Acceptance Criteria
The helper's stdout lines are observed through the #1554 fixture matrix in
lib/test/run.sh:rdnd_lineskeeps thedocgate-lines, andrdnd_runis the unfiltered capture with stderr merged.scripts/read-doc-needed-deliverables.shon the Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 adversarial fixture body (a command literal and a grant literal beside a real path) prints the existingdocgate-outcome: deliverablesanddocgate-path:lines plus exactly onedocgate-suppressed:line whose value is the first suppressed span's text with the breadcrumb's backticks removed.docgate-suppressed:line, and its existing exact-line expectations pass unchanged.rdnd_runcapture prints a line containingextract-doc-needed-paths.sh: suppressed a span, in addition to thedocgate-lines the filtered expectation asserts.skills/.skills/implement/phases/phase-4-documentation.mdnames the phrasesuppressed a spanexactly once, in the sentence that tells Stage 1 to record a workpad note naming the span only when adocgate-suppressed:line is present; that sentence delivers the span text to the workpad through a file — the span text is written with the Write tool to a.prflow/tmp/file and recorded withworkpad.py update … --note-file <that file>— and the span text never appears inside a double-quoted--note "…"shell argument (an unquoted or backticked span in a double-quoted shell argument could execute); the extract-doc-needed-paths.sh: command and grant literals inside the Documentation Needed block are tokenized into phantom doc deliverables that Block implement runs #644 pin inlib/test/run.shpasses unchanged.skills/implement/phases/phase-4-documentation.mdstates that a deferred criterion naming a check command is ticked only after the orchestrator has run, in its own tool call after the docs commit, that command — when ungranted, the covering runlib/test/modules/coverage-map.jsonnames for it — that a subagent's report that it ran the command does not discharge the criterion, that the tick note quotes the result line of the command actually run, that a tick note names no result of a gate that has not yet run, and that a tier refusal of both forms leaves the criterion unticked and takes the paragraph's existing Blocked arm with a reflection namingprflow_implement.allowed_toolsas the remedy.Discharge every 3.4-deferred documentation AC (mandatory, before §4.3)is present verbatim exactly once in the phase file, so the Wave 6A — producer/consumer gate fixes: recognize doc-ACs as Phase-4-owned in the 3.4 gate, and harden create-issue's mechanical-claim and Documentation Needed contracts #380 pin inlib/test/run.shpasses unchanged.lib/test/lint-reference-size.pyreports no violation forskills/implement/phases/phase-4-documentation.mdafter the change.docs/internal/implement-skill.mdno longer states that the gate does not capture the extractor's stderr, and states that the helper relays the first suppressed span as adocgate-suppressed:stdout line which Stage 1 records on the workpad.Implementation Notes
scripts/read-doc-needed-deliverables.sh, redirect the extractor's stderr into a scratch file beside the existing body capture under.prflow/tmp, forward that file to the helper's own stderr unchanged, and when it holds thesuppressed a spanphrase print onedocgate-suppressed: <span>line after thedocgate-outcome:line, carrying the span text from the breadcrumb with its surrounding backticks stripped. Both the phrase test and the forwarding use bash builtins (case,while IFS= read -r,${var#prefix}), because a value that decides an emitted line must not depend on a toollib/preflight.shdoes not guarantee. In the phase file, replace the disclosure paragraph with one sentence: when Stage 1's tool result holds adocgate-suppressed:line, write the textPhase 4.1: extractor suppressed a Documentation Needed span: <span>with the Write tool to.prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txtand record it withworkpad.py update $ISSUE_NUMBER --note-file .prflow/tmp/docgate-suppressed-note-$ISSUE_NUMBER.txt, cut to fit the 2048-byte note budget; Stage 2 records nothing. Amend the deferred doc-AC paragraph with the check-command rule stated in Desired Behavior, keeping its heading sentence byte-identical. Update the Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 adversarial exact-line expectation and the doc page. Add one.changeset/*.mdfile withbump: patch. Every edit to the phase file goes through thesuperpowers:writing-skillsdiscipline, and the PR description carries theWriting-skills evidence:line.scripts/read-doc-needed-deliverables.sh(extractor invocation near line 120; stdout emitters near lines 127–135);skills/implement/phases/phase-4-documentation.md(the paragraph near line 89 and the paragraph near line 177);lib/test/run.sh(the Phase 4.1's Documentation-Needed read produces no observable value and its branch logic is untestable, because it is inline shell written twice #1554 adversarial expectation near line 11371; likely nothing else);docs/internal/implement-skill.md(the sentence near line 1776);.changeset/.# STDOUT SHAPEheader table; the phase file's existing "read the token after that prefix" wording; the existing Blocked arm inside the deferred doc-AC paragraph, reused rather than duplicated.lib/test/run.sh(monolithshard). The one new case: the adversarial-body exact-line expectation gains thedocgate-suppressed:line; it is RED against today's helper, which prints no such line, and GREEN after the change. A second new assertion readsrdnd_run(notrdnd_lines) on the adversarial body and checks the merged stream forsuppressed a span; it is RED against a helper that captures the extractor's stderr without forwarding it. The clean fixtures already assert exact line sets, so absence of the new line needs no new assertion. The extractor's own stderr fixtures stay untouched. No prose-presence pin is added for the two amended paragraphs: the phase-file wording is agent-executed prose whose control is the review pass.docs/internal/implement-skill.md: correct the "ephemeral on the cloud tier" sentence and describe the new stdout line and the check-command tick rule.lib/test/lint-reference-size.pyenforces, so the two paragraph edits must net out near zero. The span is third-party issue text and is never placed inside a double-quoted shell argument (a backticked span would be executed as a command), which is why the note goes through--note-file. The extract-doc-needed-paths.sh: command and grant literals inside the Documentation Needed block are tokenized into phantom doc deliverables that Block implement runs #644 pin countssuppressed a spanin the phase file and fails on any count other than one. The Wave 6A — producer/consumer gate fixes: recognize doc-ACs as Phase-4-owned in the 3.4 gate, and harden create-issue's mechanical-claim and Documentation Needed contracts #380 pin asserts the discharge heading sentence uniquely.workpad.pyrefuses a note over 2048 bytes, so the span text in the note is cut, never the note dropped. The extractor is invoked twice on a retry, so the stderr scratch file is overwritten per attempt, never appended. Phase 3.4's deferral rule admits only documentation-edit criteria, yet Grant three drift-detecting helpers to the cloud implement tier and mirror them into the matcher-probe EXTRAS list #2123 deferred a check-command criterion; this issue leavesskills/implement/phases/phase-3-ac-gate.mdunchanged and makes Phase 4.1 handle that shape. PR Grant three drift-detecting helpers to the cloud implement tier and mirror them into the matcher-probe EXTRAS list #2124 (issue Grant three drift-detecting helpers to the cloud implement tier and mirror them into the matcher-probe EXTRAS list #2123, open at drafting time) addsBash(lib/test/lint-internal-docs.py:*)toprflow_implement.allowed_tools; that key is trigger-time-resolved from the default branch, so on a cloud run before Grant three drift-detecting helpers to the cloud implement tier and mirror them into the matcher-probe EXTRAS list #2124 merges a deferred criterion naming that command falls to the granted covering run (lib/test/run-shard.sh monolithexercises the lint insidelib/test/run.sh). Nothing in this issue's own acceptance criteria invokes that command.Generated via /prflow:create-issue (v2.36.15, medium)