devflow-kit 2.5.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +44 -19
  3. package/dist/agents/git.md +13 -15
  4. package/dist/cli/commands/ambient.js +160 -145
  5. package/dist/cli/commands/capture.js +29 -55
  6. package/dist/cli/commands/compliance.js +32 -61
  7. package/dist/cli/commands/context.js +17 -32
  8. package/dist/cli/commands/debug.js +65 -26
  9. package/dist/cli/commands/flags.js +3 -3
  10. package/dist/cli/commands/hud.js +34 -10
  11. package/dist/cli/commands/init-seed.js +40 -4
  12. package/dist/cli/commands/init.js +249 -271
  13. package/dist/cli/commands/install-report.js +10 -15
  14. package/dist/cli/commands/knowledge/index.js +1 -1
  15. package/dist/cli/commands/knowledge/toggle.js +11 -3
  16. package/dist/cli/commands/learning.js +52 -37
  17. package/dist/cli/commands/legacy-hooks.js +11 -14
  18. package/dist/cli/commands/memory.js +67 -78
  19. package/dist/cli/commands/proxy.js +23 -41
  20. package/dist/cli/commands/security.js +5 -13
  21. package/dist/cli/commands/skills.js +21 -3
  22. package/dist/cli/commands/tracker.js +100 -228
  23. package/dist/cli/commands/uninstall.js +343 -138
  24. package/dist/commands/bug-analysis.md +38 -12
  25. package/dist/commands/code-review.md +70 -21
  26. package/dist/commands/debug.md +37 -7
  27. package/dist/commands/dynamic-build.md +66 -17
  28. package/dist/commands/dynamic-plan.md +19 -8
  29. package/dist/commands/dynamic-profile.md +24 -10
  30. package/dist/commands/dynamic-tickets.md +22 -11
  31. package/dist/commands/explore.md +37 -7
  32. package/dist/commands/implement.md +96 -32
  33. package/dist/commands/plan.md +62 -19
  34. package/dist/commands/release.md +2 -2
  35. package/dist/commands/research.md +34 -8
  36. package/dist/commands/resolve.md +65 -17
  37. package/dist/commands/self-review.md +45 -9
  38. package/dist/core/compliance-compose.js +27 -27
  39. package/dist/core/evidence-policy.js +240 -24
  40. package/dist/core/feature-config.js +94 -25
  41. package/dist/core/feature-switch.js +1 -1
  42. package/dist/core/flags.js +30 -2
  43. package/dist/core/fs-atomic.js +27 -0
  44. package/dist/core/hook-log-dirs.js +104 -0
  45. package/dist/core/learning-tuning-config.js +5 -3
  46. package/dist/core/ledger-root.js +102 -0
  47. package/dist/core/manifest.js +6 -4
  48. package/dist/core/mds-variants.js +34 -97
  49. package/dist/core/migrations.js +49 -23
  50. package/dist/core/plugins.js +5 -4
  51. package/dist/core/project-paths.js +0 -17
  52. package/dist/core/same-location.js +25 -0
  53. package/dist/core/tracker.js +226 -139
  54. package/dist/hud/components/config-counts.js +15 -4
  55. package/dist/hud/components/learning-counts.js +14 -0
  56. package/dist/hud/config.js +2 -1
  57. package/dist/hud/cost-history.js +2 -4
  58. package/dist/hud/git.js +52 -7
  59. package/dist/hud/index.js +7 -9
  60. package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
  61. package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
  62. package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
  63. package/dist/skills/git/references/tracker/_mcp.md +1 -1
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
  65. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
  66. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
  67. package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
  68. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
  70. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
  71. package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
  74. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
  76. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
  77. package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
  80. package/dist/targets/claude-code/claude-paths.js +59 -57
  81. package/dist/targets/claude-code/compliance-install.js +49 -65
  82. package/dist/targets/claude-code/hooks.js +108 -3
  83. package/dist/targets/claude-code/installer.js +30 -57
  84. package/dist/targets/claude-code/post-install.js +246 -139
  85. package/dist/targets/claude-code/tracker-install.js +38 -65
  86. package/package.json +5 -4
  87. package/src/assets/agents/code.md +4 -3
  88. package/src/assets/agents/design.md +1 -0
  89. package/src/assets/agents/git.mds +55 -57
  90. package/src/assets/agents/knowledge.md +2 -2
  91. package/src/assets/agents/review.md +3 -1
  92. package/src/assets/agents/tracker.md +37 -30
  93. package/src/assets/commands/_partials/_compliance.mds +19 -1
  94. package/src/assets/commands/_partials/_decisions.mds +15 -3
  95. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  96. package/src/assets/commands/_partials/_engine.mds +2 -2
  97. package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
  98. package/src/assets/commands/_partials/_factory.mds +1 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  100. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  101. package/src/assets/commands/_partials/_preamble.mds +1 -1
  102. package/src/assets/commands/_partials/_publication.mds +6 -2
  103. package/src/assets/commands/_partials/_settings.mds +28 -0
  104. package/src/assets/commands/_partials/_ticket_template.mds +3 -3
  105. package/src/assets/commands/_partials/_tracker.mds +4 -4
  106. package/src/assets/commands/_partials/_wave.mds +4 -4
  107. package/src/assets/commands/bug-analysis.mds +19 -17
  108. package/src/assets/commands/code-review.mds +39 -33
  109. package/src/assets/commands/debug.mds +4 -5
  110. package/src/assets/commands/dynamic-build.mds +75 -53
  111. package/src/assets/commands/dynamic-plan.mds +20 -15
  112. package/src/assets/commands/dynamic-profile.mds +24 -11
  113. package/src/assets/commands/dynamic-tickets.mds +25 -20
  114. package/src/assets/commands/explore.mds +4 -5
  115. package/src/assets/commands/implement.mds +58 -45
  116. package/src/assets/commands/plan.mds +34 -29
  117. package/src/assets/commands/release.md +2 -2
  118. package/src/assets/commands/research.mds +11 -9
  119. package/src/assets/commands/resolve.mds +41 -39
  120. package/src/assets/commands/self-review.mds +24 -25
  121. package/src/assets/mds/git/_pr.mds +61 -61
  122. package/src/assets/mds/git/_references.mds +19 -19
  123. package/src/assets/mds/tracker/_common.mds +8 -8
  124. package/src/assets/mds/tracker/_github.mds +71 -71
  125. package/src/assets/mds/tracker/_jira.mds +74 -74
  126. package/src/assets/mds/tracker/_linear.mds +75 -75
  127. package/src/assets/mds/tracker/_mcp.mds +23 -17
  128. package/src/assets/scripts/hooks/background-memory-update +35 -19
  129. package/src/assets/scripts/hooks/capture-prompt +18 -12
  130. package/src/assets/scripts/hooks/capture-question +18 -12
  131. package/src/assets/scripts/hooks/capture-turn +27 -17
  132. package/src/assets/scripts/hooks/debug-trace +11 -6
  133. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  134. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  135. package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
  136. package/src/assets/scripts/hooks/git-marker +48 -0
  137. package/src/assets/scripts/hooks/json-helper.cjs +6 -1
  138. package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
  139. package/src/assets/scripts/hooks/log-paths +80 -0
  140. package/src/assets/scripts/hooks/memory-worker +17 -15
  141. package/src/assets/scripts/hooks/pre-compact-memory +41 -16
  142. package/src/assets/scripts/hooks/queue-append +104 -30
  143. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  144. package/src/assets/scripts/hooks/session-start-context +289 -122
  145. package/src/assets/scripts/hooks/session-start-memory +35 -16
  146. package/src/assets/scripts/lib/project-config.cjs +633 -0
  147. package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
  148. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  149. package/src/assets/scripts/verify-evidence.cjs +1 -1
  150. package/src/assets/skills/compliance/SKILL.md +2 -2
  151. package/src/assets/skills/docs-framework/SKILL.md +6 -7
  152. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  153. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  154. package/src/assets/skills/git/references/github-api.md +9 -9
  155. package/src/assets/skills/git/references/patterns.md +1 -1
  156. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  157. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  158. package/src/targets/claude-code/templates/managed-settings.json +25 -9
@@ -4,7 +4,6 @@ output-dir: dist/commands
4
4
  ---
5
5
  @import { knowledge_load, knowledge_writeback } from "./_partials/_knowledge.mds"
6
6
  @import { decisions_load } from "./_partials/_decisions.mds"
7
-
8
7
  # Self-Review Command
9
8
 
10
9
  Run Simplify agent and Scrutinize agent sequentially on changed files for post-implementation quality refinement.
@@ -26,11 +25,11 @@ Detect changed files and build context:
26
25
  2. Else run `git diff --name-only HEAD` + `git diff --name-only --cached` to get staged + unstaged
27
26
  3. If no changes found, report "No changes to review" and exit
28
27
  4. Build TASK_DESCRIPTION from recent commit messages or branch name
29
- {decisions_load()}
28
+ {{decisions_load()}}
30
29
 
31
30
  Pass `DECISIONS_CONTEXT` to Scrutinize agent — the compact index lists active ADR/PF entries; Scrutinize agent uses `devflow:apply-decisions` to Read full entry bodies on demand. Known pitfalls help identify reintroduced issues, prior decisions help validate architectural consistency. (Simplify agent does not consume decisions — it operates at code-shape level and Scrutinize agent runs after to catch any architectural drift.)
32
31
 
33
- {knowledge_load()}
32
+ {{knowledge_load()}}
34
33
 
35
34
  Pass `FEATURE_KNOWLEDGE` to Scrutinize agent.
36
35
 
@@ -44,8 +43,8 @@ Pass `FEATURE_KNOWLEDGE` to Scrutinize agent.
44
43
  Spawn Simplify agent to refine code for clarity and consistency:
45
44
 
46
45
  Agent(subagent_type="Simplify", run_in_background=false):
47
- "TASK_DESCRIPTION: \{task_description\}
48
- FILES_CHANGED: \{files_changed\}
46
+ "TASK_DESCRIPTION: {task_description}
47
+ FILES_CHANGED: {files_changed}
49
48
  Simplify and refine the code for clarity and consistency while preserving functionality."
50
49
 
51
50
  **Wait for completion.** Simplify agent commits changes directly.
@@ -58,10 +57,10 @@ Simplify and refine the code for clarity and consistency while preserving functi
58
57
  Spawn Scrutinize agent for quality evaluation and fixing:
59
58
 
60
59
  Agent(subagent_type="Scrutinize", run_in_background=false):
61
- "TASK_DESCRIPTION: \{task_description\}
62
- FILES_CHANGED: \{files_changed\}
63
- DECISIONS_CONTEXT: \{decisions_context\}
64
- FEATURE_KNOWLEDGE: \{feature_knowledge\}
60
+ "TASK_DESCRIPTION: {task_description}
61
+ FILES_CHANGED: {files_changed}
62
+ DECISIONS_CONTEXT: {decisions_context}
63
+ FEATURE_KNOWLEDGE: {feature_knowledge}
65
64
  Evaluate against 9-pillar framework. Fix P0/P1 issues. Return structured report.
66
65
  Follow devflow:apply-decisions to scan DECISIONS_CONTEXT and Read full ADR/PF bodies on demand. Skip if (none).
67
66
  Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. Skip if (none)."
@@ -76,7 +75,7 @@ Follow devflow:apply-feature-knowledge for FEATURE_KNOWLEDGE. Skip if (none)."
76
75
  If Scrutinize agent made changes (STATUS == FIXED):
77
76
 
78
77
  Agent(subagent_type="Validate", run_in_background=false):
79
- "FILES_CHANGED: \{scrutinize_modified_files\}
78
+ "FILES_CHANGED: {scrutinize_modified_files}
80
79
  VALIDATION_SCOPE: changed-only
81
80
  Run build, typecheck, lint, test on modified files"
82
81
 
@@ -91,31 +90,31 @@ Display summary:
91
90
 
92
91
  ## Self-Review Complete
93
92
 
94
- **Files Reviewed**: \{n\}
95
- **Status**: \{PASS|FIXED|BLOCKED\}
93
+ **Files Reviewed**: {n}
94
+ **Status**: {PASS|FIXED|BLOCKED}
96
95
 
97
96
  ### Simplify agent
98
- - \{n\} files refined for clarity
97
+ - {n} files refined for clarity
99
98
 
100
99
  ### Scrutinize agent (9-Pillar Evaluation)
101
100
  | Pillar | Status |
102
101
  |--------|--------|
103
- | Design | \{status\} |
104
- | Functionality | \{status\} |
105
- | Security | \{status\} |
106
- | Complexity | \{status\} |
107
- | Error Handling | \{status\} |
108
- | Tests | \{status\} |
109
- | Naming | \{status\} |
110
- | Consistency | \{status\} |
111
- | Documentation | \{status\} |
102
+ | Design | {status} |
103
+ | Functionality | {status} |
104
+ | Security | {status} |
105
+ | Complexity | {status} |
106
+ | Error Handling | {status} |
107
+ | Tests | {status} |
108
+ | Naming | {status} |
109
+ | Consistency | {status} |
110
+ | Documentation | {status} |
112
111
 
113
112
  ### Commits Created
114
- - \{sha\} \{message\}
113
+ - {sha} {message}
115
114
 
116
- \{If BLOCKED: ### Blocking Issue\n\{description\}\}
115
+ {If BLOCKED: ### Blocking Issue\n{description}}
117
116
 
118
- {knowledge_writeback()}
117
+ {{knowledge_writeback()}}
119
118
 
120
119
  ## Architecture
121
120
 
@@ -10,7 +10,7 @@ sub-directory come from `PR_HOST_OPS` / `PR_HOST_DESTINATION_ROOT` in
10
10
  build fails. Everything above the first section marker is module-level prose and
11
11
  is emitted nowhere.
12
12
 
13
- Why `pr/` is not `tracker/\{provider\}/`: pull requests, PR reviews and PR checks
13
+ Why `pr/` is not `tracker/{provider}/`: pull requests, PR reviews and PR checks
14
14
  stay on GitHub under every issue-tracker provider, so these steps are the SAME
15
15
  file whatever `TRACKER_PROVIDER` resolves to. A copy per provider would be one
16
16
  identical tree per registered provider; a row under `tracker/github/` would make
@@ -52,18 +52,18 @@ Load for `ensure-pr-ready` under every tracker provider.
52
52
  2. Check for uncommitted changes - if any, create atomic commit using `devflow:git` patterns
53
53
  3. Check if branch pushed to remote - if not, push with `-u` flag. If that push is refused and the branch's open PR is cross-repository with maintainer edits off (`gh pr view --json isCrossRepository,maintainerCanModify`), emit `TRACEABILITY: DEGRADED (cannot push to fork)` and go to 4a (4a–4c edit only the PR).
54
54
  4a. Check if PR exists - if not, create PR using guidance from (in priority order): (a) `PR_DESCRIPTION_GUIDANCE` if given and not `(none)`, (b) generated from branch context. Compose the PR body via the `devflow:git` template to `$DEVFLOW_BODY_RAW` (a D11 sink: it publishes at repo visibility), then append the caller blocks. Apply the Comment-sink scrub (D11) — a failed one posts neither block; on success: `gh pr create … --body-file "$DEVFLOW_BODY"`.
55
- - **Caller blocks**, in order: `PR_WAVE_BLOCK` (`check wave`), then `PR_TEST_PLAN_BLOCK` (`check block`), each when given and not `(none)`. Write it byte for byte to a fresh `mktemp` file with the Write tool, never via a shell string, and run `node "$\{DEVFLOW_DIR:-$HOME/.devflow\}/scripts/verify-evidence.cjs" check <wave|block> <file>; echo "exit=$?"`. Only `exit=0` admits it, verbatim; else omit it, never repaired or partly pasted, and emit `TRACEABILITY: DEGRADED (wave block does not match its grammar)` or `TRACEABILITY: DEGRADED (test-plan block does not match its grammar)` with steps 4b/4c's lines. An admitted wave block is the body's only `## Related Issues`: skip 4b.
55
+ - **Caller blocks**, in order: `PR_WAVE_BLOCK` (`check wave`), then `PR_TEST_PLAN_BLOCK` (`check block`), each when given and not `(none)`. Write it byte for byte to a fresh `mktemp` file with the Write tool, never via a shell string, and run `node "$HOME/.devflow/scripts/verify-evidence.cjs" check <wave|block> <file>; echo "exit=$?"`. Only `exit=0` admits it, verbatim; else omit it, never repaired or partly pasted, and emit `TRACEABILITY: DEGRADED (wave block does not match its grammar)` or `TRACEABILITY: DEGRADED (test-plan block does not match its grammar)` with steps 4b/4c's lines. An admitted wave block is the body's only `## Related Issues`: skip 4b.
56
56
  4c. Retitle, only when `APPLY_CONVENTIONS` is `true`: if the PR title breaks the convention in the PR Titles section of `.devflow/conventions.md`, retitle it; skip silently when that file is absent. Two rules, because the title derives from third-party PR titles:
57
57
  - **Validate before use.** Skip the retitle (leave the PR title as-is, no error) if the composed title contains any of `` $ ` \ " ' ; | & < > `` or a newline.
58
- - **Pass as argv, never as command text.** Bind it to a shell variable and pass that variable: `gh pr edit \{PR_NUMBER\} --title "$DEVFLOW_PR_TITLE"`. Never interpolate it: `$(...)`, backticks and `$\{...\}` all expand inside double quotes.
58
+ - **Pass as argv, never as command text.** Bind it to a shell variable and pass that variable: `gh pr edit {PR_NUMBER} --title "$DEVFLOW_PR_TITLE"`. Never interpolate it: `$(...)`, backticks and `${...}` all expand inside double quotes.
59
59
 
60
- On any 4xx/5xx from `gh pr edit`: emit `TRACEABILITY: DEGRADED (\{reason\})` and continue — a failed retitle never blocks the PR.
60
+ On any 4xx/5xx from `gh pr edit`: emit `TRACEABILITY: DEGRADED ({reason})` and continue — a failed retitle never blocks the PR.
61
61
  5. Get base branch from PR
62
62
  6. Derive branch-slug (replace `/` with `-`)
63
63
 
64
64
  ### Step 4b's PR-host half
65
65
 
66
- The provider reference's step 4b publishes its section only through this: find the open PR with `gh pr list --head \{branch\} --state open --limit 1`; compose the existing body plus the `## Related Issues` section to `$DEVFLOW_BODY_RAW` — the existing body is third-party-editable, so never interpolate it into a command string; apply the Comment-sink scrub (D11); on success: `gh pr edit \{PR_NUMBER\} --body-file "$DEVFLOW_BODY"`.
66
+ The provider reference's step 4b publishes its section only through this: find the open PR with `gh pr list --head {branch} --state open --limit 1`; compose the existing body plus the `## Related Issues` section to `$DEVFLOW_BODY_RAW` — the existing body is third-party-editable, so never interpolate it into a command string; apply the Comment-sink scrub (D11); on success: `gh pr edit {PR_NUMBER} --body-file "$DEVFLOW_BODY"`.
67
67
  @end
68
68
 
69
69
  @define validate_branch():
@@ -79,12 +79,12 @@ Load for `validate-branch` under every tracker provider.
79
79
  2. Verify working directory is clean - error if uncommitted changes
80
80
  3. Get current branch name
81
81
  4. Derive branch-slug (replace `/` with `-`)
82
- 5. Check if reviews exist at `\{WORKTREE_PATH\}/.devflow/docs/reviews/\{branch-slug\}/` (or `.devflow/docs/reviews/\{branch-slug\}/` if no WORKTREE_PATH)
82
+ 5. Check if reviews exist at `{WORKTREE_PATH}/.devflow/docs/reviews/{branch-slug}/` (or `.devflow/docs/reviews/{branch-slug}/` if no WORKTREE_PATH)
83
83
  6. Determine base branch and fetch PR details if available:
84
- - If a PR exists — the PR# context, else the current branch's PR: fetch PR details via `gh pr view \{number\} --json baseRefName,isCrossRepository,maintainerCanModify,number,headRepositoryOwner,headRepository` (omit `\{number\}` for the current branch; `gh` has no `-C` flag, so run this call from `WORKTREE_PATH` (else cwd) — never the orchestrator's own cwd, or a multi-worktree run discovers the wrong PR); use `baseRefName` as `base_branch` and `number` as the PR. If `isCrossRepository` is true, `maintainerCanModify` is false and you cannot push to that fork yourself (`gh api "repos/\{headRepositoryOwner.login\}/\{headRepository.name\}" --jq '.permissions.push'` does not print `true`), emit `TRACEABILITY: DEGRADED (cannot push to fork)`: the caller skips pushes, while PR comments and body edits still run.
85
- - If no PR exists: resolve the default remote branch via `git -C \{worktree\} rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||'`; if that fails, probe common defaults (`main`, then `master`) via `git -C \{worktree\} rev-parse --verify \{default\} 2>/dev/null`
84
+ - If a PR exists — the PR# context, else the current branch's PR: fetch PR details via `gh pr view {number} --json baseRefName,isCrossRepository,maintainerCanModify,number,headRepositoryOwner,headRepository` (omit `{number}` for the current branch; `gh` has no `-C` flag, so run this call from `WORKTREE_PATH` (else cwd) — never the orchestrator's own cwd, or a multi-worktree run discovers the wrong PR); use `baseRefName` as `base_branch` and `number` as the PR. If `isCrossRepository` is true, `maintainerCanModify` is false and you cannot push to that fork yourself (`gh api "repos/{headRepositoryOwner.login}/{headRepository.name}" --jq '.permissions.push'` does not print `true`), emit `TRACEABILITY: DEGRADED (cannot push to fork)`: the caller skips pushes, while PR comments and body edits still run.
85
+ - If no PR exists: resolve the default remote branch via `git -C {worktree} rev-parse --abbrev-ref origin/HEAD 2>/dev/null | sed 's|origin/||'`; if that fails, probe common defaults (`main`, then `master`) via `git -C {worktree} rev-parse --verify {default} 2>/dev/null`
86
86
  - If `base_branch` still cannot be determined: emit an intentional empty `### Diff Scope` block (so `DIFF_FILES=""` is a deliberate conservative degrade, not a silent error); skip step 7
87
- 7. Compute diff scope (only if `base_branch` was resolved): `git -C \{worktree\} diff \{base_branch\}...HEAD --name-only` → newline-separated file list
87
+ 7. Compute diff scope (only if `base_branch` was resolved): `git -C {worktree} diff {base_branch}...HEAD --name-only` → newline-separated file list
88
88
  @end
89
89
 
90
90
  @define post_review_summary():
@@ -98,37 +98,37 @@ Load for `post-review-summary` under every tracker provider.
98
98
 
99
99
  1. Check for existing comment with this run's marker (author-filtered — a third party posting the marker string must not suppress devflow's comment):
100
100
  - Fetch viewer login: `gh api user --jq '.login'` → store as VIEWER_LOGIN
101
- - `gh pr view \{PR_NUMBER\} --json comments --jq '[.comments[] | select(.author.login == "'"$VIEWER_LOGIN"'")] | .[].body'`
102
- - Search for `<!-- devflow:review-summary cycle:\{CYCLE_NUMBER\} ts:\{REVIEW_TIMESTAMP\}` in the viewer-authored comment bodies only (full pair match)
103
- - If found: skip — report `Skipped: already posted for cycle \{CYCLE_NUMBER\} ts:\{REVIEW_TIMESTAMP\}`
101
+ - `gh pr view {PR_NUMBER} --json comments --jq '[.comments[] | select(.author.login == "'"$VIEWER_LOGIN"'")] | .[].body'`
102
+ - Search for `<!-- devflow:review-summary cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP}` in the viewer-authored comment bodies only (full pair match)
103
+ - If found: skip — report `Skipped: already posted for cycle {CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP}`
104
104
  2. Load `references/publication-gate.md` and resolve `REVIEW_PUBLICATION` by its step 2; `off` ends the op without posting.
105
105
  3. Probe (if mode not yet determined): `gh repo view --json visibility --jq '.visibility'`, case-insensitive. `PRIVATE`/`INTERNAL` → FULL; `PUBLIC` → `STUB (public repository)`; empty output, an error or any other value → `STUB (visibility undeterminable)`. **Fail-closed: on any error or unrecognised value, treat as PUBLIC (mode STUB).**
106
106
  4. Read `REVIEW_SUMMARY_PATH` — repo-relative; read it under `WORKTREE_PATH` (else cwd).
107
107
  5. Compose body:
108
108
  - **FULL mode:**
109
109
  ```
110
- <!-- devflow:review-summary cycle:\{CYCLE_NUMBER\} ts:\{REVIEW_TIMESTAMP\} -->
111
- ## Code Review — Cycle \{CYCLE_NUMBER\}
110
+ <!-- devflow:review-summary cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP} -->
111
+ ## Code Review — Cycle {CYCLE_NUMBER}
112
112
 
113
- \{full content of review-summary.md\}
113
+ {full content of review-summary.md}
114
114
 
115
115
  ---
116
- *Posted by [devflow](https://github.com/dean0x/devflow) · cycle \{CYCLE_NUMBER\}*
116
+ *Posted by [devflow](https://github.com/dean0x/devflow) · cycle {CYCLE_NUMBER}*
117
117
  ```
118
118
  - **STUB mode** (excluded: finding titles, file:line references, Blocking/Escalations/Third-Party/Verification sections, merge recommendation):
119
119
  ```
120
- <!-- devflow:review-summary cycle:\{CYCLE_NUMBER\} ts:\{REVIEW_TIMESTAMP\} -->
121
- ## Code Review — Cycle \{CYCLE_NUMBER\}
120
+ <!-- devflow:review-summary cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP} -->
121
+ ## Code Review — Cycle {CYCLE_NUMBER}
122
122
 
123
123
  Full summary withheld (public repository).
124
124
 
125
- \{counts-by-severity table verbatim from local artifact; if unparseable: "Counts unavailable — see the local artifact."\}
125
+ {counts-by-severity table verbatim from local artifact; if unparseable: "Counts unavailable — see the local artifact."}
126
126
 
127
- Full report: \{REVIEW_SUMMARY_PATH\} (not committed; ask the author)
128
- *Posted by [devflow](https://github.com/dean0x/devflow) · cycle \{CYCLE_NUMBER\}*
127
+ Full report: {REVIEW_SUMMARY_PATH} (not committed; ask the author)
128
+ *Posted by [devflow](https://github.com/dean0x/devflow) · cycle {CYCLE_NUMBER}*
129
129
  ```
130
- Cap body at 60000 characters (GitHub rejects over 65536 with a 422, which the 4xx rule would silently skip). Truncate lowest-value sections first (Suggestions, then Pre-existing), keeping the counts table and every Blocking entry; end with `…truncated — full report in the local review artifact \{REVIEW_SUMMARY_PATH\} (not committed; ask the author)`.
131
- 6. Write body to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit or missing script → DO NOT POST. Re-check the 60000-char cap on the scrubbed body (redaction may grow it; truncate at a line boundary below 59,800 chars, keeping the truncation pointer sentence; if truncation fires here: emit `NOTE: body exceeded 60k after redaction — truncated/stub posted` in op output and prepend that notice to the body). Post: `gh pr comment \{PR_NUMBER\} --body-file "$DEVFLOW_BODY"`.
130
+ Cap body at 60000 characters (GitHub rejects over 65536 with a 422, which the 4xx rule would silently skip). Truncate lowest-value sections first (Suggestions, then Pre-existing), keeping the counts table and every Blocking entry; end with `…truncated — full report in the local review artifact {REVIEW_SUMMARY_PATH} (not committed; ask the author)`.
131
+ 6. Write body to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit or missing script → DO NOT POST. Re-check the 60000-char cap on the scrubbed body (redaction may grow it; truncate at a line boundary below 59,800 chars, keeping the truncation pointer sentence; if truncation fires here: emit `NOTE: body exceeded 60k after redaction — truncated/stub posted` in op output and prepend that notice to the body). Post: `gh pr comment {PR_NUMBER} --body-file "$DEVFLOW_BODY"`.
132
132
  7. On 5xx: retry once. If still 5xx: `TRACEABILITY: DEGRADED (5xx on post-review-summary)`, warn, return.
133
133
  @end
134
134
 
@@ -143,7 +143,7 @@ Load for `check-ci-status` under every tracker provider.
143
143
 
144
144
  1. If `PR_NUMBER` not provided, discover it: `gh pr view --json number --jq '.number' 2>/dev/null`
145
145
  2. If no PR found → output status `NO_PR`, stop
146
- 3. Fetch checks: `gh pr checks \{number\} --json name,state,bucket; echo "exit=$?"` — exit 0, or 8 (checks pending), is a result; any other exit is a failure
146
+ 3. Fetch checks: `gh pr checks {number} --json name,state,bucket; echo "exit=$?"` — exit 0, or 8 (checks pending), is a result; any other exit is a failure
147
147
  4. If the result is `[]`, or the failure says `no checks reported` → output status `NO_CI`; any other failure → output status `INDETERMINATE`
148
148
  5. Classify by `bucket` in priority order: any `pending` → `PENDING`; else any `fail` or `cancel` → `FAILING`; else every check `pass` or `skipping` with at least one `pass` → `PASSING`; else → `INDETERMINATE`
149
149
  6. List failing/pending checks with names
@@ -166,7 +166,7 @@ Load for `fetch-review-threads` under every tracker provider.
166
166
  - (PRIMARY) First comment body contains `<!-- devflow:` marker, OR
167
167
  - (SECONDARY) VIEWER_LOGIN matches thread author login AND first comment body does not appear to be a code-style review comment
168
168
  4. For each remaining external unresolved thread, create an `ext-*` record:
169
- - `id`: `ext-\{sequential-number\}` (e.g., `ext-1`, `ext-2`, ...)
169
+ - `id`: `ext-{sequential-number}` (e.g., `ext-1`, `ext-2`, ...)
170
170
  - `thread_id`: the GraphQL thread `id` (for reply/resolve mutations)
171
171
  - `file`: `path` field
172
172
  - `line`: `line` field
@@ -195,15 +195,15 @@ Each `THREAD_MAP` entry carries one verdict:
195
195
 
196
196
  Rate limits: this op fans out, so read the remaining-budget rungs in `references/github-api.md` before the first iteration.
197
197
 
198
- For each `ext-\{N\}` in THREAD_MAP (sequentially, ≤50, 1s between operations). `fetch-review-threads`
198
+ For each `ext-{N}` in THREAD_MAP (sequentially, ≤50, 1s between operations). `fetch-review-threads`
199
199
  returns up to 100 threads, so a busy PR can exceed this bound: process the first 50 in THREAD_MAP
200
- order and report the remainder as `TRUNCATED (\{n\} threads beyond the ≤50 bound)` — never report
200
+ order and report the remainder as `TRUNCATED ({n} threads beyond the ≤50 bound)` — never report
201
201
  `COMPLETE` while threads went untouched, since `check-merge-readiness` will otherwise show them as
202
202
  unexplained unresolved threads.
203
203
  1. Compose reply based on verdict:
204
- - **FIXED**: `This has been addressed in commit [\{sha\}](https://github.com/\{owner\}/\{repo\}/pull/\{PR_NUMBER\}/commits/\{commit_sha\}). Note: line references may shift on rebase. Resolved automatically by devflow (verification: PASS, commit \{sha\}).`
205
- - **FALSE_POSITIVE**: `After investigation, this appears to be a false positive: \{evidence\}. No code change needed.`
206
- - **BY_DESIGN**: `This is intentional: \{evidence\}. No code change needed.`
204
+ - **FIXED**: `This has been addressed in commit [{sha}](https://github.com/{owner}/{repo}/pull/{PR_NUMBER}/commits/{commit_sha}). Note: line references may shift on rebase. Resolved automatically by devflow (verification: PASS, commit {sha}).`
205
+ - **FALSE_POSITIVE**: `After investigation, this appears to be a false positive: {evidence}. No code change needed.`
206
+ - **BY_DESIGN**: `This is intentional: {evidence}. No code change needed.`
207
207
  - **ESCALATED**: `This thread has been escalated for human review and recorded in the resolution summary.`
208
208
  - Reply bodies MUST NOT contain verbatim content from the external thread body — cite only internal evidence (commit SHAs, file:line from this codebase, ADR IDs)
209
209
  2. Write reply to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit → DEGRADED for that thread, continue per D4. Post reply via `addPullRequestReviewThreadReply` GraphQL mutation with `-F body=@"$DEVFLOW_BODY"` (file-ref form).
@@ -223,35 +223,35 @@ Load for `post-resolution-summary` under every tracker provider.
223
223
 
224
224
  1. Check for existing marker (author-filtered — a third party posting the marker string must not suppress devflow's comment):
225
225
  - Fetch viewer login: `gh api user --jq '.login'` → store as VIEWER_LOGIN
226
- - `gh pr view \{PR_NUMBER\} --json comments --jq '[.comments[] | select(.author.login == "'"$VIEWER_LOGIN"'")] | .[].body'`
227
- - Search the viewer-authored bodies only for the exact key `<!-- devflow:resolution-summary ts:\{RESOLUTION_TS\} -->`
228
- - If found: skip — report `Skipped: already posted for ts:\{RESOLUTION_TS\}`
226
+ - `gh pr view {PR_NUMBER} --json comments --jq '[.comments[] | select(.author.login == "'"$VIEWER_LOGIN"'")] | .[].body'`
227
+ - Search the viewer-authored bodies only for the exact key `<!-- devflow:resolution-summary ts:{RESOLUTION_TS} -->`
228
+ - If found: skip — report `Skipped: already posted for ts:{RESOLUTION_TS}`
229
229
  2. Load `references/publication-gate.md` and resolve `REVIEW_PUBLICATION` by its step 2; `off` ends the op without posting.
230
230
  3. Probe (if mode not yet determined): `gh repo view --json visibility --jq '.visibility'`, case-insensitive. `PRIVATE`/`INTERNAL` → FULL; `PUBLIC` → `STUB (public repository)`; empty output, an error or any other value → `STUB (visibility undeterminable)`. **Fail-closed: on any error or unrecognised value, treat as PUBLIC (mode STUB).**
231
231
  4. Read `RESOLUTION_SUMMARY_PATH` — repo-relative; read it under `WORKTREE_PATH` (else cwd).
232
- 5. Compose body (where `\{TS\}` = `RESOLUTION_TS`):
232
+ 5. Compose body (where `{TS}` = `RESOLUTION_TS`):
233
233
  - **FULL mode:**
234
234
  ```
235
- <!-- devflow:resolution-summary ts:\{TS\} -->
236
- \{full content of resolution-summary.md\}
235
+ <!-- devflow:resolution-summary ts:{TS} -->
236
+ {full content of resolution-summary.md}
237
237
 
238
238
  ---
239
239
  *Posted by [devflow](https://github.com/dean0x/devflow)*
240
240
  ```
241
241
  - **STUB mode** (excluded: finding titles, file:line references, Blocking/Escalations/Third-Party/Verification sections):
242
242
  ```
243
- <!-- devflow:resolution-summary ts:\{TS\} -->
243
+ <!-- devflow:resolution-summary ts:{TS} -->
244
244
  ## Resolution Summary
245
245
 
246
246
  Full summary withheld (public repository).
247
247
 
248
- \{counts-by-severity table verbatim from local artifact; if unparseable: "Counts unavailable — see the local artifact."\}
248
+ {counts-by-severity table verbatim from local artifact; if unparseable: "Counts unavailable — see the local artifact."}
249
249
 
250
- Full report: \{RESOLUTION_SUMMARY_PATH\} (not committed; ask the author)
250
+ Full report: {RESOLUTION_SUMMARY_PATH} (not committed; ask the author)
251
251
  *Posted by [devflow](https://github.com/dean0x/devflow)*
252
252
  ```
253
- Cap body at 60000 characters (GitHub rejects over 65536 with a 422, which the 4xx rule would silently skip); truncate lowest-value sections first (Suggestions, then Pre-existing), keeping the counts table and every Blocking entry; end with `…truncated — full report in the local review artifact \{RESOLUTION_SUMMARY_PATH\} (not committed; ask the author)`.
254
- 6. Write body to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit or missing script → DO NOT POST. Re-check the 60000-char cap on the scrubbed body (redaction may grow it; truncate at a line boundary below 59,800 chars, keeping the truncation pointer sentence; if truncation fires here: emit `NOTE: body exceeded 60k after redaction — truncated/stub posted` in op output and prepend that notice to the body). Post: `gh pr comment \{PR_NUMBER\} --body-file "$DEVFLOW_BODY"`.
253
+ Cap body at 60000 characters (GitHub rejects over 65536 with a 422, which the 4xx rule would silently skip); truncate lowest-value sections first (Suggestions, then Pre-existing), keeping the counts table and every Blocking entry; end with `…truncated — full report in the local review artifact {RESOLUTION_SUMMARY_PATH} (not committed; ask the author)`.
254
+ 6. Write body to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit or missing script → DO NOT POST. Re-check the 60000-char cap on the scrubbed body (redaction may grow it; truncate at a line boundary below 59,800 chars, keeping the truncation pointer sentence; if truncation fires here: emit `NOTE: body exceeded 60k after redaction — truncated/stub posted` in op output and prepend that notice to the body). Post: `gh pr comment {PR_NUMBER} --body-file "$DEVFLOW_BODY"`.
255
255
  7. On 5xx: retry once. If still 5xx: `TRACEABILITY: DEGRADED (5xx on post-resolution-summary)`, warn, return.
256
256
  @end
257
257
 
@@ -264,22 +264,22 @@ Load for `check-merge-readiness` under every tracker provider.
264
264
 
265
265
  ### Process
266
266
 
267
- 1. Fetch unresolved review threads via GraphQL: `reviewThreads(first: 100) \{ nodes \{ isResolved \} totalCount \}`. Count unresolved from nodes (`isResolved == false`). If `totalCount > 100`, report the unresolved count as approximate: prefix with `>` and note `(count approximate — PR has more than 100 threads)`.
268
- 2. Fetch PR review decision: `gh pr view \{PR_NUMBER\} --json reviewDecision --jq '.reviewDecision'`
267
+ 1. Fetch unresolved review threads via GraphQL: `reviewThreads(first: 100) { nodes { isResolved } totalCount }`. Count unresolved from nodes (`isResolved == false`). If `totalCount > 100`, report the unresolved count as approximate: prefix with `>` and note `(count approximate — PR has more than 100 threads)`.
268
+ 2. Fetch PR review decision: `gh pr view {PR_NUMBER} --json reviewDecision --jq '.reviewDecision'`
269
269
  - Values: `APPROVED`, `CHANGES_REQUESTED`, `REVIEW_REQUIRED`, or null
270
270
  3. Fetch CI status (same logic as `check-ci-status`)
271
271
  Those steps are in `references/pr/check-ci-status.md` — load it and apply them to this `PR_NUMBER`, every arm unchanged.
272
- 4. Read the test-plan evidence at the current head, from `WORKTREE_PATH` (else cwd): `node "$\{DEVFLOW_DIR:-$HOME/.devflow\}/scripts/verify-evidence.cjs" verify --pr \{PR_NUMBER\} --approval; echo "exit=$?"`. The evidence is *known* only on `exit=0` with stdout exactly one `EVIDENCE pr:\{PR_NUMBER\} …` line; otherwise it is *unknown*. From it read `total`, `VERIFIED-CI`, `ATTESTED-LOCAL` (report the two apart), `exceptions` and `approval`; *verified* = `VERIFIED-CI` + `ATTESTED-LOCAL`, never inferred from an absent field. The script re-derives every state at the head and decides `approval` by the trust rule; it prints nothing it read from the PR.
272
+ 4. Read the test-plan evidence at the current head, from `WORKTREE_PATH` (else cwd): `node "$HOME/.devflow/scripts/verify-evidence.cjs" verify --pr {PR_NUMBER} --approval; echo "exit=$?"`. The evidence is *known* only on `exit=0` with stdout exactly one `EVIDENCE pr:{PR_NUMBER} …` line; otherwise it is *unknown*. From it read `total`, `VERIFIED-CI`, `ATTESTED-LOCAL` (report the two apart), `exceptions` and `approval`; *verified* = `VERIFIED-CI` + `ATTESTED-LOCAL`, never inferred from an absent field. The script re-derives every state at the head and decides `approval` by the trust rule; it prints nothing it read from the PR.
273
273
  5. Classify (first matching rule wins):
274
- - `NOT_READY (unresolved threads: \{n\})` — unresolved_threads > 0
274
+ - `NOT_READY (unresolved threads: {n})` — unresolved_threads > 0
275
275
  - `NOT_READY (changes requested)` — reviewDecision == `CHANGES_REQUESTED`
276
- - `NOT_READY (CI failing: \{checks\})` — ci_status == `FAILING`
276
+ - `NOT_READY (CI failing: {checks})` — ci_status == `FAILING`
277
277
  - `NOT_READY (CI pending)` — ci_status == `PENDING` (expected after a push; non-alarming)
278
278
  - `NOT_READY (no approving review)` — reviewDecision == `REVIEW_REQUIRED` or null
279
279
  - `NOT_READY (test-plan evidence unavailable)` — the evidence is unknown
280
280
  - `NOT_READY (no non-author approval)` — only when `REQUIRE_NON_AUTHOR_APPROVAL` is `true` and the evidence's `approval` is not `yes`
281
281
  - `NOT_READY (no test-plan evidence)` — `total` == 0 and `exceptions` has no `test-plan`
282
- - `NOT_READY (test plan: \{v\}/\{t\} verified)` — *verified* < `total`, whatever `exceptions` holds
282
+ - `NOT_READY (test plan: {v}/{t} verified)` — *verified* < `total`, whatever `exceptions` holds
283
283
  - `READY` — only when all hold: unresolved_threads == 0 and not approximate; reviewDecision == `APPROVED`; ci_status == `PASSING` or `NO_CI`; the evidence is known; `approval` is `yes` or `REQUIRE_NON_AUTHOR_APPROVAL` is `false`; *verified* == `total` ≥ 1, or `total` == 0 and `exceptions` has `test-plan`
284
284
  - `NOT_READY (status unknown)` — anything else (an `INDETERMINATE` CI status, an approximate thread count, an unrecognised value)
285
285
 
@@ -295,37 +295,37 @@ Load for `update-pr-evidence` under every tracker provider.
295
295
 
296
296
  ### Process
297
297
 
298
- Run steps 1–4 as ONE Bash invocation from `WORKTREE_PATH` (else cwd) — the trap removes `$S` and the temp files when it exits — with `V="$\{DEVFLOW_DIR:-$HOME/.devflow\}/scripts"`.
298
+ Run steps 1–4 as ONE Bash invocation from `WORKTREE_PATH` (else cwd) — the trap removes `$S` and the temp files when it exits — with `V="$HOME/.devflow/scripts"`.
299
299
 
300
- 1. **Verify.** Set `S=`, arm the D11 trap with `[ -n "$S" ] && \{ rm -- "$S/base" "$S/base.sha256"; rmdir -- "$S"; \} 2>/dev/null` added before its `exit`, then the four `mktemp`s and `S="$(mktemp -d)"`. Run `node "$V/verify-evidence.cjs" verify --pr \{PR_NUMBER\} --state "$S" --block-out "$DEVFLOW_NOTES_RAW" --comment-out "$DEVFLOW_BODY_RAW"`, adding `--publication \{REVIEW_PUBLICATION\}` only when that is `auto`, `full`, `off` or `stub`, and `--evidence "\{EVIDENCE_FILE\}"` when given — only a value matching `^[A-Za-z0-9._/-]\{1,255\}$` reaches the shell; any other is a failed run. Continue only on exit 0 with stdout exactly one line, `EVIDENCE pr:<n> head:<sha> total:<n> VERIFIED-CI:<n> ATTESTED-LOCAL:<n> UNVERIFIED:<n> STALE:<n> FAILED:<n> INDETERMINATE:<n> stale:<ids|none> exceptions:<kinds|none> approval:<yes|no|unchecked> key:<hex> posted:<yes|no|n/a> body:<same|changed>`; otherwise emit `TRACEABILITY: DEGRADED (evidence unavailable)` and stop. The script makes this op's one `gh pr view` read and prints nothing it read from the PR; never read the PR another way.
301
- 2. **Body**, only on `body:changed` (else `UNCHANGED`): `node "$V/redact-secrets.cjs" "$DEVFLOW_NOTES_RAW" "$DEVFLOW_NOTES" && node "$V/verify-evidence.cjs" splice --pr \{PR_NUMBER\} --state "$S" --block "$DEVFLOW_NOTES" --out "$DEVFLOW_BODY" && gh pr edit \{PR_NUMBER\} --body-file "$DEVFLOW_BODY"`. Only the composed block is scrubbed (D11); every byte outside its markers is the PR's own and stays identical. `splice` re-reads the body: unchanged since step 1 → it writes; changed → it splices once onto the fresh body and re-reads; changed again → `SPLICE conflict`: `SKIPPED` and `TRACEABILITY: DEGRADED (concurrent edit)`. Any other non-zero → `DEGRADED (\{reason\})`, naming a printed `SPLICE` token. No edit either way; go to step 4.
302
- 3. **Read back** after an edit: `node "$V/verify-evidence.cjs" readback --pr \{PR_NUMBER\} --expect "$DEVFLOW_BODY"`; anything but `READBACK ok` → `TRACEABILITY: DEGRADED (body read-back mismatch)`, else `EDITED`. GitHub has no conditional body edit: a human edit landing between the last re-read and `gh pr edit` is overwritten (it stays in the PR's edit history), and the read-back proves only that these bytes landed.
303
- 4. **Comment.** `posted:yes` → `SKIPPED`; `posted:n/a` → `OFF`. Otherwise apply the Comment-sink scrub (D11): `node "$V/redact-secrets.cjs" "$DEVFLOW_BODY_RAW" "$DEVFLOW_BODY" && gh pr comment \{PR_NUMBER\} --body-file "$DEVFLOW_BODY"` → `POSTED`. Never edit or delete an evidence comment. On 5xx retry once; still 5xx → `DEGRADED (\{reason\})`.
300
+ 1. **Verify.** Set `S=`, arm the D11 trap with `[ -n "$S" ] && { rm -- "$S/base" "$S/base.sha256"; rmdir -- "$S"; } 2>/dev/null` added before its `exit`, then the four `mktemp`s and `S="$(mktemp -d)"`. Run `node "$V/verify-evidence.cjs" verify --pr {PR_NUMBER} --state "$S" --block-out "$DEVFLOW_NOTES_RAW" --comment-out "$DEVFLOW_BODY_RAW"`, adding `--publication {REVIEW_PUBLICATION}` only when that is `auto`, `full`, `off` or `stub`, and `--evidence "{EVIDENCE_FILE}"` when given — only a value matching `^[A-Za-z0-9._/-]{1,255}$` reaches the shell; any other is a failed run. Continue only on exit 0 with stdout exactly one line, `EVIDENCE pr:<n> head:<sha> total:<n> VERIFIED-CI:<n> ATTESTED-LOCAL:<n> UNVERIFIED:<n> STALE:<n> FAILED:<n> INDETERMINATE:<n> stale:<ids|none> exceptions:<kinds|none> approval:<yes|no|unchecked> key:<hex> posted:<yes|no|n/a> body:<same|changed>`; otherwise emit `TRACEABILITY: DEGRADED (evidence unavailable)` and stop. The script makes this op's one `gh pr view` read and prints nothing it read from the PR; never read the PR another way.
301
+ 2. **Body**, only on `body:changed` (else `UNCHANGED`): `node "$V/redact-secrets.cjs" "$DEVFLOW_NOTES_RAW" "$DEVFLOW_NOTES" && node "$V/verify-evidence.cjs" splice --pr {PR_NUMBER} --state "$S" --block "$DEVFLOW_NOTES" --out "$DEVFLOW_BODY" && gh pr edit {PR_NUMBER} --body-file "$DEVFLOW_BODY"`. Only the composed block is scrubbed (D11); every byte outside its markers is the PR's own and stays identical. `splice` re-reads the body: unchanged since step 1 → it writes; changed → it splices once onto the fresh body and re-reads; changed again → `SPLICE conflict`: `SKIPPED` and `TRACEABILITY: DEGRADED (concurrent edit)`. Any other non-zero → `DEGRADED ({reason})`, naming a printed `SPLICE` token. No edit either way; go to step 4.
302
+ 3. **Read back** after an edit: `node "$V/verify-evidence.cjs" readback --pr {PR_NUMBER} --expect "$DEVFLOW_BODY"`; anything but `READBACK ok` → `TRACEABILITY: DEGRADED (body read-back mismatch)`, else `EDITED`. GitHub has no conditional body edit: a human edit landing between the last re-read and `gh pr edit` is overwritten (it stays in the PR's edit history), and the read-back proves only that these bytes landed.
303
+ 4. **Comment.** `posted:yes` → `SKIPPED`; `posted:n/a` → `OFF`. Otherwise apply the Comment-sink scrub (D11): `node "$V/redact-secrets.cjs" "$DEVFLOW_BODY_RAW" "$DEVFLOW_BODY" && gh pr comment {PR_NUMBER} --body-file "$DEVFLOW_BODY"` → `POSTED`. Never edit or delete an evidence comment. On 5xx retry once; still 5xx → `DEGRADED ({reason})`.
304
304
  @end
305
305
 
306
306
  <!-- op: ensure-pr-ready -->
307
- {ensure_pr_ready()}
307
+ {{ensure_pr_ready()}}
308
308
 
309
309
  <!-- op: validate-branch -->
310
- {validate_branch()}
310
+ {{validate_branch()}}
311
311
 
312
312
  <!-- op: post-review-summary -->
313
- {post_review_summary()}
313
+ {{post_review_summary()}}
314
314
 
315
315
  <!-- op: check-ci-status -->
316
- {check_ci_status()}
316
+ {{check_ci_status()}}
317
317
 
318
318
  <!-- op: fetch-review-threads -->
319
- {fetch_review_threads()}
319
+ {{fetch_review_threads()}}
320
320
 
321
321
  <!-- op: resolve-review-threads -->
322
- {resolve_review_threads()}
322
+ {{resolve_review_threads()}}
323
323
 
324
324
  <!-- op: post-resolution-summary -->
325
- {post_resolution_summary()}
325
+ {{post_resolution_summary()}}
326
326
 
327
327
  <!-- op: check-merge-readiness -->
328
- {check_merge_readiness()}
328
+ {{check_merge_readiness()}}
329
329
 
330
330
  <!-- op: update-pr-evidence -->
331
- {update_pr_evidence()}
331
+ {{update_pr_evidence()}}
@@ -18,7 +18,7 @@ that reason.
18
18
  @define decision_markers():
19
19
  ## Decision Markers
20
20
 
21
- The `D\{N\}` labels used throughout the Git agent. **D4 (degradation contract) and
21
+ The `D{N}` labels used throughout the Git agent. **D4 (degradation contract) and
22
22
  D11 (comment-sink scrub) are NOT here** — their definitions stay inline in the
23
23
  agent, because they are the only two whose controls every spawn must already have
24
24
  loaded before it can act. The rest are glossary entries: a reader consults them to
@@ -62,15 +62,15 @@ already written, and never overwrites it.
62
62
  - Branches: `git branch -r --format='%(refname:short)' | head -50` — detect prefix/separator patterns
63
63
  - Tags: `git tag --sort=-version:refname | head -20` — detect version name patterns (e.g., `v1.2.3`, `1.2.3`)
64
64
  - Merged PR titles: `gh pr list --state merged --limit 30 --json title --jq '.[].title'` — detect PR title convention
65
- - Integration branch: of the ≤5 candidates `main`, `master`, `develop`, `integration`, `trunk`, whichever exists on the remote with the most merge commits — one `git rev-list --count --merges --max-count=200 origin/\{candidate\}` per candidate (bounded to 200 merges — sufficient for heuristic ordering), at most 5 commands.
65
+ - Integration branch: of the ≤5 candidates `main`, `master`, `develop`, `integration`, `trunk`, whichever exists on the remote with the most merge commits — one `git rev-list --count --merges --max-count=200 origin/{candidate}` per candidate (bounded to 200 merges — sufficient for heuristic ordering), at most 5 commands.
66
66
  3. For each section, apply heuristics with a 50% majority rule. If no clear pattern: apply compliance defaults:
67
- - Branch Naming: `\{type\}/\{description\}` (types: feat/fix/docs/refactor/chore)
68
- - PR Titles: `\{type\}(\{scope\}): \{description\}` (conventional commits)
69
- - Version PR Titles: `chore(release): v\{version\}`
70
- - Version Names: `v\{semver\}` (e.g., `v1.2.3`)
67
+ - Branch Naming: `{type}/{description}` (types: feat/fix/docs/refactor/chore)
68
+ - PR Titles: `{type}({scope}): {description}` (conventional commits)
69
+ - Version PR Titles: `chore(release): v{version}`
70
+ - Version Names: `v{semver}` (e.g., `v1.2.3`)
71
71
  - Branching Model: trunk-based (main as integration branch)
72
- 4. Write `.devflow/conventions.md`. Every `\{...\}` below is a **pattern shape written in
73
- placeholder tokens** (`\{type\}`, `\{description\}`, `\{scope\}`, `\{semver\}`) — never a
72
+ 4. Write `.devflow/conventions.md`. Every `{...}` below is a **pattern shape written in
73
+ placeholder tokens** (`{type}`, `{description}`, `{scope}`, `{semver}`) — never a
74
74
  verbatim scanned branch name, tag or PR title. Illustrative examples must be
75
75
  synthesized from the placeholder tokens (e.g. `feat/add-login`), never lifted from the
76
76
  scan. If a convention cannot be expressed as a shape, write the step-3 default rather
@@ -79,19 +79,19 @@ already written, and never overwrites it.
79
79
  # Project Conventions
80
80
 
81
81
  ## Branch Naming
82
- \{detected or default pattern and examples\}
82
+ {detected or default pattern and examples}
83
83
 
84
84
  ## PR Titles
85
- \{detected or default pattern and examples\}
85
+ {detected or default pattern and examples}
86
86
 
87
87
  ## Version PR Titles
88
- \{detected or default pattern and examples\}
88
+ {detected or default pattern and examples}
89
89
 
90
90
  ## Version Names
91
- \{detected or default pattern and examples\}
91
+ {detected or default pattern and examples}
92
92
 
93
93
  ## Branching Model
94
- \{detected branching model description\}
94
+ {detected branching model description}
95
95
  ```
96
96
  5. Post-composition verification: after composing the file content in step 4 and before writing it to disk, scan the composed content against the raw strings collected in step 2 (branch names, tag names, PR titles). Assert that no output line reproduces any scanned string verbatim (shape-derived patterns only). If a match is found, replace that line with the step-3 generic default for that section and note the substitution in the op's output under `### Substitutions`. If no matches are found, write the file.
97
97
  @end
@@ -117,19 +117,19 @@ Applies to **`post-review-summary` and `post-resolution-summary` only.** No othe
117
117
 
118
118
  Who counts as a trusted author of a PR comment, review thread or review. `fetch-review-threads` applies it to a thread's first comment; the evidence scripts implement it (`trust()` in `pr-evidence.cjs`) for the evidence record and the approval check, and a test holds both to these terms.
119
119
 
120
- - **Trusted:** `VIEWER_LOGIN` always; otherwise only when `authorAssociation` is `OWNER`, `MEMBER` or `COLLABORATOR` **and** `gh api "repos/\{owner\}/\{repo\}/collaborators/\{login\}/permission" --jq .permission` prints `admin` or `write`. The `maintain` role prints `write`; `triage` and `read` print `read`. Any other association is untrusted, with no lookup; any other output, a 404 or an error is untrusted.
121
- - **The association arm never trusts:** a login ending in `[bot]`; a login outside `^[A-Za-z0-9][A-Za-z0-9-]\{0,38\}$`; the PR author when `isCrossRepository` is true or unknown.
120
+ - **Trusted:** `VIEWER_LOGIN` always; otherwise only when `authorAssociation` is `OWNER`, `MEMBER` or `COLLABORATOR` **and** `gh api "repos/{owner}/{repo}/collaborators/{login}/permission" --jq .permission` prints `admin` or `write`. The `maintain` role prints `write`; `triage` and `read` print `read`. Any other association is untrusted, with no lookup; any other output, a 404 or an error is untrusted.
121
+ - **The association arm never trusts:** a login ending in `[bot]`; a login outside `^[A-Za-z0-9][A-Za-z0-9-]{0,38}$`; the PR author when `isCrossRepository` is true or unknown.
122
122
  - **Bounded:** look up only a login the association arm can still trust, each at most once per spawn and at most 20 in total; a login past the cap is untrusted. `VIEWER_LOGIN` is never looked up.
123
123
  @end
124
124
 
125
125
  <!-- op: decision-markers -->
126
- {decision_markers()}
126
+ {{decision_markers()}}
127
127
 
128
128
  <!-- op: learn-conventions -->
129
- {learn_conventions()}
129
+ {{learn_conventions()}}
130
130
 
131
131
  <!-- op: publication-gate -->
132
- {publication_gate()}
132
+ {{publication_gate()}}
133
133
 
134
134
  <!-- op: trust-rule -->
135
- {trust_rule()}
135
+ {{trust_rule()}}
@@ -79,7 +79,7 @@ hoisting it would re-indent one of the three, and indentation is list grammar
79
79
  rather than whitespace here (PF-063). Normalising those lists is its own edit.
80
80
 
81
81
  @define state_batch_line(subject, subject_ref, ref_token):
82
- 2b. Render each issue's {subject} as a `**State**: \{state\}` line of its own, between that issue's `### Issue {ref_token}:` heading and its `<untrusted-issue-body>` marker — OUTSIDE the wrapper, because {subject_ref} is an enum the tracker computed, not remote prose. A caller refreshing a batch reads it to see a ticket closed out of band.
82
+ 2b. Render each issue's {{subject}} as a `**State**: {state}` line of its own, between that issue's `### Issue {{ref_token}}:` heading and its `<untrusted-issue-body>` marker — OUTSIDE the wrapper, because {{subject_ref}} is an enum the tracker computed, not remote prose. A caller refreshing a batch reads it to see a ticket closed out of band.
83
83
  @end
84
84
 
85
85
  @define bare_number_rule():
@@ -87,19 +87,19 @@ A **bare number** ⇒ `TRACEABILITY: DEGRADED (ambiguous issue reference)` — u
87
87
  @end
88
88
 
89
89
  @define ref_preflight_single(provider, grammar, normalise = "", forms = "", tail = ""):
90
- {normalise}Shape-gate it against {grammar}, anchored at both ends{forms}. {bare_number_rule()} Any other shape ⇒ `TRACEABILITY: DEGRADED (issue reference "\{ref\}" does not match {provider} reference grammar)`.{tail}
90
+ {{normalise}}Shape-gate it against {{grammar}}, anchored at both ends{{forms}}. {{bare_number_rule()}} Any other shape ⇒ `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match {{provider}} reference grammar)`.{{tail}}
91
91
  @end
92
92
 
93
93
  @define ref_preflight_list(provider, grammar, normalise = "", forms = "", tail = ""):
94
- {normalise}Pre-flight the list against {grammar}, anchored at both ends{forms}, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "\{ref\}" does not match {provider} reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider \{p\})`{tail}
94
+ {{normalise}}Pre-flight the list against {{grammar}}, anchored at both ends{{forms}}, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match {{provider}} reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider {p})`{{tail}}
95
95
  @end
96
96
 
97
97
  @define ref_preflight_entry(provider, grammar, normalise = "", forms = ""):
98
- {normalise}Every entry of `SHIPPED_ISSUES` must satisfy {grammar}, anchored at both ends of the STRING (a newline fails it){forms} — this provider's grammar is what the entry gate's shape requirement means here, and the anchored form is what keeps a ref out of a query or a command. **Drop** every entry that fails and report it as `TRACEABILITY: DEGRADED (issue reference "\{ref\}" does not match {provider} reference grammar)`.
98
+ {{normalise}}Every entry of `SHIPPED_ISSUES` must satisfy {{grammar}}, anchored at both ends of the STRING (a newline fails it){{forms}} — this provider's grammar is what the entry gate's shape requirement means here, and the anchored form is what keeps a ref out of a query or a command. **Drop** every entry that fails and report it as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match {{provider}} reference grammar)`.
99
99
  @end
100
100
 
101
101
  @define ref_preflight_branch(match_phrase):
102
- extract the segment {match_phrase} and verify it with the *fetch by key* capability. If the call fails, or the issue is not open, **skip silently** — never render a link for an unverified reference. A branch name can carry a token that merely looks like one, and the existence check is the guard.
102
+ extract the segment {{match_phrase}} and verify it with the *fetch by key* capability. If the call fails, or the issue is not open, **skip silently** — never render a link for an unverified reference. A branch name can carry a token that merely looks like one, and the existence check is the guard.
103
103
  @end
104
104
 
105
105
  @define traceable_issue_rules():
@@ -129,15 +129,15 @@ extract the segment {match_phrase} and verify it with the *fetch by key* capabil
129
129
  @end
130
130
 
131
131
  @define handoff_values(id_form, link_form):
132
- **Handoff Values:** `Issue ID` = {id_form}; `PR link line` = `{link_form}`.
132
+ **Handoff Values:** `Issue ID` = {{id_form}}; `PR link line` = `{{link_form}}`.
133
133
  @end
134
134
 
135
135
  @define last_release_tag_step():
136
- 1a. **Last release tag.** Step 1's `git describe` can return a non-release marker tag. From `WORKTREE_PATH` (else cwd), run `node "$\{DEVFLOW_DIR:-$HOME/.devflow\}/scripts/release-trace.cjs" last-tag`: `LAST_TAG <tag>` ⇒ that tag is `\{last_tag\}`; `LAST_TAG none` ⇒ step 1's initial-commit rule; anything else ⇒ keep step 1's tag and report status `INDETERMINATE (last release tag unresolved)`.
136
+ 1a. **Last release tag.** Step 1's `git describe` can return a non-release marker tag. From `WORKTREE_PATH` (else cwd), run `node "$HOME/.devflow/scripts/release-trace.cjs" last-tag`: `LAST_TAG <tag>` ⇒ that tag is `{last_tag}`; `LAST_TAG none` ⇒ step 1's initial-commit rule; anything else ⇒ keep step 1's tag and report status `INDETERMINATE (last release tag unresolved)`.
137
137
  @end
138
138
 
139
139
  @define trace_map_step(args, arm):
140
- 6. **Per-commit trace map.** {arm}From `WORKTREE_PATH` (else cwd), run `node "$\{DEVFLOW_DIR:-$HOME/.devflow\}/scripts/release-trace.cjs" map --from \{last_tag\} {args}; echo "exit=$?"`. Accept only `exit=0` after a first line `TRACE from:<ref> scanned:<n> traced:<n> untraced:<n> exempt:<n> unmatched:<n> bound:<ok|hit>`; copy every line above `exit=0` verbatim under `### TRACE_MAP`. Anything else ⇒ `TRACEABILITY: DEGRADED (trace map unavailable)`, `### TRACE_MAP` = `(unavailable)`, status `INDETERMINATE (trace map unavailable)`. `bound:hit` ⇒ status `INDETERMINATE (trace scan bound 500 hit)`. An `INDETERMINATE` status outranks every other.
140
+ 6. **Per-commit trace map.** {{arm}}From `WORKTREE_PATH` (else cwd), run `node "$HOME/.devflow/scripts/release-trace.cjs" map --from {last_tag} {{args}}; echo "exit=$?"`. Accept only `exit=0` after a first line `TRACE from:<ref> scanned:<n> traced:<n> untraced:<n> exempt:<n> unmatched:<n> bound:<ok|hit>`; copy every line above `exit=0` verbatim under `### TRACE_MAP`. Anything else ⇒ `TRACEABILITY: DEGRADED (trace map unavailable)`, `### TRACE_MAP` = `(unavailable)`, status `INDETERMINATE (trace map unavailable)`. `bound:hit` ⇒ status `INDETERMINATE (trace scan bound 500 hit)`. An `INDETERMINATE` status outranks every other.
141
141
  @end
142
142
 
143
143
  @export state_batch_line