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.
- 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/skills/dev-loop/SKILL.md +16 -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/retrospective-checkpoint-contract.md +55 -7
- package/.claude/skills/docs/spike-mode-contract.md +237 -0
- package/CHANGELOG.md +43 -0
- package/README.md +1 -0
- package/agents/refiner.agent.md +1 -0
- package/package.json +5 -3
- package/scripts/github/capture-review-threads.mjs +20 -2
- 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/check-retro-tooling.mjs +246 -0
- package/scripts/loop/copilot-pr-handoff.mjs +21 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +35 -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-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/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/validate-plan-file.mjs +64 -0
- package/scripts/refine/validate-spike-file.mjs +87 -0
- package/skills/dev-loop/SKILL.md +11 -1
- 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/retrospective-checkpoint-contract.md +55 -7
- package/skills/docs/spike-mode-contract.md +237 -0
|
@@ -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.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.5.0 - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Conflict-free (mergeable) is a required gate precondition + deterministic auto-resolve (#980).** A PR that conflicts with its base gets no `pull_request` CI run (GitHub can't compute the merge ref), so the gate silently stalls green-less. Mergeability is now a required precondition checked at every gate (draft + pre-approval) and again before merge: the detect layer fetches `gh pr view --json mergeable,mergeStateStatus` and the evaluator (`evaluatePrGateCoordination`) blocks a `CONFLICTING`/`DIRTY`/`BEHIND` PR (`gateBoundary: conflict_resolution`, `nextAction: resolve_merge_conflicts`). Because GitHub computes `mergeable` asynchronously, a freshly-pushed head briefly reads `UNKNOWN`; `fetchPrFactsWithSettledMergeable` re-polls a bounded number of times and, if it never settles, the gate fails closed to a recheck (`nextAction: wait_for_ci`) — an unsettled merge state is never treated as clean. A new conservative helper `scripts/loop/resolve-pr-conflicts.mjs` merges `origin/<base>` into the PR branch and resolves ONLY the safe additive case — a `CHANGELOG.md` conflict where both sides only ADD list/section entries (keep BOTH sides, in order) — then runs `npm run test:docs` and (with `--push`) pushes; ANY other conflicted path, or a non-additive CHANGELOG edit, FAILS CLOSED naming the conflicted paths (no general conflict-resolution engine). `loop info` surfaces a `Mergeable:` line (mergeStateStatus included) so a conflict is diagnosed immediately rather than mistaken for missing CI. `node:test` coverage: CONFLICTING/UNKNOWN/MERGEABLE evaluator cases, the bounded UNKNOWN re-poll, and real-git fixtures for the additive-CHANGELOG resolve, the non-CHANGELOG fail-closed (path named), the non-additive-CHANGELOG fail-closed, and a clean merge. Docs: [merge-preconditions](skills/docs/merge-preconditions.md) documents the conflict-check-and-resolve step (before CI/Copilot and before merge).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Pages decks auto-deploy on push to `main`** (#941, closes #940; supersedes the operator-gated deploy from #930). Pages is enabled (Source: GitHub Actions), so the deploy job no longer needs the `workflow_dispatch` gate `#930` shipped while Pages was off. `.github/workflows/pages.yml` now scopes the deploy job to the main ref (`if: github.ref == 'refs/heads/main'`) instead of `if: github.event_name == 'workflow_dispatch'`, so a merge to `main` publishes the assembled `site/` automatically. A manual `workflow_dispatch` still deploys, but only when run against `main`; dispatching any other branch/tag builds the artifact and skips deploy. The workflow header comment, the deploy-job inline comment, and `docs/presentations/README.md` were updated to describe auto-deploy on merge in place of the one-time operator "Run workflow" step.
|
|
16
|
+
- **Public materials reframed around the pull principle** (#991). The handoff-framing passages across the four public pieces now lead with the Kanban-style insight that the next step is always known: the authoritative resolver/state graph computes the next action for any change at any time, `loop info` surfaces it, and the board mirrors it, so whoever is free — agent or human — pulls the next bounded step from the visible state. The handoff becomes optional, at most additive: when one happens it adds a note and stays a recorded decision, and it is no longer a load-bearing blocking step, which is what dissolves the waiting-for-handoff. Edited in sync (`.md` + `.html`): the intro article's lede, "Model-agnostic by construction", and "Where to go deeper" pointer (`docs/articles/introducing-dev-loops.md`/`.html`); the deep-dive article's Part 1 — the two-part intro, "The one idea" (retitled "the next step is always known"), "What a known next step buys you", "Why a state graph beats a prompt" (recast as the thing that makes the next step always-known and pullable), and the close (`docs/articles/dev-loops-deep-dive.md`/`.html`); the intro deck's hero, the idea slide (retitled "The Next Step Is Always Known"), the model-agnostic slide, and the companion-pieces note (`docs/presentations/introducing-dev-loops.html`); and the deep-dive deck's hero, core-idea slide (retitled "The Next Step Is Always Known"), why-graphs slide, and close (`docs/presentations/dev-loops-deep-dive.html`). The "still a recorded decision when it happens" point is kept, now subordinate to the pull framing. Diagrams and the Diagram 1–8 caption numbering are unchanged; the deck section ids and 390px mobile-fit are intact (deep-dive deck Playwright spec green). The A/B-contrast deslop step (`docs/ab-contrast-deslop-step.md`) was applied to all rewritten prose — the pull idea is stated as plain declaratives with no "pull, not push" / "rather than" / "instead of" antithesis.
|
|
17
|
+
- **Public materials consolidated to ONE deep dive per format** (#978). The two deep-dive articles (`eliminating-coordination-delay` + `make-the-waiting-visible`) merged into one `docs/articles/dev-loops-deep-dive.md`/`.html`, and the two deep-dive decks (`applied-dev-loops.html` + `process-observability.html`) merged into one `docs/presentations/dev-loops-deep-dive.html`. Each merged piece runs in two parts — Part 1 eliminating coordination delay (explicit handoffs, fan-out/fan-in review, mid-flight steering, the state graph), then Part 2 make the waiting visible (interrupt cost, handoff discovery, the git blind spot, the four fields, the measurement loop, grounding in real mechanisms) — under one title/hero, one intro, and one close. The deep-dive article reuses the article design system (glass cards, inline-flow nodes, inline SVG diagrams) and the deck reuses the deck design system with stable per-slide section ids and mobile-fit layout. The old four published files are removed (and the orphaned `*-notes.md` review records for the two removed articles). `scripts/pages/build-site.mjs` `ARTICLES`/`DECKS`/`NAV_LINKS` collapse to the single deep-dive in each format (nav labels "Deep dive" / "Deep dive (deck)"); the article and deck share the source basename `dev-loops-deep-dive.html` under different `docs/` dirs, so the deck publishes as `dev-loops-deep-dive-deck.html` to avoid clobbering the article in `site/`. The Pages nav is now Intro article (landing) + Deep-dive article + Intro deck + Deep-dive deck. The two old deck Playwright specs/configs (`applied-deck`, `observability-deck`) and the `test:playwright:deck`/`:obs-deck` scripts are replaced by one `deep-dive-deck.spec.mjs` + `playwright.dev-loops-deep-dive.config.mjs` + `test:playwright:deep-dive` (section-ids, 390px mobile fit/no-clip, CSP, guard-the-guard). The intro article's "Where to go deeper", `docs/index.md`, and `docs/presentations/README.md` point at the consolidated pages. The A/B-contrast deslop step was applied to the merged prose (new part-divider headlines and bridge sentences kept clean; the source pieces were already deslopped).
|
|
18
|
+
- **Local-first low-noise posture made the coherent default (#953, builds on #949/#950/#951/#952).** The shipped extension-defaults layer (`packages/core/src/config/extension-defaults.yaml`) — the local-first opinion that sits above the built-in github-first code defaults and below repo config — now sets three existing knobs to their low-noise values, each with an inline intent comment: `autonomy.humanMergeOnly: true` (local-first never auto-merges; a human always merges), `queue.maxAutoFiledIssues: 1` (local-first is PR-first per #952, so auto-filing issues is near-zero; a low cap keeps tracker noise minimal), and an explicit `gates.postFindingsComments: true` (gate findings live ON the PR as review evidence, not tracker noise — keep them on). No new resolver and no new `strategy → knob` coupling: the values come purely from the existing config-merge layering (built-in < extension < repo `.pi/dev-loop/defaults.*` < repo `.devloops`), which already permits all three knobs at the file/extension layer (the runtime Zod schema needed no change). The published JSON-schema artifact `schemas/dev-loop-config.schema.json` was reconciled to match that runtime contract — its `gates` block (which used `additionalProperties: false`) now lists `postFindingsComments` plus the previously-missing `requireFanoutEvidence`/`maxFanoutReviewers`, so the schema no longer rejects the shipped config. The github-first/built-in posture is unchanged — `BUILT_IN_DEFAULTS` still yields `humanMergeOnly: false`, `maxAutoFiledIssues: 10`, and a resolved `postFindingsComments: true`. The post-promotion draft→pre-approval→human-merge flow is untouched. `node:test` coverage locks it: a merged-config assertion (`humanMergeOnly === true`, `maxAutoFiledIssues === 1`, `postFindingsComments === true` under the shipped defaults), built-in-default assertions for the unchanged github-first posture, and an explicit local-first phase-doc intake test (strategy from the shipped extension default, inputSource phase-docs from repo settings) asserting via a logging `gh` stub that NO `gh issue create` / `gh pr create` and NO Copilot request fire before promotion.
|
|
19
|
+
- **Both presentation decks deslopped — shorter headlines + A/B-contrast prose removed** (#936, applies the #944 standard step). Every slide headline on `docs/presentations/applied-dev-loops.html` and `docs/presentations/process-observability.html` was shortened to a short 3–7 word claim and stripped of the binary-contrast antipattern: "The Work Is One Loop Inside Another — and a Handoff Is Never Guessed" → "Loops Inside Loops"; "Prompt-Only Workflows Drift; a State Graph Can't" → "State Graphs Pin Behavior"; "One Interrupt Costs Five Transitions, Not Five Minutes" → "One Interrupt, Five Transitions"; "Those Four Fields Aren't a Wish — They're Where the Work Already Lives" → "The Fields Already Live in the Work"; "The Cheapest Speed-Up Is Making the Waiting Visible" → "Make the Waiting Visible" (and the rest). Slide body text in the publishable HTML renders lost the same "X, not Y" / negation-by-contrast construction in both orderings ("verified, never assumed" → "verified against the real result"; "don't add up. They multiply" → "multiply"; "Stop optimizing… Start measuring…" → "Measure how long the work waits"), with load-bearing distinctions kept plain. The HTML renders are the published source of truth; the Slidev `*-presentation.md` sources mirror the shortened headlines only (their body text already predates the HTML restructure — see `docs/presentations/README.md`). The dark visual identity, section ids, CSP guard, mobile-fit responsiveness, and all six Playwright deck tests are unchanged. Deploys to GitHub Pages on merge.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **First-class `--jq` field-selection, `--silent`/`-s` exit-code checks, and concise output across the read/action scripts** (#981, subsumes #963). One shared helper `scripts/lib/jq-output.mjs` (`evaluateJqFilter`, `emitResult`, `JQ_OUTPUT_PARSE_OPTIONS`, `JQ_OUTPUT_USAGE`, `JqFilterError`) gives the loop a token-economical way to read tool JSON so it never falls back to `gh api | python3` or inline `node -e`. `--jq <filter>` applies a gh-style jq-subset filter (field access, `.a.b` chains, `.[]`/`.field[]` iteration, `.[N]` index, `|` pipes, `select(...)`, `==`/`!=`/`<`/`<=`/`>`/`>=`, `length`, `keys`) to the script's result and prints only the filtered value; an unsupported filter fails closed (stderr + exit `2`). `--silent`/`-s` suppresses stdout and maps the result to an exit code only (`0` pass/truthy, `1` fail/falsy) for zero-output yes/no checks — composing with `--jq` as a predicate (`… --jq '.ciStatus=="success"' -s; echo $?`), where an invalid filter still fails closed at exit `2`, distinct from a clean predicate-false silent exit `1`. Without these flags every script's JSON shape is unchanged. The full subset (not full jq — overkill for the loop's field/predicate needs) is applied uniformly to `scripts/github/capture-review-threads.mjs`, `scripts/loop/copilot-pr-handoff.mjs`, `scripts/loop/pr-runner-coordination.mjs`, `scripts/github/upsert-checkpoint-verdict.mjs`, and the queue scripts `scripts/projects/add-queue-item.mjs`/`move-queue-item.mjs`/`list-queue-items.mjs` (the #963 operator-action-scripts-JSON-only goal folded in here). In addition, the two scripts that emitted a large blob with no concise mode — `scripts/loop/run-watch-cycle.mjs` and `scripts/github/probe-copilot-review.mjs` — gain a `--concise`/`--summary` human-readable mode covering loop state, Copilot round count, unresolved/actionable thread counts, round-cap-clean eligibility, CI status, next action, and (the field `loop info --pr` omits) the current round's NEW Copilot comment bodies. The token-economical reading convention (subcommand/concise → `--silent` → `--jq` → `gh --jq` → never `| python3`/`node -e`) is documented in `skills/dev-loop/SKILL.md` and referenced from the round-cap gate-cadence rule. `node:test` coverage: a helper unit spec (subset extraction success, fail-closed on unsupported syntax, `emitResult` jq/silent/invalid-filter exit-code semantics), an end-to-end CLI spec (`--jq` extraction, invalid filter exit `2` + stderr, `--silent` pass exit `0` silent, predicate-false exit `1` silent, silent+invalid-filter exit `2`, unchanged JSON shape), and concise-mode specs for run-watch-cycle/probe-copilot-review.
|
|
23
|
+
- **Developer-mode internal-tooling-only retrospective check (#982).** A new opt-in workflow flag `workflow.requireRetrospectiveInternalTooling` (default **OFF**, defined in `packages/core/src/config/config.mjs`, shipped explicitly false in `extension-defaults.yaml`, declared in `schemas/dev-loop-config.schema.json`) gates an internal-tooling-only check in the retrospective merge gate. **It is scoped to developer mode — the dev-loops maintainers dogfooding the tooling on themselves — and never blocks consumers** of the extension, who may legitimately use raw `gh`/`python`/`node -e` in their own workflow. When the flag is ON, the merge-approval evaluator (`evaluateRetrospectiveMergeApproval(checkpoint, { developerMode })` in `packages/core/src/loop/pr-gate-coordination.mjs`, routed through at every merge-ready boundary in `evaluatePrGateCoordination`) requires `behavioralReview.internalToolingOnly: true` and `behavioralReview.rawCallViolations: []`, blocking (`retrospective_gate_pending`) when `internalToolingOnly` is not `true` **OR** `rawCallViolations` is missing/non-empty. When the flag is OFF (the consumer default) the check is inert: a complete, merge-approved checkpoint passes exactly as before — even if it omits the fields or records a violation — so a consumer's state changes are never blocked (this also resolves the draft-review back-compat concern: old checkpoints only fail closed in developer mode). The dev-loops repo opts in via its own repo-root `.devloops` (`workflow.requireRetrospectiveInternalTooling: true`), so the discipline still applies to our own retros. The rule treats agent-level top-level raw `gh` (incl. `gh api`/`--jq`), `python`/`python3`, and `node -e`/`node --eval` as the same breach; `node scripts/*.mjs` and dev-loops subcommands are allowed (the scripts call gh/GraphQL internally — that IS the tooling). The dependency-free, deterministic verifier `scripts/loop/check-retro-tooling.mjs` (pure `analyzeTranscript(transcript)` export) is unchanged: it reads a newline-delimited transcript of the agent's shell commands (`--transcript <path>` or stdin) and returns the `rawCallViolations` list, distinguishing script-internal gh from agent-level raw calls and keeping a small explicit write-op allowlist (`gh pr merge`/`gh pr ready`/`gh issue create`/`gh issue edit`, recorded as `allowedWriteOps`). `node:test` coverage: developer-mode-ON gate tests for a recorded violation / `internalToolingOnly:false` / missing-field-old-checkpoint (all block) and a clean record (allows `FINAL_APPROVAL_READY`), the key developer-mode-OFF test asserting a consumer is NOT blocked even with missing fields OR a recorded violation, plus the unchanged verifier spec (`test/loop/check-retro-tooling.test.mjs`). The retrospective-checkpoint contract doc (`skills/docs/retrospective-checkpoint-contract.md` + generated `.claude/` mirror) documents the developer-mode scoping, the opt-in flag, and that consumers are never blocked. Known verifier limitation: segment splitting does not parse shell quoting, so a separator inside a quoted argument can over-report (never under-report) — the fail-closed direction.
|
|
24
|
+
- **"Introducing dev-loops" presentation deck** (#973, companion to the #971 overview article). A self-contained, CSP-safe `docs/presentations/introducing-dev-loops.html` (9 slides: the coordination-delay problem, how manual handoffs compound, every handoff a recorded decision, one bounded decision per change, model-agnostic by construction (open-source models drive the work), the aggregate proof data, install/run, and the companion pieces) reusing the existing decks’ dark glass-card design system, CSP meta, and mobile-fit responsiveness. Added to `scripts/pages/build-site.mjs` `DECKS` (navLabel "Intro (deck)") so it publishes and joins the shared Pages nav; a mirror Playwright spec + config + `test:playwright:intro-deck` script match the existing per-deck tests. Aggregate data only (no internal PR/issue numbers or script paths in slide prose); the A/B-contrast deslop step was applied.
|
|
25
|
+
- **Model-agnostic section in the intro article + open-source model proof** (#979). A new "Model-agnostic by construction" section in `docs/articles/introducing-dev-loops.md`/`.html` (the Pages landing page) explains that the guardrails — every handoff a recorded decision, the same draft → review → green-CI gate, fail-closed on ambiguity, a person merging — bound how far any single step can stray, so a strong open-source model can drive most or all of the work (cheaper, self-hostable, no single-vendor dependence; the gate holds the quality bar). Names the concrete proof: the loop has driven real work end to end on DeepSeek V4, Kimi K2.6, MiniMax M3, Qwen 3.6, and GLM 5.2. Aggregate/conceptual only; A/B-contrast deslop applied.
|
|
26
|
+
|
|
27
|
+
- **Spike-mode contract doc + worked example** (#966, docs-only; phase 3 of 3 of the spike-mode track #955, closing it; builds on #964/#965). A new canonical operator-sequence doc [`skills/docs/spike-mode-contract.md`](skills/docs/spike-mode-contract.md) walks spike mode end to end against the merged surfaces: author the exploration scaffold and validate it (`scripts/refine/validate-spike-file.mjs`, base sections Question/Approach/Findings/Recommendation), start it (`scripts/loop/resolve-dev-loop-startup.mjs --spike`, `buildSpikeInput` + `evaluateSpikeIntakeState`, intake states `spike_in_progress`/`spike_ready_for_exit`/`ambiguous_fail_closed`), run it under the relaxed `gates.spike` profile, then exit via `scripts/refine/exit-spike.mjs` with disposition `discard` (zero tracker artifacts; the findings doc is the whole record) or `graduate` (`buildGraduatedPlanBody` emits a #947-consumable plan file that enters the existing plan→PR promotion path #952). The doc documents the relaxed gate profile and its rationale — `angles: [scope, docs]`, `required: false`, `requireCi: false` (`packages/core/src/config/extension-defaults.yaml`), resolved through the same `resolveGateConfig(config, "spike")` path and absent for non-spike work — and carries one worked example (`spike-cache.md`) from a question through findings to BOTH exits, showing the actual commands/states and, for graduate, the resulting plan-file body. Cross-referenced from the [Artifact Authority Contract](skills/docs/artifact-authority-contract.md) relationship table and `docs/index.md`; the generated `.claude/skills/docs/` mirror was regenerated. No behavior, schema, defaults, or scripts changed.
|
|
28
|
+
- **Spike-mode relaxed gate profile + discard/graduate exits** (#965, phase 2 of the spike-mode track #955; builds on #964 and reuses #951/#952/#953). A spike is now runnable to a conclusion: a lighter gate posture plus an explicit exit that either discards (zero tracker artifacts) or graduates (emits a #947-consumable plan file). **Relaxed gate profile (config):** a new `gates.spike` profile (same `GateConfig` shape as `gates.draft`/`gates.preApproval`) resolved through the SAME `resolveGateConfig(config, "spike")` path and the existing config-merge layering — no new `strategy → knob` resolver. The shipped extension default (`packages/core/src/config/extension-defaults.yaml`) is intentionally lighter than the production draft → pre-approval → Copilot set: a small docs-first angle set (`scope`, `docs`), `required: false`, and `requireCi: false`, because a spike's deliverable is a findings doc, not production code. Runtime zod (`GatesConfig`/`FileGatesConfig` in `packages/core/src/config/config.mjs`) and the published JSON-schema artifact (`schemas/dev-loop-config.schema.json`, `gates.spike` → `$defs/gateConfig`) were kept in sync (#953's lesson). The github-first/production posture is UNCHANGED: `draft`/`preApproval` stay `required: true`/`requireCi: true`, and `gates.spike` is absent for non-spike work. **Discard/graduate exit contract (pure):** a new `packages/core/src/loop/spike-exit-contract.mjs` (no `scripts/` import, no fs/network/gh; mirrors spike-intake-contract) exports frozen `SPIKE_EXIT_DISPOSITION`/`SPIKE_EXIT_ACTION` plus `evaluateSpikeExit({ spikeIntakeState, disposition }) -> { ok, action?, reason?, spikeIntakeState? }`, eligible ONLY from `spike_ready_for_exit` and failing closed with `not_ready_for_exit` (non-ready/ambiguous state) or `unknown_disposition` (anything but `discard`/`graduate`). `buildGraduatedPlanBody({ question, approach, findings, recommendation })` maps the spike's four sections onto a #947-consumable plan-file body with the four base authoring sections (Status/Objective/In scope/Explicit non-goals) so it passes `validatePlanFile` and enters the existing plan→PR promotion path (#952) unchanged; it is idempotent and fails closed (throws) on an empty required section. The export is added to `packages/core/package.json`. **Thin CLI:** `scripts/refine/exit-spike.mjs` (`--spike-file`/`--disposition`/`--plan-file`/`--json`, mirroring promote-plan.mjs) owns all I/O — it reads the spike, computes intake-state facts via the P1 surfaces (`validateSpikeExplorationSections` for the scaffold + the Recommendation exit-marker), classifies via the pure contract, and on graduate writes the local-first plan file; discard creates ZERO tracker artifacts and graduation opens nothing on the tracker (it only writes a local plan file, then promotes via the existing path). Structured success/error JSON matches the sibling refine/promote scripts. `node:test` coverage: the pure exit contract (discard/graduate eligible from ready; fail-closed on unknown disposition + non-ready/ambiguous/missing state) + the plan-body builder (base-valid output passes `validatePlanFile`, carries the spike content, idempotent, throws on empty sections); the gate-profile resolution (spike profile resolves; shipped default is relaxed and lighter than draft, with draft/preApproval unchanged); and the CLI (discard → empty gh-stub log + exit 0, graduate → base-valid plan file written + empty gh log + idempotent re-run, fail-closed not-ready/unknown-disposition → empty gh log + exit 1, arg validation). Narrative skill docs are #966 (NOT in this phase); no production-flow change for non-spike work.
|
|
29
|
+
- **Top-of-funnel article "Introducing dev-loops" + CSP-safe HTML render** (#971, sibling of the #942/#943 article PRs). `docs/articles/introducing-dev-loops.md` is the single what/why/how-do-I-start entry point the two concept articles lacked: a plain-language introduction to the idea (coordination delay is where AI-assisted lead time goes; the loop makes every handoff an explicit, recorded decision; a person merges by default; work starts from a durable plan or issue), a "what it does to the work" section that uses the repository’s own ~two-week history (~100 merged PRs, ~7/day, about seven in eight tied to a tracked item, every one drafted, reviewed, green-CI gated, and human-merged) to show the straightforward, repeatable process the loop produces, and concrete setup instructions (the Claude Code plugin slash commands, the Pi extension install, the plain-language `start/auto/continue dev loop` entrypoint, and a minimal `.devloops` covering start-local vs issue intake, Copilot rounds on/off, and human-merge). A self-contained, CSP-safe `docs/articles/introducing-dev-loops.html` ports the dark glass-card identity from the sibling renders (no font/CDN/remote assets). Cross-linked from a new Articles section in `docs/index.md`, and the two existing articles gain a one-line "start here" pointer so they read as deep dives beneath the intro. Aggregate figures are gh-verified; the A/B-contrast deslop step (`docs/ab-contrast-deslop-step.md`) was applied (zero flagged constructions). The intro article is also made the **GitHub Pages landing page**: `scripts/pages/build-site.mjs` now publishes `docs/articles/introducing-dev-loops.html` as `site/index.html` (superseding the prior generated "Presentation Decks" card index from #930), publishes the two deep-dive articles and two decks alongside it, and injects a shared navigation bar into the article pages linking the other resources (`injectNav`, which fails closed if a page lacks its `<style>`/`<body>` anchors); the decks are copied as-is. `test/pages/build-site.test.mjs` updated. On merge, `pages.yml` rebuilds and deploys the new landing page.
|
|
30
|
+
- **Spike-mode entry + findings-artifact format** (#964, phase 1 of the spike-mode track #955; reuses #949/#950). A spike is a time-boxed exploratory loop startable from a local question with no GitHub issue. A new spike artifact format mirrors the plan-file format with base sections **Question / Approach / Findings / Recommendation**; a focused sibling validator `scripts/refine/validate-spike-file.mjs` exports `validateSpikeFile(markdownText) -> { checker: "validate-spike-file", ok, errors }` with a distinct `missing_*` code per absent/empty section (`missing_question`/`missing_approach`/`missing_findings`/`missing_recommendation`) plus a thin `--input`/`--json`/`--help` CLI mirroring `validate-plan-file.mjs`. The base-section-checking loop is shared with `validatePlanFile` via a small parameterized helper `checkBaseSections(markdownText, checker, sectionCodes)` in `scripts/refine/_refine-helpers.mjs` (DRY without forcing a shared abstraction across the distinct heading sets). `scripts/loop/resolve-dev-loop-startup.mjs` gains `--spike <path>`, mutually exclusive with `--issue`/`--pr`/`--input`/`--plan-file`: it reuses the `local_implementation` strategy and the existing `local_phase` target as a work-origin addition, makes ZERO `gh` calls / no tracker artifacts at entry (no production-gate ceremony), is exempt from the worktree-isolation `needs_reconcile` guard (a spike has no issue to key a worktree on), and fails closed (exit 1, no bundle) on a missing/unreadable spike file or one missing the exploration scaffold (Question/Approach/Findings). A new pure, deterministic contract `packages/core/src/loop/spike-intake-contract.mjs` (no `scripts/` import, no fs/network/gh) exports a frozen `SPIKE_INTAKE_STATE` and `evaluateSpikeIntakeState({ baseSectionsValid, hasRecommendation }) -> { state }` classifying the spike as `spike_in_progress` (exploration ongoing, no Recommendation yet — phase 2's discard exit) or `spike_ready_for_exit` (Recommendation reached — phase 2's discard/graduate exit seam), failing closed (`ambiguous_fail_closed`) on a malformed artifact. These states are DISTINCT from `PLAN_FILE_INTAKE_STATE` (a spike is not a plan-needing-refinement). The resolver threads the resulting `spikeIntakeState` onto its JSON output and the new core export is added to `packages/core/package.json`. `node:test` coverage: the pure contract spec, the validator spec (all sections ok / each missing→its code / empty-body / CLI), and resolver specs (valid spike → `spike_ready_for_exit` with zero `gh` calls via the empty-log stub, in-progress spike → `spike_in_progress`, missing/malformed → fail closed, mutual exclusivity with `--issue`/`--plan-file`). The relaxed gate profile + discard/graduate exits (#965) and narrative docs (#966) are NOT in this phase; no production-flow change.
|
|
31
|
+
- **Local-first plan-file flow documented end to end + worked example** (#954, docs-only; builds on #949/#950/#951/#952/#953). The canonical [Artifact Authority Contract](skills/docs/artifact-authority-contract.md) (at `skills/docs/`; the `docs/` path in the original stub was stale) now documents the full local-first flow across P1–P5: the plan-file artifact + `localPlanning.plansDir` default (`docs/phases/`) and validator from P1; the `--plan-file` startup entry + `PLAN_FILE_INTAKE_STATE` states (`new_plan_needs_refinement` / `plan_refined_ready_for_promotion` / `ambiguous_fail_closed`) from P2; the in-place refine + `local_human_review` checkpoint (AC/DoD/`Coverage matrix`/`Docs-grill findings` sections) from P3; PR-first promotion + the bidirectional plan↔PR link (`prNumber` front-matter, `already_promoted` idempotency) from P4; and the low-noise extension defaults from P5. The contract was reconciled to the shipped posture: the effective default is `strategy.default: local-first` from `packages/core/src/config/extension-defaults.yaml` (decision #7 of #947), which sits above the `BUILT_IN_DEFAULTS` github-first fallback in `packages/core/src/config/config.mjs`; the stale `.pi/dev-loop/defaults.yaml` shipped-default claim and the "dev-loops is tracker-first" statement were corrected (the package ships defaults via the extension layer and the repo's own `.devloops` sets `local-first`). A new operator-sequence skill doc [`skills/docs/local-planning-flow.md`](skills/docs/local-planning-flow.md) walks validate-plan-file → resolve-dev-loop-startup `--plan-file` → refine-plan-file → promote-plan, and a worked example [`skills/docs/local-planning-worked-example.md`](skills/docs/local-planning-worked-example.md) shows one plan file (`docs/phases/phase-42.md`) evolving through every stage (base sections → refinement sections + coverage matrix + docs-grill findings → `prNumber` front-matter). All three docs are reachable from `docs/index.md` and cross-link the contract; the generated `.claude/skills/docs/` mirror was regenerated. No behavior, schema, defaults, or scripts changed.
|
|
32
|
+
- **Local-first PR-FIRST plan promotion — no issue ever minted** (#952, builds on #949/#950/#951). A pure contract `packages/core/src/loop/plan-file-promote-contract.mjs` exports `evaluatePromoteEligibility({ baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone, existingPrNumber }) -> { ok, action, reason, planFileIntakeState, existingPrNumber }` (`PLAN_FILE_PROMOTE_ACTION` `promote`/`already_promoted`), `buildPromotionPrBody({ planDocPath, acceptanceCriteria, definitionOfDone })`, and a minimal additive plan front-matter parser/serializer (`parsePlanFrontMatter`, `readLinkedPrNumber`, `writeLinkedPrNumber`, `PLAN_FILE_PR_FRONT_MATTER_KEY`). Eligibility is fail-closed: promotion acts ONLY on P3's `plan_refined_ready_for_promotion` state (composed via P2's `evaluatePlanFileIntakeState`); any other state returns `not_ready_for_promotion` with no action so the caller makes zero GitHub mutation. The PR body is the self-contained spec-of-record — it references the committed plan-doc path and carries the FULL `Acceptance criteria` + `Definition of done` extracted from the refined plan, and never an issue-close keyword (no issue is minted). The module is pure (no GitHub, no network, no filesystem I/O). A thin CLI `scripts/refine/promote-plan.mjs` (`--plan-file <path> [--base <branch>] [--branch <name>] [--json]`) owns all I/O: it reads the plan, computes the section-presence facts with the P1 `validatePlanFile`/`extractSection` surfaces, runs the eligibility gate, and on `promote` commits the plan doc to a branch and opens EXACTLY ONE draft PR via the canonical `scripts/github/create-pr.mjs` wrapper (always `--draft`, self-assigned; never raw `gh pr create`), then writes the returned PR number back into the plan's `prNumber:` front-matter and commits the link — the bidirectional plan↔PR link (doc path in PR body, PR number in plan front-matter). The opened draft PR is a normal-shaped PR that enters the existing loop unchanged via `loop startup --pr <n>`; no new lifecycle state and no separate authority artifact. Idempotent: a plan already carrying `prNumber` returns `already_promoted`, opens nothing, and reports the existing PR. The front-matter is a minimal additive extension to P1's plan-file format (documented in `skills/docs/plan-file-contract.md`): plans without a leading `---` block are unchanged and fully valid. A `node:test` contract spec plus a CLI spec cover the ready-gate (refuse non-ready/ambiguous/partially-refined with zero gh calls), the single draft-PR open (one `gh pr create --draft`, asserted via a logging `gh` stub), the plan-doc commit + bidirectional link, idempotency, the fail-closed reasons, zero-pre-promotion activity (empty gh-stub log on refusal and on the idempotent no-op), and the PR body carrying the full AC + DoD.
|
|
33
|
+
- **Local-first refine + human-review checkpoint (off-tracker)** (#951, builds on #949/#950, consumes #948). A pure contract `packages/core/src/loop/plan-file-refine-contract.mjs` exports `refinePlanFileInPlace({ markdownText, baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone, payload }) -> { ok, planFileIntakeState, refinedMarkdown, grillDispositions, stop }` plus `PLAN_FILE_REFINE_STOP`/`DOCS_GRILL_FINDINGS_HEADING`/`COVERAGE_MATRIX_HEADING`. It generalizes the proposal-first intake pattern (emit a local artifact, human-gate it, make zero tracker mutation, stop and ask) to plan files: starting from P2's `new_plan_needs_refinement`, it writes the refiner-produced `Acceptance criteria`, `Definition of done`, `Coverage matrix`, and recorded `Docs-grill findings` sections in place into the single canonical plan file, advances P2's intake state to `plan_refined_ready_for_promotion` via `evaluatePlanFileIntakeState`, and returns a `local_human_review` stop so the loop pauses for human review before any promotion. The docs-grill runs as a step of refinement: each finding is classified with #948's `classifyDocsGrillFinding` and recorded into the plan file, and a malformed finding fails the grill closed. The rewrite is idempotent — a re-run strips any prior copy of each refinement section before appending, so the section count stays at one each. It is pure (no GitHub, no network, no filesystem I/O): the caller supplies the section-presence facts and the refiner payload and writes the returned markdown back, keeping the zero-tracker-mutation guarantee structural. Fail-closed paths cover a non-`new_plan_needs_refinement` starting state, an ambiguous/base-invalid plan, missing AC/DoD/coverage-matrix payload pieces, and a failed grill — each surfaces a reason and advances/writes nothing. A thin CLI `scripts/refine/refine-plan-file.mjs` (`--plan-file <path> --payload <path> [--json]`) reads the plan and the refiner output, computes the facts with the P1 `validatePlanFile`/`extractSection` surfaces, calls the contract, writes the refined plan back in place on success (exit 0, `local_human_review` stop), and fails closed without writing on a fail-closed reason (exit 1). A `node:test` contract spec plus a CLI spec cover refine-from-needs-refinement, the docs-grill-as-a-step recording, in-place idempotency, state advance + local stop, the fail-closed cases, and a zero-tracker-mutation assertion (the logging `gh` stub's call log is empty across the whole path).
|
|
34
|
+
- **Autonomous in-loop docs-grill formalized as a standard step** (#948, sibling of #944 and #929). `docs/docs-grill-step.md` documents the repeatable step that interrogates a change against the repo's own contracts/docs while the loop runs — without a manual main-agent pass: what it checks (claims vs contracts, code-vs-doc drift, stale references, contract-surface accuracy), where it fires (the [refiner](agents/refiner.agent.md) cross-checks the active phase against the contracts it references during refinement, and the existing `docs` pre-approval angle resolves to the [docs persona](agents/docs.agent.md) review mode as one fan-out angle of the [gate review sub-loop](docs/gate-review-sub-loop-contract.md)), and the keep/fix rule. The rule is codified as a pure classifier `classifyDocsGrillFinding` in `scripts/loop/docs-grill-contract.mjs` (`DOCS_GRILL_FINDING_KINDS` `drift`/`stale_reference`/`cosmetic`; `DOCS_GRILL_DISPOSITIONS` `record_finding`/`fix_in_place`/`route_followup`/`ignore_cosmetic`): real drift between code/behavior and a contract claim is recorded as a finding, doc-only drift is fixed in place or routed as a follow-up, and a cosmetic nit never blocks a gate; an unknown kind fails closed (`invalid_finding`). The refiner agent gains an explicit docs cross-check line pointing at the step. The two firing surfaces already ran in-loop, so this captures the contract without adding a new entrypoint. The local-first epic (#947) tree refinement is recorded as the first run (each node was grilled against the contracts it reuses during the refiner+grill fan-out). Cross-linked from `docs/index.md`.
|
|
35
|
+
- **`--plan-file` startup entry + local-planning intake state machine** (#950, builds on #949). `scripts/loop/resolve-dev-loop-startup.mjs` gains `--plan-file <path>`, mutually exclusive with `--issue`/`--pr`/`--input`. It reuses the `local_implementation` strategy and the existing `local_phase` target as a work-origin addition: a valid plan resolves to a `local_phase` bundle with the plan path carried as the target `phase` and no issue/PR number. The path is read-only (zero tracker mutation, no `gh` calls) and exempt from the worktree-isolation `needs_reconcile` guard, because a pre-promotion plan has no issue to key a worktree on. A missing/unreadable plan, or one failing the `validatePlanFile` base-section contract, fails closed (exit 1, no readiness bundle). A new pure, deterministic intake evaluator `evaluatePlanFileIntakeState({ baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone }) -> { state }` (frozen `PLAN_FILE_INTAKE_STATE` enum, no I/O) in `packages/core/src/loop/plan-file-intake-contract.mjs` classifies the plan as `new_plan_needs_refinement` (base sections only), `plan_refined_ready_for_promotion` (base sections plus `Acceptance criteria` + `Definition of done`), or `ambiguous_fail_closed` (invalid base, or exactly one of the two refinement sections). The resolver detects refined-vs-needs-refinement via `extractSection` for each `PLAN_FILE_REFINEMENT_SECTIONS` heading and threads the resulting `planFileIntakeState` onto its JSON output.
|
|
36
|
+
- **Plan-file contract + validator for local-planning mode** (#949). The persisted markdown plan file reuses the existing phase-doc format under `docs/phases/`; a new `localPlanning` config family adds `plansDir` (default `docs/phases/`) across all four config locations (`DevLoopConfigSchema`, `FileConfigSchema`, `BUILT_IN_DEFAULTS`, and the packaged `extension-defaults.yaml`), with `resolvePlansDir(config)` returning the configured directory or the default. A pure validator `scripts/refine/validate-plan-file.mjs` exports `validatePlanFile(markdownText) -> { checker: "validate-plan-file", ok, errors }` that checks the base authoring sections a plan carries before refinement (`Status`, `Objective`, `In scope`, `Explicit non-goals`), reporting each absent or empty-body section under a distinct `missing_*` code; its thin CLI takes `--input`/`--json`/`--help`, reports the verdict in the JSON payload, and exits non-zero on argument or path errors. The format and required sections are documented in `skills/docs/plan-file-contract.md` (mirrored to `.claude/skills/docs/`) and linked from the Artifact Authority Contract.
|
|
37
|
+
- **A/B (binary-contrast) prose removal formalized as a standard deslop step + applied to the two articles** (#944). `docs/ab-contrast-deslop-step.md` documents a repeatable editorial step that removes the binary-contrast / negation-by-contrast antipattern in both orderings ("X, not Y" and "Y, not X") — the strongest single AI tell in generated prose: the detection spec, the parallel-analysis → final-check → human-likeness flow, and the keep-load-bearing-distinctions/cut-the-scaffolding rewrite rule (with a keep-vs-cut table). It is the first documented sub-step of the broader deslop step tracked in #936. Applied as the first run to both Medium articles (`eliminating-coordination-delay`, `make-the-waiting-visible`) and their HTML renders: an audit found ~120 constructions across the articles and decks; the article prose now states each point directly while keeping real technical distinctions plain (the merge signal stays read-from-CI, automation stays state-gated), followed by a human-likeness pass to keep the cadence natural. Each article's `-notes.md` records the pass; a residual check finds zero flagged constructions in the article `.md`/`.html`.
|
|
38
|
+
- **Presentation decks publishable to GitHub Pages** (#930). `.github/workflows/pages.yml` runs the standard Pages pipeline (`actions/configure-pages` + `actions/upload-pages-artifact` + `actions/deploy-pages`, with `permissions: { pages: write, id-token: write, contents: read }` and `concurrency: { group: pages }`). A committed, reusable build script (`scripts/pages/build-site.mjs`, exported `buildSite`/`DECKS`) deterministically assembles `site/` — copying the self-contained deck renders (`docs/presentations/applied-dev-loops.html`, `process-observability.html`) and generating a CSP-safe dark-aesthetic `site/index.html` linking both. The deck HTML stays the single source of truth; `site/` is assembled (gitignored), never hand-maintained. The **build** job (assemble + upload, which validates the assembly) runs on push to `main` + `workflow_dispatch`. At the time of this PR the **deploy** job was gated to `workflow_dispatch` (`if: github.event_name == 'workflow_dispatch'`) until an operator enabled Pages, with the build job validating the assembly on every push; **#941 (below) superseded that gate** once Pages was enabled, so the deploy job now auto-publishes on every push to `main`. `docs/presentations/README.md` documents the decks, local viewing, the deploy flow, and the private-repo plan caveat (a public Pages site from a private repo needs Pro/Team/Enterprise). Also adds the missing `@media (prefers-reduced-motion: reduce)` guard to `applied-dev-loops.html` for parity with the observability deck (disables `scroll-behavior: smooth` on `html`, `scroll-snap-type` on `body`, `scroll-snap-align` on `.slide`). A `node:test` spec (`test/pages/build-site.test.mjs`) asserts the build produces `index.html` + both decks and that the index links both.
|
|
39
|
+
- **Medium-ready long-form article: "Eliminating Coordination Delay in AI-Assisted Dev Workflows"** (#934). `docs/articles/eliminating-coordination-delay.md` is a public-audience, ~1500-word prose piece derived from the Applied dev-loops narrative — zoomed-out and durable (no version-pegging, no raw enum/identifier dumps): hook (cheap code, leaky guessed handoffs) → the one idea (never guess a handoff; make every handoff an explicit, observable decision) → what it buys (safe pauses, mid-flight steering, parallel review → one consolidated verdict, "done" that means merged and read from the real merge signal) → why a state graph beats prompt-only → a one-line close. It carries four captioned `mermaid` diagrams (nested-loops state diagram, PR gate lifecycle flowchart, fan-out/fan-in evidence→verdict flow, mid-flight steering flow) plus a "Rendering the diagrams on Medium" note. A self-contained, CSP-safe preview render `docs/articles/eliminating-coordination-delay.html` ports the dark glass-card identity (navy gradient, glass cards, violet accent, blue kicker, system fonts) in an article layout (~65ch measure) with the four diagrams as inline CSS-flow / inline SVG (no mermaid runtime, no remote resources, wide diagrams scroll in their own container). One prose-adapted storytelling/editorial review pass is recorded and applied in `docs/articles/eliminating-coordination-delay-notes.md`.
|
|
40
|
+
- **Long-form article "Make the Waiting Visible" + a self-contained HTML preview render** (#935). `docs/articles/make-the-waiting-visible.md` adapts the [Process Observability deck](docs/presentations/process-observability-presentation.md) into a Medium-ready public-audience essay (~1.6k words, optional title/dek/tags front-matter): AI writes code in seconds, then the work *sits* — the slow part is the invisible waiting between actions. The arc runs hook → interrupt cost (one interrupt = five transitions) → handoffs restart discovery → git history hides the waiting → make state observable (owner / blocker / latest decision / safe next step) → measure → change → verify → ground it in real mechanisms (board lifecycle, gate evidence trail, deterministic next-action resolver, provider-agnostic CI waits, post-merge reclaim/archive) → close (*"The next agent will write your code in seconds. The lever you control is how long it waits afterward, so measure that."*). Four captioned `mermaid` diagrams (interrupt-cost chain, handoff round trip, measurement loop, observable-state grounding) plus a "rendering on Medium" note. A standalone, CSP-safe `docs/articles/make-the-waiting-visible.html` renders the full article in the dark glass-card identity (navy gradient, glass cards, violet accent, blue kicker, system fonts) as an article layout (~65ch measure), drawing the four diagrams as inline CSS flow / inline SVG — no mermaid runtime, no font/CDN/remote assets. One editorial/storytelling review pass (lens from [docs/slides-story-review-loop.md](docs/slides-story-review-loop.md), adapted for prose) is recorded and applied in `docs/articles/make-the-waiting-visible-notes.md`.
|
|
41
|
+
- **Slides content & storytelling review loop formalized as a bounded reviewer mode** (#929). A sibling of the [UI Designer + Vision Review Loop](docs/ui-designer-review-loop.md) behind `dev-loop` that judges a deck's *narrative*, not its pixels — "does it land?" rather than "does it look right?". `docs/slides-story-review-loop.md` defines the contract: the public entrypoint/dependency boundary (no new public name), a fail-closed REQUIRED INPUT BUNDLE (deck source path + slice-level acceptance criteria + a short storytelling brief + optional captured slide screenshots from the UI smoke harness), the public-audience REVIEW LENS (arc/hook/close, one message per slide + claim titles, sequencing/no-forward-refs, jargon translation, cut/merge/reorder), and a REQUIRED OUTPUT BUNDLE (findings + corrective actions + a single structured outcome `story_review_satisfied` | `needs_iteration`). The pure module `scripts/loop/slides-story-review-contract.mjs` exports the outcome constants plus a fail-closed input-bundle validator and a result-shape validator (no I/O); the prompt template lives at `skills/dev-loop/templates/slides-story-review.md`. The two inline applications already run over the decks are recorded as the first two runs (`docs/presentations/applied-dev-loops-review-notes.md` #926, `docs/presentations/process-observability-review-notes.md` #927). Cross-linked from the UI loop doc, the README, and `docs/index.md`.
|
|
42
|
+
- **Process Observability deck refreshed + a self-contained shareable HTML render** (#927). `docs/presentations/process-observability-presentation.md` (Slidev) is restructured into one 9-slide public-audience story arc in the existing dark glass-card style: claim-style titles, one message per slide, jargon trimmed (`task state` / `pipeline latency` pills cut), the two overlapping handoff-cost slides merged, and a memorable close (*"Stop optimizing how fast you write code. Start measuring how long it waits."*) replacing the bare metric grid. A new grounding slide (`instrumented`) ties the abstract "observable state cuts delay" claim to what actually exists — the queue board lifecycle (owner / safe next step), the gate evidence trail (latest decision + findings), the deterministic next-action resolver, and "automate only where state supports safe continuation" via provider-agnostic CI waits and operator-induced post-merge worktree reclaim / long-done archival — in plain language, no version labels or raw identifiers. A standalone, CSP-safe `docs/presentations/process-observability.html` ships the full deck with all CSS inline (ported from `style.css`: navy gradient, glass cards, violet accent, blue kicker, mono pills) and the flowcharts rendered as inline CSS/HTML flow (no mermaid/CDN, no remote resources) — each slide is a stable-id `<section>` (`hero`, `interrupt-cost`, `handoff`, `blind-spot`, `observable-state`, `measurement-loop`, `instrumented`, `metrics`, `close`). A thin WebKit smoke spec (`test/playwright/observability-deck.spec.mjs` + `playwright.observability-deck.config.mjs`, `npm run test:playwright:obs-deck`) asserts every named section is present with no body horizontal overflow and captures the `hero`, `interrupt-cost`, `observable-state`, `measurement-loop`, `instrumented`, `metrics`, and `close` states under `test-results/ui-smoke/observability-deck/`; one designer/vision review pass (notes in `docs/presentations/process-observability-review-notes.md`, storytelling + visual sections) confirmed equal-height cards, legible flow diagrams, and a landing close with no corrective CSS required beyond the ported baseline.
|
|
43
|
+
- **Applied dev-loops deck refreshed for v0.4.0 + a self-contained shareable HTML render** (#926). `docs/presentations/applied-dev-loops-presentation.md` (Slidev) gains two slides in the existing dark glass-card style: the **gate fan-out/fan-in sub-loop** (build-once neutral bundle = full diff + 1-hop import adjacency, size-guarded; independent per-angle reviewers seeded with the identical bundle; `consolidateFanin` merges per-angle verdicts; no fork primitive / no Workflow dependency; fail-closed `fanout_fanin` verdict with enforced severity counts) and **the coordination runtime owning the full lifecycle** (enforced human merge via `autonomy.humanMergeOnly`, managed `tmp/worktrees/dev-loops/` worktrees, provider-agnostic `watch-ci`, queue board as a deterministic adapter). A standalone, CSP-safe `docs/presentations/applied-dev-loops.html` ships the full deck with all CSS inline and the mermaid diagrams rendered as inline CSS flow (no font/mermaid CDN, no remote resources) — each slide is a stable-id `<section>` for UI smoke targeting. A thin WebKit smoke spec (`test/playwright/applied-deck.spec.mjs` + `playwright.applied-deck.config.mjs`, `npm run test:playwright:deck`) captures the `hero`, `core-idea`, `parallel-review`, `trust`, and `impact` named states under `test-results/ui-smoke/applied-deck/`; one designer/vision review pass (notes in `docs/presentations/applied-dev-loops-review-notes.md`) fixed ragged card heights via equal-height grid rows. A follow-up **public-audience storytelling pass** restructured the deck to one ~8-slide narrative arc (claim-style titles, jargon translated to plain language, raw enum/pill walls cut to at most one identifier per slide as evidence, mechanism→outcome close) while keeping the dark glass-card visual identity unchanged.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`promote-plan` pushes the head branch before opening the PR, and the plan-commit step is re-runnable (#969).** Promotion committed the plan doc and then called `create-pr.mjs` (`gh pr create --head <branch>`) without first pushing the fresh local branch; when `gh pr create` did not auto-push, promotion failed `pr_create_failed` AFTER the plan commit had landed — an unrecoverable partial state (plan committed, no PR, no `prNumber`) where a re-run re-entered PROMOTE, found nothing to stage, and dead-ended on `git_commit_failed`. Fix in `scripts/refine/promote-plan.mjs`: (1) the head branch is now pushed with `git push -u origin <branch>` BEFORE invoking `create-pr.mjs`, so a fresh branch exists on the remote and `gh pr create --head` succeeds; a push failure fails closed with the new `git_push_failed` reason + detail and makes ZERO `gh` mutation. (2) The plan-commit step is idempotent: when the plan doc is already committed at HEAD (`git diff --cached --quiet` reports nothing staged), the commit is skipped instead of failing `git_commit_failed`, so a prior partial run recovers on a plain re-run — it pushes, opens the draft PR, and writes the plan↔PR link. The existing fail-closed ready-gate (zero `gh`), `already_promoted` idempotency, and the post-PR-open `failAfterPrOpen` link-commit recovery are unchanged. `test/loop/promote-plan.test.mjs` gains a real bare `origin` remote in its setup and three asserts: the fresh-branch run pushes the branch (asserted via `git ls-remote`) then opens exactly one draft PR, a partial state (plan committed at HEAD on the head branch, no `prNumber`) recovers on re-run with one `gh` call, and a missing-remote push failure surfaces `git_push_failed` with a detail and zero `gh` calls.
|
|
48
|
+
- **Decks fit the phone screen — no horizontal scroll, no clipped content (~390px)** (#937). On a phone the inline flow diagram (`.flow { min-width: max-content }`, `.node { white-space: nowrap }`) forced its grid track wider than the viewport, and `.slide { overflow: hidden }` then **clipped** anything taller than one screen (bullets cut mid-word, flow nodes sheared, slide bottoms cut off). The requirement is that content **fits** both dimensions, not that it scrolls inside a card. Fix (both `docs/presentations/applied-dev-loops.html` and `process-observability.html`): (1) **diagrams fit by stacking** — at `≤600px` `.flow` switches to `flex-direction: column` with arrows rotated to point down and `.node { white-space: normal }`, so the diagram lays out vertically and fits the width (measured 319px flow in a 319px card, zero internal scroll); `.flow { flex-wrap: wrap }` keeps desktop rows fitting too, and `.flow-scroll { overflow-x: auto }` is now a never-triggered last-resort safety, not the fix; (2) **no vertical clip** — `.slide { overflow: hidden }` → `overflow: visible` so a tall slide grows and the page scrolls between slides instead of cutting content off; (3) **mobile sizing** — a `≤600px` breakpoint reduces heading/card/padding scale and top-aligns slides so content fits comfortably. `min-width: 0` on grid children and `overflow-wrap: anywhere` on `p`/`li`/`code` are added. The CSS lives only in the HTML renders (the Slidev `*-presentation.md` sources carry none of it). The dark visual identity, section ids, CSP guard, and reduced-motion guard are unchanged. Both Playwright deck specs (`test/playwright/applied-deck.spec.mjs`, `observability-deck.spec.mjs`) are hardened to enforce **fit** at mobile (390×844): a settle barrier (`waitForLoadState("networkidle")` + `waitForFunction(innerWidth === 390)` + `document.fonts.ready`) removes the cold-start false-fail that measured desktop geometry; the horizontal check now fails if **any** element's `getBoundingClientRect().right > innerWidth + 1` (the `overflow-x:auto` scroller exemption is dropped — diagrams must fit) and asserts `document.scrollingElement.scrollWidth <= innerWidth + 1` (no horizontal page scroll); a vertical-clip check fails any section whose `clientHeight < scrollHeight` while `overflow-y` is `hidden`/`clip`; a guard-the-guard test confirms the fit check fails on a deliberately-wide element. Each spec also captures one mobile named state.
|
|
49
|
+
|
|
7
50
|
## 0.4.0
|
|
8
51
|
|
|
9
52
|
### Added
|
package/README.md
CHANGED
|
@@ -274,3 +274,4 @@ CI splits into a small changed-files gate plus parallel `verify` and conditional
|
|
|
274
274
|
- [UI Smoke Harness](./docs/ui-smoke-harness.md) — reusable local Playwright/WebKit smoke baseline
|
|
275
275
|
- [UI Artifact Contract](./docs/ui-artifact-contract.md) — screenshot/state artifact contract and CI-promotion rules
|
|
276
276
|
- [UI Designer Review Loop](./docs/ui-designer-review-loop.md) — designer + vision (`uiReviewMode: vision`) review loop contract
|
|
277
|
+
- [Slides Story Review Loop](./docs/slides-story-review-loop.md) — bounded slides content & storytelling reviewer (narrative, not pixels)
|
package/agents/refiner.agent.md
CHANGED
|
@@ -46,6 +46,7 @@ For the active phase, require and produce:
|
|
|
46
46
|
- Do not invent audit findings when no audit artifact was provided
|
|
47
47
|
- when the phase includes watcher or predicate-driven behavior: explicit timeout semantics and negative-case expectations for non-target identities/events
|
|
48
48
|
- when the phase relies on package-first shared helpers inside a source-loaded workspace: explicit integration expectations about whether local callers use published package imports or a thin source/workspace adapter during development
|
|
49
|
+
- cross-check the phase's claims against the contracts and docs they reference (the autonomous docs-grill step, see ../docs/docs-grill-step.md): surface code-vs-doc drift, stale references, and contract-surface inaccuracies as refinement findings while the claims are still being verified
|
|
49
50
|
|
|
50
51
|
## Working style
|
|
51
52
|
- Prefer parallel fresh-context fan-out/fan-in when it improves refinement quality or surfaces materially different variants.
|
package/package.json
CHANGED
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
"test": "npm run test:assets && npm run test:extension && npm run test:scripts && npm run test:core && npm run test:docs",
|
|
26
26
|
"test:assets": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/dev-loop-init-phase-smoke.test.mjs test/contracts/*.test.mjs test/workflow-handoff-contract.test.mjs",
|
|
27
27
|
"test:extension": "node --import tsx --test --test-reporter ./test/failure-summary-reporter.mjs test/extension-checks.test.mjs test/extension-post-merge-update.test.mjs test/extension-command-contract.test.mjs test/extension-package-contract.test.mjs test/extension-pi-adapter.test.mjs test/extension-claude-adapter.test.mjs test/dev-loops-core.test.mjs test/dev-loops-cli.test.mjs",
|
|
28
|
-
"test:scripts": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/github/*.test.mjs test/loop/*.test.mjs test/docs/*.test.mjs test/projects/*.test.mjs",
|
|
28
|
+
"test:scripts": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/github/*.test.mjs test/loop/*.test.mjs test/docs/*.test.mjs test/projects/*.test.mjs test/pages/*.test.mjs",
|
|
29
29
|
"test:core": "node --test --test-reporter ./test/failure-summary-reporter.mjs packages/core/test/*.test.mjs",
|
|
30
30
|
"test:dev-loop": "node --test --test-reporter ./test/failure-summary-reporter.mjs skills/dev-loop/scripts/dev-mode-context.test.mjs skills/dev-loop/scripts/render-template.test.mjs skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs",
|
|
31
31
|
"test:playwright:viewer": "playwright test -c playwright.inspect-run-viewer.config.mjs",
|
|
32
|
+
"test:playwright:deep-dive": "playwright test -c playwright.dev-loops-deep-dive.config.mjs",
|
|
33
|
+
"test:playwright:intro-deck": "playwright test -c playwright.intro-deck.config.mjs",
|
|
32
34
|
"smoke:headless": "node scripts/claude/headless-info-smoke.mjs",
|
|
33
35
|
"test:docs": "node scripts/docs/validate-links.mjs && node scripts/docs/validate-no-duplicate-rules.mjs",
|
|
34
36
|
"repo-wiki": "node scripts/repo-wiki.mjs",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
},
|
|
69
71
|
"dependencies": {
|
|
70
72
|
"mermaid": "11.15.0",
|
|
71
|
-
"@dev-loops/core": "^0.
|
|
73
|
+
"@dev-loops/core": "^0.5.0"
|
|
72
74
|
},
|
|
73
75
|
"repository": {
|
|
74
76
|
"type": "git",
|
|
@@ -78,7 +80,7 @@
|
|
|
78
80
|
"url": "https://github.com/mfittko/dev-loops/issues"
|
|
79
81
|
},
|
|
80
82
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
81
|
-
"version": "0.
|
|
83
|
+
"version": "0.5.0",
|
|
82
84
|
"files": [
|
|
83
85
|
"cli/",
|
|
84
86
|
"lib/",
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
13
13
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
14
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
14
15
|
export const REVIEW_THREADS_QUERY = [
|
|
15
16
|
"query($owner: String!, $name: String!, $pr: Int!) {",
|
|
16
17
|
" repository(owner: $owner, name: $name) {",
|
|
@@ -45,9 +46,11 @@ Modes:
|
|
|
45
46
|
Options:
|
|
46
47
|
--output <path> Write JSON output to file in addition to stdout
|
|
47
48
|
--help, -h Show this help
|
|
49
|
+
${JQ_OUTPUT_USAGE}
|
|
48
50
|
Exit codes:
|
|
49
51
|
0 Success
|
|
50
52
|
1 Error
|
|
53
|
+
2 Invalid --jq filter
|
|
51
54
|
`;
|
|
52
55
|
export function parseCaptureCliArgs(argv) {
|
|
53
56
|
const { tokens } = parseArgs({
|
|
@@ -58,6 +61,7 @@ export function parseCaptureCliArgs(argv) {
|
|
|
58
61
|
output: { type: "string" },
|
|
59
62
|
repo: { type: "string" },
|
|
60
63
|
pr: { type: "string" },
|
|
64
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
61
65
|
},
|
|
62
66
|
allowPositionals: true,
|
|
63
67
|
strict: false,
|
|
@@ -69,6 +73,8 @@ export function parseCaptureCliArgs(argv) {
|
|
|
69
73
|
repo: undefined,
|
|
70
74
|
pr: undefined,
|
|
71
75
|
help: false,
|
|
76
|
+
jq: undefined,
|
|
77
|
+
silent: false,
|
|
72
78
|
};
|
|
73
79
|
for (const token of tokens) {
|
|
74
80
|
if (token.kind === "positional") {
|
|
@@ -97,6 +103,14 @@ export function parseCaptureCliArgs(argv) {
|
|
|
97
103
|
options.pr = parsePrNumber(requireTokenValue(token));
|
|
98
104
|
continue;
|
|
99
105
|
}
|
|
106
|
+
if (token.name === "jq") {
|
|
107
|
+
options.jq = requireTokenValue(token);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (token.name === "silent") {
|
|
111
|
+
options.silent = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
100
114
|
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
101
115
|
}
|
|
102
116
|
const hasLiveArgs = options.repo !== undefined || options.pr !== undefined;
|
|
@@ -188,10 +202,14 @@ export async function runCli(
|
|
|
188
202
|
if (options.outputPath) {
|
|
189
203
|
await writeOutputFile(options.outputPath, payload);
|
|
190
204
|
}
|
|
191
|
-
|
|
205
|
+
return emitResult(payload, { jq: options.jq, silent: options.silent, stdout });
|
|
192
206
|
}
|
|
193
207
|
if (isDirectCliRun(import.meta.url)) {
|
|
194
|
-
runCli().
|
|
208
|
+
runCli().then((code) => {
|
|
209
|
+
if (typeof code === "number") {
|
|
210
|
+
process.exitCode = code;
|
|
211
|
+
}
|
|
212
|
+
}).catch((error) => {
|
|
195
213
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
196
214
|
process.exitCode = 1;
|
|
197
215
|
});
|