deepclause-pi 0.2.0 → 0.4.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.
@@ -0,0 +1,1893 @@
1
+ # A DeepClause spec layer for `deepclause-pi`
2
+
3
+ ## Status
4
+
5
+ **Design sketch; phase 1 implemented on branch `feat/spec-layer-phase1`.** This
6
+ document records the design discussion that started from
7
+ [OpenSpec](https://github.com/Fission-AI/openspec) and asks how its ideas map onto
8
+ DML and the existing `deepclause-pi` plan/executor model.
9
+
10
+ Implemented in phase 1: the deterministic engine (`src/assets/specs.dml`), the
11
+ `spec_validate` / `spec_status` / `spec_query` / `spec_graph` skills, `/dc-check`,
12
+ the `dc_spec_graph` tool, and workspace seeding of `specs/`, `changes/` and `lib/`.
13
+
14
+ Implemented in phase 2: delta merging (`sp_archive/3`, `spec_merge.dml`,
15
+ `spec_archive.dml`) and the `/dc-archive` command, which previews the merge,
16
+ confirms, writes `specs/`, and moves the change folder to `changes/archive/`.
17
+ Unchanged requirement blocks are preserved line-for-line. The archive *move* is
18
+ done host-side because directory `rename_file/2` is unreliable in the WASM
19
+ filesystem. `RENAMED` deltas are refused for now.
20
+
21
+ Implemented in phase 3: `tasks.dml` as a first-class artifact (`plan_task/2`
22
+ facts read with native term I/O), scenario coverage reporting (`spec_coverage.dml`
23
+ and a coverage section in `/dc-check`), and a read-only `spec_scaffold.dml` that
24
+ drafts one task per delta scenario. Note the `plan_task` naming: `task/2` collides
25
+ with DML's built-in `task/N`.
26
+
27
+ Implemented in phase 4: the task driver (`lib/apply.dml`) with per-task declarative
28
+ checks (`exists`, `cmd`, `model`), bounded retry that threads the failure feedback
29
+ into the repair instruction, and a managed `plan_task_status/2` write-back block in
30
+ `tasks.dml`; the allowlisted `dc_verify_run` tool; the `spec_apply.dml` skill; and
31
+ `/dc-apply <change>`, which previews the tasks and the exact command set, confirms,
32
+ then applies with `pi_agent_step` and `dc_verify_run` enabled.
33
+
34
+ Implemented in phase 8: commit prompts. After `/dc-plan`, `/dc-apply` and `/dc-archive`
35
+ leave a dirty tree, the extension shows the changed files and offers to commit them
36
+ (`git add -A` plus a suggested `<action>: <change>` message), or reminds the user when
37
+ they decline. A clean tree is what lets the next `/dc-apply` take a rollback snapshot.
38
+
39
+ Not yet implemented: the `deltas.dml` / `index.dml` files, and `RENAMED` support in merge.
40
+
41
+ Implemented in phase 7: resumable apply and a merge guard. `/dc-plan --change` fails
42
+ before spending a planning turn when `tasks.dml` already exists; `--update` (or a
43
+ leading `update` keyword) regenerates it and resets statuses to pending. Merging now
44
+ refuses `MODIFIED`/`REMOVED` entries that do not exist in the target spec, and refuses
45
+ `MODIFIED`/`REMOVED` for a brand-new capability, instead of silently dropping them.
46
+ `/dc-apply` **preserves** an interrupted apply (working tree plus `done`/`failed`
47
+ statuses, `applyState: in_progress` in `change.json`) so a re-run resumes from the
48
+ remaining tasks; `/dc-apply --abort` discards it and restores the snapshot.
49
+
50
+ Implemented in phase 6: apply-time rollback. `dc_apply_snapshot` records a git ref
51
+ (refusing a dirty tree) in `change.json`; `dc_apply_accept` clears it on success; the
52
+ `/dc-apply` harness restores on any run that does not report `status: OK`, including
53
+ cancellation, using `git reset --hard` plus `git clean -fd`. When no snapshot is
54
+ available (not a git repo, or a dirty tree) the apply proceeds and the report says
55
+ rollback is unavailable. Two runtime quirks surfaced: `exists_file/1` is unreliable
56
+ in the WASM filesystem (use `open/2`), and a DML predicate named `snapshot/1`
57
+ collides with a runtime accessor, so the driver uses `take_snapshot/1`.
58
+
59
+ Implemented in phase 5: change-aware planning. `/dc-plan <request> --change=<slug>`
60
+ writes `changes/<slug>/tasks.dml` (`plan_task/2` with `satisfies` and encoded
61
+ `checks`, plus the managed status block) instead of a standalone plan. Check
62
+ encoding is `cmd:<command>`, `exists:<path>` or `model:<question>`; change plans
63
+ require at least one check per step, and step ids may be OpenSpec-style (`1.1`).
64
+ The planning prompt also instructs pi to create the change's proposal and delta
65
+ specs before committing. Coverage is validated by `/dc-check`, and the flow is
66
+ closed by `/dc-apply` and `/dc-archive`.
67
+
68
+ It builds directly on [DC_PLAN_PROPOSAL.md](DC_PLAN_PROPOSAL.md), which describes
69
+ the shipped `/dc-plan` + `pi_agent_step` architecture. This document does not
70
+ change that architecture; it proposes a spec layer on top of it.
71
+
72
+ Revision note: an earlier draft attached executable checks to requirements and
73
+ kept a Markdown `tasks.md`. Both were wrong and are corrected below — checks are
74
+ implementation details and live in `tasks.dml`, and Markdown is canonical only for
75
+ behavior.
76
+
77
+ Related reading:
78
+
79
+ - `.pi/deepclause/AGENTS.md` — DML authoring rules for this integration
80
+ - `.pi/deepclause/DML_REFERENCE.md` — bundled DML language reference
81
+ - `docs/AUTHORING_GUIDE_ANALYSIS.md` — why the authoring guide looks the way it does
82
+
83
+ ## Goals
84
+
85
+ - Give pi a place to agree on **what to build** before building it: specs as the
86
+ source of truth, changes as reviewable deltas.
87
+ - Make spec **validation and merging deterministic DML**, not model judgment and
88
+ not more TypeScript string handling.
89
+ - Make `apply` an **executable, verifiable plan**: per-task implementation checks,
90
+ bounded repair retries, and a deterministic rollback path.
91
+ - Stay inside the existing constraints: minimal slash commands, no Markdown-to-DML
92
+ compiler, user-triggered execution, opt-in model-callable `dc_run`.
93
+
94
+ ## Non-goals
95
+
96
+ - **No natural-language → DML compiler.** Parsing and validating a *structured*
97
+ spec is deterministic logic. Turning prose into DML is not, and stays out.
98
+ - **No second session store.** `.pi/deepclause/` is files; pi remains the session
99
+ owner. The parsed term tree is never persisted.
100
+ - **No reimplementation of OpenSpec's CLI.** It has a mature CLI and a 30+ tool
101
+ integration matrix. Only two things are borrowed: the spec/change convention and
102
+ the artifact-graph idea.
103
+ - **No new top-level commands beyond the agreed set.** OpenSpec's phase commands
104
+ (`propose`, `apply`, `archive`, `verify`, …) become arguments to `/dc-plan` and
105
+ named skills run with `/dc-run`.
106
+
107
+ ## The mental model
108
+
109
+ > **Specs are behavior. Changes are executable. `/dc-plan` thinks and writes;
110
+ > `/dc-run` proves and applies.**
111
+
112
+ Two verbs for the user, not twelve. Four artifacts per change, each with one job:
113
+
114
+ | Layer | Artifact | Answers | Canonical form |
115
+ |---|---|---|---|
116
+ | Behavior | `specs/**`, delta | *what must be true* | Markdown |
117
+ | Approach | `design.md` (optional) | *how, and why* | Markdown |
118
+ | Implementation | `tasks.dml` | *what to do, and how far we got* | DML facts |
119
+ | Execution | `apply.dml` | *how to drive it* | DML program |
120
+
121
+ The rule that keeps them separate:
122
+
123
+ > **Markdown is canonical only for behavior and rationale. Anything executable,
124
+ > enumerable, or stateful — tasks, checks, progress — is DML data.**
125
+
126
+ ## The workflow in practice (user perspective)
127
+
128
+ You describe what you want in plain language — "add dark mode with
129
+ system-preference detection" — and run `/dc-plan`. Pi explores the repository,
130
+ reads the existing specs, and writes a change folder: a proposal, a behavior-only
131
+ delta under `changes/<slug>/specs/`, an optional `design.md`, plus `tasks.dml` (the
132
+ implementation plan, each task carrying its verification) and `apply.dml` (the
133
+ executable entry). Nothing is final until you approve it at the commit dialog, and
134
+ the specs themselves are plain Markdown you can read and hand-edit at any time.
135
+
136
+ `/dc-check <change>` then validates everything deterministically with zero model
137
+ calls — grammar, delta consistency, scenario coverage, discoverable checks, and
138
+ conflicts with other in-flight changes. `/dc-run <change>` executes the plan: it
139
+ delegates each bounded step to pi with exactly the tools that step needs, runs the
140
+ task's declared checks, and on a failed check retries with the failure evidence fed
141
+ back into the repair attempt. You approve the verification commands once as a suite
142
+ rather than per invocation, and if a task cannot be repaired the working tree is
143
+ restored from the snapshot taken at the start, so a failed apply never leaves a
144
+ half-applied change.
145
+
146
+ When it succeeds, `/dc-run spec_archive <change>` shows a diff and merges the delta
147
+ into `specs/`, moves the change to `changes/archive/`, and updates the delta index.
148
+ Because features, deltas, tasks, and checks are all queryable facts, you can ask
149
+ questions no chat history can answer: which changes touch `ui/theme`, which
150
+ scenarios are still uncovered, whether two in-flight changes collide on the same
151
+ requirement, and which check proved which scenario. The conversation proposes, the
152
+ spec is the contract, the plan executes, and the facts let you audit.
153
+
154
+ The rest of this section walks the same path with exact commands, showing what
155
+ lands on disk at each step and what the runtime does while a plan is executing.
156
+
157
+ ### 0. Bootstrap — `/dc`
158
+
159
+ ```text
160
+ > /dc
161
+
162
+ DeepClause pi runtime
163
+ Model: anthropic/claude-sonnet-4
164
+ Status: idle
165
+ Root: .pi/deepclause
166
+ Skills: .pi/deepclause/skills
167
+ Plans: .pi/deepclause/plans
168
+ Context: turn (verbose default: false)
169
+ Model tool (dc_run): disabled
170
+ Commands: /dc-list, /dc-plan, /dc-run, /dc-tool, /dc-cancel
171
+ ```
172
+
173
+ **Files.** On first use, `initializeWorkspace()` creates
174
+ `.pi/deepclause/{config.json,AGENTS.md,DML_REFERENCE.md,skills/,plans/}` and seeds
175
+ `example.dml` and `deep_research.dml`. Every write uses `writeIfMissing`, so
176
+ existing user files are never overwritten. The spec layer adds `specs/`,
177
+ `changes/`, and `lib/`, seeded the same way.
178
+
179
+ ### 1. Explore — just talk
180
+
181
+ ```text
182
+ > how should we do theming without adding dependencies?
183
+
184
+ [pi reads src/ styles, package.json, and specs/ui/system.spec.md,
185
+ then answers in the session]
186
+ ```
187
+
188
+ **No command, no files.** Exploration is ordinary conversation: pi's system prompt
189
+ already carries the DeepClause authoring instructions, so nothing has to be
190
+ "unlocked." There is no planning transaction, no `dc_plan_commit`, and no change
191
+ folder. Using `/dc-plan` here would open a transaction that never commits (and log
192
+ that fact in debug), so it is the wrong tool for a discussion you may never act on.
193
+
194
+ ### 2. Plan — `/dc-plan`
195
+
196
+ ```text
197
+ > /dc-plan add dark mode with system-preference detection --name=add_dark_mode
198
+
199
+ ⚠ Starting a contextual pi planning turn. Review the generated plan before it is written.
200
+
201
+ [pi explores: src/theme.ts, package.json, specs/ui/system.spec.md, changes/]
202
+ [pi writes the Markdown artifacts with its normal tools]
203
+ [pi calls dc_plan_commit]
204
+
205
+ ⚠ Create executable DeepClause plan?
206
+ Add dark mode
207
+ Objective: Add a light/dark theme that defaults to the system preference.
208
+ Steps: 5
209
+ Pi tools: read, edit
210
+ 1. [pi] 1.1 Add a ThemeProvider context
211
+ 2. [pi] 1.2 Add light/dark CSS custom properties
212
+ 3. [pi] 1.3 Default to prefers-color-scheme
213
+ 4. [pi] 1.4 Reject invalid stored values
214
+ 5. [pi] 1.5 Add the theme toggle to the header
215
+ [Confirm] [Cancel]
216
+ ```
217
+
218
+ **Files after the turn:**
219
+
220
+ ```text
221
+ changes/add_dark_mode/
222
+ ├── change.json # schema, created, digests, snapshot ref
223
+ ├── proposal.md # pi, normal tools
224
+ ├── specs/ui/theme.spec.md # pi, normal tools — behavior only
225
+ ├── design.md # pi, normal tools
226
+ ├── deltas.dml # emitted: delta ops + delta_status(pending)
227
+ ├── tasks.dml # emitted: plan_task/2 definitions + plan_task_status/2
228
+ └── apply.dml # emitted: entry + "% Required pi tools:" metadata
229
+ ```
230
+
231
+ **What happened during the turn.** Pi never writes DML. The Markdown artifacts are
232
+ written with ordinary file tools. `dc_plan_commit` then runs `validatePlanSpec`
233
+ against the live snapshot — every requested tool must exist *and* be currently
234
+ active, no step may request a control tool, every delta scenario must be covered,
235
+ and every `cmd(...)` check must be discoverable in the repo — then assembles the
236
+ DML with `assemblePlanDml`, validates it with `validateWithProlog`, and writes it
237
+ non-destructively. Result:
238
+
239
+ ```text
240
+ DeepClause
241
+ Created change add_dark_mode
242
+ proposal.md, specs/ui/theme.spec.md, design.md
243
+ tasks.dml 5 tasks, 5 checks, 3 scenarios covered
244
+ apply.dml contextual plan (2 pi tools)
245
+ Run it with: /dc-run add_dark_mode
246
+ ```
247
+
248
+ **What is in the generated files.**
249
+
250
+ `proposal.md` — why, what, capabilities, impact:
251
+
252
+ ```markdown
253
+ # Add dark mode
254
+
255
+ ## Why
256
+ Users on dark-preference systems get a bright UI with no way to change it.
257
+
258
+ ## What Changes
259
+ - Introduce a `ui/theme` capability for runtime theme selection.
260
+ - Default to the operating system colour-scheme preference on first run.
261
+ - Persist an explicit user choice.
262
+
263
+ ## Capabilities
264
+ ### New Capabilities
265
+ - `ui/theme`: runtime light/dark theme selection and persistence.
266
+
267
+ ### Modified Capabilities
268
+ - `ui/system`: theme switching must no longer require a reload.
269
+
270
+ ## Impact
271
+ - `src/theme/` (new), `src/app/App.tsx`, `index.html`
272
+ - No new dependencies.
273
+ ```
274
+
275
+ `specs/ui/theme.spec.md` (inside the change) — the delta. Behavior only, no
276
+ implementation detail:
277
+
278
+ ```markdown
279
+ ---
280
+ change: add_dark_mode
281
+ schema: spec-driven
282
+ ---
283
+
284
+ # Spec Delta
285
+
286
+ ## Purpose
287
+ Lets users choose between light and dark themes, defaulting to the operating
288
+ system preference.
289
+
290
+ ## ADDED Requirements
291
+
292
+ ### Requirement: Theme selection
293
+ The app SHALL let users switch between light and dark themes at runtime.
294
+
295
+ #### Scenario: User toggles dark mode
296
+ - **WHEN** the user clicks the theme toggle
297
+ - **THEN** the app switches to dark mode and persists the choice
298
+
299
+ #### Scenario: Invalid stored value is rejected
300
+ - **WHEN** a stored theme value is neither "light" nor "dark"
301
+ - **THEN** the app falls back to the system preference and shows no error
302
+
303
+ ### Requirement: System-preference default
304
+ The app SHALL default to the operating system colour-scheme preference when no
305
+ choice has been stored.
306
+
307
+ #### Scenario: First run on a dark-preference system
308
+ - **WHEN** the app starts with no stored theme and the OS reports dark
309
+ - **THEN** it renders dark without writing a stored choice
310
+ ```
311
+
312
+ `specs/ui/system.spec.md` (inside the change) — the modification to an existing
313
+ capability:
314
+
315
+ ```markdown
316
+ ---
317
+ change: add_dark_mode
318
+ schema: spec-driven
319
+ ---
320
+
321
+ # Spec Delta
322
+
323
+ ## MODIFIED Requirements
324
+
325
+ ### Requirement: Theme switching
326
+ The app SHALL apply theme changes without a full page reload.
327
+
328
+ #### Scenario: No reload on toggle
329
+ - **WHEN** the user toggles the theme
330
+ - **THEN** the visible theme updates in place and the document is not reloaded
331
+ ```
332
+
333
+ `design.md` — approach and trade-offs, where implementation detail is allowed:
334
+
335
+ ```markdown
336
+ # Design
337
+
338
+ ## Context
339
+ The app reads a theme once from localStorage at boot (`src/app/App.tsx`).
340
+
341
+ ## Goals / Non-Goals
342
+ **Goals:** no new dependencies; no reload on toggle.
343
+ **Non-Goals:** per-component theming; high-contrast mode.
344
+
345
+ ## Decisions
346
+ - CSS custom properties on `:root`, not a CSS-in-JS theme object — no dependency,
347
+ and it works with the existing stylesheet.
348
+ - `prefers-color-scheme` via `matchMedia`, read once at boot and subscribed for
349
+ later changes.
350
+
351
+ ## Risks / Trade-offs
352
+ - [Flash of the wrong theme on first paint] → set the class from an inline script
353
+ before hydration.
354
+ ```
355
+
356
+ `deltas.dml` — the delta as queryable facts plus lifecycle state:
357
+
358
+ ```prolog
359
+ % deltas.dml — spec deltas for change add_dark_mode
360
+ % Derived from:
361
+ % changes/add_dark_mode/specs/ui/theme.spec.md sha256:9f2c…
362
+ % changes/add_dark_mode/specs/ui/system.spec.md sha256:41ab…
363
+
364
+ delta("add_dark_mode", added, "ui/theme", req("theme-selection", "Theme selection")).
365
+ delta("add_dark_mode", added, "ui/theme", req("system-preference", "System-preference default")).
366
+ delta("add_dark_mode", modified, "ui/system", req("theme-switching", "Theme switching")).
367
+
368
+ change_meta("add_dark_mode", schema(spec_driven), skip_specs(false),
369
+ created("2025-09-17"), snapshot(none)).
370
+
371
+ % --- lifecycle state (managed by apply / spec_archive) ---
372
+ delta_status("add_dark_mode", "ui/theme", "theme-selection", pending).
373
+ delta_status("add_dark_mode", "ui/theme", "system-preference", pending).
374
+ delta_status("add_dark_mode", "ui/system", "theme-switching", pending).
375
+ ```
376
+
377
+ `tasks.dml` — the implementation plan and its state. Note `satisfies` closes the loop
378
+ back to the delta's scenario ids, and every task declares at least one check:
379
+
380
+ ```prolog
381
+ % tasks.dml — implementation plan for change add_dark_mode
382
+ % Plan format: 2
383
+
384
+ plan_task("1.1", task{
385
+ executor: pi,
386
+ do: "Add a ThemeProvider context exposing theme and setTheme.",
387
+ tools: ["read", "edit"],
388
+ expected: "src/theme/ThemeProvider.tsx exports ThemeProvider and typechecks.",
389
+ satisfies: ["ui/theme#theme-selection"],
390
+ checks: [ exists("src/theme/ThemeProvider.tsx"),
391
+ cmd("npm run typecheck") ]
392
+ }).
393
+
394
+ plan_task("1.2", task{
395
+ executor: pi,
396
+ do: "Add light/dark CSS custom properties and apply them to the document root.",
397
+ tools: ["read", "edit"],
398
+ expected: "Toggling updates the visible theme without a reload.",
399
+ satisfies: ["ui/theme#theme-selection", "ui/system#theme-switching"],
400
+ checks: [ cmd("npx vitest run src/theme") ]
401
+ }).
402
+
403
+ plan_task("1.3", task{
404
+ executor: pi,
405
+ do: "Default to prefers-color-scheme when nothing is stored, without persisting.",
406
+ tools: ["read", "edit"],
407
+ expected: "A first run on a dark system renders dark and writes no stored key.",
408
+ satisfies: ["ui/theme#system-preference"],
409
+ checks: [ cmd("npx vitest run src/theme") ]
410
+ }).
411
+
412
+ plan_task("1.4", task{
413
+ executor: pi,
414
+ do: "Reject a stored value that is neither light nor dark, falling back to the system preference.",
415
+ tools: ["read", "edit"],
416
+ expected: "An invalid stored value renders the system preference and shows no error.",
417
+ satisfies: ["ui/theme#invalid-stored-value"],
418
+ checks: [ cmd("npx vitest run src/theme") ]
419
+ }).
420
+
421
+ plan_task("1.5", task{
422
+ executor: pi,
423
+ do: "Add the theme toggle to the header.",
424
+ tools: ["read", "edit"],
425
+ expected: "The toggle switches themes and persists the choice.",
426
+ satisfies: ["ui/theme#theme-selection"],
427
+ checks: [ exists("src/components/ThemeToggle.tsx"),
428
+ cmd("npm run typecheck") ]
429
+ }).
430
+
431
+ % --- execution state (managed by apply.dml; do not edit by hand) ---
432
+ plan_task_status("1.1", pending).
433
+ plan_task_status("1.2", pending).
434
+ plan_task_status("1.3", pending).
435
+ plan_task_status("1.4", pending).
436
+ plan_task_status("1.5", pending).
437
+ ```
438
+
439
+ `apply.dml` — only wiring and the metadata `/dc-run` preflight reads, so the plan
440
+ file stays clean data:
441
+
442
+ ```prolog
443
+ % apply.dml — executable entry for change add_dark_mode
444
+ % Plan format: 2
445
+ % Change: add_dark_mode
446
+ % Required pi tools: read, edit
447
+ % Contextual: true
448
+
449
+ :- consult('.pi/deepclause/lib/apply.dml').
450
+ :- consult('.pi/deepclause/changes/add_dark_mode/tasks.dml').
451
+
452
+ agent_main :-
453
+ run_plan('.pi/deepclause/changes/add_dark_mode/tasks.dml',
454
+ "add_dark_mode", 3).
455
+ ```
456
+
457
+ `change.json` — the machine manifest the harness reads: schema, drift digests, and
458
+ the snapshot ref (still `null` until apply starts):
459
+
460
+ ```json
461
+ {
462
+ "schema": "spec-driven",
463
+ "slug": "add_dark_mode",
464
+ "created": "2025-09-17",
465
+ "contextMode": "branch",
466
+ "digests": {
467
+ "specs/ui/theme.spec.md": "sha256:9f2c…",
468
+ "specs/ui/system.spec.md": "sha256:41ab…"
469
+ },
470
+ "snapshot": null,
471
+ "skipSpecs": false,
472
+ "retireCapabilities": false
473
+ }
474
+ ```
475
+
476
+ Why four artifacts instead of one: the **delta** is what a reviewer reads and what
477
+ archive merges; `deltas.dml` is that same delta as **facts** so `/dc-check` and
478
+ `spec_status` can reason without re-parsing Markdown; `tasks.dml` is the
479
+ **implementation plan**; and `apply.dml` carries only wiring plus preflight
480
+ metadata.
481
+
482
+ ### 3. Validate — `/dc-check`
483
+
484
+ ```text
485
+ > /dc-check add_dark_mode
486
+
487
+ DeepClause CHECK add_dark_mode (spec_validate.dml) 0 tokens
488
+
489
+ requirements 2 added, 1 modified, 0 removed, 0 renamed
490
+ scenarios 3 ok (every requirement has ≥1)
491
+ coverage 3/3 scenarios referenced by tasks.dml
492
+ checks 5/5 tasks declare verification; 5 commands discoverable
493
+
494
+ 0 errors, 0 warnings, 0 model calls.
495
+ ```
496
+
497
+ **Files.** Nothing is written. `spec_validate.dml` consults `lib/specs.dml`, parses
498
+ the delta and the existing specs, reads `tasks.dml` as facts, and emits a report. If
499
+ an error is reported, fix it with `/dc-plan update add_dark_mode fix the validation
500
+ errors` (a planning turn that edits the Markdown and re-commits the plan) and run
501
+ `/dc-check` again.
502
+
503
+ ### 4. Execute — `/dc-run`
504
+
505
+ ```text
506
+ > /dc-run add_dark_mode
507
+
508
+ ⚠ Run contextual DeepClause plan?
509
+ Change: add_dark_mode (5 tasks, 3 scenarios)
510
+ Delegates bounded steps to pi with these tools: read, edit
511
+ Verification commands (approved once for this run):
512
+ npm run typecheck
513
+ npx vitest run src/theme
514
+ npm run build
515
+ Max attempts per task: 3.
516
+ The working tree is snapshotted first and restored if a task cannot be repaired.
517
+ [Confirm] [Cancel]
518
+
519
+ DeepClause RUNNING changes/add_dark_mode/apply.dml 12.4s
520
+ anthropic/claude-sonnet-4 | context=branch | verbose
521
+ Phase: task 1.3, attempt 1/3: default to prefers-color-scheme
522
+ Usage: 41,207 input / 6,940 output tokens
523
+ Output:
524
+ Task 1.1, attempt 1/3
525
+ Task 1.2, attempt 1/3
526
+ Task 1.2 failed verification: command failed: npx vitest run src/theme
527
+ Task 1.2, attempt 2/3
528
+ Task 1.3, attempt 1/3
529
+ ```
530
+
531
+ **What happens during execution, step by step:**
532
+
533
+ 1. **Preflight.** `/dc-run` resolves the change to `changes/add_dark_mode/apply.dml`,
534
+ reads `% Required pi tools:` and `% Contextual:` from its metadata, verifies each
535
+ tool is installed and active, and collects the verification suite from the
536
+ `checks(...)` of every task in `tasks.dml`. You approve the suite once.
537
+ 2. **Snapshot.** `dc_apply_snapshot` records `HEAD` and `git status`; the ref is
538
+ written to `change.json`. A dirty tree is refused unless `--allow-dirty`.
539
+ 3. **Per task, in order:** delegate the step through `pi_agent_step`, which swaps in
540
+ exactly that step's tools and restores the previous tool set on every exit path;
541
+ then run the task's checks through `dc_verify_run` (restricted to the approved
542
+ commands). On success, write `plan_task_status(Id, done(N))` into the managed block of
543
+ `tasks.dml` and move on.
544
+ 4. **On a failed check:** append the failure evidence to the *next* attempt's
545
+ instruction ("the previous attempt failed verification with: … fix only what is
546
+ needed") and retry, up to the attempt budget. Each attempt rolls memory back, so
547
+ a repair starts clean and only sees the threaded evidence.
548
+ 5. **Change-level gate.** After all tasks, `verify_change/2` asserts that every
549
+ scenario in the delta has at least one passing check. A plan cannot answer
550
+ successfully without this.
551
+ 6. **Accept.** `dc_apply_accept` marks the snapshot accepted; the result is published
552
+ into the pi session with usage and status.
553
+
554
+ ```text
555
+ DeepClause
556
+ Change add_dark_mode applied 42.1s
557
+ 5/5 tasks verified (1 repair), 3/3 scenarios covered
558
+ Snapshot abc1234 accepted
559
+ Usage: 118,442 input / 21,309 output tokens
560
+ Next: /dc-run spec_archive add_dark_mode
561
+ ```
562
+
563
+ `tasks.dml` after the run:
564
+
565
+ ```prolog
566
+ % --- execution state (managed by apply.dml; do not edit by hand) ---
567
+ plan_task_status("1.1", done(1)).
568
+ plan_task_status("1.2", done(2)).
569
+ plan_task_status("1.3", done(1)).
570
+ plan_task_status("1.4", done(1)).
571
+ plan_task_status("1.5", done(1)).
572
+ ```
573
+
574
+ ### 5. Revise — `/dc-plan update`
575
+
576
+ ```text
577
+ > /dc-plan update add_dark_mode make the toggle keyboard-accessible
578
+
579
+ ⚠ Starting a contextual pi planning turn...
580
+ [pi edits specs/ui/theme.spec.md and tasks.dml non-destructively]
581
+ [dc_plan_commit re-validates coverage and writes the updated files]
582
+ ```
583
+
584
+ **Files.** The delta and `tasks.dml` are edited in place; already-verified tasks keep
585
+ their `done(N)` status, newly added tasks start `pending`. `change.json` digests are
586
+ refreshed.
587
+
588
+ ### 6. Land — `/dc-run spec_archive`
589
+
590
+ ```text
591
+ > /dc-run spec_archive add_dark_mode
592
+
593
+ ⚠ Archive will modify .pi/deepclause/specs/ui/theme.spec.md
594
+ + ADDED Theme selection
595
+ + ADDED System-preference default
596
+ ~ MODIFIED Theme switching (2 lines changed)
597
+ Digest check: specs match the delta (sha256:9f2c…)
598
+ [Confirm] [Cancel]
599
+
600
+ DeepClause
601
+ Archived changes/add_dark_mode → changes/archive/2025-09-17-add_dark_mode
602
+ specs/ui/theme.spec.md updated (+2, ~1)
603
+ index.dml regenerated; delta_status set to archived
604
+ ```
605
+
606
+ **What happens.** `spec_merge.dml` re-parses the existing spec and the delta,
607
+ re-checks the drift digest, applies RENAMED → REMOVED → MODIFIED → ADDED, validates
608
+ the merged spec, and only then writes `specs/`. The change folder moves to
609
+ `changes/archive/<date>-<slug>`, `delta_status` becomes `archived`, and the derived
610
+ `index.dml` is regenerated.
611
+
612
+ **After the archive**, `specs/` holds the capability as the new source of truth:
613
+
614
+ ```markdown
615
+ ---
616
+ capability: ui/theme
617
+ ---
618
+
619
+ # Theme Specification
620
+
621
+ ## Purpose
622
+ Lets users choose between light and dark themes, defaulting to the operating
623
+ system preference.
624
+
625
+ ## Requirements
626
+
627
+ ### Requirement: Theme selection
628
+ The app SHALL let users switch between light and dark themes at runtime.
629
+
630
+ #### Scenario: User toggles dark mode
631
+ - **WHEN** the user clicks the theme toggle
632
+ - **THEN** the app switches to dark mode and persists the choice
633
+
634
+ #### Scenario: Invalid stored value is rejected
635
+ - **WHEN** a stored theme value is neither "light" nor "dark"
636
+ - **THEN** the app falls back to the system preference and shows no error
637
+
638
+ ### Requirement: System-preference default
639
+ The app SHALL default to the operating system colour-scheme preference when no
640
+ choice has been stored.
641
+
642
+ #### Scenario: First run on a dark-preference system
643
+ - **WHEN** the app starts with no stored theme and the OS reports dark
644
+ - **THEN** it renders dark without writing a stored choice
645
+ ```
646
+
647
+ and the regenerated `index.dml` looks like this:
648
+
649
+ ```prolog
650
+ % index.dml — DERIVED. Do not edit. Regenerate with /dc-run spec_reindex.
651
+ capability("ui/theme", "Theme Specification",
652
+ purpose("Lets users choose between light and dark themes, defaulting to the operating system preference."),
653
+ source("specs/ui/theme.spec.md", "sha256:6d10…")).
654
+ requirement("ui/theme", "theme-selection", "Theme selection").
655
+ requirement("ui/theme", "system-preference", "System-preference default").
656
+ scenario("ui/theme", "user-toggles-dark-mode", "User toggles dark mode", "theme-selection").
657
+ scenario("ui/theme", "invalid-stored-value", "Invalid stored value is rejected", "theme-selection").
658
+ scenario("ui/theme", "first-run-dark-system", "First run on a dark-preference system", "system-preference").
659
+ touch("add_dark_mode", "ui/theme").
660
+ touch("add_dark_mode", "ui/system").
661
+ ```
662
+
663
+ ### 7. Query — `/dc-run spec_status` and `spec_query`
664
+
665
+ ```text
666
+ > /dc-run spec_status
667
+
668
+ DeepClause SPECS (spec_status.dml) 0 tokens
669
+ capabilities 2 specs/ui/theme.spec.md, specs/ui/system.spec.md
670
+ in flight 1 add_dark_mode 5/5 tasks, 3/3 scenarios
671
+ archived 3
672
+ ```
673
+
674
+ ```text
675
+ > /dc-run spec_query ui/theme
676
+
677
+ capability ui/theme — Theme Specification
678
+ requirements
679
+ theme-selection 2 scenarios verified
680
+ system-preference 1 scenario verified
681
+ touched by
682
+ add_dark_mode archived 2025-09-17
683
+ ```
684
+
685
+ **Files.** Both are read-only and derived-on-demand: they parse the specs (or read
686
+ `index.dml` when it is present and its digests match) and write nothing.
687
+
688
+ ### When things go wrong
689
+
690
+ ```text
691
+ > /dc-run add_dark_mode
692
+ ...
693
+ Phase: task 1.4, attempt 3/3: add the theme toggle
694
+ Task 1.4 failed verification: command failed: npx vitest run src/theme
695
+
696
+ ⚠ Change add_dark_mode could not be verified after 3 attempts.
697
+ Restoring the working tree to snapshot abc1234…
698
+
699
+ DeepClause execution failed: step_exhausted(1.4, 3)
700
+ Working tree restored to abc1234. tasks.dml reset to pending.
701
+ Re-run /dc-check add_dark_mode for details, then /dc-plan update add_dark_mode.
702
+ ```
703
+
704
+ **What happens.** `attempt/7` exhausts the budget, writes `failed(3, Evidence)`, and
705
+ throws. The DML `catch` calls `dc_apply_restore`; the harness `finally` performs the
706
+ same restore on abort or crash, so `/dc-cancel` and a killed session are covered
707
+ too. Because progress lives in a tracked file, the restore reverts it along with the
708
+ code — the change genuinely is not done.
709
+
710
+ ### Cancel
711
+
712
+ ```text
713
+ > /dc-cancel
714
+
715
+ ⚠ Cancelling DeepClause execution
716
+ ```
717
+
718
+ Aborts the active controller: a running `pi_agent_step` is aborted, the tool set is
719
+ restored, and the harness `finally` restores the snapshot. `tasks.dml` returns to
720
+ its last persisted state.
721
+
722
+ ### Brownfield onboarding
723
+
724
+ ```text
725
+ > /dc-plan onboard the checkout flow
726
+
727
+ [pi reads src/checkout and drafts specs/checkout/checkout.spec.md,
728
+ marked status: draft and inferred: true; no change folder, no execution]
729
+ ```
730
+
731
+ **Files.** Writes a draft capability spec directly under `specs/` (not a delta),
732
+ because there is no change to apply — the behaviour already exists. You review and
733
+ hand-edit it; once it looks right, remove the draft marker and it becomes source of
734
+ truth. Onboarding never runs `dc_plan_commit` and never modifies code.
735
+
736
+ ## What was borrowed from OpenSpec
737
+
738
+ | Borrowed | Not borrowed |
739
+ |---|---|
740
+ | `specs/` (current behavior) vs `changes/` (proposed deltas) | the npm CLI / binary |
741
+ | `### Requirement:` + `#### Scenario:` grammar | the 30+ tool integrations |
742
+ | Delta ops: ADDED / MODIFIED / REMOVED / RENAMED | Markdown as the only interface |
743
+ | "Every task states how to verify completion" | the imperative `specs-apply.ts` implementation |
744
+ | Artifact graph with `requires:` + instructions | `tasks.md` (replaced by `tasks.dml`) |
745
+
746
+ OpenSpec's deterministic spec logic is ~4,400 lines of TypeScript
747
+ (`src/core/specs-apply.ts` alone is 1,378). It encodes ordering
748
+ (RENAMED → REMOVED → MODIFIED → ADDED) and a pile of conflict rules imperatively,
749
+ and its own instructions warn about silent failures such as *"Scenarios MUST use
750
+ exactly 4 hashtags; using 3 fails silently."* That is precisely the kind of logic
751
+ DML is good at, and the kind of failure a grammar prevents.
752
+
753
+ ## User-facing surface
754
+
755
+ ### Commands
756
+
757
+ | OpenSpec | DeepClause surface | Who does the work |
758
+ |---|---|---|
759
+ | `/opsx:explore` | just talk to pi (no command, no transaction) | pi turn |
760
+ | `/opsx:new`, `/opsx:propose` | `/dc-plan <request>` | pi turn + `dc_plan_commit` |
761
+ | `/opsx:continue`, `/opsx:update` | `/dc-plan update <change> <what>` | pi turn |
762
+ | `/opsx:apply` | `/dc-apply <change>` | `lib/apply.dml` + `pi_agent_step` + `dc_verify_run` |
763
+ | `/opsx:verify` | `/dc-check <change>` | **pure DML, zero model calls** |
764
+ | `/opsx:archive`, `/opsx:bulk-archive` | `/dc-archive <change>` (preview + confirm + merge + move) | **pure DML merge + one confirm** |
765
+ | `/opsx:sync` | `/dc-run spec_sync <change>` | pure DML |
766
+ | `/opsx:onboard` | nothing — pi reads the repo natively | pi turn |
767
+
768
+ `/dc-check` graduates from "optional later" in `AGENTS.md` to required. No other
769
+ commands are added; `/dc`, `/dc-list`, `/dc-tool`, `/dc-cancel` keep their current
770
+ meaning.
771
+
772
+ ### Walkthrough
773
+
774
+ ```text
775
+ > /dc-plan add dark mode with system-preference detection
776
+
777
+ ⚠ Starting a contextual pi planning turn. Review the generated plan before it is written.
778
+
779
+ [pi explores src/theme.ts, package.json, specs/ui/system.spec.md, changes/]
780
+
781
+ DeepClause change add_dark_mode
782
+ Created .pi/deepclause/changes/add_dark_mode/
783
+ proposal.md why / what / impact
784
+ specs/ui/theme.spec.md +2 requirements, +3 scenarios (behavior only)
785
+ design.md 3 decisions, 2 risks
786
+ tasks.dml 5 tasks, 5 checks, 3 scenarios covered
787
+ apply.dml executable entry (3 attempts, 2 pi tools)
788
+ Capabilities: new `ui/theme`, modified `ui/system`
789
+ Check: /dc-check add_dark_mode Apply: /dc-run add_dark_mode
790
+ ```
791
+
792
+ ```text
793
+ > /dc-check add_dark_mode
794
+
795
+ DeepClause CHECK add_dark_mode (spec_validate.dml) 0 tokens
796
+
797
+ requirements 4 added, 1 modified, 0 removed, 1 renamed
798
+ scenarios 9 ok (every requirement has ≥1)
799
+ coverage 9/9 scenarios referenced by tasks.dml
800
+ checks 5/5 tasks declare verification; 5 commands discoverable
801
+
802
+ ERROR specs/ui/theme.spec.md:71 `### Scenario:` uses 3 hashes; must be `####`.
803
+ ERROR archive would fail: MODIFIED "Theme selection" not found
804
+ (closest: "Theme switching").
805
+ ERROR tasks.dml: task "1.4" satisfies unknown scenario
806
+ ui/theme#no-such-scenario
807
+ ```
808
+
809
+ ```text
810
+ > /dc-run add_dark_mode
811
+
812
+ ⚠ Run contextual DeepClause plan?
813
+ Delegates bounded steps to pi with these tools: read, edit
814
+ Verification commands (approved once for this run):
815
+ npm run typecheck
816
+ npx vitest run src/theme
817
+ npm run build
818
+ Max attempts per task: 3. Restores the working tree on failure.
819
+ [Confirm]
820
+
821
+ DeepClause RUNNING changes/add_dark_mode/apply.dml 4.2s
822
+ provider/model | context=branch | verbose
823
+ Phase: task 1.2, attempt 2/3: add CSS custom properties
824
+ Usage: 31,204 input / 5,881 output tokens
825
+ ```
826
+
827
+ ```text
828
+ > /dc-run spec_archive add_dark_mode
829
+
830
+ ⚠ Archive will modify .pi/deepclause/specs/ui/theme.spec.md
831
+ + ADDED Theme selection
832
+ + ADDED System-preference default
833
+ ~ MODIFIED Theme switching (2 lines changed)
834
+ [Confirm]
835
+
836
+ Archived → changes/archive/2025-09-17-add_dark_mode/
837
+ specs/ui/theme.spec.md updated (+2 requirements, 1 modified)
838
+ ```
839
+
840
+ ## Workspace layout
841
+
842
+ ```text
843
+ .pi/deepclause/
844
+ ├── specs/ # source of truth (current behavior, pure Markdown)
845
+ │ └── ui/theme.spec.md
846
+ ├── changes/ # in-flight work
847
+ │ ├── add_dark_mode/
848
+ │ │ ├── change.json # schema, digests, snapshot ref
849
+ │ │ ├── proposal.md # why / what / impact
850
+ │ │ ├── specs/ui/theme.spec.md # delta: behavior only
851
+ │ │ ├── design.md # approach, decisions (optional)
852
+ │ │ ├── tasks.dml # implementation plan + execution state
853
+ │ │ ├── deltas.dml # this change's delta ops + lifecycle status
854
+ │ │ └── apply.dml # executable entry + preflight metadata
855
+ │ └── archive/2025-09-17-add_dark_mode/
856
+ ├── index.dml # OPTIONAL derived inventory (disposable cache)
857
+ ├── lib/
858
+ │ ├── specs.dml # spec/delta grammar, validators, merge
859
+ │ └── apply.dml # plan driver: verify, retry, rollback
860
+ ├── skills/
861
+ │ ├── spec_validate.dml
862
+ │ ├── spec_merge.dml
863
+ │ ├── spec_archive.dml
864
+ │ ├── spec_coverage.dml
865
+ │ ├── spec_status.dml
866
+ │ ├── spec_query.dml
867
+ │ ├── spec_reindex.dml # planned
868
+ │ ├── spec_graph.dml
869
+ │ ├── spec_sync.dml # planned
870
+ │ ├── spec_scaffold.dml
871
+ │ └── spec_apply.dml
872
+ ├── plans/ # standalone plans not tied to a change
873
+ ├── diagrams/
874
+ ├── AGENTS.md
875
+ ├── DML_REFERENCE.md
876
+ ├── SPEC_AUTHORING.md # spec-writing guide (new)
877
+ └── config.json
878
+ ```
879
+
880
+ > **Scope note.** The current `AGENTS.md` contract lists only `config.json`,
881
+ > `AGENTS.md`, `DML_REFERENCE.md`, `skills/`, and `plans/`. Adding `specs/`,
882
+ > `changes/`, `lib/`, and a derived `index.dml` is a deliberate extension and
883
+ > needs an explicit decision.
884
+ > All new files follow the existing non-destructive initialization rule
885
+ > (`writeIfMissing`): user files are never overwritten.
886
+
887
+ ## Spec format: behavior only
888
+
889
+ Specs are plain, OpenSpec-compatible Markdown. A capability spec:
890
+
891
+ ````markdown
892
+ ---
893
+ capability: ui/theme
894
+ owners: [frontend]
895
+ related: [ui/system]
896
+ ---
897
+
898
+ # Theme Specification
899
+
900
+ ## Purpose
901
+ Lets users choose between light and dark themes, defaulting to the operating
902
+ system preference.
903
+
904
+ ## Requirements
905
+
906
+ ### Requirement: Theme selection
907
+ The app SHALL let users switch between light and dark themes at runtime.
908
+
909
+ #### Scenario: User toggles dark mode
910
+ - **WHEN** the user clicks the theme toggle
911
+ - **THEN** the app switches to dark mode and persists the choice
912
+
913
+ #### Scenario: Invalid stored value is rejected
914
+ - **WHEN** a stored theme value is neither "light" nor "dark"
915
+ - **THEN** the app falls back to the system preference and shows no error
916
+
917
+ ### Requirement: System-preference default
918
+ The app SHALL default to the operating system colour-scheme preference when no
919
+ choice has been stored.
920
+
921
+ #### Scenario: First run on a dark-preference system
922
+ - **WHEN** the app starts with no stored theme and the OS reports dark
923
+ - **THEN** it renders dark without writing a stored choice
924
+ ````
925
+
926
+ A change delta uses `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`. `MODIFIED`
927
+ carries the full replacement requirement, `REMOVED` carries `**Reason**` and
928
+ `**Migration**`, `RENAMED` uses `FROM:`/`TO:`.
929
+
930
+ ### What does not belong in a spec
931
+
932
+ No commands, no file paths, no test runners, no library choices, no task lists,
933
+ and no `prolog` blocks. The test is OpenSpec's own: *if the implementation can
934
+ change without changing externally visible behavior, it does not belong here.*
935
+
936
+ This was a correction to an earlier draft of this document, which attached
937
+ `check(cmd("npx vitest run src/theme"))` to requirements. That leaks
938
+ implementation into the behavior contract and belongs in `tasks.dml`.
939
+
940
+ ### Scenario ids are the join key
941
+
942
+ Each scenario gets a stable id derived from its heading:
943
+ `capability#scenario-slug`, e.g. `ui/theme#user-toggles-dark-mode`. Nothing else
944
+ in the spec needs machine-readable content. The id is what tasks reference, and
945
+ what coverage and conformance are computed over.
946
+
947
+ ### Delta integrity
948
+
949
+ The delta records no digests itself. Drift digests live in `change.json`, because
950
+ a self-digest inside a hand-editable file goes stale the moment anyone edits it.
951
+
952
+ ## The engine: DCG parsing
953
+
954
+ DML reasons over terms; specs are text. A DCG is the bridge, and it parses only
955
+ the **syntax envelope** — never the prose.
956
+
957
+ Parses (`lib/specs.dml`):
958
+
959
+ - spec files: frontmatter, `# Title`, `## Purpose`, `### Requirement: <name>`,
960
+ `#### Scenario: <name>`, GIVEN/WHEN/THEN bullets, RFC 2119 keywords
961
+ - delta files: section headers, `FROM:`/`TO:`, `**Reason**`/`**Migration**`
962
+ - fenced code blocks are swallowed so heading-like text inside them never matches
963
+
964
+ `change.json` is JSON. `tasks.dml` is DML facts and is not parsed by the grammar
965
+ at all — it is consulted.
966
+
967
+ Produces a tree of terms:
968
+
969
+ ```prolog
970
+ spec(
971
+ purpose("Theme and layout behaviour for the application."),
972
+ [ requirement("Theme selection",
973
+ "The app SHALL let users switch between light and dark themes.",
974
+ [ scenario("user-toggles-dark-mode", "User toggles dark mode",
975
+ [when("the user clicks the theme toggle"),
976
+ then("the app switches to dark mode and persists the choice")]) ])
977
+ ]).
978
+ ```
979
+
980
+ Rules are ordinary DCG clauses over a line list, e.g.:
981
+
982
+ ```prolog
983
+ requirement(req(Name, Text, Scenarios)) -->
984
+ heading(3, Line),
985
+ { parse_requirement_header(Line, Name) },
986
+ body(Text),
987
+ scenarios(Scenarios).
988
+
989
+ scenario(scenario(Id, Name, Steps)) -->
990
+ heading(4, Line), % 4 hashes: 3 simply does not match
991
+ { parse_scenario_header(Line, Name, Id) },
992
+ steps(Steps).
993
+ ```
994
+
995
+ Why a grammar rather than regex:
996
+
997
+ | Situation | Regex | DCG |
998
+ |---|---|---|
999
+ | Wrong hashtag count | counts, often silently accepts | rule does not match → hard error with position |
1000
+ | `### Requirement:` inside a fence | false positive | consumed as opaque text |
1001
+ | Requirement with no scenario | post-hoc cross-check | scenarios are a child non-terminal |
1002
+ | Heading inside `## Notes` | matches out of context | unreachable from the spec state |
1003
+ | MODIFIED names an absent requirement | manual lookup | unification against the parsed spec |
1004
+
1005
+ Because DML is Prolog, **parse failure is validation failure**: the caller learns
1006
+ which non-terminal failed and where.
1007
+
1008
+ ### The parse tree is ephemeral
1009
+
1010
+ The tree exists only for one `/dc-run` execution and is then discarded. Markdown
1011
+ is the database. Re-parsing is cheap and lossless, so caching would only introduce
1012
+ drift. Only four things persist: merged `specs/**`, the moved `archive/` folder,
1013
+ `tasks.dml` status updates, and the pi session's final answer.
1014
+
1015
+ If a run needs to reason repeatedly over the parse, it may `assertz` facts for the
1016
+ duration — session-scoped, discarded at run end.
1017
+
1018
+ ### Lossless merge
1019
+
1020
+ `spec_merge` parses **both** the existing spec and the delta, rewrites, and
1021
+ re-renders. Because the grammar retains each block's raw text, unchanged
1022
+ requirements round-trip byte-for-byte and only edited blocks change. Apply order is
1023
+ RENAMED → REMOVED → MODIFIED → ADDED. The merge emits a `Trace` that is what the
1024
+ confirm dialog shows before any write.
1025
+
1026
+ ## Verification
1027
+
1028
+ Verification splits in two, and conflating them was the earlier draft's mistake.
1029
+
1030
+ | Kind | Question | Lives in | Nature |
1031
+ |---|---|---|---|
1032
+ | **Behavioral** | does the system do what the scenario says? | the spec, *as the scenario* | implementation-neutral acceptance |
1033
+ | **Technical** | does it typecheck, build, pass tests, exist? | `tasks.dml` checks | implementation detail |
1034
+
1035
+ The spec's only verification artifact is the scenario. The concrete check that
1036
+ proves a scenario is change-scoped and lives on the task.
1037
+
1038
+ ### Checks are data terms, not clauses
1039
+
1040
+ A check is a **declarative term** dispatched by a fixed interpreter — never an
1041
+ asserted clause and never `call/1`:
1042
+
1043
+ ```prolog
1044
+ checks([ exists("src/theme/ThemeProvider.tsx"),
1045
+ cmd("npm run typecheck"),
1046
+ cmd("npx vitest run src/theme", retry(2)),
1047
+ model("does the toggle persist across reload?") ])
1048
+ ```
1049
+
1050
+ ```prolog
1051
+ run_check(exists(P), ok) :- exists_file(P), !.
1052
+ run_check(exists(P), missing(P)).
1053
+
1054
+ run_check(cmd(C), Result) :-
1055
+ exec(dc_verify_run(command: C), Dict),
1056
+ get_dict(exitCode, Dict, Code),
1057
+ ( Code =:= 0 -> Result = ok ; Result = failed_command(C, Code) ).
1058
+
1059
+ run_check(model(Q), Result) :- ... pi_agent_step, PASS/FAIL ...
1060
+ ```
1061
+
1062
+ Why data and not code:
1063
+
1064
+ - `task/N` and `prompt/N` are LLM calls: non-deterministic, token-costing, and able
1065
+ to claim success without checking. **Never use them as gates.** They remain the
1066
+ right tool for prose and synthesis.
1067
+ - Asserting arbitrary clauses from a spec or task file turns a review artifact into
1068
+ executable code, a supply-chain risk if specs are shared. Terms-of-known-shape
1069
+ have no such surface: the parser can only produce `exists/1`, `cmd/1`, `cmd/2`,
1070
+ `model/1`.
1071
+ - A deterministic predicate does **not** need a runtime tool. DML supports
1072
+ `:- consult('lib/specs.dml').`, so validators are shared Prolog predicates.
1073
+
1074
+ ### Where checks come from
1075
+
1076
+ Pi grounds each task's checks in the repository during the planning turn — real
1077
+ `package.json` scripts, real paths, real test targets. The plan validator enforces:
1078
+
1079
+ 1. **Every task declares at least one check.**
1080
+ 2. **`cmd(...)` entries must be discoverable** — an npm script that exists in
1081
+ `package.json`, a test path that exists, a CI command. An invented
1082
+ `cmd("npm test:theme")` is worse than no check: the repair loop chases a
1083
+ phantom. Non-discoverable checks are demoted to `model(...)` with a warning.
1084
+ 3. **`model(...)` checks are labelled** in output, so a green apply never reports
1085
+ unqualified success when a human-judgment check was involved.
1086
+
1087
+ ### Coverage, at two levels
1088
+
1089
+ Scenario ids are the join key.
1090
+
1091
+ - **Plan-time (deterministic):** every scenario in the delta appears in at least
1092
+ one task's `satisfies`. A change that drops a scenario cannot commit a plan.
1093
+ - **Apply-time:** each task's checks run; the driver records a
1094
+ `scenario → check → result` trace. The change-level gate `verify_change/2`
1095
+ asserts every scenario in the delta has at least one passing check. So
1096
+ end-to-end traceability exists without duplicating test invocations in the spec.
1097
+
1098
+ An optional phase-2 refinement is test annotations
1099
+ (`@dc ui/theme#user-toggles-dark-mode` in the test file) so the suite discovers the
1100
+ scenario→test mapping instead of the plan declaring it.
1101
+
1102
+ ### Approving checks once, not per invocation
1103
+
1104
+ `pi_bash` prompts per command. A retry loop with 3 checks × 3 attempts × 6 tasks
1105
+ would be a wall of dialogs, and the unattended rollback path cannot prompt at all.
1106
+ So verification commands are approved **as a suite** at `/dc-run` confirm time,
1107
+ after which a scoped `dc_verify_run(command)` tool is available for that run only,
1108
+ restricted to the declared commands (exact match, workspace cwd, timeout, returns
1109
+ exit code and stdout).
1110
+
1111
+ This is the same justification as `dc_apply_snapshot`/`dc_apply_restore`: it needs
1112
+ the host shell and must run unattended. There is no pure-Prolog substitute.
1113
+
1114
+ ## The change artifacts
1115
+
1116
+ ### `tasks.dml` — implementation plan and execution state
1117
+
1118
+ Data only. Definitions in `plan_task/2`, state in `plan_task_status/2`, joined by task id.
1119
+ The fact functor is `plan_task/2`, **not** `task/2`: `task/2` collides with DML's
1120
+ built-in `task/N` predicate, and a term read from the file then refuses to unify
1121
+ with `task(Id, Props)`. This was found while implementing phase 3.
1122
+
1123
+ ```prolog
1124
+ % tasks.dml — implementation plan for change add_dark_mode
1125
+
1126
+ plan_task("1.1", task{
1127
+ executor: pi,
1128
+ do: "Add a ThemeProvider context exposing theme and setTheme.",
1129
+ tools: ["read", "edit"],
1130
+ expected: "src/theme/ThemeProvider.tsx exports ThemeProvider and typechecks.",
1131
+ satisfies: ["ui/theme#user-toggles-dark-mode"],
1132
+ checks: [ exists("src/theme/ThemeProvider.tsx"),
1133
+ cmd("npm run typecheck") ]
1134
+ }).
1135
+
1136
+ plan_task("1.2", task{
1137
+ executor: pi,
1138
+ do: "Add light/dark CSS custom properties applied to the document root.",
1139
+ tools: ["read", "edit"],
1140
+ expected: "Toggling updates the visible theme without a reload.",
1141
+ satisfies: ["ui/theme#user-toggles-dark-mode"],
1142
+ checks: [ cmd("npx vitest run src/theme") ]
1143
+ }).
1144
+
1145
+ % --- execution state (managed by apply.dml; do not edit by hand) ---
1146
+ plan_task_status("1.1", pending).
1147
+ plan_task_status("1.2", pending).
1148
+ ```
1149
+
1150
+ Status is a plain compound term, chosen over a dict so it is trivial to match,
1151
+ write, and round-trip:
1152
+
1153
+ ```prolog
1154
+ plan_task_status("1.5", pending).
1155
+ plan_task_status("1.1", done(1)). % verified on attempt 1
1156
+ plan_task_status("1.2", failed(3, "vitest: 1 failing (ThemeProvider.test.tsx:42)")).
1157
+ plan_task_status("1.3", skipped("subsumed by 1.1")).
1158
+ ```
1159
+
1160
+ Rules:
1161
+
1162
+ - `done` is written **only after `verify/3` returns `ok`** — it means verified, not
1163
+ attempted.
1164
+ - `failed` and `skipped` are recorded rather than dropped, so the change-level gate
1165
+ can explain itself and a re-run is auditable.
1166
+ - Progress is a query, not a separate file:
1167
+
1168
+ ```prolog
1169
+ remaining(Id) :- plan_task(Id, _), \+ plan_task_status(Id, done(_)).
1170
+ all_done :- forall(plan_task(Id, _), plan_task_status(Id, done(_))).
1171
+ ```
1172
+
1173
+ OpenSpec's "all tasks complete" archive check becomes `all_done`.
1174
+
1175
+ ### `apply.dml` — executable entry and preflight metadata
1176
+
1177
+ Small, generated, stable per change:
1178
+
1179
+ ```prolog
1180
+ % apply.dml — executable entry for change add_dark_mode
1181
+ % Plan format: 2
1182
+ % Change: add_dark_mode
1183
+ % Required pi tools: read, edit
1184
+ % Contextual: true
1185
+
1186
+ :- consult('.pi/deepclause/lib/apply.dml').
1187
+ :- consult('.pi/deepclause/changes/add_dark_mode/tasks.dml').
1188
+
1189
+ agent_main :-
1190
+ run_plan('.pi/deepclause/changes/add_dark_mode/tasks.dml',
1191
+ "add_dark_mode", 3).
1192
+ ```
1193
+
1194
+ Why both files rather than one:
1195
+
1196
+ - `tasks.dml` is **content** — readable, diffable, reviewable, greppable, and only
1197
+ changes status as work proceeds.
1198
+ - `apply.dml` is **wiring and metadata** — which driver, which slug, attempt
1199
+ budget, and the fields `/dc-run` preflight needs.
1200
+
1201
+ ### How `apply.dml` reads and writes `tasks.dml`
1202
+
1203
+ Read: `consult(tasks.dml)` at start, which loads definitions and current status.
1204
+
1205
+ Write: a **managed marker block**, so the driver never touches the definitions:
1206
+
1207
+ ```prolog
1208
+ % in lib/apply.dml
1209
+ record_status(TasksPath, Id, Status) :-
1210
+ read_file_to_string(TasksPath, Text, []),
1211
+ split_managed_block(Text, Head, _OldBlock), % split at the marker comment
1212
+ findall(Id-S, ( plan_task(Id0, _), plan_task_status(Id0, S), Id = Id0 ), Statuses),
1213
+ render_status_block(Statuses, Block),
1214
+ atomic_write(TasksPath, Head, Block). % temp file + rename
1215
+ ```
1216
+
1217
+ Properties this buys:
1218
+
1219
+ - **Definitions and comments are never rewritten.** Only the status block is
1220
+ regenerated, so the diff per task is one line and hand-written comments above the
1221
+ marker survive.
1222
+ - **Crash-safe resume.** Write after each *verified* task, so an interrupted run
1223
+ leaves the block reflecting the last completed task, and `remaining/1` resumes.
1224
+ - **No fragile round-tripping.** The driver never parses and re-renders DML dicts;
1225
+ it only serializes `plan_task_status/2` facts, which is trivial.
1226
+ - **Atomicity.** Temp file plus `rename_file/2`, so a crash mid-write cannot leave
1227
+ a truncated plan.
1228
+
1229
+ Progress is versioned in git. Diffs are readable. A failed apply that triggers
1230
+ `git reset --hard <snapshot>` reverts progress along with code — which is correct,
1231
+ because the tasks genuinely are not done.
1232
+
1233
+ ## Feature and delta index
1234
+
1235
+ Tasks are authored facts (`tasks.dml`); features and deltas are **derived** from
1236
+ canonical Markdown. The distinction matters: a derived fact set must be disposable,
1237
+ digest-stamped, and never hand-edited, or it becomes a second source of truth. The
1238
+ one genuinely *authored* piece is delta lifecycle status, which Markdown does not
1239
+ capture.
1240
+
1241
+ ### Per-change delta record — `changes/<slug>/deltas.dml`
1242
+
1243
+ ```prolog
1244
+ % deltas.dml — spec deltas for change add_dark_mode
1245
+ % Source digests:
1246
+ % specs/ui/theme.spec.md sha256:9f2c…
1247
+ % specs/ui/system.spec.md sha256:41ab…
1248
+
1249
+ delta("add_dark_mode", added, "ui/theme", req("theme-selection", "Theme selection")).
1250
+ delta("add_dark_mode", modified, "ui/system", req("theme-switching", "Theme switching")).
1251
+ delta("add_dark_mode", removed, "ui/system", req("legacy-theme"),
1252
+ reason("Replaced by theme-selection"),
1253
+ migration("Use ui/theme#theme-selection")).
1254
+ delta("add_dark_mode", renamed, "ui/system",
1255
+ from("theme-switch"), to("theme-switching")).
1256
+
1257
+ change_meta("add_dark_mode", schema(spec_driven), skip_specs(false),
1258
+ snapshot("abc1234"), created("2025-09-17")).
1259
+
1260
+ % --- lifecycle state (managed by apply / spec_archive) ---
1261
+ delta_status("add_dark_mode", "ui/theme", "theme-selection", verified("2025-09-17")).
1262
+ delta_status("add_dark_mode", "ui/system", "theme-switching", applied("2025-09-17")).
1263
+ ```
1264
+
1265
+ Requirements get stable slugs (`theme-selection`) just like scenarios, so identity
1266
+ survives a `RENAMED`.
1267
+
1268
+ ### Workspace inventory — `index.dml` (optional, derived)
1269
+
1270
+ ```prolog
1271
+ % index.dml — DERIVED. Do not edit. Regenerate with /dc-run spec_reindex.
1272
+ capability("ui/theme", "Theme Specification", purpose("Lets users choose …"),
1273
+ source("specs/ui/theme.spec.md", "sha256:9f2c…")).
1274
+ requirement("ui/theme", "theme-selection", "Theme selection").
1275
+ scenario("ui/theme", "user-toggles-dark-mode", "User toggles dark mode", "theme-selection").
1276
+ touch("add_dark_mode", "ui/theme").
1277
+ touch("add_dark_mode", "ui/system").
1278
+ ```
1279
+
1280
+ ### What it makes queryable
1281
+
1282
+ ```prolog
1283
+ in_flight(Ch) :- change_meta(Ch, _, _, _, _), \+ archived(Ch).
1284
+ touches(Ch, Cap) :- delta(Ch, _, Cap, _).
1285
+
1286
+ % conflict: two in-flight changes modify the same requirement
1287
+ conflict(Cap, Req, C1, C2) :-
1288
+ in_flight(C1), in_flight(C2), C1 \== C2,
1289
+ delta(C1, modified, Cap, req(Req, _)),
1290
+ delta(C2, modified, Cap, req(Req, _)).
1291
+
1292
+ % coverage hole in a change
1293
+ uncovered(Ch, Cap, S) :-
1294
+ delta(Ch, added, Cap, _), scenario(Cap, S, _, _),
1295
+ \+ task_satisfies(Ch, Cap, S).
1296
+
1297
+ % spec hygiene
1298
+ orphan_requirement(Cap, R) :- requirement(Cap, R, _), \+ scenario(Cap, _, _, R).
1299
+ barren_capability(Cap) :- capability(Cap, _, _, _), \+ requirement(Cap, _, _).
1300
+ ```
1301
+
1302
+ The payoff is a traceability matrix — change → requirement → scenario → task →
1303
+ check → status — which answers "which check proved which scenario, when, and with
1304
+ what result" without a sidecar evidence file.
1305
+
1306
+ ### Rules
1307
+
1308
+ 1. Markdown stays canonical for behavior. The definition half of `deltas.dml` and
1309
+ all of `index.dml` are derived; `delta_status/2` and `change_meta/1` are authored,
1310
+ like `plan_task_status/2`.
1311
+ 2. Every derived fact carries its source digest. Mismatch means stale.
1312
+ 3. Stale means **regenerate**, never patch. Query skills refuse or auto-reindex on
1313
+ mismatch.
1314
+ 4. The index is never hand-edited, and should probably not be committed — it churns
1315
+ on every spec edit.
1316
+ 5. **Default to deriving on demand.** For a handful of capabilities the query skill
1317
+ parses and asserts in-run and writes nothing. Persist `index.dml` only when
1318
+ parsing cost or cross-session/cross-repo queries justify it. This is the same
1319
+ principle as the ephemeral parse tree; the file is an optimization, not a source.
1320
+
1321
+ ### Exposure
1322
+
1323
+ - User: `/dc-run spec_status`, `/dc-run spec_query <capability>` — deterministic,
1324
+ 0 tokens, tabular output.
1325
+ - Model: an optional **read-only `dc_spec_query` tool** so pi can ask "which changes
1326
+ touch `ui/system`?" mid-turn. This is not a contradiction of the rejected
1327
+ `dc_spec_verify`: that was a **gate** (a correctness decision, which must not be
1328
+ LLM-invocable), whereas read-only introspection is exactly what the model should
1329
+ be able to call, like `pi_workspace_list`. Either way the query language is a
1330
+ **fixed allowlist of predicates**, never arbitrary `call/1`.
1331
+
1332
+ ## Capability and change graphs
1333
+
1334
+ The index already *is* the graph — capabilities, requirements, scenarios, deltas,
1335
+ tasks, checks and statuses. Rendering it is a view over those facts, and
1336
+ `deepclause-pi` already has the renderer: `dc_diagram` turns a `.dml` file into a
1337
+ presentation- or specification-grade Mermaid diagram, writes an offline viewer
1338
+ under `.pi/deepclause/diagrams/`, and opens it (`src/diagram/*`). Spec graphs reuse
1339
+ that viewer; only the source changes from "one DML program" to "the spec facts".
1340
+
1341
+ ### Views
1342
+
1343
+ Every view is generated from derived facts, so it is deterministic and costs
1344
+ 0 tokens.
1345
+
1346
+ **Capabilities** — the inventory tree:
1347
+
1348
+ ```mermaid
1349
+ flowchart LR
1350
+ CapTheme["ui/theme — Theme Specification"]
1351
+ CapTheme --> ReqSel["Requirement: Theme selection"]
1352
+ CapTheme --> ReqDef["Requirement: System-preference default"]
1353
+ ReqSel --> SceToggle["Scenario: User toggles dark mode"]
1354
+ ReqSel --> SceInvalid["Scenario: Invalid stored value is rejected"]
1355
+ ReqDef --> SceFirstRun["Scenario: First run on a dark-preference system"]
1356
+ ```
1357
+
1358
+ **Changes** — what each change touches, edges labelled by delta op:
1359
+
1360
+ ```mermaid
1361
+ flowchart LR
1362
+ ChAdd["add_dark_mode<br/>verified"] -- "ADDED ×2" --> CapTheme
1363
+ ChAdd -- "MODIFIED" --> CapSystem["ui/system — System Specification"]
1364
+ ```
1365
+
1366
+ **Traceability** — change → requirement → scenario → task → check, coloured by
1367
+ status:
1368
+
1369
+ ```mermaid
1370
+ flowchart LR
1371
+ Req["Requirement: Theme selection"]
1372
+ Sce["Scenario: User toggles dark mode"]
1373
+ T11["1.1 ThemeProvider · done(1)"]
1374
+ T12["1.2 CSS custom properties · done(2)"]
1375
+ Chk["check: npm run typecheck · ok"]
1376
+ Req --> Sce --> T11 --> Chk
1377
+ Sce --> T12
1378
+ classDef done fill:#e8f5e9
1379
+ class T11,T12 done
1380
+ ```
1381
+
1382
+ **Lifecycle** — the change state machine:
1383
+
1384
+ ```mermaid
1385
+ stateDiagram-v2
1386
+ [*] --> proposed
1387
+ proposed --> checked
1388
+ checked --> applied
1389
+ applied --> verified
1390
+ verified --> archived
1391
+ ```
1392
+
1393
+ **Conflicts** — emitted only when non-empty: two in-flight changes modifying the
1394
+ same requirement.
1395
+
1396
+ **Dependencies** — capability-to-capability links (`related`/`requires` in
1397
+ frontmatter), once those exist.
1398
+
1399
+ ### How it is built
1400
+
1401
+ ```
1402
+ spec facts (index.dml / deltas.dml / tasks.dml)
1403
+ │ spec_graph.dml — pure DML, deterministic
1404
+ ▼
1405
+ Mermaid text ──► src/diagram/viewer.ts ──► diagrams/spec-<view>.html ──► opens
1406
+ ```
1407
+
1408
+ - `spec_graph.dml` selects and emits the graph, reading the same facts as
1409
+ `spec_query`. Zero model calls.
1410
+ - The existing viewer modules (`buildViewer`, `openViewerInBrowser`,
1411
+ `validateMermaid`, `writeSidecar`) render and open it — no new rendering code.
1412
+ - Grade follows `dc_diagram`: **presentation** collapses to capability level and
1413
+ shows changes and status only; **specification** expands to
1414
+ requirement → scenario → task → check.
1415
+
1416
+ ### Invocation
1417
+
1418
+ - **Natural language:** "show me the graph of capabilities and changes" → pi calls
1419
+ an optional read-only **`dc_spec_graph`** tool, exactly as it calls `dc_diagram`
1420
+ today (the `AUTHORING_INSTRUCTION` gains one sentence). No new slash command.
1421
+ - **Deterministic:** `/dc-run spec_graph [view] [target]
1422
+ [--grade=presentation|specification]`, e.g. `/dc-run spec_graph trace ui/theme`.
1423
+ Useful in CI, or when you want the graph without the model.
1424
+ - Optional later: a top-level `/dc-graph` command if usage justifies it. Not needed
1425
+ initially.
1426
+
1427
+ ### Rules
1428
+
1429
+ - Read-only and derived. Never edits specs, tasks, or the index.
1430
+ - Generated from facts, not prose, so the same workspace always yields the same
1431
+ graph (digests make staleness visible).
1432
+ - Large workspaces: filter by target (`ui/theme`), group collapsed nodes
1433
+ (`+7 requirements`), and cap edge counts. A graph of 500 requirements is a
1434
+ hairball — presentation grade should aggregate.
1435
+ - No model polishing by default. Unlike `dc_diagram`'s optional polish step, a spec
1436
+ graph has a deterministic correct answer.
1437
+
1438
+ ## Generation pipeline
1439
+
1440
+ The pipeline exists today; the spec layer extends it.
1441
+
1442
+ 1. `/dc-plan <request>` opens a `planningTransaction` and sends
1443
+ `buildPlanningPrompt(...)`.
1444
+ 2. `setPlanCommitActive(true)` registers `dc_plan_commit` and adds it to the
1445
+ active tool set for that turn only.
1446
+ 3. Pi explores with its normal tools and calls `dc_plan_commit` exactly once.
1447
+ 4. `validatePlanSpec(params, snapshot)` checks the spec against the live snapshot.
1448
+ 5. `ctx.ui.confirm` shows a preview.
1449
+ 6. `assemblePlanDml(plan, snapshot)` emits the DML.
1450
+ 7. `validateGeneratedPlan(dml)` rejects `.deepclause/` and runs
1451
+ `validateWithProlog(dml)`.
1452
+ 8. `writePlanNonDestructively` writes the files, never overwriting.
1453
+
1454
+ Two guarantees fall out: **the model cannot produce invalid DML**, and **nothing is
1455
+ written until the user confirms**.
1456
+
1457
+ ### Change-aware generation
1458
+
1459
+ Three stages make task↔scenario mapping deterministic rather than model-invented:
1460
+
1461
+ - **Stage 1 — DML scaffold (0 tokens).** Before the planning turn,
1462
+ `spec_scaffold.dml` parses the delta and emits a draft: one task per planned
1463
+ implementation step, each carrying the scenario ids it must satisfy and a
1464
+ suggested check derived from what the repo actually offers. Coverage is computed
1465
+ from the parse tree, not guessed.
1466
+ - **Stage 2 — pi enrichment.** The draft is injected into `buildPlanningPrompt`.
1467
+ Pi's job is bounded: choose `executor`, pick `requiredTools` from the exact
1468
+ active set, ground each check in real commands and paths, and phrase `do` and
1469
+ `expected`.
1470
+ - **Stage 3 — validated assembly.** `validatePlanSpec` gains the coverage and
1471
+ discoverability checks, then `assemblePlanDml` emits `tasks.dml` + `apply.dml`.
1472
+
1473
+ The commit payload **is** the task list. There is no Markdown intermediate and no
1474
+ `tasks.md`.
1475
+
1476
+ ### Why the model fills a typed payload, not DML
1477
+
1478
+ `validatePlanSpec` checks the plan against the **live pi environment** — that every
1479
+ requested tool actually exists *and* is currently active, and that no step requests
1480
+ `dc_run`/`dc_plan_commit`/`pi_agent_step`. That check can only happen in TypeScript,
1481
+ because the DML runtime is pure Prolog with no access to pi's tool registry. If pi
1482
+ wrote raw DML, that validation would be unavailable at commit time.
1483
+
1484
+ So: **the file is DML; the authoring interface is a typed commit.** Hand-editing
1485
+ `tasks.dml` or `apply.dml` remains allowed, and gets its environment check later at
1486
+ `/dc-run` preflight (`readPlanRequiredTools` plus the active-tool check).
1487
+
1488
+ The split in one line: **DML derives, pi decides, TypeScript assembles.**
1489
+
1490
+ ## The driver: plan as data + consulted interpreter
1491
+
1492
+ All loop-shaped logic lives once in `lib/apply.dml` and is exercised by every
1493
+ change. `% Plan format:` versions the file; `consult` means old plans pick up
1494
+ driver fixes.
1495
+
1496
+ ```prolog
1497
+ run_plan(TasksPath, Change, Max) :-
1498
+ remaining_tasks(Ids),
1499
+ run_all(TasksPath, Change, Ids, Max),
1500
+ verify_change(Change, ok),
1501
+ final_report(Change).
1502
+
1503
+ remaining_tasks(Ids) :-
1504
+ findall(Id, (plan_task(Id, _), \+ plan_task_status(Id, done(_))), Ids).
1505
+
1506
+ run_all(_, _, [], _).
1507
+ run_all(TasksPath, Change, [Id|Rest], Max) :-
1508
+ plan_task(Id, Step),
1509
+ run_task(TasksPath, Change, Id, Step, Max),
1510
+ run_all(TasksPath, Change, Rest, Max).
1511
+
1512
+ run_task(TasksPath, Change, Id, Step, Max) :-
1513
+ attempt(TasksPath, Change, Id, Step, 1, Max, none, Summary),
1514
+ synthesize_task(Id, Step, Summary).
1515
+
1516
+ attempt(TasksPath, Change, Id, Step, N, Max, Feedback, Summary) :-
1517
+ N =< Max,
1518
+ format(string(Progress), "Task ~w, attempt ~w/~w", [Id, N, Max]),
1519
+ output(Progress),
1520
+ execute(Id, Step, Feedback, Summary),
1521
+ verify(Step, Summary, Verdict),
1522
+ ( Verdict = ok
1523
+ -> record_status(TasksPath, Id, done(N)),
1524
+ record_verified(Change, Id, N, Summary)
1525
+ ; Verdict = fail(Evidence),
1526
+ N1 is N + 1,
1527
+ format(string(Msg), "Task ~w failed verification: ~w", [Id, Evidence]),
1528
+ output(Msg),
1529
+ record_status(TasksPath, Id, failed(N1, Evidence)),
1530
+ attempt(TasksPath, Change, Id, Step, N1, Max, Evidence, Summary)
1531
+ ).
1532
+
1533
+ attempt(TasksPath, _, Id, _, N, Max, _, _) :-
1534
+ N > Max,
1535
+ record_status(TasksPath, Id, failed(Max, "attempts exhausted")),
1536
+ throw(step_exhausted(Id, Max)).
1537
+ ```
1538
+
1539
+ The retry is a **repair**, not a rerun: the failed attempt's evidence is threaded
1540
+ back into the delegated instruction.
1541
+
1542
+ ```prolog
1543
+ execute(Id, Step, none, Summary) :-
1544
+ instruction(Id, Step, Instruction, Tools, Expected),
1545
+ exec(pi_agent_step(instruction: Instruction, tools: Tools,
1546
+ expected: Expected, skills: []), Summary).
1547
+
1548
+ execute(Id, Step, Feedback, Summary) :-
1549
+ Feedback \= none,
1550
+ instruction(Id, Step, Instruction, Tools, Expected),
1551
+ format(string(Fix),
1552
+ "~w~n~nThe previous attempt failed verification with:~n~w~n~nFix only what is needed; do not redo the whole step.",
1553
+ [Instruction, Feedback]),
1554
+ exec(pi_agent_step(instruction: Fix, tools: Tools,
1555
+ expected: Expected, skills: []), Summary).
1556
+ ```
1557
+
1558
+ `verify/3` **always succeeds** and binds a verdict, so the caller can decide:
1559
+
1560
+ ```prolog
1561
+ verify(Step, Summary, ok) :-
1562
+ Summary \= "",
1563
+ get_dict(checks, Step, Checks),
1564
+ \+ ( member(C, Checks), run_check(C, R), R \= ok ),
1565
+ !.
1566
+ verify(Step, Summary, fail(Evidence)) :-
1567
+ ( Summary == ""
1568
+ -> Evidence = "delegated step returned no summary"
1569
+ ; get_dict(checks, Step, Checks),
1570
+ findall(M, (member(C, Checks), run_check(C, M), M \= ok), Msgs),
1571
+ ( Msgs = [] -> Evidence = "verification failed"
1572
+ ; atomic_list_concat(Msgs, "; ", Evidence) )
1573
+ ).
1574
+ ```
1575
+
1576
+ Design notes:
1577
+
1578
+ - **Feedback is threaded as an argument**, not asserted. Backtracking rolls memory
1579
+ back in DML, so a locally-bound failure would be lost and an asserted one would
1580
+ accumulate. Threading keeps each repair self-contained.
1581
+ - **The gate uses negation-as-failure** ("all checks pass"), the natural Prolog
1582
+ idiom for a requirement.
1583
+ - **`throw` on exhaustion** is what makes the rollback path reachable.
1584
+
1585
+ The illustrative predicates need smoke tests against the WASM runtime before they
1586
+ are load-bearing: `consult` path resolution, `exists_file/1`,
1587
+ `atomic_list_concat/3`, `rename_file/2`, `get_dict/3` over consulted dicts, and
1588
+ `sub_string/5` are documented as available, but the DML reference warns that not
1589
+ all SWI builtins are guaranteed.
1590
+
1591
+ ## Preflight and detection changes
1592
+
1593
+ Two existing helpers need to change:
1594
+
1595
+ - **`isContextualPlan` must not search for `pi_agent_step(`.** With the driver in
1596
+ `lib/apply.dml`, that string is no longer in the change file. Detection must key
1597
+ off metadata: `% Contextual: true`, or the presence of `% Required pi tools:`.
1598
+ - **`resolveDmlPath` needs a change rule.** A bare name currently resolves to
1599
+ `skills/<name>.dml`. It needs `changes/<slug>/apply.dml` with a documented
1600
+ precedence (skills → plans → changes), or users type
1601
+ `/dc-run changes/add_dark_mode/apply`.
1602
+
1603
+ ## Rollback and atomicity
1604
+
1605
+ The goal: apply mutates the working tree and must be rollbackable, and a failed
1606
+ apply must never leave a half-applied change.
1607
+
1608
+ ### Why not a git worktree
1609
+
1610
+ `pi_agent_step` runs inside the live pi session, whose working directory is
1611
+ `ctx.cwd`. The delegated turn's `read`/`edit`/`bash` tools operate there, and there
1612
+ is no per-turn cwd override. Pointing the model at a separate worktree would
1613
+ require a separate pi process or path-rewriting every tool call — isolation
1614
+ theater. Additionally, `specs/` and `changes/` live inside the repo, so a worktree
1615
+ would check out its own copy of the specs the plan is reading.
1616
+
1617
+ A worktree becomes viable only if pi grows a per-turn working-directory override.
1618
+ Until then, snapshot + restore in place. (A worktree *is* the right tool for
1619
+ parallel *development* workstreams — see the parallel strategy section — just not
1620
+ for runtime apply isolation.)
1621
+
1622
+ ### Snapshot + restore
1623
+
1624
+ ```
1625
+ snapshot → run tasks → verify → accept
1626
+ ↘ failure/abort → restore
1627
+ ```
1628
+
1629
+ - At start: record `git rev-parse HEAD` and `git status --porcelain`; refuse if the
1630
+ tree is dirty unless `--allow-dirty` (then `git stash create` for tracked
1631
+ changes). Record untracked paths that did not exist before.
1632
+ - On failure: `git checkout -- <paths>` / `git reset --hard <snapshot>`, plus
1633
+ removal of newly created untracked paths. Reset only ever targets the
1634
+ harness-recorded snapshot.
1635
+ - Persist the ref in `change.json` (`applySnapshot: <sha>`) so a failed apply is
1636
+ recoverable and idempotent, and print the exact recovery command in the result.
1637
+
1638
+ ### The fallback clause is not enough on its own
1639
+
1640
+ - The DML fallback clause only fires on **logical** failure (a goal fails or
1641
+ throws). It does **not** fire on `/dc-cancel` or a crash: the abort stops the
1642
+ generator and `executeDml`'s `finally` disposes the SDK. So the authoritative
1643
+ restore lives in the **harness `finally`**, which sees logical failure, abort,
1644
+ and exceptions uniformly.
1645
+ - `answer/1` commits. A plan that partially succeeds and then answers will not
1646
+ reset — which is why the verification gate must `throw`/`fail`, not merely
1647
+ report.
1648
+
1649
+ Shape:
1650
+
1651
+ ```prolog
1652
+ agent_main :-
1653
+ catch(
1654
+ ( exec(dc_apply_snapshot(change: "add_dark_mode"), Snap),
1655
+ run_plan(TasksPath, "add_dark_mode", 3),
1656
+ exec(dc_apply_accept(change: "add_dark_mode", snapshot: Snap), _),
1657
+ answer(Report)
1658
+ ),
1659
+ Error,
1660
+ ( exec(dc_apply_restore(change: "add_dark_mode", snapshot: Snap), _),
1661
+ format(string(Msg), "Apply failed and was rolled back: ~w", [Error]),
1662
+ throw(rolled_back(Msg))
1663
+ )
1664
+ ).
1665
+
1666
+ agent_main :- % belt-and-braces for pure logical failure
1667
+ answer("Change add_dark_mode did not complete. The working tree was restored by the harness.").
1668
+ ```
1669
+
1670
+ Both layers are needed because they cover different failure modes.
1671
+
1672
+ ### Progress and rollback interaction
1673
+
1674
+ Three policy questions fall out of writing status into a tracked file:
1675
+
1676
+ 1. **Cross-run attempt counts revert on rollback**, so a task could retry forever
1677
+ across separate runs. Keep attempt history outside the worktree if that matters,
1678
+ or write a `failed(N)` line *after* restore deliberately.
1679
+ 2. **Partial progress is lost.** If apply fails at task 5 of 6, restore reverts
1680
+ tasks 1–4 as well. Preserving partial progress requires restore scoped to the
1681
+ files each task touched rather than a blanket reset — an explicit policy choice,
1682
+ not a default.
1683
+ 3. **Failure evidence lands in a committed file** via
1684
+ `failed(N, Evidence)`. If that is unwanted, store `failed(N)` only and keep
1685
+ detail in the pi session.
1686
+
1687
+ ## Runtime tools
1688
+
1689
+ | Tool | Scope | Justification |
1690
+ |---|---|---|
1691
+ | `pi_workspace_list` | read-only directory listing | exists today |
1692
+ | `pi_bash` | approval-gated shell | exists today |
1693
+ | `pi_agent_step` | bounded delegated pi turn | exists today; contextual plans only |
1694
+ | `dc_verify_run` | declared verification commands only | needs host shell; must run unattended |
1695
+ | `dc_apply_snapshot` / `dc_apply_restore` | harness-recorded git snapshot | needs host git; must run unattended |
1696
+ | `dc_spec_query` (optional) | read-only spec/delta index queries | introspection, not a gate; fixed predicate allowlist |
1697
+ | `dc_spec_graph` (optional) | read-only capability/change graph rendering | view over derived facts via the existing diagram viewer |
1698
+
1699
+ `tasks.dml` is **not** written through a tool. The driver rewrites its managed
1700
+ status block with native Prolog file I/O (`open/3`, `rename_file/2`) in the WASM
1701
+ filesystem, where the workspace is mounted at `/workspace`. Reads and writes stay
1702
+ inside the workspace by construction. Human-facing writes to `specs/` and
1703
+ `changes/` still go through the same non-destructive policy as the rest of the
1704
+ extension.
1705
+
1706
+ ## Validation gates
1707
+
1708
+ | Layer | Gate | Blocks |
1709
+ |---|---|---|
1710
+ | spec | every requirement has ≥1 scenario; no implementation detail; RFC 2119 usage | `/dc-check` failure |
1711
+ | spec | delta consistency (MODIFIED exists, no ADDED/MODIFIED collision) | archive |
1712
+ | plan | every scenario covered by ≥1 task `satisfies` | writing `tasks.dml` |
1713
+ | plan | every task declares verification | writing `tasks.dml` |
1714
+ | plan | `cmd(...)` checks discoverable in the repo | silently bogus checks |
1715
+ | plan | no in-flight change modifies the same requirement (index query) | committing a conflicting change |
1716
+ | apply | all checks pass; change-level `verify_change/2` | `answer/1` |
1717
+ | archive | merged spec well-formed; drift digest matches | writing `specs/` |
1718
+ | archive | index regenerated; delta status set to archived | stale inventory |
1719
+
1720
+ Today only the plan-shape checks exist (`validatePlanSpec`: steps, ids, tools
1721
+ exist/active, no control tools; `validateGeneratedPlan`: DML parses). The spec
1722
+ relationship — coverage, requirement existence, behavioral purity — is absent and
1723
+ is what this layer adds.
1724
+
1725
+ ## Security and hardening
1726
+
1727
+ - **No executable content in specs.** Moving checks out of the spec removes the
1728
+ supply-chain risk of asserting clauses from a shared review artifact. The
1729
+ remaining executable surfaces are `tasks.dml` (consulted data, dispatched by a
1730
+ fixed interpreter) and `dc_verify_run` (an exact command allowlist).
1731
+ - **Shell-metacharacter bypass.** The "discoverable command" check is string
1732
+ matching. A discovered-looking command that smuggles metacharacters must be
1733
+ rejected at approval time; the suite approval should render the exact argv.
1734
+ - **WASM workspace escape.** The assumption that Prolog file I/O cannot leave
1735
+ `/workspace` needs adversarial tests: `..` traversal, symlinks, absolute paths,
1736
+ and `consult` targets.
1737
+ - **Concurrency.** `activeController` guards one pi process. Two sessions in the
1738
+ same repo can still race on `specs/`, `changes/`, and the managed status block.
1739
+ A workspace lock (or "one writer per workspace") is needed before multi-user use.
1740
+ - **Cost and latency.** Retries multiply tokens; suite approval multiplies
1741
+ commands. There is no budget cap today, only usage reporting. A run budget
1742
+ (`--max-tokens`/`--max-cost`) and a circuit breaker are prerequisites for
1743
+ unattended use.
1744
+ - **Provenance.** Consider `changes/<slug>/evidence.json` or session linking so an
1745
+ archived change can answer "which checks proved which scenario, when, and with
1746
+ what result."
1747
+
1748
+ ## Open questions
1749
+
1750
+ 1. **Layout scope.** Add `specs/`, `changes/`, `lib/` under `.pi/deepclause/`?
1751
+ This extends the `AGENTS.md` contract.
1752
+ 2. **`/dc-check`.** Accept it as a required command rather than "optional later"?
1753
+ 3. **`consult` smoke test.** Verify relative-path resolution, `exists_file/1`,
1754
+ `atomic_list_concat/3`, `rename_file/2`, consulted dicts, and `sub_string/5` in
1755
+ the WASM runtime.
1756
+ 4. **Driver versioning.** `lib/apply.dml` is mutable and shared. Semver it
1757
+ (`lib/apply-v2.dml`) or pin a digest per plan, so a driver change cannot
1758
+ silently alter the meaning of an approved plan.
1759
+ 5. **Drift digest scheme.** What to hash, and where in `change.json`.
1760
+ 6. **Failure evidence in committed files.** Keep `failed(N, Evidence)` or
1761
+ `failed(N)` only?
1762
+ 7. **Partial-progress restore.** Blanket `reset --hard`, or restore scoped to
1763
+ files each task touched?
1764
+ 8. **Cross-run attempt counts.** Keep history outside the worktree?
1765
+ 9. **Model-assisted checks.** Always allowed with a label, or opt-in per change?
1766
+ 10. **Retire capabilities.** Adopt OpenSpec's `retire_capabilities` opt-in for the
1767
+ one destructive archive step?
1768
+ 11. **Escalation.** After attempt exhaustion, ask the user before restoring, or
1769
+ restore immediately?
1770
+ 12. **Full suite.** Optional `full_suite/1` run after all tasks, before the
1771
+ change-level gate?
1772
+ 13. **Tasks-in-one-file.** Revisit whether `tasks.dml` + `apply.dml` should ever
1773
+ collapse into one file; two is right for now because it separates content from
1774
+ metadata.
1775
+ 14. **Index persistence.** Derive on demand, or commit a digest-stamped
1776
+ `index.dml`? At what scale does lazy parsing stop being fast enough?
1777
+ 15. **`delta_status` granularity.** Per requirement, or per change/operation?
1778
+ Per requirement allows partial application but costs more bookkeeping.
1779
+ 16. **Stacked changes.** Two changes touching the same requirement need a
1780
+ supersede/depends-on relation, or conflict detection becomes noise.
1781
+ 17. **Cross-store queries.** The index is per workspace; OpenSpec-style stores
1782
+ would need merged indexes or a fan-out query.
1783
+ 18. **Graph views and defaults.** Which views ship first, and what is the default
1784
+ grade for a plain "show me the spec graph"?
1785
+ 19. **Graph scale.** Aggregation and filtering strategy for large workspaces.
1786
+ 20. **`/dc-graph` command vs `dc_spec_graph` tool only.** Add a top-level command,
1787
+ or keep it as natural language plus `/dc-run spec_graph`?
1788
+
1789
+ ## Implementation phases
1790
+
1791
+ Serial foundation first — see the parallel strategy below.
1792
+
1793
+ **Phase 0 (serial, prerequisite)**
1794
+
1795
+ 0.1 Freeze the contracts: normative `docs/SPEC_FORMAT.md` (grammar, term schema,
1796
+ error codes), `change.json` schema + `schemaVersion`, the `plan_task`/`plan_task_status`
1797
+ shapes, and the `dc_plan_commit` payload v2.
1798
+ 0.2 Build the test harness: grammar conformance corpus, golden merge files, a
1799
+ recording/replay LLM backend, an extension harness with a scriptable `pi` fake.
1800
+ 0.3 Split `src/index.ts` into command/tool modules; `index.ts` becomes a thin
1801
+ composition root.
1802
+ 0.4 Implement `lib/specs.dml` (grammar + validators) and `spec_validate.dml`.
1803
+
1804
+ **Phases (parallelizable after Phase 0)**
1805
+
1806
+ 1. **Decide layout** (open question 1).
1807
+ 2. **`/dc-check`** wiring — highest value, lowest risk, no change to `/dc-plan`.
1808
+ 3. **`lib/apply.dml` driver** — task/status model, per-task verification, bounded
1809
+ repair, managed status block.
1810
+ 4. **`dc_verify_run` + suite approval** at `/dc-run` confirm time.
1811
+ 5. **`dc_apply_snapshot` / `dc_apply_restore` + harness `finally`** rollback.
1812
+ 6. **`spec_scaffold` + coverage and discoverability checks** in `validatePlanSpec`.
1813
+ 7. **`assemblePlanDml` v2** — emits `tasks.dml` + `apply.dml`.
1814
+ 8. **`spec_merge.dml` + `spec_archive`** with diff-and-confirm and digest checks.
1815
+ 9. **Change resolution and detection** — `resolveDmlPath` change rule,
1816
+ metadata-based `isContextualPlan`, `/dc-list` and `/dc` spec/change awareness.
1817
+ 10. **Feature/delta index** — `deltas.dml` emission, `spec_query.dml`,
1818
+ `spec_reindex.dml`, and the index-based conflict and coverage checks.
1819
+ 11. Optionally, a **DML-declared artifact schema** (facts like
1820
+ `artifact(id, requires, instruction, template)`) to generalize today's
1821
+ hard-coded `PlanSpec` — the OpenSpec artifact-graph idea, in DML. Gate this
1822
+ behind evidence that plan schemas actually vary.
1823
+ 12. **Spec graphs** — `spec_graph.dml`, the optional read-only `dc_spec_graph`
1824
+ tool, and reuse of the existing diagram viewer for the capability, changes,
1825
+ traceability, lifecycle and conflict views.
1826
+
1827
+ ## Parallel implementation strategy
1828
+
1829
+ **The governing rule: parallelism is gated by interface stability, not headcount.**
1830
+ Subagents are separate `pi` processes with isolated context, up to 8 tasks / 4
1831
+ concurrent, with a per-task `cwd` and output capped at 50 KB. They share the
1832
+ filesystem, so parallel writers collide unless isolated.
1833
+
1834
+ **Serial critical path (single owner):** Phase 0 above. Contracts first, harness
1835
+ second, `index.ts` split third, deterministic core fourth. Everything else can fan
1836
+ out after that.
1837
+
1838
+ **Workstreams (four concurrent, one writer per file/module):**
1839
+
1840
+ | WS | Scope | Files (single-writer) | Depends on |
1841
+ |---|---|---|---|
1842
+ | WS1 | Grammar, validators, merge | `lib/specs.dml`, `skills/spec_*.dml`, fixtures | — |
1843
+ | WS2 | Harness runtime tools | extracted `src/tools/verify.ts`, `src/tools/apply.ts`, `src/runtime.ts` | frozen tool schemas |
1844
+ | WS3 | Planner/assembly | `src/planner.ts` (task/status emission, coverage checks) | WS1 term schema |
1845
+ | WS4 | Change lifecycle | `src/commands/{plan,list,check}.ts`, `spec_scaffold.dml` | WS1, WS3 |
1846
+ | WS5 | Docs + fixtures | `docs/`, `SPEC_AUTHORING.md`, `tests/fixtures/` | starts immediately |
1847
+
1848
+ **How to run them:**
1849
+
1850
+ - **Scouts are free parallelism.** Read-only recon (existing planner behaviour,
1851
+ OpenSpec's `specs-apply.ts` semantics, SDK `consult`/WASM file-IO behaviour) has
1852
+ no conflict risk. Also use scouts to adversarially probe the security assumptions
1853
+ above.
1854
+ - **One writer per file.** Where overlap is unavoidable, give each writer a **git
1855
+ worktree** via the subagent `cwd` parameter. A worktree is the wrong tool for
1856
+ runtime apply isolation (pi cannot chdir the session) but exactly the right tool
1857
+ for build-time isolation.
1858
+ - **Chain mode for dependency:** `scout → planner → worker`.
1859
+ - **Reviewers must not write.** Gate each workstream against the frozen spec and
1860
+ conformance corpus, then merge.
1861
+ - **Workers commit, not paste.** Each worker commits in its worktree and returns
1862
+ branch + commit + files changed.
1863
+ - **Cap at four live workstreams** given the concurrency limit and the hot files
1864
+ (`planner.ts`, `runtime.ts`, `commands/*`).
1865
+ - **Budget tokens.** Four parallel workers burn cost quickly; tie each workstream
1866
+ to an attempt/cost ceiling.
1867
+
1868
+ **What parallelization will not fix:** the human remains the integrator; the
1869
+ conformance corpus is what lets workers self-check instead of asking; cross-cutting
1870
+ `PlanSpec`/`plan_task_status` changes must stay single-owner; integration testing is
1871
+ serial and needs a dedicated window.
1872
+
1873
+ ## Decisions log
1874
+
1875
+ | Consideration | Outcome |
1876
+ |---|---|
1877
+ | `dc_spec_verify` runtime tool | **Rejected.** Pure Prolog; use `consult('lib/specs.dml')` and plain predicates. Do not make a deterministic predicate LLM-callable. |
1878
+ | `task/N`/`prompt/N` for verification | **Rejected as gates.** LLM calls are non-deterministic; keep them for prose and synthesis. |
1879
+ | Checks attached to requirements/scenarios in the spec | **Rejected.** Implementation detail; the spec is behavioral only. Checks live on tasks. |
1880
+ | Embedded executable `prolog` blocks in specs | **Rejected.** Both clause embedding (supply-chain risk) and annotation mini-syntaxes (second grammar, hidden from review). Checks are declarative data terms in `tasks.dml`. |
1881
+ | `tasks.md` (Markdown task list) | **Rejected.** A redundant view of the plan; drift surface. Replaced by `tasks.dml`. |
1882
+ | Single DML file holding tasks + driver + metadata | **Rejected for now.** `tasks.dml` (content) and `apply.dml` (wiring + preflight metadata) have different owners and change rates. Revisit in open question 13. |
1883
+ | `change.json.completed` progress record | **Rejected.** Progress belongs in `tasks.dml` as `plan_task_status/2`, queryable and diffable. |
1884
+ | Status as a dict | **Rejected.** Compound terms (`done(1)`, `failed(3, E)`) match, write, and round-trip trivially. |
1885
+ | Read/write `tasks.dml` via a runtime tool | **Rejected.** Native Prolog file I/O with a managed marker block is sufficient and keeps logic in DML. |
1886
+ | Git worktree for runtime apply isolation | **Rejected** while pi has no per-turn cwd override; use snapshot + restore in place. (Worktrees remain correct for parallel development.) |
1887
+ | Restore only in the DML fallback clause | **Rejected.** Does not fire on abort/crash; authoritative restore belongs in the harness `finally`. |
1888
+ | Persisted parsed-term database | **Rejected as a source.** The tree is ephemeral; Markdown is the only source of truth for behavior. A digest-stamped, disposable index is allowed only as a cache that is regenerated on mismatch, never hand-edited, and derived on demand by default. |
1889
+ | Per-command shell approval for checks | **Rejected.** Approve the verification suite once; scope `dc_verify_run` to declared commands. |
1890
+ | Straight-line generated plan | **Rejected.** Emit plan data + consulted driver so gating, retry, and repair are shared and testable. |
1891
+ | `pi_agent_step(` string search for contextual detection | **Rejected.** Driver lives in `lib/`; key off `% Contextual:`/`% Required pi tools:` metadata instead. |
1892
+ | `dc_spec_query` read-only tool | **Accepted (optional).** Unlike the rejected `dc_spec_verify` gate, read-only introspection is a legitimate model-callable tool; the query language stays a fixed predicate allowlist. |
1893
+ | New rendering code for spec graphs | **Rejected.** Reuse the existing `dc_diagram` / `src/diagram/*` Mermaid viewer; `spec_graph.dml` only supplies the graph as generated Mermaid text. |