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.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/agents/refiner.md +1 -0
- package/.claude/commands/auto.md +7 -0
- package/.claude/commands/continue.md +15 -0
- package/.claude/commands/info.md +7 -0
- package/.claude/commands/start-spike.md +16 -0
- package/.claude/commands/start.md +7 -0
- package/.claude/commands/status.md +6 -0
- package/.claude/hooks/_run-context.mjs +11 -4
- package/.claude/skills/dev-loop/SKILL.md +21 -6
- package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/.claude/skills/docs/artifact-authority-contract.md +86 -31
- package/.claude/skills/docs/local-planning-flow.md +63 -0
- package/.claude/skills/docs/local-planning-worked-example.md +139 -0
- package/.claude/skills/docs/merge-preconditions.md +35 -0
- package/.claude/skills/docs/plan-file-contract.md +37 -0
- package/.claude/skills/docs/release-runbook.md +45 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/.claude/skills/docs/spike-mode-contract.md +237 -0
- package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
- package/.claude/skills/local-implementation/SKILL.md +1 -1
- package/CHANGELOG.md +73 -0
- package/README.md +21 -1
- package/agents/dev-loop.agent.md +8 -1
- package/agents/refiner.agent.md +1 -0
- package/cli/index.mjs +2 -0
- package/extension/index.ts +10 -1
- package/extension/presentation.ts +15 -0
- package/lib/dev-loops-core.mjs +141 -0
- package/package.json +8 -3
- package/scripts/claude/generate-claude-assets.mjs +15 -1
- package/scripts/github/capture-review-threads.mjs +20 -2
- package/scripts/github/comment-issue.mjs +181 -0
- package/scripts/github/fetch-ci-logs.mjs +215 -0
- package/scripts/github/list-issues.mjs +191 -0
- package/scripts/github/probe-copilot-review.mjs +69 -3
- package/scripts/github/upsert-checkpoint-verdict.mjs +18 -3
- package/scripts/lib/jq-output.mjs +297 -0
- package/scripts/loop/_handoff-contract.mjs +1 -0
- package/scripts/loop/check-retro-tooling.mjs +246 -0
- package/scripts/loop/copilot-pr-handoff.mjs +21 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +65 -2
- package/scripts/loop/docs-grill-contract.mjs +70 -0
- package/scripts/loop/info.mjs +21 -2
- package/scripts/loop/pr-runner-coordination.mjs +20 -4
- package/scripts/loop/resolve-dev-loop-startup.mjs +176 -5
- package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
- package/scripts/loop/run-conductor-cycle.mjs +5 -0
- package/scripts/loop/run-watch-cycle.mjs +77 -3
- package/scripts/loop/slides-story-review-contract.mjs +123 -0
- package/scripts/pages/build-site.mjs +136 -0
- package/scripts/projects/add-queue-item.mjs +12 -2
- package/scripts/projects/list-queue-items.mjs +12 -2
- package/scripts/projects/move-queue-item.mjs +12 -2
- package/scripts/projects/resolve-active-board-item.mjs +193 -0
- package/scripts/refine/_refine-helpers.mjs +20 -0
- package/scripts/refine/exit-spike.mjs +186 -0
- package/scripts/refine/promote-plan.mjs +387 -0
- package/scripts/refine/refine-plan-file.mjs +165 -0
- package/scripts/refine/scaffold-spike-file.mjs +183 -0
- package/scripts/refine/validate-plan-file.mjs +64 -0
- package/scripts/refine/validate-spike-file.mjs +87 -0
- package/scripts/release/extract-changelog-section.mjs +111 -0
- package/skills/dev-loop/SKILL.md +24 -2
- package/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/skills/docs/artifact-authority-contract.md +86 -31
- package/skills/docs/local-planning-flow.md +63 -0
- package/skills/docs/local-planning-worked-example.md +139 -0
- package/skills/docs/merge-preconditions.md +35 -0
- package/skills/docs/plan-file-contract.md +37 -0
- package/skills/docs/release-runbook.md +45 -0
- package/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/skills/docs/spike-mode-contract.md +237 -0
- package/skills/docs/ui-e2e-scoping-step.md +102 -0
- 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
|
-
|
|
|
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
|
|
115
|
-
requireRetrospectiveGate: true
|
|
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
|
-
|
|
|
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
|
|
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:
|