dev-loops 0.4.0 → 0.6.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 (76) 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/commands/auto.md +7 -0
  5. package/.claude/commands/continue.md +15 -0
  6. package/.claude/commands/info.md +7 -0
  7. package/.claude/commands/start-spike.md +16 -0
  8. package/.claude/commands/start.md +7 -0
  9. package/.claude/commands/status.md +6 -0
  10. package/.claude/hooks/_run-context.mjs +11 -4
  11. package/.claude/skills/dev-loop/SKILL.md +21 -6
  12. package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
  13. package/.claude/skills/docs/artifact-authority-contract.md +86 -31
  14. package/.claude/skills/docs/local-planning-flow.md +63 -0
  15. package/.claude/skills/docs/local-planning-worked-example.md +139 -0
  16. package/.claude/skills/docs/merge-preconditions.md +35 -0
  17. package/.claude/skills/docs/plan-file-contract.md +37 -0
  18. package/.claude/skills/docs/release-runbook.md +45 -0
  19. package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
  20. package/.claude/skills/docs/spike-mode-contract.md +237 -0
  21. package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
  22. package/.claude/skills/local-implementation/SKILL.md +1 -1
  23. package/CHANGELOG.md +73 -0
  24. package/README.md +21 -1
  25. package/agents/dev-loop.agent.md +8 -1
  26. package/agents/refiner.agent.md +1 -0
  27. package/cli/index.mjs +2 -0
  28. package/extension/index.ts +10 -1
  29. package/extension/presentation.ts +15 -0
  30. package/lib/dev-loops-core.mjs +141 -0
  31. package/package.json +8 -3
  32. package/scripts/claude/generate-claude-assets.mjs +15 -1
  33. package/scripts/github/capture-review-threads.mjs +20 -2
  34. package/scripts/github/comment-issue.mjs +181 -0
  35. package/scripts/github/fetch-ci-logs.mjs +215 -0
  36. package/scripts/github/list-issues.mjs +191 -0
  37. package/scripts/github/probe-copilot-review.mjs +69 -3
  38. package/scripts/github/upsert-checkpoint-verdict.mjs +18 -3
  39. package/scripts/lib/jq-output.mjs +297 -0
  40. package/scripts/loop/_handoff-contract.mjs +1 -0
  41. package/scripts/loop/check-retro-tooling.mjs +246 -0
  42. package/scripts/loop/copilot-pr-handoff.mjs +21 -3
  43. package/scripts/loop/detect-pr-gate-coordination-state.mjs +65 -2
  44. package/scripts/loop/docs-grill-contract.mjs +70 -0
  45. package/scripts/loop/info.mjs +21 -2
  46. package/scripts/loop/pr-runner-coordination.mjs +20 -4
  47. package/scripts/loop/resolve-dev-loop-startup.mjs +176 -5
  48. package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
  49. package/scripts/loop/run-conductor-cycle.mjs +5 -0
  50. package/scripts/loop/run-watch-cycle.mjs +77 -3
  51. package/scripts/loop/slides-story-review-contract.mjs +123 -0
  52. package/scripts/pages/build-site.mjs +136 -0
  53. package/scripts/projects/add-queue-item.mjs +12 -2
  54. package/scripts/projects/list-queue-items.mjs +12 -2
  55. package/scripts/projects/move-queue-item.mjs +12 -2
  56. package/scripts/projects/resolve-active-board-item.mjs +193 -0
  57. package/scripts/refine/_refine-helpers.mjs +20 -0
  58. package/scripts/refine/exit-spike.mjs +186 -0
  59. package/scripts/refine/promote-plan.mjs +387 -0
  60. package/scripts/refine/refine-plan-file.mjs +165 -0
  61. package/scripts/refine/scaffold-spike-file.mjs +183 -0
  62. package/scripts/refine/validate-plan-file.mjs +64 -0
  63. package/scripts/refine/validate-spike-file.mjs +87 -0
  64. package/scripts/release/extract-changelog-section.mjs +111 -0
  65. package/skills/dev-loop/SKILL.md +24 -2
  66. package/skills/dev-loop/templates/slides-story-review.md +54 -0
  67. package/skills/docs/artifact-authority-contract.md +86 -31
  68. package/skills/docs/local-planning-flow.md +63 -0
  69. package/skills/docs/local-planning-worked-example.md +139 -0
  70. package/skills/docs/merge-preconditions.md +35 -0
  71. package/skills/docs/plan-file-contract.md +37 -0
  72. package/skills/docs/release-runbook.md +45 -0
  73. package/skills/docs/retrospective-checkpoint-contract.md +55 -7
  74. package/skills/docs/spike-mode-contract.md +237 -0
  75. package/skills/docs/ui-e2e-scoping-step.md +102 -0
  76. package/skills/local-implementation/SKILL.md +1 -1
@@ -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.
@@ -0,0 +1,102 @@
1
+ # UI e2e scoping step
2
+
3
+ Canonical owner for **when the shared UI/mobile e2e loop is required**. Inclusion
4
+ is **path-triggered and deterministic** — it does not depend on a human annotating
5
+ the PR or a phase doc.
6
+
7
+ ## Trigger: a rendered-artifact change requires UI e2e coverage
8
+
9
+ A PR that **adds or modifies a rendered HTML artifact** MUST run the shared UI e2e
10
+ assertions (mobile + desktop) AND register that artifact in the e2e suite. A
11
+ "rendered artifact" is anything that renders to a served page or component:
12
+
13
+ - a presentation deck — `docs/presentations/*.html` (registered in `DECK_REGISTRY`)
14
+ - an article page — `docs/articles/*.html` (registered in `ARTICLE_REGISTRY`; the
15
+ intro article is the published landing page)
16
+ - the inspect-run viewer's served page/component — `scripts/loop/inspect-run-viewer.mjs`
17
+
18
+ The trigger is the PR's **changed-file set** matched against these explicit globs.
19
+ It is conservative on purpose (issue #976 scope): only artifacts that render to a
20
+ page/component, matched by exact directory + single-segment `*.html` globs (no
21
+ recursion) and the viewer source path.
22
+
23
+ Registration is keyed on the **full repo-relative path**, not the basename, so
24
+ `docs/articles/X.html` and `docs/presentations/X.html` (which share basenames,
25
+ e.g. `introducing-dev-loops.html`) are **distinct** artifacts and can never alias
26
+ onto each other — editing the article never counts as deck coverage and vice versa.
27
+
28
+ ### Examples — required
29
+
30
+ - `docs/presentations/introducing-dev-loops.html` edited → UI e2e **required**
31
+ (deck-smoke CI job runs the deck fit specs).
32
+ - `docs/articles/introducing-dev-loops.html` (the landing page) edited → UI e2e
33
+ **required** (article-smoke CI job runs the article fit specs); it is its own
34
+ registration, NOT covered by the same-named deck.
35
+ - A new `docs/articles/new-page.html` added → UI e2e **required**, and because the
36
+ new page is not yet in `ARTICLE_REGISTRY` the gate **fails closed** until it is
37
+ registered (add an entry + a thin spec calling `defineArticleSuite`).
38
+ - `scripts/loop/inspect-run-viewer.mjs` edited → UI e2e **required** (the viewer is
39
+ registered in `VIEWER_REGISTRY`).
40
+
41
+ ### Examples — not required
42
+
43
+ - `packages/core/src/loop/copilot-loop-state.mjs`, `README.md`, `*.test.mjs`,
44
+ `docs/articles/foo.md` (not `.html`), or a nested `docs/articles/sub/x.html`
45
+ (the glob is single-segment) → UI e2e **not required**; the gate passes through.
46
+
47
+ ## Register the artifact in the suite
48
+
49
+ "Registered" means the artifact appears in the shared registries:
50
+
51
+ - decks → `DECK_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
52
+ (served from `docs/presentations/<deck>`) with a thin spec calling `defineDeckSuite`.
53
+ - articles → `ARTICLE_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
54
+ (served from `docs/articles/<file>`) with a thin spec calling `defineArticleSuite`
55
+ (shared fit/CSP/no-horizontal-scroll assertions, minus the deck's per-section
56
+ named captures).
57
+ - the viewer → `VIEWER_REGISTRY` in
58
+ `test/playwright/harness/inspect-run-viewer-harness.mjs`.
59
+
60
+ The deterministic membership list the gate checks against lives in
61
+ `packages/core/src/loop/ui-e2e-scoping.mjs` (`REGISTERED_ARTIFACT_PATHS`, keyed by
62
+ full repo-relative path; `VIEWER_ARTIFACT_ID`). A sync test keeps
63
+ `REGISTERED_ARTIFACT_PATHS` from drifting from `DECK_REGISTRY` + `ARTICLE_REGISTRY`;
64
+ the single-valued `VIEWER_ARTIFACT_ID` is not import-checked against `VIEWER_REGISTRY`
65
+ (that harness pulls `@playwright/test` into core), so keep the two in sync by hand.
66
+
67
+ ## Fail-closed semantics
68
+
69
+ The check is wired as a gate precondition in `evaluatePrGateCoordination`
70
+ (`packages/core/src/loop/pr-gate-coordination.mjs`), at a seam distinct from the
71
+ mergeability (#980) and retrospective (#982) preconditions. It fails closed:
72
+
73
+ - **Unregistered rendered artifact touched** → gate blocks with
74
+ `nextAction: run_ui_e2e_suite`, reason naming the artifact and that it needs
75
+ registration in the suite.
76
+ - **Registered, but UI e2e suite did not pass for this head** (`uiE2ePassed` is
77
+ `false`/unknown) → gate blocks with the same action, reason that the suite must
78
+ run and pass first.
79
+ - **Non-UI change** → `required: false`, gate passes through untouched.
80
+
81
+ The detect layer reads the changed-file set from `gh pr view --json files` and
82
+ derives `uiE2ePassed` from the `statusCheckRollup` UI e2e check(s)
83
+ (`UI_E2E_CHECK_NAMES`): present UI e2e checks must all be `SUCCESS`; if none is
84
+ present the value is unknown and the gate fails closed.
85
+
86
+ Each rendered-artifact family has a stable, satisfiable CI job in
87
+ `.github/workflows/ci.yml`, path/diff-conditioned in the `changes` job and named
88
+ to match `UI_E2E_CHECK_NAMES` so the gate can read a real signal:
89
+
90
+ - `viewer-smoke` → inspect-run viewer spec (triggered by the viewer source set).
91
+ - `deck-smoke` → both presentation deck fit specs (triggered by
92
+ `docs/presentations/**` and the deck specs/harness/configs).
93
+ - `article-smoke` → both article fit specs (triggered by `docs/articles/**` and the
94
+ article specs/harness/configs).
95
+
96
+ So a deck- or article-only PR has a CI check that can actually satisfy the gate;
97
+ without these jobs such a PR would fail closed with no satisfiable signal.
98
+
99
+ ## Non-goals
100
+
101
+ Not always-on screenshot testing; not mandatory multi-browser. The criterion is
102
+ only the conservative path-glob + registry-membership + passing-coverage check.
@@ -512,7 +512,7 @@ After the phase plan passes review:
512
512
  3. Run local validation:
513
513
  - `npm run verify`
514
514
  - when a narrower local check is more honest for the touched slice, say so explicitly and run the narrowest justified subset instead of pretending the full verify path was unnecessary
515
- - for user-facing HTML/UI/component slices when the user opts in, add a bounded deterministic browser smoke harness (prefer fixture-backed Playwright WebKit plus screenshot capture); use `npm run test:playwright:viewer` when that viewer/browser surface is part of the slice, and wire it into CI once it becomes required validation for that slice
515
+ - for a *rendered* HTML artifact (a deck `docs/presentations/*.html`, an article `docs/articles/*.html`, or the inspect-run viewer source) UI e2e coverage is **required and auto-scoped** — not opt-in. Touching one obliges you to register it in the shared harness and have a passing UI e2e suite, or the gate fails closed (see [UI e2e scoping step](../docs/ui-e2e-scoping-step.md)); run the matching shared suite locally, e.g. `npm run test:playwright:intro-deck` / `:intro-article` / `:viewer`. For a bespoke local UI surface that is not a registered rendered artifact, the WebKit smoke seam (fixture-backed Playwright WebKit plus screenshot capture) remains available without being mandatory
516
516
  - **Commit immediately after validation passes.** Once `npm run verify` (or the narrowest justified validation subset) is green, stage and commit the current slice of work with an atomic commit message. For tracker-backed sessions, also push the commit. In non-interactive subagent sessions, commit authorization is implicit — the subagent was dispatched to implement and must commit before exiting. In interactive sessions, wait for coordination agent authorization. Do not defer commits — uncommitted changes when the session terminates are a workflow defect.
517
517
  4. Review the implementation against the merged phase plan.
518
518
  5. Run the default pre-approval gate as a full review / fix loop on the branch before calling it review-complete, approval-ready, merge-ready, or ready for final handoff: