dev-loops 0.4.0 → 0.5.0

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 (49) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/agents/refiner.md +1 -0
  4. package/.claude/skills/dev-loop/SKILL.md +16 -6
  5. package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
  6. package/.claude/skills/docs/artifact-authority-contract.md +86 -31
  7. package/.claude/skills/docs/local-planning-flow.md +63 -0
  8. package/.claude/skills/docs/local-planning-worked-example.md +139 -0
  9. package/.claude/skills/docs/merge-preconditions.md +35 -0
  10. package/.claude/skills/docs/plan-file-contract.md +37 -0
  11. package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
  12. package/.claude/skills/docs/spike-mode-contract.md +237 -0
  13. package/CHANGELOG.md +43 -0
  14. package/README.md +1 -0
  15. package/agents/refiner.agent.md +1 -0
  16. package/package.json +5 -3
  17. package/scripts/github/capture-review-threads.mjs +20 -2
  18. package/scripts/github/probe-copilot-review.mjs +69 -3
  19. package/scripts/github/upsert-checkpoint-verdict.mjs +18 -3
  20. package/scripts/lib/jq-output.mjs +297 -0
  21. package/scripts/loop/check-retro-tooling.mjs +246 -0
  22. package/scripts/loop/copilot-pr-handoff.mjs +21 -3
  23. package/scripts/loop/detect-pr-gate-coordination-state.mjs +35 -2
  24. package/scripts/loop/docs-grill-contract.mjs +70 -0
  25. package/scripts/loop/info.mjs +21 -2
  26. package/scripts/loop/pr-runner-coordination.mjs +20 -4
  27. package/scripts/loop/resolve-dev-loop-startup.mjs +176 -5
  28. package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
  29. package/scripts/loop/run-watch-cycle.mjs +77 -3
  30. package/scripts/loop/slides-story-review-contract.mjs +123 -0
  31. package/scripts/pages/build-site.mjs +136 -0
  32. package/scripts/projects/add-queue-item.mjs +12 -2
  33. package/scripts/projects/list-queue-items.mjs +12 -2
  34. package/scripts/projects/move-queue-item.mjs +12 -2
  35. package/scripts/refine/_refine-helpers.mjs +20 -0
  36. package/scripts/refine/exit-spike.mjs +186 -0
  37. package/scripts/refine/promote-plan.mjs +387 -0
  38. package/scripts/refine/refine-plan-file.mjs +165 -0
  39. package/scripts/refine/validate-plan-file.mjs +64 -0
  40. package/scripts/refine/validate-spike-file.mjs +87 -0
  41. package/skills/dev-loop/SKILL.md +11 -1
  42. package/skills/dev-loop/templates/slides-story-review.md +54 -0
  43. package/skills/docs/artifact-authority-contract.md +86 -31
  44. package/skills/docs/local-planning-flow.md +63 -0
  45. package/skills/docs/local-planning-worked-example.md +139 -0
  46. package/skills/docs/merge-preconditions.md +35 -0
  47. package/skills/docs/plan-file-contract.md +37 -0
  48. package/skills/docs/retrospective-checkpoint-contract.md +55 -7
  49. package/skills/docs/spike-mode-contract.md +237 -0
@@ -0,0 +1,63 @@
1
+ # Local-planning flow
2
+
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
+
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/`.
6
+
7
+ ## Stages
8
+
9
+ | Stage | Helper script | Pure logic |
10
+ |---|---|---|
11
+ | Validate | `scripts/refine/validate-plan-file.mjs` | `validatePlanFile` (in the helper script; not a `@dev-loops/core` export) |
12
+ | Start | `scripts/loop/resolve-dev-loop-startup.mjs --plan-file <path>` | `evaluatePlanFileIntakeState` (`@dev-loops/core/loop/plan-file-intake-contract`) |
13
+ | Refine | `scripts/refine/refine-plan-file.mjs` | `refinePlanFileInPlace` (`@dev-loops/core/loop/plan-file-refine-contract`) |
14
+ | Promote | `scripts/refine/promote-plan.mjs` | `evaluatePromoteEligibility` / `buildPromotionPrBody` (`@dev-loops/core/loop/plan-file-promote-contract`) |
15
+
16
+ ## 1. Author and validate the plan
17
+
18
+ Author a phase-doc-format plan under `docs/phases/` with the four base sections `## Status`, `## Objective`, `## In scope`, `## Explicit non-goals`. Check it against the base-section contract:
19
+
20
+ ```
21
+ node scripts/refine/validate-plan-file.mjs --input docs/phases/phase-<n>.md --json
22
+ ```
23
+
24
+ The JSON payload reports `{ checker: "validate-plan-file", ok, errors }`. Each absent or empty-body base section contributes one entry with a distinct `missing_*` code; `ok` is `true` when every base section is present with a non-empty body.
25
+
26
+ ## 2. Start the local-planning session
27
+
28
+ Hand the plan to startup with `--plan-file` (mutually exclusive with `--issue`, `--pr`, and `--input`):
29
+
30
+ ```
31
+ node scripts/loop/resolve-dev-loop-startup.mjs --plan-file docs/phases/phase-<n>.md
32
+ ```
33
+
34
+ Startup validates the plan and threads an intake state onto its output. A plan with valid base sections and no refinement sections is `new_plan_needs_refinement`; the same plan once it also carries `Acceptance criteria` and `Definition of done` is `plan_refined_ready_for_promotion`. A base-valid plan that carries only one of the two refinement sections is reported as `ambiguous_fail_closed` with exit 0 and is not routed forward — the operator completes the missing section first. A plan that is missing/unreadable or fails the base validator makes startup fail closed (exit 1, no readiness bundle).
35
+
36
+ ## 3. Refine in place and stop at the local human-review checkpoint
37
+
38
+ The refine step writes the refiner output back into the same plan file:
39
+
40
+ ```
41
+ node scripts/refine/refine-plan-file.mjs --plan-file docs/phases/phase-<n>.md --payload <payload.json>
42
+ ```
43
+
44
+ `refinePlanFileInPlace` appends the `Acceptance criteria` and `Definition of done` sections, a `Coverage matrix` section, and a `Docs-grill findings` section, then advances the intake state to `plan_refined_ready_for_promotion` and stops at the `local_human_review` checkpoint. The docs-grill runs as a step within refinement: each finding is classified with `classifyDocsGrillFinding` (see the [Docs-Grill Step](../../docs/docs-grill-step.md)) and the dispositions are recorded into the plan. The step makes no GitHub or network call; the human reviews the refined plan before anything is promoted.
45
+
46
+ ## 4. Promote to a single draft PR
47
+
48
+ Once the human approves the refined plan, promote it:
49
+
50
+ ```
51
+ node scripts/refine/promote-plan.mjs --plan-file docs/phases/phase-<n>.md
52
+ ```
53
+
54
+ Promotion is PR-first: it commits the plan doc and opens exactly one draft PR via the canonical PR wrapper, and mints no GitHub issue. It records the plan↔PR link bidirectionally — the PR body references the committed plan-doc path, and the plan's front-matter gains a `prNumber:` entry. The committed plan doc is the spec-of-record; the draft PR enters the standard draft → pre-approval → human-merge flow. Promotion is idempotent: re-running on a plan that already carries `prNumber` resolves to `already_promoted` and opens nothing.
55
+
56
+ ## Relationship to other docs
57
+
58
+ | Doc | Relationship |
59
+ |---|---|
60
+ | [Artifact Authority Contract](artifact-authority-contract.md) | Canonical model and per-phase contract details for the local-first flow |
61
+ | [Plan-file Contract](plan-file-contract.md) | Plan-file format, base sections, validator, and the optional `prNumber` front-matter |
62
+ | [Local-Planning Worked Example](local-planning-worked-example.md) | One plan file shown through every stage of this sequence |
63
+ | [Docs-Grill Step](../../docs/docs-grill-step.md) | The in-loop grill that runs as a step within refinement |
@@ -0,0 +1,139 @@
1
+ # Local-planning worked example
2
+
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
+
5
+ The repo runs local-planning by default (`strategy.default: local-first`), and the plan lives under `localPlanning.plansDir` (`docs/phases/`).
6
+
7
+ ## Stage 1 — Authored
8
+
9
+ The operator writes the four base sections. The file at this point:
10
+
11
+ ```markdown
12
+ # phase-42 plan
13
+
14
+ ## Status
15
+
16
+ draft
17
+
18
+ ## Objective
19
+
20
+ Add a `--dry-run` flag to the queue dispatcher so an operator can preview the
21
+ dispatch order without mutating the board.
22
+
23
+ ## In scope
24
+
25
+ - Parse `--dry-run` in the queue dispatcher CLI.
26
+ - Print the resolved dispatch order and exit 0 without any board mutation.
27
+
28
+ ## Explicit non-goals
29
+
30
+ - Changing the dispatch ordering algorithm.
31
+ - Any change to non-dry-run dispatch behavior.
32
+ ```
33
+
34
+ Validation passes:
35
+
36
+ ```
37
+ $ node scripts/refine/validate-plan-file.mjs --input docs/phases/phase-42.md --json
38
+ { "checker": "validate-plan-file", "ok": true, "errors": [] }
39
+ ```
40
+
41
+ ## Stage 2 — Started (intake state)
42
+
43
+ Startup reads the plan and reports the intake state. With the four base sections present and no refinement sections yet, the state is `new_plan_needs_refinement`:
44
+
45
+ ```
46
+ $ node scripts/loop/resolve-dev-loop-startup.mjs --plan-file docs/phases/phase-42.md
47
+ # ... startup output carries planFileIntakeState: "new_plan_needs_refinement"
48
+ ```
49
+
50
+ ## Stage 3 — Refined in place
51
+
52
+ `refine-plan-file.mjs` appends the refinement sections, the coverage matrix, and the recorded docs-grill findings to the same file, then stops at the `local_human_review` checkpoint. The file now reads (added sections shown):
53
+
54
+ ```markdown
55
+ # phase-42 plan
56
+
57
+ ## Status
58
+
59
+ draft
60
+
61
+ ## Objective
62
+
63
+ Add a `--dry-run` flag to the queue dispatcher so an operator can preview the
64
+ dispatch order without mutating the board.
65
+
66
+ ## In scope
67
+
68
+ - Parse `--dry-run` in the queue dispatcher CLI.
69
+ - Print the resolved dispatch order and exit 0 without any board mutation.
70
+
71
+ ## Explicit non-goals
72
+
73
+ - Changing the dispatch ordering algorithm.
74
+ - Any change to non-dry-run dispatch behavior.
75
+
76
+ ## Acceptance criteria
77
+
78
+ - `--dry-run` prints the resolved dispatch order to stdout.
79
+ - `--dry-run` performs zero board mutation (no Projects V2 writes).
80
+ - Exit code is 0 on a successful dry-run preview.
81
+
82
+ ## Definition of done
83
+
84
+ - CLI parses `--dry-run`; unit test covers the parse path.
85
+ - A test asserts no board-mutation call fires under `--dry-run`.
86
+ - The queue docs note the flag.
87
+
88
+ ## Coverage matrix
89
+
90
+ | Item | Type | Status | Evidence | Notes |
91
+ |---|---|---|---|---|
92
+ | `--dry-run` prints dispatch order | AC | Unverified | pending implementation | |
93
+ | `--dry-run` performs zero mutation | AC | Unverified | pending implementation | |
94
+ | Exit code 0 on preview | AC | Unverified | pending implementation | |
95
+ | No-mutation test | DoD | Unverified | pending test | |
96
+ | Dispatch algorithm unchanged | Non-goal | Unverified | scope boundary | |
97
+
98
+ ## Docs-grill findings
99
+
100
+ - [fix_in_place] (stale_reference) Queue docs example omits the new flag; add it on this branch.
101
+ ```
102
+
103
+ The intake state for this file is now `plan_refined_ready_for_promotion`. The loop stops here for the operator to review the refined plan before promotion.
104
+
105
+ ## Stage 4 — Local human-review checkpoint
106
+
107
+ The operator reads the refined plan, confirms the acceptance criteria and coverage matrix, and dispositions the docs-grill findings. No GitHub mutation has happened so far; the plan file is the only artifact. When the operator approves, the flow moves to promotion.
108
+
109
+ ## Stage 5 — Promoted to a draft PR
110
+
111
+ `promote-plan.mjs` commits the plan doc, opens one draft PR, and writes the PR number back into the plan's front-matter. The file gains a leading front-matter block (the body is unchanged):
112
+
113
+ ```markdown
114
+ ---
115
+ prNumber: 1234
116
+ ---
117
+ # phase-42 plan
118
+
119
+ ## Status
120
+
121
+ draft
122
+
123
+ ## Objective
124
+
125
+ Add a `--dry-run` flag to the queue dispatcher so an operator can preview the
126
+ dispatch order without mutating the board.
127
+
128
+ # ... base, refinement, coverage-matrix, and docs-grill sections unchanged ...
129
+ ```
130
+
131
+ The PR body references `docs/phases/phase-42.md` as the spec-of-record, completing the bidirectional plan↔PR link. No GitHub issue was minted. Re-running `promote-plan.mjs` on this file now resolves to `already_promoted` and opens nothing, because the front-matter already carries `prNumber`.
132
+
133
+ ## Relationship to other docs
134
+
135
+ | Doc | Relationship |
136
+ |---|---|
137
+ | [Local-Planning Flow](local-planning-flow.md) | The operator sequence this example traces |
138
+ | [Artifact Authority Contract](artifact-authority-contract.md) | Canonical model and per-phase contract details |
139
+ | [Plan-file Contract](plan-file-contract.md) | Plan-file format, base sections, and the `prNumber` front-matter |
@@ -2,8 +2,43 @@
2
2
 
3
3
  Canonical owner for merge preconditions across all workflow families.
4
4
 
5
+ ## Conflict-free (mergeable) is a precondition at every gate
6
+
7
+ A PR that conflicts with its base gets **no `pull_request` CI run** — GitHub can't
8
+ compute the merge ref — so the gate silently stalls (green-less with no obvious
9
+ cause). Conflict-free is therefore a required gate precondition, checked at **two
10
+ seams**: before requesting CI/Copilot, and again before merge.
11
+
12
+ - `gh pr view --json mergeable,mergeStateStatus` drives it. A `CONFLICTING` /
13
+ `DIRTY` / `BEHIND` PR does **not** pass any gate (`gateBoundary:
14
+ conflict_resolution`, `nextAction: resolve_merge_conflicts`).
15
+ - `mergeable` is computed asynchronously, so a freshly-pushed head briefly reads
16
+ `UNKNOWN`. The detect layer **re-polls a bounded number of times**; if it never
17
+ settles, the gate **fails closed to a recheck** (`nextAction: wait_for_ci`) —
18
+ an unsettled merge state is never treated as clean.
19
+ - `loop info` surfaces a **Mergeable:** line (mergeStateStatus included) so a
20
+ conflict is diagnosed immediately, never mistaken for missing CI.
21
+
22
+ ### Deterministic auto-resolve (additive CHANGELOG only)
23
+
24
+ When a PR is behind/`CONFLICTING`, run the conservative resolver before resuming
25
+ the gate path:
26
+
27
+ ```sh
28
+ node scripts/loop/resolve-pr-conflicts.mjs [--base <branch>] [--push]
29
+ ```
30
+
31
+ It merges `origin/<base>` into the PR branch and resolves **only the safe additive
32
+ case** — a `CHANGELOG.md` conflict where both sides only ADD list/section entries
33
+ (keep BOTH sides, in order) — then runs `npm run test:docs` and (with `--push`)
34
+ pushes. **Any other conflicted path, or a non-additive CHANGELOG edit, FAILS
35
+ CLOSED** naming the conflicted paths (no silent stall, no guessing — resolve those
36
+ by hand). This encodes the exact additive-CHANGELOG resolution the loop has been
37
+ doing manually; it is not a general conflict-resolution engine.
38
+
5
39
  ## Required before merge
6
40
 
41
+ 1. ✅ Conflict-free with base (`mergeable: MERGEABLE`; not `CONFLICTING`/`DIRTY`/`BEHIND`/`UNKNOWN`)
7
42
  1. ✅ CI green on current head (or crediblyGreen via `--local-validation-head-sha`)
8
43
  2. ✅ Draft gate satisfied (clean verdict)
9
44
  3. ✅ Pre-approval gate satisfied (clean verdict, current head)
@@ -0,0 +1,37 @@
1
+ # Plan-file contract
2
+
3
+ A plan file is the persisted markdown artifact that drives work in local-planning mode. It reuses the existing phase-doc format under `docs/phases/` (for example, `docs/phases/phase-<n>.md`). The artifact authority model that selects local-planning mode lives in the [Artifact Authority Contract](artifact-authority-contract.md); this document defines the file format and its required base sections.
4
+
5
+ ## Format
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/`.
8
+
9
+ ## Base authoring sections
10
+
11
+ A plan file carries these base sections when it is authored, before refinement adds acceptance criteria and a definition of done:
12
+
13
+ | Heading | Purpose |
14
+ |---|---|
15
+ | `## Status` | The current lifecycle state of the plan (for example, in progress, deferred). |
16
+ | `## Objective` | What the plan sets out to achieve. |
17
+ | `## In scope` | The bounded work this plan covers. |
18
+ | `## Explicit non-goals` | What this plan leaves out. |
19
+
20
+ Each base section has a non-empty body. Refinement is a later phase that adds acceptance criteria and a definition of done; those are outside the base authoring contract this document defines.
21
+
22
+ ## Validator
23
+
24
+ `scripts/refine/validate-plan-file.mjs` checks a plan file against the base sections above. It exports a pure function `validatePlanFile(markdownText)` that returns `{ checker: "validate-plan-file", ok, errors }`, where each absent or empty-body base section contributes one entry with a distinct `missing_*` code. `ok` is `true` when every base section is present with a non-empty body.
25
+
26
+ The thin CLI accepts `--input <path>`, `--json`, and `--help`. The validation verdict is reported in the JSON payload; the process exits non-zero for argument or path errors (house style — see the [Validation Policy](validation-policy.md)).
27
+
28
+ ## Optional front-matter (plan↔PR link)
29
+
30
+ A plan file may carry an optional leading YAML front-matter block — a `---` line, `key: value` lines, a closing `---` line — before its first heading. It is additive: a plan without front-matter is unchanged and fully valid. The only key the tooling reads or writes today is `prNumber:`, the number of the draft PR a plan was promoted into (PR-FIRST promotion never mints an issue; the committed plan doc is the spec-of-record and the PR body links it). The link is recorded bidirectionally — the PR body references the committed plan-doc path; the plan's `prNumber` front-matter references the PR. Promotion (`scripts/refine/promote-plan.mjs`) writes `prNumber` and is idempotent on a plan that already carries one. The parser/serializer lives in the pure `@dev-loops/core/loop/plan-file-promote-contract` module.
31
+
32
+ ## Relationship to other docs
33
+
34
+ | Doc | Relationship |
35
+ |---|---|
36
+ | [Artifact Authority Contract](artifact-authority-contract.md) | Owns the artifact-selection model; selects local-planning mode and points here for the plan-file format. |
37
+ | [Validation Policy](validation-policy.md) | Owns the shared CLI verdict-versus-exit-code convention this validator follows. |
@@ -93,17 +93,48 @@ When `workflow.requireRetrospectiveGate` is enabled in `.devloops` at repo root,
93
93
  - has `state: "complete"`
94
94
  - includes a `behavioralReview` with `mergeApproved: true`, `followedWorkingAgreement` (boolean), `gateQualityAcceptable` (boolean), and `drifts` (array)
95
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
96
97
 
97
- The enforcement function is `evaluateRetrospectiveMergeApproval(checkpoint)` in `packages/core/src/loop/pr-gate-coordination.mjs`, called from `evaluatePrGateCoordination` at each merge-ready boundary.
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.
98
127
 
99
128
  ### Merge gate states
100
129
 
101
130
  | Retrospective state | Merge gate result |
102
131
  |---|---|
103
132
  | No checkpoint file | Blocked: `retrospective_gate_pending` |
104
- | `state: "complete"` with `mergeApproved: true` and valid fields | Allowed: proceeds to `FINAL_APPROVAL_READY` |
133
+ | `state: "complete"` with `mergeApproved: true` and valid base fields (consumer default; internal-tooling check OFF) | Allowed: proceeds to `FINAL_APPROVAL_READY` |
105
134
  | `state: "complete"` without `mergeApproved: true` | Blocked |
106
- | Missing required fields (`followedWorkingAgreement`, `gateQualityAcceptable`, `drifts`, `mergeRecommendation`) | 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 |
107
138
  | `state: "skipped"` or `state: "required"` | Blocked |
108
139
 
109
140
  ### Configuration
@@ -111,8 +142,9 @@ The enforcement function is `evaluateRetrospectiveMergeApproval(checkpoint)` in
111
142
  ```yaml
112
143
  # .devloops at repo root
113
144
  workflow:
114
- requireRetrospective: true # startup/resume gate
115
- requireRetrospectiveGate: true # merge gate after pre_approval_gate
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)
116
148
  ```
117
149
 
118
150
  ## Durable artifact format
@@ -130,11 +162,25 @@ The checkpoint file is written by `.pi/extensions/dev-loop-behavioral-review.ts`
130
162
 
131
163
  ### After retrospective is done (written by operator or skill)
132
164
 
165
+ A minimal completion clears the startup gate. To also clear the **merge gate**
166
+ (`requireRetrospectiveGate`), include the `behavioralReview`, `gateQuality`, and
167
+ `mergeRecommendation` fields:
168
+
133
169
  ```json
134
170
  {
135
171
  "state": "complete",
136
172
  "completedAt": "2026-05-29T16:30:00.000Z",
137
- "notes": "Loop followed working agreement; minor drift on thread resolution."
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
+ }
138
184
  }
139
185
  ```
140
186
 
@@ -153,7 +199,9 @@ The checkpoint file is written by `.pi/extensions/dev-loop-behavioral-review.ts`
153
199
  | Artifact | Location |
154
200
  |---|---|
155
201
  | Checkpoint state machine | `packages/core/src/loop/retrospective-checkpoint.mjs` (internal core module; not part of the public package exports surface) |
156
- | Tests | `packages/core/test/retrospective-checkpoint.test.mjs` |
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` |
157
205
  | Extension (writes required marker, fires review prompt) | `.pi/extensions/dev-loop-behavioral-review.ts` |
158
206
  | Checkpoint file | `.pi/dev-loop-retrospective-checkpoint.json` |
159
207
  | AGENTS.md repo contract | [Agent Instructions](../../AGENTS.md) — concise repo contract and working rules |
@@ -0,0 +1,237 @@
1
+ # Spike-mode contract
2
+
3
+ A spike is a time-boxed, exploratory dev-loop run started from a local question with no GitHub issue. Its deliverable is a findings document. When the exploration reaches a recommendation, the operator concludes it with one of two exits: **discard** (drop it with zero tracker artifacts) or **graduate** (emit a #947-style local-first plan file that enters the existing plan→PR promotion path).
4
+
5
+ This doc is the canonical operator sequence for spike mode and carries a worked example showing both exits. The spike artifact lives outside the tracker; it is exempt from the production-gate ceremony at entry and runs under a relaxed gate profile (`gates.spike`). The [Artifact Authority Contract](artifact-authority-contract.md) owns the local-planning model a graduated spike feeds into.
6
+
7
+ The spike track shipped across three phases: P1 (#964) the `--spike` intake state machine, P2 (#965) the relaxed gate profile and the discard/graduate exits, P3 (#966) this doc.
8
+
9
+ ## Shipped surfaces
10
+
11
+ | Stage | Helper script | Pure logic |
12
+ |---|---|---|
13
+ | Validate | `scripts/refine/validate-spike-file.mjs` | `validateSpikeFile` / `validateSpikeExplorationSections` (in the helper script) |
14
+ | Start | `scripts/loop/resolve-dev-loop-startup.mjs --spike <path>` | `buildSpikeInput` (in the script) + `evaluateSpikeIntakeState` (`@dev-loops/core/loop/spike-intake-contract`) |
15
+ | Exit | `scripts/refine/exit-spike.mjs` | `evaluateSpikeExit` / `buildGraduatedPlanBody` (`@dev-loops/core/loop/spike-exit-contract`) |
16
+
17
+ The spike findings artifact carries four authoring sections (`scripts/refine/validate-spike-file.mjs`, `SPIKE_FILE_BASE_SECTIONS`):
18
+
19
+ - `## Question` — what the spike investigates
20
+ - `## Approach` — how the exploration was carried out
21
+ - `## Findings` — the evidence gathered
22
+ - `## Recommendation` — the exit marker
23
+
24
+ The first three (`SPIKE_FILE_EXPLORATION_SECTIONS`) are the exploration scaffold required to **start** a spike. `Recommendation` (`SPIKE_FILE_EXIT_MARKER_SECTION`) is filled in during the spike; once it is present and non-empty, the intake state flips to ready-for-exit.
25
+
26
+ ## Intake states
27
+
28
+ `evaluateSpikeIntakeState` (`packages/core/src/loop/spike-intake-contract.mjs`) classifies the artifact into one of three `SPIKE_INTAKE_STATE` values from two facts — whether the exploration scaffold is valid, and whether a Recommendation is present:
29
+
30
+ | State | Meaning |
31
+ |---|---|
32
+ | `spike_in_progress` | Scaffold (Question/Approach/Findings) valid; no Recommendation yet |
33
+ | `spike_ready_for_exit` | Scaffold valid and a non-empty Recommendation is present; an exit decision can be made |
34
+ | `ambiguous_fail_closed` | Scaffold invalid (a missing or empty base section); fail closed, no routing |
35
+
36
+ ## Operator sequence
37
+
38
+ ### 1. Author the exploration scaffold
39
+
40
+ Write a findings artifact carrying `## Question`, `## Approach`, and `## Findings` with non-empty bodies. `## Recommendation` is left for the spike to fill in. Check the full set of base sections with the validator:
41
+
42
+ ```
43
+ node scripts/refine/validate-spike-file.mjs --input <spike.md> --json
44
+ ```
45
+
46
+ The payload is `{ checker: "validate-spike-file", ok, errors }`, with a distinct `missing_*` code per absent or empty section (`missing_question`, `missing_approach`, `missing_findings`, `missing_recommendation`). An in-progress spike reports `missing_recommendation` here and is still startable, because startup gates on the exploration scaffold (`validateSpikeExplorationSections`), which omits the Recommendation.
47
+
48
+ ### 2. Start the spike
49
+
50
+ Hand the artifact to startup with `--spike` (mutually exclusive with `--issue`, `--pr`, `--input`, `--plan-file`):
51
+
52
+ ```
53
+ node scripts/loop/resolve-dev-loop-startup.mjs --spike <spike.md>
54
+ ```
55
+
56
+ `buildSpikeInput` (`scripts/loop/resolve-dev-loop-startup.mjs`) reads the file read-only — no tracker mutation, no GitHub call, no issue or PR number. It requires the exploration scaffold; a missing/unreadable file, or one failing `validateSpikeExplorationSections`, throws and startup fails closed (exit 1, no readiness bundle). On success it builds a `local_phase` startup input with the resolved spike path as the target `phase` and threads `spikeIntakeState` onto the output. A spike with no Recommendation reports `spike_in_progress`. The spike path is exempt from the worktree-isolation guard because there is no issue to key a worktree on (`planFileExempt: true`).
57
+
58
+ ### 3. Run under the relaxed gate profile
59
+
60
+ A spike runs under `gates.spike`, a lighter angle set than the production draft → pre-approval gates. See [Relaxed gate profile](#relaxed-gate-profile) below.
61
+
62
+ ### 4. Reach a Recommendation, then exit
63
+
64
+ When the exploration concludes, fill in `## Recommendation`. The intake state is now `spike_ready_for_exit`, and `exit-spike.mjs` can conclude the spike. It recomputes the intake facts (`validateSpikeExplorationSections` for the scaffold, `extractSection` for the Recommendation), runs `evaluateSpikeExit`, and routes to one of two dispositions.
65
+
66
+ ```
67
+ node scripts/refine/exit-spike.mjs --spike-file <spike.md> --disposition <discard|graduate> [--plan-file <path>] [--json]
68
+ ```
69
+
70
+ `evaluateSpikeExit` (`packages/core/src/loop/spike-exit-contract.mjs`) is eligible only from `spike_ready_for_exit`. From any other state it fails closed with `not_ready_for_exit`; an unrecognized disposition fails closed with `unknown_disposition`. On a fail-closed path the CLI makes zero tracker mutation, writes no plan file, and exits 1.
71
+
72
+ ## Exit dispositions
73
+
74
+ The two dispositions are `SPIKE_EXIT_DISPOSITION.DISCARD` (`"discard"`) and `SPIKE_EXIT_DISPOSITION.GRADUATE` (`"graduate"`).
75
+
76
+ ### discard
77
+
78
+ The recommendation is "don't pursue". The spike is dropped with zero tracker artifacts; the findings document on disk is the whole record. The CLI writes nothing and creates no GitHub artifact. `--plan-file` is irrelevant for a discard.
79
+
80
+ ### graduate
81
+
82
+ The recommendation is "pursue this". `buildGraduatedPlanBody` builds a #947-consumable plan-file body from the spike's `Question`, `Approach`, `Findings`, and `Recommendation`, and `exit-spike.mjs` writes it to the required `--plan-file` path. The mapping:
83
+
84
+ - `Question` + `Approach` become the Objective's context.
85
+ - `Recommendation` becomes the In-scope work.
86
+ - `Findings` are recorded as supporting evidence.
87
+ - A fixed Explicit non-goals block keeps the plan from re-opening the concluded exploration.
88
+
89
+ The emitted body carries the four plan-file base sections (`## Status`, `## Objective`, `## In scope`, `## Explicit non-goals` — see [Plan-file Contract](plan-file-contract.md)), so it passes `validatePlanFile` and enters the local-first plan→PR promotion path (#952) unchanged: refine it, hold the local human-review checkpoint, then `promote-plan.mjs`. Graduation is idempotent — `buildGraduatedPlanBody` is pure, so re-running reproduces the same plan file. `buildGraduatedPlanBody` fails closed (throws) on an empty required section, so a graduate exit cannot emit a plan the validator would reject. `--plan-file` is required for graduate (the CLI never guesses an output path).
90
+
91
+ ## Relaxed gate profile
92
+
93
+ A spike's deliverable is a findings doc, so it does not carry the full production draft → pre-approval → Copilot gate set. `gates.spike` is a relaxed profile, shipped in `packages/core/src/config/extension-defaults.yaml`:
94
+
95
+ ```yaml
96
+ gates:
97
+ spike:
98
+ angles:
99
+ - scope
100
+ - docs
101
+ excludeAngles: []
102
+ required: false
103
+ requireCi: false
104
+ mandatoryAngles: []
105
+ ```
106
+
107
+ | Knob | Spike value | Production draft/pre-approval | Why for a spike |
108
+ |---|---|---|---|
109
+ | `angles` | `scope`, `docs` | the full production angle set | A findings doc needs a scope check and a docs check, so the angle set is small and docs-first |
110
+ | `required` | `false` | `true` | The spike's record is the findings doc, so the gate is advisory and the loop proceeds without a passing verdict |
111
+ | `requireCi` | `false` | `true` (pre-approval always requires CI) | A spike produces no production code to run CI against |
112
+
113
+ `gates.spike` resolves through the same config-merge layering and the same `resolveGateConfig(config, "spike")` path as `draft` and `preApproval` (`packages/core/src/config/config.mjs`) — no new strategy→knob resolver. It is `optional()` in the schema (`packages/core/src/config/config.mjs`, `schemas/dev-loop-config.schema.json`) and absent for non-spike work, so production gates are unaffected. A repo `.devloops` can override any of these knobs.
114
+
115
+ ## Worked example
116
+
117
+ One spike, `spike-cache.md`, from question to a Recommendation, shown through both exits.
118
+
119
+ ### Authored scaffold
120
+
121
+ ```markdown
122
+ # Spike: response cache
123
+
124
+ ## Question
125
+
126
+ Would an in-process LRU cache on the metadata fetch measurably cut p95 latency?
127
+
128
+ ## Approach
129
+
130
+ Add a throwaway `@lru_cache` wrapper behind a flag, replay a captured request
131
+ trace, and compare p95 with and without the cache.
132
+
133
+ ## Findings
134
+
135
+ The trace replay shows p95 dropping from 240ms to 90ms with the cache, with a
136
+ ~3MB steady-state memory cost and no correctness regressions on the replay set.
137
+ ```
138
+
139
+ Validation reports the spike is still in progress — the Recommendation is not yet written, so the full-section validator flags it while the scaffold itself is complete:
140
+
141
+ ```
142
+ $ node scripts/refine/validate-spike-file.mjs --input spike-cache.md --json
143
+ { "checker": "validate-spike-file", "ok": false, "errors": [ { "code": "missing_recommendation", "message": "..." } ] }
144
+ ```
145
+
146
+ Startup accepts it anyway, because the entry gate is the exploration scaffold:
147
+
148
+ ```
149
+ $ node scripts/loop/resolve-dev-loop-startup.mjs --spike spike-cache.md
150
+ # ... startup output carries spikeIntakeState: "spike_in_progress"
151
+ ```
152
+
153
+ ### Recommendation reached
154
+
155
+ The operator fills in the exit marker:
156
+
157
+ ```markdown
158
+ ## Recommendation
159
+
160
+ Adopt the cache behind a default-on flag; the latency win is large and the
161
+ memory cost is small. Ship it as a follow-up plan.
162
+ ```
163
+
164
+ The intake state is now `spike_ready_for_exit`, so the spike is exitable.
165
+
166
+ ### Exit A — discard (leaves nothing behind)
167
+
168
+ Had the findings come out the other way — say the latency win was within noise — the recommendation would be "don't pursue", and the operator discards:
169
+
170
+ ```
171
+ $ node scripts/refine/exit-spike.mjs --spike-file spike-cache.md --disposition discard --json
172
+ { "ok": true, "action": "discard", "spikeFile": "/abs/spike-cache.md" }
173
+ ```
174
+
175
+ No plan file is written, no GitHub artifact is created, and no tracker entry is made. The `spike-cache.md` findings doc on disk is the entire record of the exploration.
176
+
177
+ ### Exit B — graduate (emits a plan file)
178
+
179
+ With the "adopt it" recommendation above, the operator graduates the spike into a plan file:
180
+
181
+ ```
182
+ $ node scripts/refine/exit-spike.mjs --spike-file spike-cache.md --disposition graduate --plan-file docs/phases/phase-43.md --json
183
+ { "ok": true, "action": "graduate", "spikeFile": "/abs/spike-cache.md", "planFile": "/abs/docs/phases/phase-43.md" }
184
+ ```
185
+
186
+ `docs/phases/phase-43.md` is written with the spike's sections mapped onto the plan-file base sections:
187
+
188
+ ```markdown
189
+ # Graduated spike plan
190
+
191
+ ## Status
192
+
193
+ Draft (graduated from a spike). Needs refinement before promotion.
194
+
195
+ ## Objective
196
+
197
+ Act on the spike's recommendation. The spike asked: Would an in-process LRU
198
+ cache on the metadata fetch measurably cut p95 latency?
199
+
200
+ Approach explored:
201
+
202
+ Add a throwaway `@lru_cache` wrapper behind a flag, replay a captured request
203
+ trace, and compare p95 with and without the cache.
204
+
205
+ ## In scope
206
+
207
+ Adopt the cache behind a default-on flag; the latency win is large and the
208
+ memory cost is small. Ship it as a follow-up plan.
209
+
210
+ Supporting findings from the spike:
211
+
212
+ The trace replay shows p95 dropping from 240ms to 90ms with the cache, with a
213
+ ~3MB steady-state memory cost and no correctness regressions on the replay set.
214
+
215
+ ## Explicit non-goals
216
+
217
+ - Re-running the spike's exploration; that question is concluded.
218
+ - Work beyond the recommendation above.
219
+ ```
220
+
221
+ This plan file passes `validatePlanFile`, so it enters the local-planning flow as a new plan needing refinement: refine it, hold the local human-review checkpoint, then `promote-plan.mjs` opens the draft PR (see the [Local-Planning Worked Example](local-planning-worked-example.md)). Re-running the graduate exit reproduces the same plan body, because `buildGraduatedPlanBody` is pure.
222
+
223
+ ## Relationship to other docs
224
+
225
+ | Doc | Relationship |
226
+ |---|---|
227
+ | [Artifact Authority Contract](artifact-authority-contract.md) | Canonical artifact model; a graduated spike feeds the local-planning tier |
228
+ | [Local-Planning Flow](local-planning-flow.md) | The operator sequence a graduated plan file then follows |
229
+ | [Local-Planning Worked Example](local-planning-worked-example.md) | One plan file through promotion, where a graduated spike lands |
230
+ | [Plan-file Contract](plan-file-contract.md) | Plan-file format and base sections the graduated body satisfies |
231
+ | [Public Dev Loop Contract](public-dev-loop-contract.md) | Canonical routing contract; `--spike` is one startup input |
232
+
233
+ ## Non-goals
234
+
235
+ - Changing spike intake, exit, or gate behavior (this is a docs-only surface).
236
+ - Defining a tracker-backed spike — a spike is local by construction.
237
+ - A discard that records a tracker artifact — discard is zero-mutation by contract.