dev-loops 0.6.0 → 0.7.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 (172) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/agents/review.md +2 -1
  4. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  5. package/.claude/commands/loop-continue.md +15 -0
  6. package/.claude/commands/loop-enqueue.md +22 -0
  7. package/.claude/commands/loop-grill.md +17 -0
  8. package/.claude/commands/{info.md → loop-info.md} +2 -2
  9. package/.claude/commands/loop-queue-status.md +24 -0
  10. package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
  11. package/.claude/commands/{start.md → loop-start.md} +1 -1
  12. package/.claude/commands/{status.md → loop-status.md} +1 -1
  13. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  14. package/.claude/hooks/_hook-decisions.mjs +138 -15
  15. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  16. package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
  17. package/.claude/skills/dev-loop/SKILL.md +15 -11
  18. package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
  19. package/.claude/skills/docs/anti-patterns.md +2 -1
  20. package/.claude/skills/docs/artifact-authority-contract.md +22 -4
  21. package/.claude/skills/docs/copilot-loop-operations.md +1 -1
  22. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  23. package/.claude/skills/docs/issue-intake-procedure.md +1 -0
  24. package/.claude/skills/docs/local-planning-flow.md +1 -1
  25. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  26. package/.claude/skills/docs/merge-preconditions.md +17 -1
  27. package/.claude/skills/docs/plan-file-contract.md +1 -1
  28. package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
  29. package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
  30. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  31. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  32. package/.claude/skills/local-implementation/SKILL.md +25 -14
  33. package/.claude/skills/loop-grill/SKILL.md +163 -0
  34. package/AGENTS.md +1 -0
  35. package/CHANGELOG.md +95 -0
  36. package/README.md +9 -9
  37. package/agents/dev-loop.agent.md +3 -2
  38. package/agents/developer.agent.md +1 -1
  39. package/agents/docs.agent.md +1 -1
  40. package/agents/fixer.agent.md +1 -1
  41. package/agents/quality.agent.md +1 -1
  42. package/agents/refiner.agent.md +1 -1
  43. package/agents/review.agent.md +3 -2
  44. package/cli/index.mjs +35 -42
  45. package/extension/README.md +4 -4
  46. package/extension/checks.ts +1 -5
  47. package/extension/harness-types.ts +1 -0
  48. package/extension/index.ts +6 -0
  49. package/extension/pi-extension-adapter.ts +2 -0
  50. package/extension/presentation.ts +7 -19
  51. package/package.json +9 -10
  52. package/scripts/github/capture-review-threads.mjs +2 -9
  53. package/scripts/github/comment-issue.mjs +2 -9
  54. package/scripts/github/create-label.mjs +133 -0
  55. package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
  56. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  57. package/scripts/github/edit-pr.mjs +259 -0
  58. package/scripts/github/fetch-ci-logs.mjs +2 -9
  59. package/scripts/github/list-issues.mjs +2 -9
  60. package/scripts/github/manage-sub-issues.mjs +46 -10
  61. package/scripts/github/offer-human-handoff.mjs +8 -5
  62. package/scripts/github/post-gate-findings.mjs +14 -2
  63. package/scripts/github/probe-ci-status.mjs +8 -2
  64. package/scripts/github/probe-copilot-review.mjs +21 -14
  65. package/scripts/github/ready-for-review.mjs +26 -8
  66. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  67. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  68. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  69. package/scripts/github/request-copilot-review.mjs +8 -2
  70. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  71. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  72. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  73. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  74. package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
  75. package/scripts/github/verify-fresh-review-context.mjs +150 -31
  76. package/scripts/github/view-pr.mjs +150 -0
  77. package/scripts/github/write-gate-context.mjs +248 -61
  78. package/scripts/github/write-gate-findings-log.mjs +75 -1
  79. package/scripts/lib/jq-output.mjs +18 -0
  80. package/scripts/loop/_post-convergence-change.mjs +211 -0
  81. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  82. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  83. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  84. package/scripts/loop/check-retro-tooling.mjs +14 -3
  85. package/scripts/loop/checkpoint-contract.mjs +7 -4
  86. package/scripts/loop/cleanup-worktree.mjs +12 -3
  87. package/scripts/loop/conductor-monitor.mjs +12 -18
  88. package/scripts/loop/copilot-pr-handoff.mjs +135 -14
  89. package/scripts/loop/debt-remediate.mjs +24 -12
  90. package/scripts/loop/detect-change-scope.mjs +36 -7
  91. package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
  92. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  93. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  94. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  95. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  96. package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
  97. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  98. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  99. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  100. package/scripts/loop/ensure-worktree.mjs +8 -3
  101. package/scripts/loop/info.mjs +12 -4
  102. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  103. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  104. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  105. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  106. package/scripts/loop/inspect-run.mjs +8 -2
  107. package/scripts/loop/outer-loop.mjs +8 -4
  108. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  109. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  110. package/scripts/loop/pre-flight-gate.mjs +9 -9
  111. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  112. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  113. package/scripts/loop/provision-worktree.mjs +74 -3
  114. package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
  115. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  116. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  117. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  118. package/scripts/loop/run-queue.mjs +18 -9
  119. package/scripts/loop/run-refinement-audit.mjs +8 -9
  120. package/scripts/loop/run-watch-cycle.mjs +2 -9
  121. package/scripts/loop/sanctioned-commands.mjs +104 -0
  122. package/scripts/loop/steer-loop.mjs +29 -16
  123. package/scripts/loop/validate-pr-body-spec.mjs +207 -0
  124. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  125. package/scripts/pages/build-site.mjs +59 -8
  126. package/scripts/pages/build-state-atlas.mjs +441 -0
  127. package/scripts/projects/_resolve-project.mjs +148 -0
  128. package/scripts/projects/add-queue-item.mjs +60 -54
  129. package/scripts/projects/archive-done-items.mjs +49 -84
  130. package/scripts/projects/ensure-queue-board.mjs +10 -36
  131. package/scripts/projects/list-queue-items.mjs +116 -65
  132. package/scripts/projects/move-queue-item.mjs +28 -49
  133. package/scripts/projects/reconcile-queue.mjs +253 -0
  134. package/scripts/projects/reorder-queue-item.mjs +41 -47
  135. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  136. package/scripts/projects/sync-item-status.mjs +15 -10
  137. package/scripts/refine/_refine-helpers.mjs +21 -5
  138. package/scripts/refine/exit-spike.mjs +18 -8
  139. package/scripts/refine/promote-plan.mjs +22 -16
  140. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  141. package/scripts/refine/refine-plan-file.mjs +13 -4
  142. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  143. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  144. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  145. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  146. package/scripts/refine/validate-plan-file.mjs +1 -1
  147. package/scripts/refine/validate-spike-file.mjs +1 -1
  148. package/scripts/refine/verify.mjs +11 -6
  149. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  150. package/scripts/release/extract-changelog-section.mjs +16 -2
  151. package/skills/copilot-pr-followup/SKILL.md +24 -12
  152. package/skills/dev-loop/SKILL.md +10 -6
  153. package/skills/docs/acceptance-criteria-verification.md +6 -2
  154. package/skills/docs/anti-patterns.md +2 -1
  155. package/skills/docs/artifact-authority-contract.md +22 -4
  156. package/skills/docs/copilot-loop-operations.md +1 -1
  157. package/skills/docs/cross-harness-regression-contract.md +60 -0
  158. package/skills/docs/issue-intake-procedure.md +1 -0
  159. package/skills/docs/local-planning-flow.md +1 -1
  160. package/skills/docs/local-planning-worked-example.md +1 -1
  161. package/skills/docs/merge-preconditions.md +17 -1
  162. package/skills/docs/plan-file-contract.md +1 -1
  163. package/skills/docs/public-dev-loop-contract.md +1 -1
  164. package/skills/docs/retrospective-checkpoint-contract.md +90 -76
  165. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  166. package/skills/docs/workflow-handoff-contract.md +39 -0
  167. package/skills/local-implementation/SKILL.md +25 -14
  168. package/skills/loop-grill/SKILL.md +165 -0
  169. package/.claude/commands/continue.md +0 -15
  170. package/scripts/loop/conductor.mjs +0 -233
  171. package/scripts/loop/detect-stale-runner.mjs +0 -265
  172. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -12,9 +12,16 @@
12
12
 
13
13
  import { resolveRunId } from "./_run-context.mjs";
14
14
  import {
15
- isGhPrReadyCommand,
16
- extractPrNumberFromGhPrReady,
17
- extractRepoFlagFromGhPrReady,
15
+ commandContainsGhPrReady,
16
+ commandContainsGhPrMerge,
17
+ commandContainsGhPrCreate,
18
+ extractPrNumberFromGhPrReadyAnywhere,
19
+ extractRepoFlagFromGhPrReadyAnywhere,
20
+ extractPrNumberFromGhPrMergeAnywhere,
21
+ extractRepoFlagFromGhPrMergeAnywhere,
22
+ extractRepoFlagsFromGhPrCreateSegments,
23
+ commandContainsRawExternalWrite,
24
+ extractRepoFlagsFromExternalWriteSegments,
18
25
  TARGET_REPO_SLUG,
19
26
  } from "./_bash-command-classify.mjs";
20
27
 
@@ -26,6 +33,16 @@ import {
26
33
 
27
34
  const ALLOW = Object.freeze({ decision: "allow" });
28
35
 
36
+ /**
37
+ * Whether the command string also invokes an evidence-writing script (findings-log ledger or
38
+ * checkpoint-verdict upsert). Used only to enrich the merge-block message (#1172) — a compound
39
+ * command combining an evidence write with `gh pr merge` is blocked pre-execution, so the write
40
+ * never runs; this substring check has no false-negative cost (worst case: the plain message).
41
+ */
42
+ function commandContainsEvidenceWrite(command) {
43
+ return command.includes("write-gate-findings-log") || command.includes("upsert-checkpoint-verdict");
44
+ }
45
+
29
46
  /**
30
47
  * The agent type (Claude `agent_type` / the canonical agent name) that owns repo mutations.
31
48
  * Only this subagent — not arbitrary subagents (Explore, Plan, generic Task agents) — may
@@ -34,26 +51,114 @@ const ALLOW = Object.freeze({ decision: "allow" });
34
51
  export const DEV_LOOP_AGENT_TYPE = "dev-loop";
35
52
 
36
53
  /**
37
- * Decide whether a PreToolUse Bash command must be blocked by the draft-gate boundary.
54
+ * Decide whether a PreToolUse Bash command must be blocked by a dev-loop gate boundary.
55
+ *
56
+ * Three gated commands on the target repo:
57
+ * - `gh pr create` — blocked outright; PR creation must flow through the canonical wrapper
58
+ * (`scripts/github/create-pr.mjs` / `dev-loops pr create`), which always drafts and
59
+ * self-assigns. Closes the hole where raw `gh pr create` opens a ready PR, bypassing draft-first.
60
+ * - `gh pr ready` — blocked without clean draft_gate evidence (`pre-pr-ready-gate`).
61
+ * - `gh pr merge` — blocked without the full pre-merge gate evidence (`detect-checkpoint-evidence`:
62
+ * clean current-head draft_gate + pre_approval_gate). The loop runs this check before merging;
63
+ * gating it here closes the hole where a hand-run `gh pr merge` skips the pre-approval gate
64
+ * entirely. Everything else passes through.
65
+ * - raw `gh issue create` / `gh issue comment` / `gh pr comment` — blocked ONLY when the call
66
+ * originates from a SUBAGENT context (`agentType` is a non-null string) and targets the repo.
67
+ * Sanctioned external writes flow through node wrappers (gate-verdict comments via
68
+ * `upsert-checkpoint-verdict.mjs`, review replies via `reply-resolve*.mjs`, board sync,
69
+ * `comment-issue.mjs`), whose Bash command string is `node scripts/…` and never matches these
70
+ * raw-`gh` matchers. The MAIN AGENT / operator (agentType null) retains direct `gh issue
71
+ * create` — that path is authorized (#1051).
38
72
  *
39
- * Mirrors the Pi extension's `onUserBash`: the only blocked case is `gh pr ready` for the
40
- * target repo without clean draft_gate evidence. Everything else (including merges, which
41
- * trigger the post-merge step, not a block) is allowed through.
73
+ * The hook computes `gatePassed`/`gateError` from the gate script appropriate to the command kind.
42
74
  *
43
75
  * @param {Object} params
44
76
  * @param {string} params.command - The Bash command string.
45
77
  * @param {string|null} [params.repoSlug] - Resolved owner/name of the cwd repo (null if unknown).
46
- * @param {boolean} [params.gatePassed] - Whether `pre-pr-ready-gate` evidence exists for the PR.
78
+ * @param {boolean} [params.gatePassed] - Whether the relevant gate evidence exists for the PR.
47
79
  * @param {string|null} [params.gateError] - Error detail when the gate guard could not run.
80
+ * @param {string|null} [params.agentType] - Claude `agent_type` from the hook payload; non-null
81
+ * string inside a subagent, null in the main agent. Scopes the external-write guard.
48
82
  * @returns {HookDecision}
49
83
  */
50
- export function decideBashGate({ command, repoSlug = null, gatePassed = false, gateError = null }) {
51
- if (typeof command !== "string" || !isGhPrReadyCommand(command)) {
84
+ export function decideBashGate({ command, repoSlug = null, gatePassed = false, gateError = null, agentType = null }) {
85
+ if (typeof command !== "string") {
86
+ return ALLOW;
87
+ }
88
+ // Subagent-scoped external-write guard: block ad-hoc `gh issue create`/`gh issue comment`/
89
+ // `gh pr comment` on the target repo from a subagent, so external writes flow through the
90
+ // sanctioned node wrappers. The main-agent/operator path (agentType null) is unaffected (#1051).
91
+ if (typeof agentType === "string" && commandContainsRawExternalWrite(command)) {
92
+ const cwdTargets = (repoSlug ?? "").toLowerCase() === TARGET_REPO_SLUG.toLowerCase();
93
+ // Scope PER segment, mirroring the `gh pr create` block: in scope when no explicit --repo and
94
+ // cwd is the target, or an explicit --repo/-R equals the target. An explicit non-target --repo
95
+ // passes through. DENY if ANY external-write segment is in scope.
96
+ const anyWriteInScope = extractRepoFlagsFromExternalWriteSegments(command).some((seg) =>
97
+ seg.explicitRepo == null
98
+ ? cwdTargets
99
+ : seg.explicitRepo.toLowerCase() === TARGET_REPO_SLUG.toLowerCase(),
100
+ );
101
+ if (anyWriteInScope) {
102
+ return {
103
+ decision: "deny",
104
+ reason:
105
+ "Ad-hoc GitHub issue/PR creation and comments from a subagent are blocked. Use the sanctioned " +
106
+ "node wrappers instead — gate-verdict comments via scripts/github/upsert-checkpoint-verdict.mjs, " +
107
+ "review-thread replies via scripts/github/reply-resolve*.mjs, board sync, or scripts/github/comment-issue.mjs. " +
108
+ "Direct `gh issue create` is reserved for the main agent / operator.",
109
+ };
110
+ }
111
+ }
112
+ // Scan ALL shell segments — the PreToolUse gate blocks pre-emptively, so a gated verb in any
113
+ // segment (even after `&&` or `;`) must be caught. This differs from the Pi extension's
114
+ // post-execute `isGhPrReadyCommand`/`isGhPrMergeCommand` which scan only the first segment
115
+ // (correct there: `false && gh pr ready 42` short-circuits so ready never ran).
116
+ const isReady = commandContainsGhPrReady(command);
117
+ const isMerge = commandContainsGhPrMerge(command);
118
+ const isCreate = commandContainsGhPrCreate(command);
119
+ if (!isReady && !isMerge && !isCreate) {
52
120
  return ALLOW;
53
121
  }
54
122
 
123
+ // Raw `gh pr create` is blocked outright on the target repo (no PR number / gate evidence
124
+ // exists yet): PR creation must flow through the canonical wrapper, which always drafts and
125
+ // self-assigns. This closes the draft-first hole where raw `gh pr create` opens a ready PR.
126
+ if (isCreate) {
127
+ const cwdTargets = (repoSlug ?? "").toLowerCase() === TARGET_REPO_SLUG.toLowerCase();
128
+ // Evaluate scope PER create segment, not just the first: a create is in scope when it
129
+ // explicitly targets the repo, or (with no explicit --repo) the cwd is the repo. An explicit
130
+ // `--repo <target>` is denied regardless of cwd (#1047). DENY if ANY create segment is in
131
+ // scope — otherwise a leading out-of-scope create (`gh pr create --repo other/repo`) would
132
+ // short-circuit and shield a later in-scope raw create (`&& gh pr create --fill`).
133
+ const anyCreateInScope = extractRepoFlagsFromGhPrCreateSegments(command).some((seg) =>
134
+ seg.explicitRepo == null
135
+ ? cwdTargets
136
+ : seg.explicitRepo.toLowerCase() === TARGET_REPO_SLUG.toLowerCase(),
137
+ );
138
+ if (anyCreateInScope) {
139
+ return {
140
+ decision: "deny",
141
+ reason:
142
+ "gh pr create blocked: open PRs via the canonical wrapper `node scripts/github/create-pr.mjs` " +
143
+ "(a.k.a. `dev-loops pr create`), which always creates a draft and self-assigns. Raw `gh pr create` " +
144
+ "defaults to ready-for-review and bypasses the draft-first contract (workflow.requireDraftFirst).",
145
+ };
146
+ }
147
+ // The create is out of scope. Only allow outright when there is no ready/merge segment to
148
+ // evaluate — otherwise fall through so a gated `gh pr ready`/`gh pr merge` in the same
149
+ // compound command (e.g. `gh pr create --repo other/repo && gh pr merge 5`) is still gated
150
+ // below rather than short-circuited.
151
+ if (!isReady && !isMerge) {
152
+ return ALLOW;
153
+ }
154
+ }
155
+ // When both verbs appear in a compound command, apply the stricter merge gate — if it passes,
156
+ // the draft_gate (a subset of the pre-merge evidence check) is also satisfied.
157
+ const verb = isMerge ? "gh pr merge" : "gh pr ready";
55
158
  // An explicit `--repo other/repo` that is not the target → not our concern, pass through.
56
- const explicitRepo = extractRepoFlagFromGhPrReady(command);
159
+ const explicitRepo = isMerge
160
+ ? extractRepoFlagFromGhPrMergeAnywhere(command)
161
+ : extractRepoFlagFromGhPrReadyAnywhere(command);
57
162
  if (explicitRepo && explicitRepo.toLowerCase() !== TARGET_REPO_SLUG.toLowerCase()) {
58
163
  return ALLOW;
59
164
  }
@@ -62,23 +167,41 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
62
167
  return ALLOW;
63
168
  }
64
169
 
65
- const prNumber = extractPrNumberFromGhPrReady(command);
170
+ const prNumber = isMerge
171
+ ? extractPrNumberFromGhPrMergeAnywhere(command)
172
+ : extractPrNumberFromGhPrReadyAnywhere(command);
66
173
  if (prNumber === null) {
67
174
  return {
68
175
  decision: "deny",
69
- reason:
70
- "gh pr ready blocked: could not determine the PR number from the command. Include the PR number explicitly.",
176
+ reason: `${verb} blocked: could not determine the PR number from the command. Include the PR number explicitly.`,
71
177
  };
72
178
  }
73
179
 
74
180
  if (gateError) {
181
+ const which = isMerge ? "pre-merge gate" : "draft-gate";
75
182
  return {
76
183
  decision: "deny",
77
- reason: `gh pr ready blocked: draft-gate evidence check failed (${gateError}).`,
184
+ reason: `${verb} blocked: ${which} evidence check failed (${gateError}).`,
78
185
  };
79
186
  }
80
187
 
81
188
  if (!gatePassed) {
189
+ if (isMerge) {
190
+ // This hook evaluates PreToolUse — BEFORE the Bash tool call runs. A compound command that
191
+ // writes gate evidence (findings-log ledger, checkpoint verdict) and merges in the same call
192
+ // is blocked here with the write never having executed, which looks like the evidence
193
+ // "vanished" (#1172). Hint the split when the command carries an evidence-writing invocation
194
+ // alongside the merge, so the failure is self-explaining instead of looking like data loss.
195
+ const alsoWritesEvidence = commandContainsEvidenceWrite(command);
196
+ return {
197
+ decision: "deny",
198
+ reason:
199
+ `gh pr merge blocked: missing pre-merge gate evidence for PR #${prNumber} (need clean current-head draft_gate + pre_approval_gate; inline verdicts are not accepted). Run the dev-loop gates instead of merging directly.` +
200
+ (alsoWritesEvidence
201
+ ? " This command also writes gate evidence, but hooks evaluate before the command runs — write the evidence in a separate call, then merge alone."
202
+ : ""),
203
+ };
204
+ }
82
205
  return {
83
206
  decision: "deny",
84
207
  reason: `gh pr ready blocked: no visible clean draft_gate checkpoint verdict comment found for PR #${prNumber}.`,
@@ -2,18 +2,28 @@
2
2
  /**
3
3
  * PreToolUse Bash gate hook (#773).
4
4
  *
5
- * Reproduces the Pi extension's `onUserBash` draft-gate guard for Claude Code: blocks
6
- * `gh pr ready` for the target repo unless a clean draft_gate checkpoint verdict exists for the
7
- * PR (via scripts/loop/pre-pr-ready-gate.mjs). Merges are NOT blocked here (they trigger the
8
- * post-merge hook). All other commands pass through.
5
+ * Blocks three commands on the target repo; everything else passes through:
6
+ * - `gh pr create` blocked outright (no gate evidence to check): PR creation must flow through
7
+ * the canonical wrapper scripts/github/create-pr.mjs (dev-loops pr create), which always drafts
8
+ * and self-assigns. Closes the hole where raw `gh pr create` opens a ready PR (draft-first breach).
9
+ * - `gh pr ready` — needs a clean draft_gate verdict (via scripts/loop/pre-pr-ready-gate.mjs).
10
+ * - `gh pr merge` — needs full pre-merge evidence (clean current-head draft_gate +
11
+ * pre_approval_gate, via scripts/github/detect-checkpoint-evidence.mjs). This closes the hole
12
+ * where a hand-run merge skips the loop's pre-merge gate check (and thus the pre-approval gate).
13
+ * - raw `gh issue create` / `gh issue comment` / `gh pr comment` — blocked only from a SUBAGENT
14
+ * context (agent_type present); the main agent/operator retains direct issue creation (#1051).
9
15
  */
10
16
  import { execFileSync } from "node:child_process";
11
17
  import path from "node:path";
12
18
 
13
19
  import { decideBashGate } from "./_hook-decisions.mjs";
14
20
  import {
15
- isGhPrReadyCommand,
16
- extractPrNumberFromGhPrReady,
21
+ commandContainsGhPrReady,
22
+ commandContainsGhPrMerge,
23
+ commandContainsGhPrCreate,
24
+ commandContainsRawExternalWrite,
25
+ extractPrNumberFromGhPrReadyAnywhere,
26
+ extractPrNumberFromGhPrMergeAnywhere,
17
27
  normalizeGitHubRepoSlug,
18
28
  TARGET_REPO_SLUG,
19
29
  } from "./_bash-command-classify.mjs";
@@ -22,7 +32,14 @@ import { readHookInput, emitDeny, emitAllow } from "./_hook-io.mjs";
22
32
 
23
33
  const input = readHookInput();
24
34
  const command = input?.tool_input?.command;
25
- if (typeof command !== "string" || !isGhPrReadyCommand(command)) {
35
+ // Claude exposes `agent_type` only inside a subagent; null in the main agent. Scopes the
36
+ // external-write guard (raw `gh issue create` etc. is blocked only from a subagent).
37
+ const agentType = typeof input?.agent_type === "string" ? input.agent_type : null;
38
+ const isReady = typeof command === "string" && commandContainsGhPrReady(command);
39
+ const isMerge = typeof command === "string" && commandContainsGhPrMerge(command);
40
+ const isCreate = typeof command === "string" && commandContainsGhPrCreate(command);
41
+ const isExternalWrite = typeof command === "string" && commandContainsRawExternalWrite(command);
42
+ if (!isReady && !isMerge && !isCreate && !isExternalWrite) {
26
43
  emitAllow();
27
44
  }
28
45
 
@@ -44,12 +61,16 @@ try {
44
61
  let gatePassed = false;
45
62
  let gateError = null;
46
63
  if (repoSlug === TARGET_REPO_SLUG) {
47
- const pr = extractPrNumberFromGhPrReady(command);
64
+ // When both verbs appear (compound command), apply the stricter merge gate.
65
+ const pr = isMerge ? extractPrNumberFromGhPrMergeAnywhere(command) : extractPrNumberFromGhPrReadyAnywhere(command);
48
66
  if (pr !== null && repoRoot) {
49
- // The gate guard script is overridable for deterministic testing (stub instead of the
50
- // network-touching real guard); defaults to the bundled pre-pr-ready-gate.
51
- const gateScript =
52
- process.env.DEVLOOPS_PRE_PR_READY_GATE_SCRIPT || path.join(repoRoot, "scripts/loop/pre-pr-ready-gate.mjs");
67
+ // Each gate script is overridable for deterministic testing (stub instead of the
68
+ // network-touching real guard). `gh pr ready` draft-gate only; `gh pr merge` → the full
69
+ // pre-merge evidence check (draft_gate + pre_approval_gate).
70
+ const gateScript = isMerge
71
+ ? process.env.DEVLOOPS_PRE_MERGE_GATE_SCRIPT ||
72
+ path.join(repoRoot, "scripts/github/detect-checkpoint-evidence.mjs")
73
+ : process.env.DEVLOOPS_PRE_PR_READY_GATE_SCRIPT || path.join(repoRoot, "scripts/loop/pre-pr-ready-gate.mjs");
53
74
  try {
54
75
  execFileSync("node", [gateScript, "--repo", repoSlug, "--pr", String(pr)], {
55
76
  cwd: repoRoot,
@@ -57,16 +78,16 @@ if (repoSlug === TARGET_REPO_SLUG) {
57
78
  });
58
79
  gatePassed = true;
59
80
  } catch (error) {
60
- // Exit 1 from the guard = no clean draft_gate evidence (gatePassed stays false).
81
+ // Exit 1 from the guard = gate evidence missing/insufficient (gatePassed stays false).
61
82
  // A missing/unspawnable guard (no numeric status) = could-not-run → gateError.
62
83
  if (typeof error?.status !== "number") {
63
- gateError = "could not run the draft-gate guard script";
84
+ gateError = "could not run the gate guard script";
64
85
  }
65
86
  }
66
87
  }
67
88
  }
68
89
 
69
- const decision = decideBashGate({ command, repoSlug, gatePassed, gateError });
90
+ const decision = decideBashGate({ command, repoSlug, gatePassed, gateError, agentType });
70
91
  if (decision.decision === "deny") {
71
92
  emitDeny(decision.reason);
72
93
  }
@@ -230,7 +230,8 @@ When unresolved feedback exists, use a narrow follow-up loop:
230
230
  - resolve the review-round cap from config via `resolveRefinementConfig(config, "maxCopilotRounds")` from `@dev-loops/core/config`; default config ships `maxCopilotRounds: 5`
231
231
  - **Opt out entirely:** `maxCopilotRounds: 0` disables the external Copilot review gate for the repo — the loop runs `draft_gate → pre_approval_gate` with the local harness only, never requesting or waiting on Copilot. Use this when the repo has no Copilot reviewer configured or prefers local-harness-only review.
232
232
  - use the completed Copilot review-round count from `detect-copilot-loop-state.mjs` / `copilot-pr-handoff.mjs` as the current PR's review-round count
233
- - if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review
233
+ - if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review within that concluded cycle
234
+ - if the loop already converged and then significant post-convergence changes land on a newer head (new/changed product or test logic, not doc/message/comment-only edits), treat that as a NEW cycle and re-request Copilot review when regular rounds are already > 0 (the prior cycle's cap does not suppress this new-cycle request)
234
235
  - when the round limit is reached **and** the refreshed thread snapshot proves zero unresolved threads **and** current-head CI is green or credibly green, treat that clean state as eligible for `pre_approval_gate` fallback instead of deadlocking on another Copilot rerequest
235
236
  - when using that fallback, add a short round-exhaustion note to the visible `pre_approval_gate` gate evidence so the PR records why no further Copilot rerequest occurred
236
237
  - if the round cap is reached before the PR is thread-clean or before CI is green/credibly green, reply-resolve any remaining intentionally deferred threads with a short `deferred to follow-up` note, then stop and report that the Copilot round limit was reached
@@ -292,7 +293,7 @@ Do NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments.
292
293
  Both gates run this same checkpoint review chain. It is an **agent-orchestrated skill procedure** — a node script cannot spawn the per-angle reviewers, so the conductor agent drives the fan-out and uses the pure `@dev-loops/core/loop/gate-fanin` helpers only for consolidation, batching, and ledger mapping. The cost model is **build once, seed many**: the context-builder script builds ONE deterministic, neutral context bundle (diff + adjacent code) and each independent reviewer is seeded with that identical bundle verbatim instead of re-deriving it (work-dedup; no fork primitive, no Workflow tool). See [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md).
293
294
 
294
295
  1. **Context (Phase 1) — build the neutral bundle ONCE:** build/read the gate-context artifact via `scripts/github/write-gate-context.mjs` (`buildGateContext` resolves the angle set through `resolveGateAnglesDynamic`; mandatory angles always survive). The artifact at `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json` carries the resolved angles + change scope + acceptance-criteria pointer. When the resolver was handed a diff with `diffOutput`, `buildGateContext` also persists the FULL diff to `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.diff` and records `scope.diffPath` (relative) plus `scope.changedFiles` (full repo-relative paths). It ADDITIONALLY builds a deterministic, neutral `adjacentCode` bundle — each changed file plus its 1-hop import in/out-edges (callers/callees/imports), with size guards (skip lockfiles/generated/binary/minified; cap per-file bytes; truncate the long tail) recorded in a `stripped`/`truncated`/`missing` manifest. This single bundle is the build-once seed; reviewers do not re-derive the diff + adjacent code.
295
- 2. **Fan-out (Phase 2) — seed each reviewer with the bundle verbatim:** plan batches with `planFanoutBatches(angles, resolveMaxFanoutReviewers(config))` (default cap 8). Spawn one scoped `review` agent per resolved angle (plain Agent tool), parallel up to the cap, batching any overflow sequentially. When `degraded` is true, record the sequential degradation in the gate evidence. Reviewers are independent: each is seeded with the IDENTICAL neutral bundle (the gate-context artifact's `scope.diffPath` + `adjacentCode`) verbatim plus its single angle, and never inherits the conductor agent's conversation or opinions. Each reviewer follows the [review agent scoped angle-review mode](../../agents/review.agent.md): fresh context, single angle, read-only, writing its per-angle findings artifact to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json`. Brief each reviewer to use the provided bundle as its base and to review like an external code reviewer hunting real bugs: read the FULL diff (from `scope.diffPath`, or `git diff` when null) and the bundled adjacent code (callers, callees, imports) rather than re-deriving them, then review adversarially for concrete defects (edge cases, input validation, numeric coercion incl. NaN/Infinity/floats/negatives, null/undefined, boundary conditions, mismatched caller/callee contracts, dedup/identity bugs) with `file:line` + the failing scenario — not process nits like "no test exists". Reviewers are read-only and MAY widen scope (open adjacent repo files beyond the bundle) only when their angle genuinely needs more; that widening is recorded in the optional `contextWidened` field on each per-angle findings artifact.
296
+ 2. **Fan-out (Phase 2) — seed each reviewer with the bundle verbatim:** plan batches with `planFanoutBatches(angles, cap)` using the `gates.maxFanoutReviewers` cap (default 8). Spawn one scoped `review` agent per resolved angle (plain Agent tool), parallel up to the cap, batching any overflow sequentially. When `degraded` is true, record the sequential degradation in the gate evidence. Reviewers are independent: each is seeded with the IDENTICAL neutral bundle (the gate-context artifact's `scope.diffPath` + `adjacentCode`) verbatim plus its single angle, and never inherits the conductor agent's conversation or opinions. Each reviewer follows the [review agent scoped angle-review mode](../../agents/review.agent.md): fresh context, single angle, read-only, writing its per-angle findings artifact to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json`. Brief each reviewer to use the provided bundle as its base and to review like an external code reviewer hunting real bugs: read the FULL diff (from `scope.diffPath`, or `git diff` when null) and the bundled adjacent code (callers, callees, imports) rather than re-deriving them, then review adversarially for concrete defects (edge cases, input validation, numeric coercion incl. NaN/Infinity/floats/negatives, null/undefined, boundary conditions, mismatched caller/callee contracts, dedup/identity bugs) with `file:line` + the failing scenario — not process nits like "no test exists". Reviewers are read-only and MAY widen scope (open adjacent repo files beyond the bundle) only when their angle genuinely needs more; that widening is recorded in the optional `contextWidened` field on each per-angle findings artifact.
296
297
  3. **Fan-in (Phase 3):** consolidate the per-angle artifacts via `consolidateFanin({ angleResults, blockCleanOnFindingSeverities })` (blocking severities from `resolveGateConfig(config, <configKey>).blockCleanOnFindingSeverities`, where `<configKey>` is the config key for the gate — `draft` for `draft_gate`, `preApproval` for `pre_approval_gate` — not the `draft_gate|pre_approval_gate` artifact name). It yields the gate `verdict` (`clean` when no blocking-severity finding remains, `findings_present` when one does, `blocked` when any per-angle artifact is malformed/missing). Map consolidated findings with `toFindingsLogShape(...)` and write the disposition ledger via `write-gate-findings-log.mjs` **before** the visible comment. The ledger is written regardless of any comment opt-out. Then, when `resolveGatePostFindingsComments(config)` is true (default; opt-out via `gates.postFindingsComments: false`), post the consolidated findings as a visible, marker-tagged PR comment via `node <resolved-skill-scripts>/github/post-gate-findings.mjs --repo <owner/name> --pr <number> --gate <gate> --head-sha <current_head_sha> --findings '<toFindingsLogShape JSON>'` so the findings are auditable and Copilot/humans are aware of them. The helper is idempotent per gate (exactly one comment per gate, updated in place on each run; the reviewed head is shown in the body) and posts a brief "no findings" note when the consolidated set is empty. When `postFindingsComments` is false the helper no-ops with a `skipped` result and you skip this post step; the ledger still records the findings.
297
298
  4. **Verdict (Phase 4):** post the verdict with the [Gate comment command](#mandatory-gate-comment-command-contract) using `--execution-mode fanout_fanin` and `--findings-json <path>`. Write that JSON from the collected per-angle results (the same per-angle `{angle, verdict, findings}` artifacts at `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json` that fed `consolidateFanin` in Phase 3; the flat `toFindingsLogShape` output is also accepted and grouped by `.angle`) so the verdict comment renders the structured per-angle breakdown instead of a wall-of-text summary. Do NOT use `--findings-summary` for a fan-out gate — that is the inline_single_agent fallback.
298
299
  5. **Retry (Phase 5):** on blocking findings, drive each internal fan-out finding through the SAME fix → reply-with-resolving-commit → resolve loop used for external Copilot review comments (Step 7 above): fix on the branch, push the resolving commit, then re-run only the `findings_present` angles from the previous pass (the context-builder and fan-in always re-run); repeat until the consolidated verdict is `clean` for the current head SHA. Internal fan-out findings and external review comments are handled identically by that loop.
@@ -309,11 +310,7 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
309
310
  - **CI prerequisite:** resolve the draft gate config first (`resolveGateConfig(config, "draft")`). When `requireCi=true` (default), wait for green current-head CI before entering `draft_gate`. When `requireCi=false`, the draft gate may proceed without green CI. This draft-only override does **not** relax `pre_approval_gate`; final approval and merge readiness still require green current-head CI.
310
311
  - **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
311
312
  - **Next step after passing:** mark the PR ready for review.
312
- - **Board status sync (best-effort, after ready-for-review):** once the PR is created/marked ready for review, sync the linked issue's board item to "In Progress" so the queue board reflects active work without a manual `dev-loops queue sync-status --to-column <col>`:
313
- ```sh
314
- node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "In Progress"
315
- ```
316
- Best-effort and NON-FATAL: it uses local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks marking the PR ready.
313
+ - **Board status sync (built-in, after ready-for-review):** the In-Progress board move is now performed automatically as a deterministic tail of `ready-for-review.mjs` marking the PR ready couples the board move to the ready transition (#1069), so no separate `sync-item-status` step is needed. It stays best-effort and NON-FATAL: it uses local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks marking the PR ready.
317
314
  - **Non-substitution rule:** a clean `draft_gate` comment only authorizes the draft → ready-for-review transition for that head SHA. It does **not** satisfy `pre_approval_gate`, final-approval readiness, or merge-ready requirements.
318
315
  - **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Keep validation reporting concise: include command names with pass/fail status. Do **not** paste raw passing test output into the visible gate comment. If you include a failing validation excerpt, keep it focused and truncate it to a deterministic retained-prefix length before posting the comment. See [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). Do not run `gh pr ready` unless a visible `clean` `draft_gate` checkpoint verdict comment exists for the current head SHA. A checkpoint verdict comment for an older head SHA does not satisfy this requirement for the current head. If findings exist, the PR stays draft and needs fixes before retry. If fixes advance the head SHA while still draft, post a new checkpoint verdict comment for the new head. If the checkpoint verdict comment cannot be posted, fail closed and do not run `gh pr ready`.
319
316
 
@@ -327,7 +324,7 @@ This is the default pre-approval gate for this workflow boundary. The canonical
327
324
  - **Review angles:** resolved at runtime from config via `resolveGateAngles(config, "preApproval")` from `@dev-loops/core/config`. Default config enables all configured pre-approval gate angle families; consumer repos may opt out individual angles via `excludeAngles`.
328
325
  - **Persona mapping:** each angle resolves to a reviewer persona via `resolveReviewerRole(config, angle)` from `@dev-loops/core/config`. Include this prompt in each reviewer's briefing so the reviewer knows exactly what to look for.
329
326
  - **Pass criteria:** the sub-loop completes with verdict `clean`; all configured angles pass; if parallel execution is impractical, still run all configured lenses and explicitly record the limitation.
330
- - **Acceptance criteria verification:** follow the canonical procedure in [Acceptance Criteria Verification](../docs/acceptance-criteria-verification.md) before posting the `pre_approval_gate` comment.
327
+ - **Acceptance criteria verification:** follow the canonical procedure in [Acceptance Criteria Verification](../docs/acceptance-criteria-verification.md) before posting the `pre_approval_gate` comment. After a clean verification this also ticks the verified PR-body checkboxes via `scripts/github/tick-verified-checkboxes.mjs`, so the merged PR shows checked AC.
331
328
  - **Next step after passing:** continue the Step 7 flow and then proceed to the human approval checkpoint below.
332
329
  - **Non-substitution rule:** a clean `pre_approval_gate` comment is separate from `draft_gate` evidence. It governs final-approval readiness for that head SHA; it does **not** replace the required `draft_gate` evidence for leaving draft.
333
330
  - **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Keep validation reporting concise: include command names with pass/fail status. Do **not** paste raw passing test output into the visible gate comment. If you include a failing validation excerpt, keep it focused and truncate it to a deterministic retained-prefix length before posting the comment. Do not declare final-approval readiness unless a visible `clean` `pre_approval_gate` checkpoint verdict comment exists for the current head SHA. Final-approval readiness must not rely only on local or hidden artifacts; the visible PR comment is the required auditable evidence. If the checkpoint verdict comment cannot be posted, fail closed and do not declare final-approval readiness. A checkpoint verdict comment for an older head SHA does not satisfy this requirement for the current head. If findings exist, follow-up fixes are required before final approval. The `pre_approval_gate` procedure must be entered and completed (visible comment posted) before any merge-ready or approval-ready declaration. Skipping the gate is not recoverable by asserting convergence. If fixes advance the head SHA, post a new checkpoint verdict comment for the new head.
@@ -337,14 +334,14 @@ This is the default pre-approval gate for this workflow boundary. The canonical
337
334
  Before any merge-ready or final-approval claim, run `detect-pr-gate-coordination-state.mjs` for the current PR. If it reports `gateBoundary=conflict_resolution` or `mergeStateStatus` is conflicted, stop the normal gate path immediately and use this recovery flow:
338
335
 
339
336
  1. fetch fresh `origin/main`, confirm the current PR head SHA, and summarize the conflict scope from `mergeStateStatus` plus any reported `conflictFiles`
340
- 2. ask for explicit authorization before any rebase or other branch-state-changing reconciliation command
341
- 3. after authorization, reconcile locally on the PR branch; default to rebase onto latest `origin/main`, unless the operator explicitly chooses another conflict-resolution command
337
+ 2. ask for explicit authorization before any merge commit or other branch-state-changing reconciliation command
338
+ 3. after authorization, reconcile locally on the PR branch; default to a merge commit (`git merge origin/main`) per the behind-branch integration policy in [Local Implementation Skill](../local-implementation/SKILL.md#branch--review--merge-policy), unless the operator explicitly chooses another conflict-resolution command
342
339
  4. auto-resolve simple conflicts when the correct fix is mechanical and clearly in scope; report complex conflicts explicitly and fix them manually only for in-scope files
343
340
  5. rerun the smallest honest local validation for the touched conflict slice
344
341
  6. rerun `detect-pr-gate-coordination-state.mjs` for the new head
345
342
  7. because the head changed, rerun `pre_approval_gate` for the new head before any approval-ready or merge-ready claim
346
343
  8. wait for current-head CI again before retrying merge evaluation
347
- 9. if the chosen reconciliation rewrote branch history (for example rebase), ask for explicit authorization before `git push --force-with-lease`, then continue the loop on the updated head
344
+ 9. if the chosen reconciliation rewrote branch history (rebase only — merge commits push as a normal fast-forward), ask for explicit authorization before `git push --force-with-lease` (`--force-with-lease` only, never bare `--force`), then continue the loop on the updated head
348
345
 
349
346
  `mergeStateStatus: CLEAN` alone is not enough to resume approval or merge claims. The existing merge-ready preconditions still apply: zero unresolved review threads, a clean current-head `pre_approval_gate`, and green current-head CI.
350
347
 
@@ -371,6 +368,21 @@ node <resolved-skill-scripts>/github/detect-checkpoint-evidence.mjs \
371
368
 
372
369
  This helper is always-on: it uses `gh api` to fetch visible PR issue comments and fails closed unless both required gate comments exist: a clean `draft_gate` comment for the one-time draft boundary and a clean current-head `pre_approval_gate` comment. Do not run `gh pr merge` if this command exits non-zero. There is no opt-out flag. Resolved threads, green CI, clean Copilot rereview, or local notes do not substitute for this successful helper output. If a final approval or merge boundary sees `gh pr merge` without a same-boundary successful check, treat that as a workflow violation and stop.
373
370
 
371
+ ### Stale runner-coordination lock held by a completed run
372
+
373
+ The pre-merge gate evidence check fails closed on the PR's runner-coordination claim (`.pi/runner-coordination/<owner>/<name>/pr-<n>.json`): a fresh merge re-dispatch (new run id) is refused with `ownership_lost`, or `stale_runner` once the claim ages past the max-age window.
374
+
375
+ The auto-loop now releases its claim best-effort when a run reaches a terminal stop (clean-converged, blocked, or done — including the stop at the human approval checkpoint), so a merge-authorized re-dispatch normally inherits a cleared claim and proceeds. The release is non-fatal: it never blocks the stop, and it never clears a claim owned by a genuinely active competing run.
376
+
377
+ If a stale claim still blocks the merge because the completing run could not release (crash, killed process, or a pre-#1109 run), the sanctioned recovery for a lock held by a COMPLETED run is an explicit takeover by the merge run:
378
+
379
+ ```sh
380
+ node <resolved-skill-scripts>/loop/pr-runner-coordination.mjs takeover \
381
+ --repo <owner/name> --pr <number>
382
+ ```
383
+
384
+ `takeover` seizes ownership for the current run id and records the displaced run under `previousRun`. Only take over when the prior owner is genuinely completed/dead. A genuinely active (non-stale) run must still be allowed to block — do not take over to race a live run.
385
+
374
386
  ### Mandatory post-merge retrospective checkpoint write
375
387
 
376
388
  After a merge succeeds (or an explicit retrospective skip is authorized), write the durable retrospective checkpoint before exiting the subagent session:
@@ -397,7 +409,7 @@ node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name
397
409
 
398
410
  Board and threshold resolve from `.devloops` (`queue.projectNumber`/`queue.boardTitle`, `queue.archiveOlderThanDays`, default 7d), using local `gh` auth — no CI, cron, or PAT. This step is best-effort and NON-FATAL: ignore any failure and never let it block the merge or the retrospective.
399
411
 
400
- Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `dev-loops queue sync-status --to-column <col>`:
412
+ The deterministic mechanism that converges merged→Done is `dev-loops queue reconcile` idempotent, run best-effort at loop startup — so the merge lands on the "Done" column without a remembered manual step. The manual sync below is now an optional fallback (e.g. to converge immediately without waiting for the next startup reconcile):
401
413
 
402
414
  ```sh
403
415
  node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
@@ -9,7 +9,7 @@ user-invocable: true
9
9
 
10
10
  **No-implicit-start rule:** Never start implementation without explicit instruction.
11
11
 
12
- **Work-origin rule:** All work must originate from a tracked artifact: a GitHub issue (tracker-first) or a persisted markdown plan file (local-planning). See [Artifact Authority Contract](../docs/artifact-authority-contract.md) for canonical mode definitions and settings. No work may originate from a PR or direct local change unless explicitly requested.
12
+ **Work-origin rule:** All work must originate from a tracked artifact: a GitHub issue (tracker-first), a persisted markdown plan file (local-planning), or — on the sanctioned lightweight path — the PR description itself as the spec-of-record (`--lightweight`, `canonicalSpecSource: pr_body`; no committed plan artifact). See [Artifact Authority Contract](../docs/artifact-authority-contract.md) for canonical mode definitions and settings. No work may originate from a PR (other than the sanctioned lightweight PR-body-as-spec path) or a direct local change unless explicitly requested.
13
13
 
14
14
  # Unified Dev Loop
15
15
 
@@ -25,7 +25,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
25
25
 
26
26
  > Under the Claude Code harness the dev-loop runs as a single agent: run these steps directly — no read-only boundary and no separate async-subagent dispatch. See [Main Agent Contract](../docs/main-agent-contract.md).
27
27
 
28
- Resolve authoritative state via the startup resolver (`npx dev-loops@0.6.0 loop startup --issue <n>` for issues, `npx dev-loops@0.6.0 loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `npx dev-loops@0.6.0 loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
28
+ Resolve authoritative state via the startup resolver (`npx dev-loops@0.7.1 loop startup --issue <n>` for issues, `npx dev-loops@0.7.1 loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `npx dev-loops@0.7.1 loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
29
29
 
30
30
  **Retrospective checkpoint gate:** the resolver reads `.pi/dev-loop-retrospective-checkpoint.json` and injects the state. When the checkpoint is `missing` and the repo config `workflow.requireRetrospective` (set via `.devloops` at repo root) is `true`, the resolver returns `needs_reconcile`. Complete or explicitly skip the retrospective before starting.
31
31
 
@@ -99,10 +99,10 @@ When `@dev-loops/core` is available again, switch back to the full helper. The f
99
99
 
100
100
  ## Read-only info shortcut
101
101
 
102
- Info/handoff requests can be served directly via `npx dev-loops@0.6.0 loop info` (read-only; no full dev-loop run required):
103
- - `npx dev-loops@0.6.0 loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
104
- - `npx dev-loops@0.6.0 loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
105
- - `npx dev-loops@0.6.0 loop info --issue <n> --json` — machine-readable JSON output
102
+ Info/handoff requests can be served directly via `npx dev-loops@0.7.1 loop info` (read-only; no full dev-loop run required):
103
+ - `npx dev-loops@0.7.1 loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
104
+ - `npx dev-loops@0.7.1 loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
105
+ - `npx dev-loops@0.7.1 loop info --issue <n> --json` — machine-readable JSON output
106
106
 
107
107
  ## Reading tool output (token-economical convention)
108
108
 
@@ -110,13 +110,16 @@ When you need a fact from a dev-loops JSON-emitting script, climb this ladder an
110
110
 
111
111
  1. **Prefer the dev-loops subcommand / concise mode.** Use `loop info` or a script's `--concise`/`--summary` mode (e.g. `run-watch-cycle.mjs --concise`, `probe-copilot-review.mjs --concise`) for a human-readable digest. The concise modes surface loop state, Copilot round count, unresolved/actionable thread counts, round-cap-clean eligibility, CI status, next action, and the current round's new Copilot comment bodies.
112
112
  2. **`--silent` / `-s` for a yes/no check.** Reads ZERO output: `… --jq '<predicate>' --silent; echo $?` exits `0` for true / `1` for false. Without `--jq`, `--silent` maps the script's success (`ok:true`) to exit `0`, failure to `1`. Example: `probe-copilot-review.mjs --repo o/r --pr N --jq '.status=="idle"' -s`.
113
- 3. **`--jq <filter>` to extract a single field.** The `--jq`-wired scripts (`probe-copilot-review.mjs`, `capture-review-threads.mjs`, `upsert-checkpoint-verdict.mjs`, the `scripts/projects/*` queue scripts) accept a gh-style `--jq` filter (jq subset: field access, `.[]`/`.[N]`, `|`, `select(...)`, `==`/`!=`/`<`/`<=`/`>`/`>=`, `length`, `keys`). It prints only the filtered value. An invalid filter fails closed (stderr + exit `2`), distinct from a clean predicate-false (silent exit `1`).
114
- 4. **Use a dev-loops wrapper for `gh` reads — never an agent-level raw `gh`.** With `workflow.requireRetrospectiveInternalTooling: true`, a raw `gh` call is a recorded retro violation (fail-closed). If no script covers the read you need, treat it as a tooling gap: file/build a thin wrapper (reuse `scripts/lib/jq-output.mjs`, like the three below), don't shell out. The reads that already have wrappers:
113
+ 3. **`--jq <filter>` to extract a single field.** `--jq`/`--silent` are a BASE-CLI GUARANTEE across every operator-facing JSON-result dev-loops command each accepts a gh-style `--jq` filter (jq subset: field access, `.[]`/`.[N]`, `|`, `select(...)`, `==`/`!=`/`<`/`<=`/`>`/`>=`, `length`, `keys`), not just a named subset. (A few scripts are out of scope — e.g. build/smoke tooling, dashboard servers, dormant/unwired adapters, and scripts that write JSON to a file rather than stdout — each carried with its reason in the contract test's exclusion list.) It prints only the filtered value. An invalid filter fails closed (stderr + exit `2`), distinct from a clean predicate-false (silent exit `1`). This is enforced by a contract test (`test/contracts/jq-output-base-guarantee-contract.test.mjs`) that fails the build if a new JSON-emitting command ships without wiring the shared `scripts/lib/jq-output.mjs` emit path.
114
+ 4. **Use a dev-loops wrapper for `gh` reads — never an agent-level raw `gh`.** A raw `gh` call is a recorded advisory retro violation (issue #1077: reported, never blocking). If no script covers the read you need, treat it as a tooling gap: file/build a thin wrapper (reuse `scripts/lib/jq-output.mjs`, like the ones below), don't shell out. The reads/edits that already have wrappers:
115
115
  - CI run-log tail (a failing PR's job log) → `scripts/github/fetch-ci-logs.mjs --repo <o/r> --pr <n> [--failed-only] [--tail <n>]`, **never raw `gh run view --log`/`--log-failed`**. (`probe-ci-status.mjs` names the failed checks; this returns the LOG.)
116
116
  - Issue list/filter → `scripts/github/list-issues.mjs --repo <o/r> [--state <open|closed|all>] [--label <l>] [--limit <n>]`, **never raw `gh issue list`**. (The queue tool lists the project board; this is for arbitrary issue queries.)
117
117
  - Issue comment → `scripts/github/comment-issue.mjs --repo <o/r> --issue <n> (--body <text> | --body-file <path>)`, **never raw `gh issue comment`**. Returns `{ ok, commentUrl }`.
118
+ - PR facts read (branch/state/mergeStateStatus/head SHA/etc.) → `scripts/github/view-pr.mjs --repo <o/r> --pr <n> [--json <fields>]`, **never raw `gh pr view`**. (For composite loop-routing/CI facts prefer `loop info --pr`; this is the thin field-read counterpart.)
119
+ - PR edit (title/body/assignee/milestone) → `scripts/github/edit-pr.mjs --repo <o/r> --pr <n> [--title <t>] [--body <b> | --body-file <path>] [--add-assignee <u>] [--remove-assignee <u>] [--milestone <m>]`, **never raw `gh pr edit`**. Returns `{ ok, repo, pr, edited }`.
120
+ - PR checks/status → `scripts/github/probe-ci-status.mjs --repo <o/r> --pr <n> --timeout-ms 0` for a single live combined-CI check, **never raw `gh pr checks`**. (Provider-agnostic; drop the `--timeout-ms 0` to block-wait.)
118
121
 
119
- All three accept the same `--jq`/`--silent` output flags as the other JSON-emitting scripts.
122
+ These accept the same `--jq`/`--silent` output flags as every other JSON-emitting script (base-CLI guarantee) — including `probe-ci-status.mjs`, watch-shaped as it is.
120
123
  5. **NEVER `| python3` or `node -e`** to parse tool JSON. If a field you need is missing from a script's output, add it to the script (or its concise mode), not an inline parser.
121
124
 
122
125
  ## Guard rules
@@ -127,9 +130,9 @@ When you need a fact from a dev-loops JSON-emitting script, climb this ladder an
127
130
 
128
131
  **Inline-first rule:** Prefer inline commands over nested async delegation when managing a single PR. Use nested delegation only for parallel fan-out or when the parent needs to continue other work.
129
132
 
130
- **Bounded async task contract:** Break work into discrete tasks with clear inputs, explicit outputs, bounded scope. No shell polling — use `run-watch-cycle.mjs` or `gh run watch`.
133
+ **Bounded async task contract:** Break work into discrete tasks with clear inputs, explicit outputs, bounded scope. No shell polling — use `run-watch-cycle.mjs` or `gh run watch`. For fan-out reviewer waits (gate sub-loops or any Agent-tool fan-out), await completion via the harness completion notification or the reviewer's findings artifact at its deterministic path, then join via `consolidateFanin` — never tail/parse a subagent transcript, never `node -e`/`python3`-parse tool JSON, never `sleep`-poll (see the [fan-in wait anti-pattern](../docs/anti-patterns.md) and [Checkpoint Review Chain Contract](../../docs/gate-review-sub-loop-contract.md) Phase 2).
131
134
 
132
- **Round-cap budget check (enforced):** After every watch cycle, fix pass, or reply-resolve, check whether completed Copilot review rounds have reached the maximum (default: 5). Stop re-requesting Copilot review when the limit is reached never re-request after the cap. Read these gate-cadence facts via the token-economical convention above (`run-watch-cycle.mjs --concise`, or `--jq`/`--silent` for a single field/predicate) — never `| python3` or `node -e`.
135
+ **Round-cap budget check (enforced):** After every watch cycle, fix pass, or reply-resolve, check whether completed Copilot review rounds have reached the maximum (default: 5). Stop re-requesting Copilot review when the limit is reached **within that review cycle**. Exception: if the loop already converged and then significant post-convergence changes land on a newer head (new/changed product or test logic, not doc/message/comment-only edits), open a new Copilot review cycle and re-request review when regular rounds are already > 0, even if the previous cycle hit the cap. Read these gate-cadence facts via the token-economical convention above (`run-watch-cycle.mjs --concise`, or `--jq`/`--silent` for a single field/predicate) — never `| python3` or `node -e`.
133
136
 
134
137
  ## Shorthand issue-based auto trigger contract
135
138
 
@@ -148,3 +151,4 @@ All PRs must pass the full gate pipeline before merge. No scope is exempt: docs-
148
151
  - Before any state-changing action, get explicit confirmation unless already authorized.
149
152
  - A question requires an answer, not an action.
150
153
  - Stop and ask rather than guessing when facts don't agree.
154
+ - Cross-harness regression contract: [Cross-Harness Regression Contract](../docs/cross-harness-regression-contract.md).
@@ -4,7 +4,9 @@ Extracted procedure for verifying issue acceptance criteria during the `pre_appr
4
4
 
5
5
  ## Procedure
6
6
 
7
- Before posting the `pre_approval_gate` comment, verify every acceptance criteria checklist item in the issue linked to this PR:
7
+ Before posting the `pre_approval_gate` comment, verify every acceptance criteria checklist item in the issue linked to this PR, and reflect the verified items back into both the linked issue body and the PR body (exception: under the lightweight path — `specSource: pr_body` — the step 5 issue-body mirroring is skipped, since the PR body, not the issue body, is the canonical spec surface; see the lightweight fork below):
8
+
9
+ > **Lightweight (PR-body-as-spec) fork:** when the session is lightweight — the resolver output / handoff envelope carries `specSource: pr_body` (`canonicalSpecSource: pr_body`) — a linked issue still exists (`--lightweight` runs on the `--issue` path, so keep the `Closes #N` linkage and all issue-level tracking), but the **PR body**, not the issue body, is the canonical spec-of-record read for AC/DoD. In that case, still resolve the linked issue for tracking, but read the AC/DoD/invariants from the PR body rather than the issue body (skip step 2's issue-body read for spec purposes). Run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --expected-issue <issue-number>` (reuses `@dev-loops/core/loop/issue-refinement-artifact`) to confirm the body carries the required invariants — it fails closed with a per-section `missing_*` reason if any is absent, in which case post the gate comment with verdict `blocked`. The PR body MUST carry the `Closes #N` linkage (or GitHub's other closing-keyword forms): `validate-pr-body-spec` fails closed with `missing_closing_issue_reference` without it, and with `closes_wrong_issue` when `--expected-issue` doesn't match. Then continue at step 3, extracting the **Acceptance criteria** checklist items from the PR body itself; step 5's issue-body AC-tick update is unnecessary for lightweight since the issue body is not the spec surface (the PR body is ticked in step 6 as usual). The default issue-backed procedure below is unchanged.
8
10
 
9
11
  1. **Resolve the linked issue number deterministically:** use `gh pr view <pr-number> --repo <owner/name> --json closingIssuesReferences,body` and apply this decision tree: if there is exactly one closing issue reference, use it; else if there is exactly one PR-body `Closes #N` / `Fixes #N` pattern, use it; otherwise (zero or multiple candidates), post the gate comment with verdict `blocked` (gate cannot complete deterministically) rather than guessing.
10
12
 
@@ -16,6 +18,8 @@ Before posting the `pre_approval_gate` comment, verify every acceptance criteria
16
18
 
17
19
  5. **Update the issue body once:** compute the fully-updated issue body by replacing each verified item's `- [ ]` with `- [x]`, write it to a temporary file, and perform a single `gh issue edit <issue-number> --body-file <tmp-file> --repo <owner/name>`. Do not issue one edit per item; prefer `--body-file` over inline `--body` to avoid shell quoting/escaping hazards.
18
20
 
19
- 6. **Post the gate comment:** always post a `pre_approval_gate` comment (the checkpoint verdict comment contract requires a visible comment even for non-`clean` verdicts). Use verdict `clean` only when all AC items are verified; use verdict `findings_present` when any AC item is not satisfied and requires follow-up fixes; use verdict `blocked` when the gate cannot complete deterministically (for example no linked issue, ambiguous issue linkage, or the issue body is unavailable). In all cases include a note on AC verification status.
21
+ 6. **Tick the PR body once:** after the verification is clean, flip each verified item's `- [ ]` to `- [x]` in the PR body via a single `gh pr edit --body-file` update, so the merged PR shows checked AC/DoD. Run `node scripts/github/tick-verified-checkboxes.mjs --repo <owner/name> --pr <pr-number> --verified <exact label>...` (one edit; exact-label match; only items actually verified are ticked; unverified or deferred items stay `- [ ]`; fail closed). This runs automatically as part of the `pre_approval_gate`, not by memory.
22
+
23
+ 7. **Post the gate comment:** always post a `pre_approval_gate` comment (the checkpoint verdict comment contract requires a visible comment even for non-`clean` verdicts). Use verdict `clean` only when all AC items are verified; use verdict `findings_present` when any AC item is not satisfied and requires follow-up fixes; use verdict `blocked` when the gate cannot complete deterministically (for example no linked issue, ambiguous issue linkage, or the issue body is unavailable). In all cases include a note on AC verification status.
20
24
 
21
25
  When the issue body has no AC checklist items, post the gate comment with verdict `findings_present` and note that fact explicitly rather than assuming satisfaction.
@@ -11,7 +11,8 @@ Canonical owner for anti-pattern guidance across all workflow families.
11
11
  5. **Duplicate worktree paths**: Check `git worktree list` before creating new worktrees; reuse existing matching worktrees.
12
12
  6. **Main-checkout mutation**: Reserve main checkout for inspection/control; use `tmp/worktrees/` paths for mutation work.
13
13
  7. **Spelunking tooling internals instead of using the public surface**: Do not read installed package internals, scan tooling source, or run ad-hoc scripts to understand a tool's behavior. Use the CLI, its `--help` subcommands, and `skills/docs/`. Read tool source only when the task is to inspect or change that tool, or when a concrete failure path is inside it and no public CLI/docs path exists. Once a failure is concrete, search changed files for the exact pattern — don't run duplicate broad searches.
14
- 8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`queue.projectNumber`/`queue.boardTitle` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops queue add ... --column "Next Up"`), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the operator guides under `docs/` (queue board usage/setup) for the workflow.
14
+ 8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`queue.projectNumber`/`queue.boardTitle` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops queue add ... --next-up` to land directly in the normative `Next Up` pickup queue; default lands in Backlog, which is unprioritized intake and never auto-picked), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the operator guides under `docs/` (queue board usage/setup) for the workflow.
15
+ 9. **Hand-rolling a fan-in wait by polling a subagent transcript or parsing tool JSON with `node -e`/`python3`**: When awaiting fan-out reviewers (gate `draft_gate`/`pre_approval_gate` sub-loops, or any Agent-tool fan-out), NEVER tail/parse another agent's JSONL transcript, NEVER use `node -e`/`python3` to parse tool/subagent JSON, and NEVER `sleep`-poll in a shell loop for completion. These improvisations breach the [internal-tooling-only rule (issue #982)](retrospective-checkpoint-contract.md#internal-tooling-only-rule-issue-982--now-advisory) (no `node -e`/`python3` parsing of tool JSON) and the no-shell-polling rule, and a single improvised wait adds a non-empty entry to the advisory `retrospectiveFindings.rawCallViolations` array (issue #1077: reported to the conductor, **never blocking**). **Sanctioned wait:** rely on the harness completion notification, or read each reviewer's findings artifact at its deterministic output path (see [Checkpoint Review Chain Contract](../../docs/gate-review-sub-loop-contract.md) Phase 2), then join via `consolidateFanin` from `@dev-loops/core/loop/gate-fanin` (Phase 3). See also the dev-loop SKILL "Bounded async task contract".
15
16
 
16
17
  ## Light mode exception
17
18