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
When PRFlow's implement run stamps a label on an issue or a pull request, the skill prose does most of the work and the helper does little of it. Four separate places in the prompt each read the label list from configuration in their own shell block, then each restate the same table explaining what the helper's four stderr messages mean. Two of those places are read on every implement run, so their words are paid for every time. A maintainer who changes one thing about label routing has to edit four prose copies, keep two message strings identical across every file that quotes them, and reconcile a set of test assertions that pin the helper's exact call syntax. The helper already owns the hard parts — normalising the label list, refusing a bad argument, and making the REST call — but it does not own reading configuration, and it does not tell the caller in one word what happened.
Current Behavior
Four call sites each carry their own copy of the routing table, and three of them separately resolve the label list from configuration before calling the helper.
The helper reports its outcome only as English sentences on stderr. A caller has to match on phrases such as devflow: applied label(s) or devflow: warning: could not apply to work out which of four things happened, and the absence of any output is a fifth meaning.
Smaller defects sit on the same surface.
scripts/ensure-label.sh misreports a normal outcome on a host without grep. Triggering input: run scripts/ensure-label.sh for a label that already exists, on a host whose PATH has no grep. Observed result: the helper writes its failure breadcrumb, because the test that recognises an already-exists response cannot run. Expected result: the helper reports that the label already exists, which is a success. Environment: any host lacking grep; lib/preflight.sh guarantees only git, gh, jq, python3 and PyYAML, so grep is not assured. The helper still writes something, so this does not break the rule that silence means the command was refused — it reports the wrong outcome rather than no outcome.
Two sentences in docs/internal/implement-skill.md describe the label channels wrongly. They say the deferred-label channels resolve and apply labels with the same idiom the documentation channel uses, and that all the channels print the raw configured value next to the normalised one. The deferred channels normalise with a tr/sed/grep pipeline and print both values; the documentation channel forbids that pipeline, normalises in the agent, and prints neither. A reader who trusts those sentences writes the wrong shape into whichever channel they are editing.
A stale comment in lib/test/run.sh also states that the helper stays silent when it is given an empty label set. That was true before, and the change recorded in scripts/apply-labels.sh reversed it; the comment now sits directly above the assertions that prove the opposite.
Desired Behavior
A call site applies configured labels with a single call. It names the configuration key and the fallback value on that call, and the helper reads the configuration, creates each label if it is missing, applies them, and prints one short token on standard output saying what happened. The call site routes on that token instead of matching English sentences. The helper keeps every stderr message it writes today, so a human reading a log sees exactly what they see now, and the existing assertions that match those messages keep passing. scripts/ensure-label.sh decides whether a label already exists without grep, so a host without grep gets the right answer. The two wrong sentences in the internal documentation describe what the channels actually do, and the stale comment in the test file describes the behaviour the helper actually has.
A call site that reads a non-success token still records the same durable note on the workpad that it records today, so a dropped label still reaches the weekly retrospective.
Outcomes that produce no token stay in prose. A command the harness refuses prints nothing at all, and a command whose helper path does not exist fails before the helper runs. Neither reaches the helper's own reporting, so the call site still infers both.
User Impact
The implement run carries fewer words on every run, because the two unconditional call sites lose their routing tables. A maintainer changing label routing edits the helper and one short line per call site instead of four prose copies. A consumer on a host without grep stops seeing a spurious label-creation failure. A reader of the internal documentation stops being told the four label channels work the same way when two of them work in opposite ways.
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.
Relevant Classes/Files — scripts/apply-labels.sh (applies labels, owns the stderr contract), scripts/ensure-label.sh (creates labels), scripts/config-get.sh (resolves configuration), the four call sites skills/implement/phases/phase-3-review.md, skills/implement/phases/phase-4-documentation.md, skills/implement/references/deferred-ac-followups.md and skills/implement/references/deferred-review-findings.md, plus skills/create-issue/references/step-4-present-create.md, skills/init/SKILL.md, lib/meta-issue.sh and .github/workflows/matcher-probe.yml.
Architecture Alignment — the helper gains a flag and a stdout token; it does not change its always-exit-0, never-silent-on-a-path-it-runs contract. Other helpers across the repository cite these two as the model for that contract, so the contract itself stays as it is.
Dependencies — no new library. The helper already reaches gh through lib/resolve-gh.sh, and configuration reading already goes through scripts/config-get.sh.
Data/Schema Considerations — none. The configuration keys docs.labels and deferred.labels keep their current meaning and their current defaults.
Cross-layer Impact — the shell helpers, the agent-facing prompt prose, the desk lints that read that prose, and the test assertions that pin both.
Verified: the implement profile already grants the helper with a wildcard over its arguments, so a new flag needs no grant change — lib/capability-profiles.json contains "Bash(.prflow/vendor/prflow/scripts/apply-labels.sh:*)".
Verified: an empty configured value means the default, not "no labels" — .prflow/config.schema.json contains "A literal empty string resolves to this default rather than meaning no-labels, matching how config defaults resolve; set a whitespace value to apply none."
Verified: a sibling helper on the same phase already uses the stdout-token shape this change adopts — skills/implement/phases/phase-3-review.md contains "It always exits 0 and prints exactly one outcome token to stdout".
Verified: the helper deliberately sends the API call's standard output to /dev/null, which is why standard output is free to carry the token — scripts/apply-labels.sh contains "Capture stderr only (stdout → /dev/null) so a genuine failure names its cause in".
Verified: the command-shape half of the original note stays out of scope because the only executable guard for it has no caller — .github/workflows/devflow.yml contains "devflow-runner.yml is retained without a caller".
Acceptance Criteria
The criteria below cover the helper's new interface, the outcome token's value set, the behaviour that must not change, and the corrections. The token's value set is written as a closed set inside its own criterion. No criterion states a count, a size, or a threshold, so none names a measuring command.
A call site applies configured labels with one invocation of scripts/apply-labels.sh that names the configuration key and the fallback value, and performs no separate configuration read and no separate scripts/ensure-label.sh call.
scripts/apply-labels.sh writes exactly one outcome token to standard output on every path on which it runs, and writes nothing else to standard output.
The outcome token's value set is exactly these five — applied, nothing-to-apply, arg-slip, api-failure, config-unreadable — complete by construction, and no value in that set contains the text "already exists".
Every assertion in lib/test/run.sh that matches a helper stderr message by fixed string passes unchanged after the change.
A configured label value that is an empty string resolves to the caller's fallback value, and a configured value containing only whitespace and separators applies no labels.
scripts/ensure-label.sh decides whether a label already exists without running grep, and reports an already-exists response as a success on a host whose PATH contains no grep.
No call site captures the outcome token into a shell variable or reads it inside a shell condition, and the command-shape lint that lib/test/run.sh drives reports no violation for any call site.
Each implement call site that applies a label names the outcome token once and restates none of the helper's stderr messages.
A call site that reads a non-success outcome token records a durable workpad note, as it does today when it reads a non-success stderr message.
Each of these three anchors resolves to text present after the change — exactly these three, complete by construction: the sed range in lib/test/run.sh that ends on the documentation-label configuration read, the cross-skill routing pointer in skills/create-issue/references/step-4-present-create.md, and the count of cloud-emission notes that lib/test/run.sh asserts.
docs/internal/implement-skill.md states that the deferred and documentation label channels share a normalisation rule and use different mechanisms.
The comment above the empty-label-set assertions in lib/test/run.sh states that the helper writes a breadcrumb for an empty label set.
A call site reports a helper invocation that produced no output at all as a refusal by the harness, and reports one that failed because the helper path does not exist as an anchor-resolution failure, treating neither as a label outcome.
The change adds no new entry to lib/capability-profiles.json, so a consumer whose installed workflow file predates the change still permits every command the new call sites emit.
Implementation Notes
Approach — give scripts/apply-labels.sh a flag pair that names a configuration key and a fallback value, so the helper resolves the label list itself through scripts/config-get.sh, creates each label through the existing creation path, applies them, and prints one token on standard output. Keep every stderr message byte-identical, because assertions elsewhere match them by fixed string. Put the token on standard output rather than stderr, because standard output is empty today and no assertion pins it, while stderr already carries text copied in from gh. Replace grep in the already-exists test with a shell case match. Then collapse each call site to one invocation plus one line routing on the token, and fix the two documentation sentences and the stale test comment.
Relevant files — this certainly touches scripts/apply-labels.sh, scripts/ensure-label.sh, the four call-site prose files, lib/test/run.sh, and docs/internal/implement-skill.md. It likely touches skills/create-issue/references/step-4-present-create.md (whose routing pointer aims at one of the collapsed blocks), and plausibly .github/workflows/matcher-probe.yml (which invokes both helpers as live shape probes), lib/meta-issue.sh (whose wrapper calls the helper without redirecting standard output), and lib/test/regenerate-artifacts.py (whose comment about the cloud-writer manifest is out of date).
Code Patterns — mirror scripts/apply-pr-triggerer.sh, which already prints exactly one outcome token on standard output and always exits 0. Keep the label-list derivation on shell builtins, as scripts/apply-labels.sh already does, because a value that decides both what is applied and which message is printed must not come from a tool the preflight does not guarantee. Preserve the two side effects of the configuration resolver's fallback path: it probes for a superseded key first, and a missing python3 exits with status 2.
Testing Strategy — the change has an automated boundary at the shell level, so drive both helpers against a stubbed gh and assert the token and the exit status directly; the prose changes have a boundary at the desk lints, so run those over the changed call sites. Write the grep defect as a failing test first: stub a PATH without grep, call scripts/ensure-label.sh for an existing label, and assert it reports success — it reports failure today. Because the helper now reads configuration written by hand, walk the adversarial input matrix for a configuration consumer over the new flag: object, array, scalar, an explicitly false-like value, a missing key, and a wrong type, asserting exit 0 with a specific message for each. Add the cases the criteria do not pin: a label list that normalises to nothing, a number argument that did not survive into the command, a configuration read that fails hard, and a repeated call proving applying the same label twice is safe. governing conventions consulted: CLAUDE.md (the six-shape adversarial matrix for consumers of configuration JSON, and the rule that a decisive value must not be derived through a tool the preflight does not guarantee); searched CONTRIBUTING.md and the internal-docs testing guidance, which carry no governing matrix for this surface.
Documentation Needed — docs/internal/implement-skill.md and docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md.
Potential Gotchas — the sed range in lib/test/run.sh that slices a stage of the documentation phase ends on the documentation-label configuration read; removing that read without re-anchoring the range widens it to the end of the file and silently corrupts three counts that have nothing to do with labels. lib/test/run.sh also asserts an exact count of cloud-emission notes across the call sites, and those notes exist for the loop-and-capture ban rather than for the routing table, so they survive the collapse and the count must still match. lib/meta-issue.sh calls the helper without redirecting standard output, so confirm neither of its two call sites sits inside a command substitution before the helper starts writing there. The helper's stderr message strings are quoted verbatim by the test file, by the four call sites, and by two internal documentation pages, so re-wording one is a change to every one of them and reads as new pin authorship. The grant matches on the command's leading token, so the helper path stays first on the line and the new flags follow it; a flag written before the path stops matching and the command is refused with no output. Finally, this change adds a flag to an already-granted helper path, which needs no grant edit — creating a new helper instead would need one, and a grant a pull request ships is resolved from the default branch at trigger time, so it would not take effect for that pull request's own implementing run.
Generated via /prflow:create-issue (v2.33.39, claude-opus-5, high)
Problem Statement
When PRFlow's implement run stamps a label on an issue or a pull request, the skill prose does most of the work and the helper does little of it. Four separate places in the prompt each read the label list from configuration in their own shell block, then each restate the same table explaining what the helper's four stderr messages mean. Two of those places are read on every implement run, so their words are paid for every time. A maintainer who changes one thing about label routing has to edit four prose copies, keep two message strings identical across every file that quotes them, and reconcile a set of test assertions that pin the helper's exact call syntax. The helper already owns the hard parts — normalising the label list, refusing a bad argument, and making the REST call — but it does not own reading configuration, and it does not tell the caller in one word what happened.
Current Behavior
Four call sites each carry their own copy of the routing table, and three of them separately resolve the label list from configuration before calling the helper.
The helper reports its outcome only as English sentences on stderr. A caller has to match on phrases such as
devflow: applied label(s)ordevflow: warning: could not applyto work out which of four things happened, and the absence of any output is a fifth meaning.Smaller defects sit on the same surface.
scripts/ensure-label.shmisreports a normal outcome on a host withoutgrep. Triggering input: runscripts/ensure-label.shfor a label that already exists, on a host whosePATHhas nogrep. Observed result: the helper writes its failure breadcrumb, because the test that recognises an already-exists response cannot run. Expected result: the helper reports that the label already exists, which is a success. Environment: any host lackinggrep;lib/preflight.shguarantees onlygit,gh,jq,python3and PyYAML, sogrepis not assured. The helper still writes something, so this does not break the rule that silence means the command was refused — it reports the wrong outcome rather than no outcome.Two sentences in
docs/internal/implement-skill.mddescribe the label channels wrongly. They say the deferred-label channels resolve and apply labels with the same idiom the documentation channel uses, and that all the channels print the raw configured value next to the normalised one. The deferred channels normalise with atr/sed/greppipeline and print both values; the documentation channel forbids that pipeline, normalises in the agent, and prints neither. A reader who trusts those sentences writes the wrong shape into whichever channel they are editing.A stale comment in
lib/test/run.shalso states that the helper stays silent when it is given an empty label set. That was true before, and the change recorded inscripts/apply-labels.shreversed it; the comment now sits directly above the assertions that prove the opposite.Desired Behavior
A call site applies configured labels with a single call. It names the configuration key and the fallback value on that call, and the helper reads the configuration, creates each label if it is missing, applies them, and prints one short token on standard output saying what happened. The call site routes on that token instead of matching English sentences. The helper keeps every stderr message it writes today, so a human reading a log sees exactly what they see now, and the existing assertions that match those messages keep passing.
scripts/ensure-label.shdecides whether a label already exists withoutgrep, so a host withoutgrepgets the right answer. The two wrong sentences in the internal documentation describe what the channels actually do, and the stale comment in the test file describes the behaviour the helper actually has.A call site that reads a non-success token still records the same durable note on the workpad that it records today, so a dropped label still reaches the weekly retrospective.
Outcomes that produce no token stay in prose. A command the harness refuses prints nothing at all, and a command whose helper path does not exist fails before the helper runs. Neither reaches the helper's own reporting, so the call site still infers both.
User Impact
The implement run carries fewer words on every run, because the two unconditional call sites lose their routing tables. A maintainer changing label routing edits the helper and one short line per call site instead of four prose copies. A consumer on a host without
grepstops seeing a spurious label-creation failure. A reader of the internal documentation stops being told the four label channels work the same way when two of them work in opposite ways.Technical Context
Relevant Classes/Files —
scripts/apply-labels.sh(applies labels, owns the stderr contract),scripts/ensure-label.sh(creates labels),scripts/config-get.sh(resolves configuration), the four call sitesskills/implement/phases/phase-3-review.md,skills/implement/phases/phase-4-documentation.md,skills/implement/references/deferred-ac-followups.mdandskills/implement/references/deferred-review-findings.md, plusskills/create-issue/references/step-4-present-create.md,skills/init/SKILL.md,lib/meta-issue.shand.github/workflows/matcher-probe.yml.Architecture Alignment — the helper gains a flag and a stdout token; it does not change its always-exit-0, never-silent-on-a-path-it-runs contract. Other helpers across the repository cite these two as the model for that contract, so the contract itself stays as it is.
Dependencies — no new library. The helper already reaches
ghthroughlib/resolve-gh.sh, and configuration reading already goes throughscripts/config-get.sh.Data/Schema Considerations — none. The configuration keys
docs.labelsanddeferred.labelskeep their current meaning and their current defaults.Cross-layer Impact — the shell helpers, the agent-facing prompt prose, the desk lints that read that prose, and the test assertions that pin both.
Verified: the implement profile already grants the helper with a wildcard over its arguments, so a new flag needs no grant change —
lib/capability-profiles.jsoncontains "Bash(.prflow/vendor/prflow/scripts/apply-labels.sh:*)".Verified: an empty configured value means the default, not "no labels" —
.prflow/config.schema.jsoncontains "A literal empty string resolves to this default rather than meaning no-labels, matching how config defaults resolve; set a whitespace value to apply none."Verified: a sibling helper on the same phase already uses the stdout-token shape this change adopts —
skills/implement/phases/phase-3-review.mdcontains "It always exits 0 and prints exactly one outcome token to stdout".Verified: the helper deliberately sends the API call's standard output to
/dev/null, which is why standard output is free to carry the token —scripts/apply-labels.shcontains "Capture stderr only (stdout → /dev/null) so a genuine failure names its cause in".Verified: the command-shape half of the original note stays out of scope because the only executable guard for it has no caller —
.github/workflows/devflow.ymlcontains "devflow-runner.yml is retained without a caller".Acceptance Criteria
The criteria below cover the helper's new interface, the outcome token's value set, the behaviour that must not change, and the corrections. The token's value set is written as a closed set inside its own criterion. No criterion states a count, a size, or a threshold, so none names a measuring command.
scripts/apply-labels.shthat names the configuration key and the fallback value, and performs no separate configuration read and no separatescripts/ensure-label.shcall.scripts/apply-labels.shwrites exactly one outcome token to standard output on every path on which it runs, and writes nothing else to standard output.lib/test/run.shthat matches a helper stderr message by fixed string passes unchanged after the change.scripts/ensure-label.shdecides whether a label already exists without runninggrep, and reports an already-exists response as a success on a host whosePATHcontains nogrep.lib/test/run.shdrives reports no violation for any call site.sedrange inlib/test/run.shthat ends on the documentation-label configuration read, the cross-skill routing pointer inskills/create-issue/references/step-4-present-create.md, and the count of cloud-emission notes thatlib/test/run.shasserts.docs/internal/implement-skill.mdstates that the deferred and documentation label channels share a normalisation rule and use different mechanisms.lib/test/run.shstates that the helper writes a breadcrumb for an empty label set.lib/capability-profiles.json, so a consumer whose installed workflow file predates the change still permits every command the new call sites emit.Implementation Notes
scripts/apply-labels.sha flag pair that names a configuration key and a fallback value, so the helper resolves the label list itself throughscripts/config-get.sh, creates each label through the existing creation path, applies them, and prints one token on standard output. Keep every stderr message byte-identical, because assertions elsewhere match them by fixed string. Put the token on standard output rather than stderr, because standard output is empty today and no assertion pins it, while stderr already carries text copied in fromgh. Replacegrepin the already-exists test with a shell case match. Then collapse each call site to one invocation plus one line routing on the token, and fix the two documentation sentences and the stale test comment.scripts/apply-labels.sh,scripts/ensure-label.sh, the four call-site prose files,lib/test/run.sh, anddocs/internal/implement-skill.md. It likely touchesskills/create-issue/references/step-4-present-create.md(whose routing pointer aims at one of the collapsed blocks), and plausibly.github/workflows/matcher-probe.yml(which invokes both helpers as live shape probes),lib/meta-issue.sh(whose wrapper calls the helper without redirecting standard output), andlib/test/regenerate-artifacts.py(whose comment about the cloud-writer manifest is out of date).scripts/apply-pr-triggerer.sh, which already prints exactly one outcome token on standard output and always exits 0. Keep the label-list derivation on shell builtins, asscripts/apply-labels.shalready does, because a value that decides both what is applied and which message is printed must not come from a tool the preflight does not guarantee. Preserve the two side effects of the configuration resolver's fallback path: it probes for a superseded key first, and a missingpython3exits with status 2.ghand assert the token and the exit status directly; the prose changes have a boundary at the desk lints, so run those over the changed call sites. Write thegrepdefect as a failing test first: stub aPATHwithoutgrep, callscripts/ensure-label.shfor an existing label, and assert it reports success — it reports failure today. Because the helper now reads configuration written by hand, walk the adversarial input matrix for a configuration consumer over the new flag: object, array, scalar, an explicitly false-like value, a missing key, and a wrong type, asserting exit 0 with a specific message for each. Add the cases the criteria do not pin: a label list that normalises to nothing, a number argument that did not survive into the command, a configuration read that fails hard, and a repeated call proving applying the same label twice is safe.governing conventions consulted: CLAUDE.md (the six-shape adversarial matrix for consumers of configuration JSON, and the rule that a decisive value must not be derived through a tool the preflight does not guarantee); searched CONTRIBUTING.md and the internal-docs testing guidance, which carry no governing matrix for this surface.docs/internal/implement-skill.mdanddocs/internal/DEVFLOW_SYSTEM_OVERVIEW.md.sedrange inlib/test/run.shthat slices a stage of the documentation phase ends on the documentation-label configuration read; removing that read without re-anchoring the range widens it to the end of the file and silently corrupts three counts that have nothing to do with labels.lib/test/run.shalso asserts an exact count of cloud-emission notes across the call sites, and those notes exist for the loop-and-capture ban rather than for the routing table, so they survive the collapse and the count must still match.lib/meta-issue.shcalls the helper without redirecting standard output, so confirm neither of its two call sites sits inside a command substitution before the helper starts writing there. The helper's stderr message strings are quoted verbatim by the test file, by the four call sites, and by two internal documentation pages, so re-wording one is a change to every one of them and reads as new pin authorship. The grant matches on the command's leading token, so the helper path stays first on the line and the new flags follow it; a flag written before the path stops matching and the command is refused with no output. Finally, this change adds a flag to an already-granted helper path, which needs no grant edit — creating a new helper instead would need one, and a grant a pull request ships is resolved from the default branch at trigger time, so it would not take effect for that pull request's own implementing run.Generated via /prflow:create-issue (v2.33.39, claude-opus-5, high)