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
@@ -10,7 +10,7 @@ Other repo docs may summarize or link this contract, but they should not redefin
10
10
 
11
11
  dev-loops supports two mutually exclusive artifact authority modes. Every work item originates from exactly one authoritative artifact: a GitHub issue or a persisted markdown plan file. Work originates from a PR or a direct local change only when explicitly requested.
12
12
 
13
- The shipped extension default selects local-planning; see [Shipped default posture](#shipped-default-posture) below. The mode names that follow describe the two tiers; "default" in their headings refers to the github-first code-level fallback in `BUILT_IN_DEFAULTS`, which the shipped extension layer overrides to local-first.
13
+ The shipped extension default selects local-planning; see [Shipped default posture](#shipped-default-posture) below. The mode names that follow describe the two tiers; "default" in their headings refers to the local-first code-level default in `BUILT_IN_DEFAULTS`.
14
14
 
15
15
  ### Tracker-first
16
16
 
@@ -25,6 +25,7 @@ Key contract:
25
25
  - GitHub issue state is authoritative — not local notes or chat context
26
26
  - A linked PR is the single canonical follow-up artifact for the issue
27
27
  - When an open linked PR exists, reuse it rather than opening another
28
+ - Follow-ups discovered while working a PR/loop are noted on the originating issue or PR body by default; a standalone issue is filed only when the follow-up is genuinely independent of the PR and outlives it (see [Sub-Issue Tree Contract](../../docs/sub-issue-tree-contract.md))
28
29
  - Implementation may proceed through either the GitHub-first routed path or the local implementation strategy (see [Public Dev Loop Contract](public-dev-loop-contract.md) `targetPreference`)
29
30
 
30
31
  ### Local-planning
@@ -41,12 +42,29 @@ Key contract:
41
42
  - GitHub issues may still be used for tracking or linking, but the plan file is authoritative for scope and acceptance criteria
42
43
  - A tracker-backed local implementation session (GitHub issue as canonical spec) must not also maintain a duplicate `docs/phases/phase-<n>.md` — see [Public Dev Loop Contract](public-dev-loop-contract.md) "Tracker-backed local implementation input-source contract"
43
44
 
45
+ ### Lightweight (PR-body-as-spec)
46
+
47
+ **The PR description itself is the authoritative artifact store — no committed plan artifact.** This is a lightweight modifier on the local `--issue` path (`resolve-dev-loop-startup.mjs --issue <n> --lightweight`, `canonicalSpecSource: pr_body`), not a settings-level mode. No phase/plan doc is minted or committed; the PR body carries the spec-of-record invariants directly. The gate sequence is identical to the phase-doc path (draft → pre-approval fanout → detect-evidence → human merge); only the backing artifact differs (PR body vs phase doc).
48
+
49
+ Artifacts:
50
+ - **Planning + execution artifact:** the GitHub PR — its description is the spec, its diff is the execution
51
+ - **No committed plan doc:** no `docs/phases/*.md` is created for the session
52
+
53
+ Key contract:
54
+ - The PR body must carry the same invariants a durable spec would: **Objective/why, in-scope + explicit non-goals, testable acceptance criteria, definition of done, open questions/risks**. `scripts/loop/validate-pr-body-spec.mjs` (reusing the generic markdown logic of `@dev-loops/core/loop/issue-refinement-artifact`, `validatePrBodySpec`) validates these and **fails closed** with a distinct `missing_*` reason per absent invariant.
55
+ - The PR body MUST also carry the `Closes #N` linkage (GitHub's other closing keywords count too); `validate-pr-body-spec` fails closed with `missing_closing_issue_reference` without it, and with `closes_wrong_issue` when an `--expected-issue` is given and doesn't match — the lightweight path's issue-tracking state must never silently diverge from PR state (issue #1181).
56
+ - This flips the promotion invariant below (P4, "the PR body carries the committed plan-doc **path**"): under lightweight there is no committed plan doc — the PR body **is** the spec, not a pointer to one.
57
+ - The explicit `--lightweight` flag is the primary, deterministic trigger. The secondary heuristic (chore/fix commit type + no `--plan-file` + small change) is a documented manual signal for when to reach for the flag; it is not an automatic selector.
58
+ - `--lightweight` is rejected when combined with `--plan-file` (they are opposites: `--plan-file` commits a durable plan doc as the spec, `--lightweight` makes the PR body the spec) and only composes with `--issue`.
59
+ - Pre-approval acceptance-criteria verification reads the AC/DoD/invariants directly from the PR body rather than a linked issue body; see [Acceptance Criteria Verification](acceptance-criteria-verification.md).
60
+
44
61
  ### Mode selection table
45
62
 
46
63
  | Mode | Canonical artifact | GitHub issue required | Settings value |
47
64
  |---|---|---|---|
48
65
  | Tracker-first | GitHub issue | Yes | `strategy.default: github-first` |
49
66
  | Local-planning (shipped default) | Markdown plan file | No | `strategy.default: local-first` |
67
+ | Lightweight (PR-body-as-spec) | GitHub PR description | Yes (`--issue`) | modifier: `--lightweight` (`canonicalSpecSource: pr_body`) |
50
68
 
51
69
  `inputSource.default` further disambiguates local-first startup:
52
70
  | inputSource | Meaning |
@@ -81,7 +99,7 @@ The `inputSource.default` key disambiguates local-first startup:
81
99
 
82
100
  The effective default for a consumer comes from the config-merge layering in `packages/core/src/config/config.mjs`. Precedence, low to high:
83
101
 
84
- 1. `BUILT_IN_DEFAULTS` (frozen in `config.mjs`) — `strategy.default: github-first`. This is the code-level fallback when no other layer sets the key.
102
+ 1. `BUILT_IN_DEFAULTS` (frozen in `config.mjs`) — `strategy.default: local-first`. This is the code-level fallback when no other layer sets the key.
85
103
  2. Extension-packaged defaults (`packages/core/src/config/extension-defaults.yaml`, loaded as the `extensionDefaults` layer) — `strategy.default: local-first`. This is the opinion the package ships and the layer that wins over the built-in fallback.
86
104
  3. Repo-local `.pi/dev-loop/defaults.*` (legacy) — applied when present.
87
105
  4. Repo `.devloops` at repo root — the per-repo override, highest precedence. When `.devloops` is absent, the legacy `.pi/dev-loop/settings.*` / `overrides.*` apply at this position instead.
@@ -95,7 +113,7 @@ Two legacy repo-local layers also exist under `.pi/dev-loop/` (the package no lo
95
113
  These are not valid artifact authority mode selectors:
96
114
  - `strategy.default: copilot` — not a valid mode; the enum accepts only `github-first` or `local-first` (`packages/core/src/config/config.mjs`)
97
115
  - Free-form string values — fail closed
98
- - Omitting `strategy.default` from every layer — resolves to `github-first` from `BUILT_IN_DEFAULTS`; with the shipped extension layer present it resolves to `local-first`
116
+ - Omitting `strategy.default` from every layer — resolves to `local-first` from `BUILT_IN_DEFAULTS`
99
117
 
100
118
  ## Local-first plan-file flow end to end
101
119
 
@@ -103,7 +121,7 @@ Under local-planning, one plan file moves through four stages. Each stage has a
103
121
 
104
122
  ### P1 — Plan-file artifact + config (#949)
105
123
 
106
- The plan file is a phase-doc-format markdown document. Its directory is `localPlanning.plansDir`, which defaults to `docs/phases/` (built-in default in `config.mjs`, mirrored in `extension-defaults.yaml`; `resolvePlansDir(config)` resolves it). Its required base authoring sections — `## Status`, `## Objective`, `## In scope`, `## Explicit non-goals` — and the validator `scripts/refine/validate-plan-file.mjs` (`validatePlanFile`, distinct `missing_*` codes per absent or empty section) are defined in the [Plan-file Contract](plan-file-contract.md).
124
+ The plan file is a phase-doc-format markdown document. It lives under `docs/phases/`, the existing phase-docs directory. Its required base authoring sections — `## Status`, `## Objective`, `## In scope`, `## Explicit non-goals` — and the validator `scripts/refine/validate-plan-file.mjs` (`validatePlanFile`, distinct `missing_*` codes per absent or empty section) are defined in the [Plan-file Contract](plan-file-contract.md).
107
125
 
108
126
  ### P2 — Intake (#950)
109
127
 
@@ -188,7 +188,7 @@ Preferred defaults for this repo:
188
188
  - parent/subagent no-activity threshold for watcher-style runs: at least **15 minutes**
189
189
  - active-long-running notice threshold for watcher-style runs: about **30 minutes**
190
190
 
191
- These are the defaults built into `probe-copilot-review.mjs`, `run-watch-cycle.mjs`, and the `watchArgs` emitted by `copilot-pr-handoff.mjs`. Do not pass removed CLI policy flags (`--poll-interval-ms`, `--timeout-ms`, `--probe-only`) helpers hard-error when they are provided. Timeouts and intervals are derived from `packages/core/src/loop/policy-constants.mjs`.
191
+ These are the defaults built into `probe-copilot-review.mjs`, `run-watch-cycle.mjs`, and the `watchArgs` emitted by `copilot-pr-handoff.mjs`. Do not pass removed CLI policy flags (`--poll-interval-ms`, `--probe-only`): the scripts reject them as usage errors, but do not rely on that as a guaranteed hard stop in the `dev-loops gate …` path — the CLI retry-wrapper (`buildCorrectedArgs`) may strip an unrecognized/removed flag and silently retry with defaults, so passing one can quietly fall back to default behavior rather than failing. `probe-copilot-review.mjs` does accept `--timeout-ms` (watch budget in ms; `0` = single immediate idle check, no watch); timeouts and intervals are otherwise derived from `packages/core/src/loop/policy-constants.mjs`.
192
192
 
193
193
  ### Outer-loop checkpoint: canonical re-attachment artifact
194
194
 
@@ -0,0 +1,60 @@
1
+ # Cross-Harness Regression Contract
2
+
3
+ Canonical owner for harness-agnostic non-regression across Pi and Claude Code.
4
+
5
+ ## What counts as a harness-specific change
6
+
7
+ A change is harness-specific when it touches a seam that branches by runtime harness (Pi vs Claude Code), including:
8
+
9
+ - Runner coordination (`scripts/loop/pr-runner-coordination.mjs`, `scripts/loop/_pr-runner-coordination.mjs`)
10
+ - Run-id injection / async-start semantics (`packages/core/src/loop/async-start-contract.mjs`, `packages/core/src/loop/run-context.mjs`)
11
+ - Subagent / Task fan-out tool provisioning
12
+ - Harness-specific environment variables (`PI_SUBAGENT_RUN_ID`, `DEVLOOPS_RUN_ID`)
13
+ - Session/intercom wiring
14
+ - TUI behavior
15
+ - Any capability detection or `process.env` branch whose outcome differs per harness
16
+
17
+ ## Required cross-harness test coverage
18
+
19
+ Every harness-specific change MUST add or keep a test that exercises the path of the harness it does not directly target. A pull request that alters behavior on one harness without any test evidence for the other harness fails this contract.
20
+
21
+ ## The additive/no-op bar
22
+
23
+ State the bar symmetrically, in both directions:
24
+
25
+ - A change additive on Pi MUST be a no-op on Claude Code, or its effect on Claude Code MUST be explicitly validated by a test.
26
+ - A change additive on Claude Code MUST be a no-op on Pi, or its effect on Pi MUST be explicitly validated by a test.
27
+
28
+ Definitions:
29
+
30
+ - **No-op**: the existing test suite for the other harness passes unmodified — no assertion needed to change, no new test required, because the seam does not fire for that harness.
31
+ - **Explicitly validated**: a new or updated test directly asserts the other harness's resulting behavior (not merely "did not throw"), covering both the case where the seam is inert and the case where it participates.
32
+
33
+ ## Claude-Code-path test inventory
34
+
35
+ When a change targets a Claude-Code-specific seam, the following suites are the baseline that MUST stay green, and are the first place to add coverage for a Pi-originated change that must not regress Claude Code:
36
+
37
+ - `npm run test:assets` — runs `test/contracts/*.test.mjs`, including `test/contracts/claude-assets-reproducible.test.mjs`, `test/contracts/claude-headless-smoke.test.mjs`, `test/contracts/claude-plugin-manifest.test.mjs`, `test/contracts/claude-plugin-marketplace.test.mjs`, `test/contracts/claude-hooks-settings.test.mjs`, and `test/contracts/cli-harness-agnostic.test.mjs`
38
+ - `npm run smoke:headless` — runs `scripts/claude/headless-info-smoke.mjs`; this is a manual/local-only check, not part of `npm test` or `npm run verify` and not run by CI — run it directly when a change touches Claude Code headless-info behavior
39
+ - `npm run test:core` — runs `packages/core/test/*.test.mjs`, including `packages/core/test/claude-headless-entry.test.mjs`, `packages/core/test/claude-hook-decisions.test.mjs`, `packages/core/test/run-context.test.mjs`, `packages/core/test/async-start-contract.test.mjs`
40
+
41
+ Symmetrically, for a change targeting the Claude-Code-specific seam that must not regress Pi, `npm run test:extension` (which runs `test/extension-*.test.mjs`, including `test/extension-pi-adapter.test.mjs`, the Pi-side adapter inventory) is the baseline suite to check and extend.
42
+
43
+ ## How to add coverage
44
+
45
+ 1. Identify the harness-specific seam the change touches (see the surface list above).
46
+ 2. Locate the existing test file that already covers that seam for the harness being changed, and its counterpart for the other harness (from the inventory above, or the nearest analogous file).
47
+ 3. Add or extend a test asserting the other harness's behavior directly — do not rely on an untested assumption that the change is inert there.
48
+ 4. Run the relevant suite locally (`npm run test:assets`, `npm run test:core`, `npm run test:extension`, or `npm run smoke:headless` as applicable) before opening the pull request.
49
+
50
+ ## Non-goals
51
+
52
+ - No new or expanded CI matrix: `npm run verify` (which CI runs on every pull request) already covers `test:assets`, `test:extension`, `test:scripts`, `test:core`, `test:docs`, and `test:dev-loop` — every suite named above except `npm run smoke:headless`, which is manual/local-only by design (see the test inventory above).
53
+ - No bespoke cross-harness enforcement script: naming the required suites here, combined with CI already running them on every pull request, satisfies the coverage bar.
54
+ - No rewriting of existing harness-specific code as part of adopting this contract.
55
+
56
+ ## Cross-references
57
+
58
+ - [AGENTS.md](../../AGENTS.md)
59
+ - [Dev Loop Skill](../dev-loop/SKILL.md)
60
+ - [Public Dev Loop Contract](public-dev-loop-contract.md)
@@ -111,6 +111,7 @@ Preflight verdicts:
111
111
  - do not rely only on PR title/body containing a literal issue number
112
112
  - treat an open linked PR as the active implementation for this issue
113
113
  - once an open linked PR exists, that PR is the only canonical follow-up artifact for the issue; attach follow-up work to it and do not open another PR unless the prior PR was explicitly superseded and reconciled first
114
+ - **follow-up-capture rule:** when a follow-up is discovered while working a PR/loop, note it on the originating issue (or the PR body); file a standalone issue only if the follow-up is genuinely independent of the PR **and** outlives it (a real separate bug/feature that would be lost as a note on a soon-closed issue). Prefer noting PR-scoped follow-ups on the originating artifact over spinning up standalone tracker items for work that is part of the same effort or will be resolved imminently. See [Sub-Issue Tree Contract](../../docs/sub-issue-tree-contract.md).
114
115
  - if a PR already exists, classify bootstrap-wait versus follow-up:
115
116
  `node <resolved-skill-scripts>/loop/detect-initial-copilot-pr-state.mjs --repo <resolved-repo> --issue <number>`
116
117
  - `waiting_for_initial_copilot_implementation`: keep waiting; in durable-auto mode use:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The operator sequence for the local-first plan-file flow. The [Artifact Authority Contract](artifact-authority-contract.md) owns the model and the per-phase contract details; this doc names the shipped helper scripts in the order they run for one plan file. The [Local-Planning Worked Example](local-planning-worked-example.md) shows a single plan file evolving through these steps.
4
4
 
5
- The flow applies under local-planning mode (`strategy.default: local-first`, the shipped default — see the contract's [Shipped default posture](artifact-authority-contract.md#shipped-default-posture)). The plan file lives under `localPlanning.plansDir`, which defaults to `docs/phases/`.
5
+ The flow applies under local-planning mode (`strategy.default: local-first`, the shipped default — see the contract's [Shipped default posture](artifact-authority-contract.md#shipped-default-posture)). The plan file lives under `docs/phases/`.
6
6
 
7
7
  ## Stages
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  One plan file, `docs/phases/phase-42.md`, carried through every stage of the [Local-Planning Flow](local-planning-flow.md): authored, validated, refined in place, held at the local human-review checkpoint, then promoted to a draft PR. Each stage below shows the same file's content as it evolves. The contract details live in the [Artifact Authority Contract](artifact-authority-contract.md); this is a concrete trace of one file.
4
4
 
5
- The repo runs local-planning by default (`strategy.default: local-first`), and the plan lives under `localPlanning.plansDir` (`docs/phases/`).
5
+ The repo runs local-planning by default (`strategy.default: local-first`), and the plan lives under `docs/phases/`.
6
6
 
7
7
  ## Stage 1 — Authored
8
8
 
@@ -47,6 +47,22 @@ doing manually; it is not a general conflict-resolution engine.
47
47
  6. ✅ PR body contains `Closes #N` or `Fixes #N`
48
48
  7. ✅ PR **title** free of merge-blocking markers — `WIP`, `[WIP]`, `DRAFT`, `DO NOT MERGE`, `🚧` (case-insensitive)
49
49
 
50
+ > Runner-coordination lock: the pre-merge evidence check fails closed on a stale/foreign runner claim for the PR. A completing run releases its claim best-effort at every terminal stop (including the human approval checkpoint), so a merge re-dispatch normally proceeds. If a lock held by a completed/dead run still blocks the merge, take it over explicitly with `node <resolved-skill-scripts>/loop/pr-runner-coordination.mjs takeover --repo <owner/name> --pr <number>`. Never take over a genuinely active (non-stale) run — that fail-closed block is intentional.
51
+
52
+ ### Evidence writes and `gh pr merge` MUST be separate tool calls (#1172)
53
+
54
+ The PreToolUse Bash gate evaluates `gh pr merge` **before** the Bash tool call executes. A compound
55
+ command that both writes gate evidence (the findings-log ledger, `upsert-checkpoint-verdict`) and
56
+ merges in the same call is blocked at hook-evaluation time — the write never runs. This can look
57
+ like the ledger "vanished" between writing and merging; it was never written. The block message
58
+ names this when it detects an evidence-writing invocation in the same command string.
59
+
60
+ Documented pattern — **write, verify, then merge alone**:
61
+
62
+ 1. Write the gate evidence (findings-log ledger / checkpoint verdict) in its own Bash call.
63
+ 2. Verify it landed (e.g. `ls tmp/gate-findings/<slug>/pr-<n>/`) in a separate call.
64
+ 3. Run `gh pr merge` alone, with no other command chained via `&&`/`;`/newline.
65
+
50
66
  ## Title markers
51
67
 
52
68
  The PR title is a contract surface, so a merge-blocking marker in the title is enforced
@@ -89,7 +105,7 @@ explicit instruction can unlock.
89
105
  When a repo sets `approval.humanHandoff.enabled: true` in `.devloops`, the loop
90
106
  does not just park silently at the human-merge stop — at the
91
107
  `pre_approval_gate` / `waiting_for_merge_authorization` boundary it **offers**
92
- to route the PR to a named human (pairs with `autonomy.humanMergeOnly`: when
108
+ to route the PR to a contributor (pairs with `autonomy.humanMergeOnly`: when
93
109
  human-merge is enforced, the handoff names who should take it). Disabled by
94
110
  default; no candidate sourcing when disabled.
95
111
 
@@ -4,7 +4,7 @@ A plan file is the persisted markdown artifact that drives work in local-plannin
4
4
 
5
5
  ## Format
6
6
 
7
- A plan file is a markdown document whose sections are level-2 (`##`) headings. It follows the same shape as the phase docs already committed under `docs/phases/`. The directory that holds plan files is `localPlanning.plansDir`, which defaults to `docs/phases/`.
7
+ A plan file is a markdown document whose sections are level-2 (`##`) headings. It follows the same shape as the phase docs already committed under `docs/phases/`, which is also the directory that holds plan files.
8
8
 
9
9
  ## Base authoring sections
10
10
 
@@ -437,7 +437,7 @@ Any conflict that would materially change artifact identity, ownership truth, or
437
437
  | `mode` | `bounded_handoff` (default) \| `durable_auto` | Steers execution mode; `durable_auto` uses the same durable-auto execution-mode semantics as `auto_continue_current`, without replacing the selected intent |
438
438
  | `watch` | boolean | Explicitly request wait/watch semantics; fails closed for otherwise-successful non-wait routed results, while preserving authoritative `stop` and `needs_reconcile` outcomes |
439
439
  | `intent` | any existing public `dev-loop` intent | Disambiguates the supported public intent; maps to existing contract values |
440
- | `targetPreference` | `prefer_github_first` (default) \| `prefer_local` | Steers routing preference; must not override authoritative linked-PR or active-artifact truth |
440
+ | `targetPreference` | `prefer_local` (default) \| `prefer_github_first` | Steers routing preference; must not override authoritative linked-PR or active-artifact truth |
441
441
 
442
442
  The bounded allow-list is exported from `packages/core/src/loop/public-dev-loop-routing.mjs` as `DEV_LOOP_VARIATION_PARAMETER_CONTRACT`.
443
443
 
@@ -87,65 +87,87 @@ Callers have two supported integration options:
87
87
 
88
88
  If the gate result is `needs_reconcile`, the caller must not proceed with the proposed routing. The `nextAction` field instructs the operator to complete or explicitly skip the retrospective.
89
89
 
90
- ## Merge gate (`requireRetrospectiveGate`)
91
-
92
- When `workflow.requireRetrospectiveGate` is enabled in `.devloops` at repo root, merge-ready progression is blocked after `pre_approval_gate` unless the retrospective checkpoint:
93
- - has `state: "complete"`
94
- - includes a `behavioralReview` with `mergeApproved: true`, `followedWorkingAgreement` (boolean), `gateQualityAcceptable` (boolean), and `drifts` (array)
95
- - includes `mergeRecommendation` (non-empty string)
96
- - **(developer mode only)** includes `behavioralReview.internalToolingOnly: true` (boolean) and `behavioralReview.rawCallViolations: []` (empty array) — see **Internal-tooling-only rule** below
97
-
98
- The enforcement function is `evaluateRetrospectiveMergeApproval(checkpoint, { developerMode })` in `packages/core/src/loop/pr-gate-coordination.mjs`, called from `evaluatePrGateCoordination` at each merge-ready boundary.
99
-
100
- ### Internal-tooling-only rule (issue #982) — DEVELOPER MODE ONLY, opt-in
101
-
102
- This is a **developer-mode** retro step: it enforces the dev-loops maintainers' own dogfooding discipline and is **opt-in** via `workflow.requireRetrospectiveInternalTooling` (**default OFF**). **Consumers of the dev-loops extension are never blocked by it** — a consumer may legitimately use raw `gh`/`python`/`node -e` in their own workflow, so when the flag is OFF (the consumer default) the `internalToolingOnly`/`rawCallViolations` fields are neither required nor enforced, and a complete, merge-approved checkpoint passes exactly as it would without these fields. The check only requires and enforces them when `workflow.requireRetrospectiveInternalTooling: true` (the dev-loops repo opts in via its own repo-root `.devloops`).
103
-
104
- When developer mode is ON, the loop's own execution must use internal dev-loops tooling only. **Agent-level top-level raw calls are all the same breach and all block the merge gate:**
105
-
106
- - `gh ...` (including `gh api`, `gh ... --jq`) — use `gate capture-threads`, `gate reply-resolve`, `dev-loops loop info`, `queue …`, or a `node scripts/...` wrapper instead
107
- - `python` / `python3` parse JSON with the tool's `--jq`/built-in output, not an inline Python one-liner
108
- - `node -e` / `node --eval` — inline eval to read/parse tool output is a violation; `node scripts/foo.mjs` is **not**
109
-
110
- **Allowed (NOT violations):** dev-loops subcommands, and `node scripts/*.mjs` invocations — those scripts legitimately call `gh`/GraphQL internally; that is the tooling. The rule targets the agent's own top-level shell calls, not a script's internals.
111
-
112
- The retrospective author records the result in the checkpoint:
113
- - `behavioralReview.internalToolingOnly` (boolean) `true` only when the run used internal tooling throughout
114
- - `behavioralReview.rawCallViolations` (array of strings) — one entry per agent-level raw call; empty when clean
115
-
116
- **Fail-closed (developer mode only):** when `requireRetrospectiveInternalTooling` is ON, the merge gate blocks (`retrospective_gate_pending`) if `internalToolingOnly` is not `true` **OR** `rawCallViolations` is missing/non-empty. `state: "complete"` alone is **not** sufficient a clean tooling record is also required. When the flag is OFF (consumer default) this check is inert: it never blocks, even for a checkpoint that omits the fields or records a violation.
117
-
118
- **Back-compat:** an OLD checkpoint that omits `internalToolingOnly`/`rawCallViolations` only fails closed in developer mode; with the flag OFF (consumer default) it passes unchanged. Re-record the retrospective with the new fields to clear a developer-mode block.
119
-
120
- **Write-op allowlist (verifier only):** `gh pr merge`, `gh pr ready`, `gh issue create`, `gh issue edit` have no internal wrapper today. The deterministic verifier (below) records these as `allowedWriteOps` rather than violations so the gate is not blocked forever on an unavoidable gap. Close the gap with a wrapper to remove an entry. The gate itself still fails on any non-empty `rawCallViolations` the author records.
121
-
122
- ### Deterministic verifier
123
-
124
- `node scripts/loop/check-retro-tooling.mjs [--transcript <path>] [--json]` reads a newline-delimited transcript of the shell commands the agent ran (one top-level command per line, via `--transcript` or stdin) and reports agent-level raw `gh`/`python`/`python3`/`node -e`/`node --eval` calls. Use it to derive the `rawCallViolations` array the retrospective author records. Exit code `1` when violations are found, `0` when clean. The pure `analyzeTranscript(transcript)` export returns `{ violations, allowedWriteOps, internalToolingOnly }`.
125
-
126
- Matching rules: a tool name at the start of a command segment (start of line, or after `&&`/`||`/`|`/`;`); `node` is a violation only with `-e`/`--eval`. Before classifying, the verifier normalizes the segment head — it strips leading `NAME=value` env-assignment prefixes (`GH_TOKEN=x gh api`), strips a leading wrapper binary from `{sudo, env, xargs, time, nice, command}` (`sudo gh api`, `xargs gh api`), and reduces a path-prefixed binary to its basename (`./node_modules/.bin/gh`, `/usr/bin/python3`) — so the common prefixed/wrapped raw-call forms are caught. Known limitation: it does NOT fully parse shell quoting/substitution. A separator inside a quoted argument can over-report; deeply obfuscated calls (command substitution `$(...)`, aliases, `eval`) may evade it — prefer single-line, single-purpose commands in transcripts.
127
-
128
- ### Merge gate states
129
-
130
- | Retrospective state | Merge gate result |
131
- |---|---|
132
- | No checkpoint file | Blocked: `retrospective_gate_pending` |
133
- | `state: "complete"` with `mergeApproved: true` and valid base fields (consumer default; internal-tooling check OFF) | Allowed: proceeds to `FINAL_APPROVAL_READY` |
134
- | `state: "complete"` without `mergeApproved: true` | Blocked |
135
- | **Developer mode ON** + `internalToolingOnly` not `true`, or `rawCallViolations` missing/non-empty | Blocked |
136
- | **Developer mode OFF** (consumer default) + missing `internalToolingOnly`/`rawCallViolations` or a recorded violation | Allowed (check is inert) |
137
- | Missing base required fields (`followedWorkingAgreement`, `gateQualityAcceptable`, `drifts`, `mergeRecommendation`) | Blocked |
138
- | `state: "skipped"` or `state: "required"` | Blocked |
139
-
140
- ### Configuration
141
-
142
- ```yaml
143
- # .devloops at repo root
144
- workflow:
145
- requireRetrospective: true # startup/resume gate
146
- requireRetrospectiveGate: true # merge gate after pre_approval_gate
147
- requireRetrospectiveInternalTooling: true # developer-mode internal-tooling-only check (default OFF; consumers leave unset)
148
- ```
90
+ ## Advisory findings — never a merge gate (issue #1077, Reading B)
91
+
92
+ The retrospective is **advisory**: it runs, records flagged raw-call / discipline
93
+ observations honestly, and passes them back to the conductor (main agent) to
94
+ **decide** what to do with them but it **NEVER blocks a merge or any lifecycle
95
+ transition**. The pre-merge retrospective gate (`evaluateRetrospectiveMergeApproval`
96
+ and the `requireRetrospectiveGate` / `requireRetrospectiveInternalTooling` config
97
+ keys) has been **removed**. There is no `retrospective_gate_pending` / `blocked`
98
+ disposition on account of the internal-tooling raw-call record.
99
+
100
+ ### How findings travel (Reading B)
101
+
102
+ 1. **Deterministic return contract.** The loop subagent's handoff envelope carries
103
+ the retrospective findings as a structured `retrospectiveFindings` field — the
104
+ `check-retro-tooling.mjs` JSON output (`{ internalToolingOnly, rawCallViolations,
105
+ allowedWriteOps }`), not prose. The conductor reads that field. This is a hard
106
+ contract; see [Workflow Handoff Contract](./workflow-handoff-contract.md).
107
+ 2. **Durabilityadvisory PR comment.** The conductor posts a single advisory PR
108
+ comment carrying the findings (`rawCallViolations`, `internalToolingOnly`,
109
+ `allowedWriteOps`). Durable and on-GitHub, but **not a gate**. No disk artifact
110
+ is written for retrospective *findings*.
111
+ 3. **No config.** There is nothing to configure: the retrospective always runs and
112
+ always returns findings. `requireRetrospectiveGate` and
113
+ `requireRetrospectiveInternalTooling` no longer exist.
114
+
115
+ A PR that is otherwise green becomes merge-ready with the violations **recorded**,
116
+ not blocked. The conductor may note them, open a follow-up, coach or ignore.
117
+
118
+ ### Internal-tooling-only rule (issue #982) now advisory
119
+
120
+ This rule records the dev-loops maintainers' own dogfooding discipline: the loop's
121
+ own execution should use internal dev-loops tooling, not agent-level raw
122
+ `gh`/`python`/`node -e` escape hatches. **It no longer blocks.** The flagged calls
123
+ are reported as advisory findings via the envelope + PR comment.
124
+
125
+ **Flagged as raw-call violations:** `gh ...` (including `gh api`, `gh ... --jq`),
126
+ `python` / `python3`, `node -e` / `node --eval` (inline eval). **Allowed (NOT
127
+ violations):** dev-loops subcommands and `node scripts/*.mjs` invocations — those
128
+ scripts legitimately call `gh`/GraphQL internally; that is the tooling. The rule
129
+ targets the agent's own top-level shell calls, not a script's internals.
130
+
131
+ **Write-op allowlist (verifier only):** `gh pr merge`, `gh pr ready`,
132
+ `gh issue create`, `gh issue edit` have no internal wrapper today. The deterministic
133
+ verifier records these as `allowedWriteOps` rather than violations so they are
134
+ surfaced distinctly, not as breaches. Close the gap with a wrapper to remove an
135
+ entry. None of these block anything.
136
+
137
+ ### Deterministic verifier (findings-producer)
138
+
139
+ `node scripts/loop/check-retro-tooling.mjs [--transcript <path>] [--json]` reads a
140
+ newline-delimited transcript of the shell commands the agent ran (one top-level
141
+ command per line, via `--transcript` or stdin) and reports agent-level raw
142
+ `gh`/`python`/`python3`/`node -e`/`node --eval` calls. It is a **findings-producer**:
143
+ its JSON output (`{ ok, internalToolingOnly, rawCallViolations, allowedWriteOps }`)
144
+ is returned to the conductor via the envelope's `retrospectiveFindings` field (the
145
+ envelope carries the normalized shape `{ internalToolingOnly, rawCallViolations,
146
+ allowedWriteOps }` the redundant `ok` flag is dropped by normalization) — it
147
+ is **not** written to a checkpoint and **not** a gate. Exit code `1` when violations
148
+ are found, `0` when clean. The pure `analyzeTranscript(transcript)` export returns
149
+ `{ violations, allowedWriteOps, internalToolingOnly }`.
150
+
151
+ Matching rules: a tool name at the start of a command segment (start of line, or
152
+ after `&&`/`||`/`|`/`;`); `node` is a violation only with `-e`/`--eval`. Before
153
+ classifying, the verifier normalizes the segment head — it strips leading
154
+ `NAME=value` env-assignment prefixes (`GH_TOKEN=x gh api`), strips a leading
155
+ wrapper binary from `{sudo, env, xargs, time, nice, command}` (`sudo gh api`,
156
+ `xargs gh api`), and reduces a path-prefixed binary to its basename
157
+ (`./node_modules/.bin/gh`, `/usr/bin/python3`) — so the common prefixed/wrapped
158
+ raw-call forms are caught. Known limitation: it does NOT fully parse shell
159
+ quoting/substitution. A separator inside a quoted argument can over-report; deeply
160
+ obfuscated calls (command substitution `$(...)`, aliases, `eval`) may evade it —
161
+ prefer single-line, single-purpose commands in transcripts.
162
+
163
+ ### Lifecycle reconciliation
164
+
165
+ The retrospective is described consistently as a **post-merge / advisory
166
+ reflection**, never a pre-merge blocker. The former contradiction —
167
+ `lifecycle-state.mjs` documenting the retro as a post-merge write while
168
+ `pr-gate-coordination.mjs` enforced it pre-merge — is resolved by removing the
169
+ pre-merge gate: the merge lifecycle step proceeds, and the retrospective is an
170
+ advisory reflection whose findings reach the conductor via the envelope.
149
171
 
150
172
  ## Durable artifact format
151
173
 
@@ -162,25 +184,17 @@ The checkpoint file is written by `.pi/extensions/dev-loop-behavioral-review.ts`
162
184
 
163
185
  ### After retrospective is done (written by operator or skill)
164
186
 
165
- A minimal completion clears the startup gate. To also clear the **merge gate**
166
- (`requireRetrospectiveGate`), include the `behavioralReview`, `gateQuality`, and
167
- `mergeRecommendation` fields:
187
+ A minimal completion clears the startup/resume completion gate. The checkpoint
188
+ file carries **only completion state** — retrospective *findings*
189
+ (`behavioralReview`, `rawCallViolations`, `internalToolingOnly`) no longer live on
190
+ disk; they travel in the handoff envelope's `retrospectiveFindings` field and an
191
+ advisory PR comment (issue #1077, Reading B):
168
192
 
169
193
  ```json
170
194
  {
171
195
  "state": "complete",
172
196
  "completedAt": "2026-05-29T16:30:00.000Z",
173
- "notes": "Loop followed working agreement; minor drift on thread resolution.",
174
- "gateQuality": "Real gates with concrete findings and follow-through.",
175
- "mergeRecommendation": "Merge approved — all gates passed clean.",
176
- "behavioralReview": {
177
- "mergeApproved": true,
178
- "followedWorkingAgreement": true,
179
- "gateQualityAcceptable": true,
180
- "drifts": [],
181
- "internalToolingOnly": true,
182
- "rawCallViolations": []
183
- }
197
+ "notes": "Loop followed working agreement; minor drift on thread resolution."
184
198
  }
185
199
  ```
186
200
 
@@ -199,9 +213,9 @@ A minimal completion clears the startup gate. To also clear the **merge gate**
199
213
  | Artifact | Location |
200
214
  |---|---|
201
215
  | Checkpoint state machine | `packages/core/src/loop/retrospective-checkpoint.mjs` (internal core module; not part of the public package exports surface) |
202
- | Merge gate evaluator | `packages/core/src/loop/pr-gate-coordination.mjs` `evaluateRetrospectiveMergeApproval` |
203
- | Internal-tooling verifier | `scripts/loop/check-retro-tooling.mjs` (+ `test/loop/check-retro-tooling.test.mjs`) |
204
- | Tests | `packages/core/test/retrospective-checkpoint.test.mjs`, `packages/core/test/pr-gate-coordination.test.mjs` |
216
+ | Internal-tooling verifier (findings-producer) | `scripts/loop/check-retro-tooling.mjs` (+ `test/loop/check-retro-tooling.test.mjs`) |
217
+ | Advisory findings envelope field | `packages/core/src/loop/handoff-envelope.mjs` `retrospectiveFindings` |
218
+ | Tests | `packages/core/test/retrospective-checkpoint.test.mjs`, `packages/core/test/pr-gate-coordination.test.mjs`, `packages/core/test/handoff-envelope.test.mjs` |
205
219
  | Extension (writes required marker, fires review prompt) | `.pi/extensions/dev-loop-behavioral-review.ts` |
206
220
  | Checkpoint file | `.pi/dev-loop-retrospective-checkpoint.json` |
207
221
  | AGENTS.md repo contract | [Agent Instructions](../../AGENTS.md) — concise repo contract and working rules |
@@ -96,6 +96,38 @@ to match `UI_E2E_CHECK_NAMES` so the gate can read a real signal:
96
96
  So a deck- or article-only PR has a CI check that can actually satisfy the gate;
97
97
  without these jobs such a PR would fail closed with no satisfiable signal.
98
98
 
99
+ ## Verifying a UI change: render and look, but don't persist ad-hoc checks
100
+
101
+ When you change a rendered artifact's layout, **verify it visually before merging** —
102
+ render the built page and inspect real geometry (`getBoundingClientRect` for
103
+ edges/widths, a full-page screenshot to actually look at it) at desktop and mobile
104
+ widths. This intermediary e2e pass is a required *verification strategy*: a layout
105
+ claim ("the columns align now") is only credible once measured against the rendered
106
+ page, not the source CSS.
107
+
108
+ It is **not** something to persist into the codebase. The registered, always-on
109
+ coverage is the shared fit/CSP suite (`defineArticleSuite`/`defineDeckSuite`); do
110
+ not grow it with one-off pixel/alignment assertions written to confirm a single
111
+ fix. Use a throwaway script to render, measure, and screenshot while iterating,
112
+ then drop it. Adding a bespoke geometry assertion per fix bloats the suite and
113
+ couples it to incidental layout numbers.
114
+
115
+ ## An assertion must be able to fail on the defect
116
+
117
+ A UI assertion that is **true for the broken state validates the bug**. Real
118
+ example (the layout regression this rule comes from): a check asserted
119
+ `margin-left > 1px` on the prose to prove it was "centered" — but the visible bug
120
+ *was* the prose floating in a narrow centered measure, misaligned from the
121
+ full-width boxes beside it. `margin-left` was large in exactly the broken state, so
122
+ the check passed green on every gate while the page rendered visibly broken.
123
+
124
+ Before trusting any UI assertion, confirm it **fails on the defect**: run it against
125
+ the broken layout, or inject the defect (e.g. shove one block's left edge) and check
126
+ that the assertion goes red. An assertion that cannot fail on the bug it names is
127
+ not coverage — it is false confidence. Prefer asserting the invariant that actually
128
+ defines "correct" (e.g. all content blocks share one left edge within tolerance)
129
+ over a proxy that happens to correlate with it.
130
+
99
131
  ## Non-goals
100
132
 
101
133
  Not always-on screenshot testing; not mandatory multi-browser. The criterion is
@@ -17,6 +17,35 @@ template.
17
17
  | Gate state (detectors) + strategy defaults | `currentGate`, `worktreeRequired` |
18
18
  | Settings (`.devloops` at repo root + `defaults.yaml`) | `gateConfig`, `stopRules`, `asyncStartMode`, `requireDraftFirst`, `maxCopilotRounds` |
19
19
  | Gate state (detectors) | `currentHeadSha`, `ciStatus`, `unresolvedThreadCount`, `copilotRoundCount` |
20
+ | Canonical sanctioned-command map (`scripts/loop/sanctioned-commands.mjs`) | `sanctionedCommands` |
21
+
22
+ ## Sanctioned commands (MANDATORY DEFAULT — issue #1081)
23
+
24
+ `sanctionedCommands` is a **mandatory default** element of every handoff
25
+ envelope. It is the operation → wrapper command map (which wrapper performs
26
+ which GitHub/loop operation), plus the forbidden and orchestrator-owned lists.
27
+ The `loop build-envelope` CLI injects it into every emitted envelope, so a
28
+ spawned dev-loop subagent receives it verbatim without the briefer adding it —
29
+ no re-deriving which wrapper does `gh pr ready` etc.
30
+
31
+ The **single source of truth** is `scripts/loop/sanctioned-commands.mjs` (a
32
+ frozen data module). `@dev-loops/core` stays consumer-agnostic: it defines the
33
+ envelope SHAPE and carries whatever `sanctionedCommands` object the consumer
34
+ supplies; the repo-specific `scripts/...` paths live only in the consumer
35
+ module. Do not duplicate the map into prose — reference the module.
36
+
37
+ ```typescript
38
+ sanctionedCommands?: {
39
+ reads: Record<string, string>; // operation → wrapper path (some also accept `loop info`)
40
+ edits: Record<string, string>;
41
+ lifecycle: Record<string, string>;
42
+ forbidden: string[]; // raw `gh pr view/checks/edit`, `node -e`, `python -c`, transcript tailing, sleep-poll loops
43
+ orchestratorOwned: string[]; // `gh pr merge`, board status transitions — never done by a subagent
44
+ };
45
+ ```
46
+
47
+ A contract test (`test/contracts/sanctioned-commands-exist.test.mjs`) asserts
48
+ every mapped wrapper exists on disk and fails closed if one is renamed/removed.
20
49
 
21
50
  ## Acceptance templates
22
51
 
@@ -111,6 +140,15 @@ interface HandoffEnvelope {
111
140
  scopeConstraint?: string;
112
141
  customStopAt?: string;
113
142
  };
143
+
144
+ // Mandatory default (issue #1081). Source: scripts/loop/sanctioned-commands.mjs
145
+ sanctionedCommands?: {
146
+ reads: Record<string, string>;
147
+ edits: Record<string, string>;
148
+ lifecycle: Record<string, string>;
149
+ forbidden: string[];
150
+ orchestratorOwned: string[];
151
+ };
114
152
  }
115
153
  ```
116
154
 
@@ -121,6 +159,7 @@ interface HandoffEnvelope {
121
159
  3. Execute `nextAction`.
122
160
  4. Respect `stopRules` — do not proceed past a gated stop point without authorization.
123
161
  5. Use `acceptance` to self-validate before declaring completion.
162
+ 6. Use `sanctionedCommands` as the authoritative operation → wrapper map: never call a raw `gh`/`node -e`/`python -c` for an operation the map covers, and never perform an `orchestratorOwned` action.
124
163
 
125
164
  ## Backward compatibility
126
165