@dtlabs/ai-toolkit 0.10.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/bin/cli.js +1457 -143
  3. package/docs/installation.md +74 -8
  4. package/docs/reference.md +60 -12
  5. package/lib/asset-catalog.js +52 -0
  6. package/lib/execution-ledger.js +515 -0
  7. package/package.json +9 -7
  8. package/{.claude → src/claude}/agents/define-feature.md +36 -60
  9. package/{.claude → src/claude}/agents/install-toolkit.md +30 -103
  10. package/{.claude → src/claude}/scripts/wb-render.js +101 -2
  11. package/{.claude → src/claude}/scripts/wb-validate.js +32 -2
  12. package/{.claude → src/claude}/skills/implement-feature/SKILL.md +4 -2
  13. package/{.claude → src/claude}/workflows/am-phase1.js +9 -12
  14. package/{.claude → src/claude}/workflows/pm-phase1.js +25 -75
  15. package/{.claude → src/claude}/workflows/pm-phase2.js +57 -108
  16. package/{.claude → src/claude}/workflows/pm-phase3.js +162 -75
  17. package/.claude/scripts/.gitkeep +0 -0
  18. /package/{.claude → src/claude}/agents/concurrency-safety-assessment.md +0 -0
  19. /package/{.claude → src/claude}/agents/dependency-injection-refactoring.md +0 -0
  20. /package/{.claude → src/claude}/agents/dependency-supply-chain-security.md +0 -0
  21. /package/{.claude → src/claude}/agents/developer-backend.md +0 -0
  22. /package/{.claude → src/claude}/agents/developer-frontend.md +0 -0
  23. /package/{.claude → src/claude}/agents/developer-testing.md +0 -0
  24. /package/{.claude → src/claude}/agents/domain-model-refactoring.md +0 -0
  25. /package/{.claude → src/claude}/agents/generate-requirements.md +0 -0
  26. /package/{.claude → src/claude}/agents/generate-tech-spec.md +0 -0
  27. /package/{.claude → src/claude}/agents/generate-work-breakdown.md +0 -0
  28. /package/{.claude → src/claude}/agents/generic-software-assessment.md +0 -0
  29. /package/{.claude → src/claude}/agents/god-class-decomposition.md +0 -0
  30. /package/{.claude → src/claude}/agents/init-agents-md.md +0 -0
  31. /package/{.claude → src/claude}/agents/intervention-documentation-standard.md +0 -0
  32. /package/{.claude → src/claude}/agents/layered-architecture-assessment.md +0 -0
  33. /package/{.claude → src/claude}/agents/review-solution.md +0 -0
  34. /package/{.claude → src/claude}/agents/security-hardening.md +0 -0
  35. /package/{.claude → src/claude}/agents/validate-feature-docs.md +0 -0
  36. /package/{.claude → src/claude}/agents/validate-work-breakdown-semantic.md +0 -0
  37. /package/{.claude → src/claude}/commands/assessment-status.md +0 -0
  38. /package/{.claude → src/claude}/commands/check-docs.md +0 -0
  39. /package/{.claude → src/claude}/commands/check-interventions.md +0 -0
  40. /package/{.claude → src/claude}/commands/feature-status.md +0 -0
  41. /package/{.claude → src/claude}/commands/next-intervention.md +0 -0
  42. /package/{.claude → src/claude}/commands/next-task.md +0 -0
  43. /package/{.claude → src/claude}/commands/pr-description.md +0 -0
  44. /package/{.claude → src/claude}/skills/assess-codebase/SKILL.md +0 -0
  45. /package/{.claude → src/claude}/skills/define-feature/SKILL.md +0 -0
  46. /package/{.claude → src/claude}/skills/hi-gaia/SKILL.md +0 -0
  47. /package/{.claude → src/claude}/skills/init-agents/SKILL.md +0 -0
  48. /package/{.claude → src/claude}/skills/install-toolkit/SKILL.md +0 -0
  49. /package/{.claude → src/claude}/workflows/am-phase2.js +0 -0
@@ -80,56 +80,53 @@ Ask the user to **confirm or correct the draft**. Then grill ONLY on the `❓` g
80
80
 
81
81
  ## Phase 1 — Setup
82
82
 
83
- ### 1a. Discover the next FTR number
83
+ ### 1a. Resolve the features root (MANDATORY — single source of truth)
84
84
 
85
- Scan `docs/features/` for existing folders matching the pattern `FTR-[0-9]+*`. Extract the highest number and increment by 1. If no folders exist, start at `FTR-001`.
85
+ Before any file-system operation, run the following command **once** via Bash and capture its stdout into `featuresRoot`:
86
86
 
87
+ ```bash
88
+ featuresRoot=$(ai-toolkit resolve-features-root) && echo "$featuresRoot"
87
89
  ```
88
- docs/features/FTR-001-user-management/ → max = 1
89
- docs/features/FTR-002-product-catalog/ → max = 2
90
+
91
+ If the command exits non-zero, stop immediately and report the error to the user — do not proceed.
92
+
93
+ This single captured value (`featuresRoot`) is the **sole source of truth** for the features root in every subsequent step. Do **not** hard-code `docs/features/`, `internal_docs/features/`, or any other path. Both the directory under which `feature.md` is written (step 1d) and the `--dir` argument passed to the ledger facade must use this exact value — they must always agree, with no divergent or duplicate roots.
94
+
95
+ ### 1b. Discover the next FTR number
96
+
97
+ Scan `{featuresRoot}` for existing folders matching the pattern `FTR-[0-9]+*`. Extract the highest number and increment by 1. If no folders exist, start at `FTR-001`.
98
+
99
+ ```
100
+ {featuresRoot}/FTR-001-user-management/ → max = 1
101
+ {featuresRoot}/FTR-002-product-catalog/ → max = 2
90
102
  → next = FTR-003
91
103
  ```
92
104
 
93
- ### 1b. Ask for a feature name
105
+ ### 1c. Ask for a feature name
94
106
 
95
107
  Ask the user: **"What is the name of this feature?"** (short, descriptive, in their language). Use it to build the folder slug in kebab-case.
96
108
 
97
109
  Example: "Supplier Onboarding" → `FTR-003-supplier-onboarding`
98
110
 
99
- ### 1c. Create feature directory and initialize ledger
111
+ ### 1d. Create feature directory and open ledger entry
100
112
 
101
- Once you know the PREFIX (from 1a) and the feature slug (from 1b), do the following **before asking any grilling questions**:
113
+ Once you know the PREFIX (from 1b) and the feature slug (from 1c), do the following **before asking any grilling questions**:
102
114
 
103
- 1. Compute the paths:
104
- - `featureDir` = `docs/features/{PREFIX}-{slug}` (e.g. `docs/features/FTR-003-supplier-onboarding`)
105
- - `ledgerPath` = `{featureDir}/{PREFIX}-token-ledger.json`
115
+ 1. Compute `featureDir` using the captured `featuresRoot`:
116
+ - `featureDir` = `{featuresRoot}/{PREFIX}-{slug}` (e.g. `{featuresRoot}/FTR-003-supplier-onboarding`)
117
+ - This path is also the `--dir` value for every `ai-toolkit ledger` call that follows.
106
118
 
107
119
  2. Create the feature directory via Bash:
108
120
  ```bash
109
- mkdir -p {featureDir}
121
+ mkdir -p "{featureDir}"
110
122
  ```
111
123
 
112
- 3. Capture the current UTC timestamp via Bash:
124
+ 3. Open the ledger entry via the facade. Run the following command via Bash (quote the `--dir` path to handle spaces):
113
125
  ```bash
114
- started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && echo $started_at
126
+ ai-toolkit ledger open --dir "{featureDir}" --prefix {PREFIX} --agent define-feature:define --phase define --model sonnet --attempt 1
115
127
  ```
116
128
 
117
- 4. Write the initial ledger file at `{ledgerPath}` using the Write tool with exactly this content (substitute the actual PREFIX and timestamp):
118
- ```json
119
- [
120
- {
121
- "agent": "define-feature:define",
122
- "phase": "define",
123
- "model": "sonnet",
124
- "status": "running",
125
- "phase_delta_tokens": 0,
126
- "started_at": "<started_at from step 3>",
127
- "completed_at": null
128
- }
129
- ]
130
- ```
131
-
132
- The ledger records that the define-feature agent is now running. Proceed to Phase 2.
129
+ The ledger now records that the define-feature agent is running. Proceed to Phase 2.
133
130
 
134
131
  ---
135
132
 
@@ -254,12 +251,12 @@ Synthesize all answers into a structured `feature.md` file.
254
251
  ### Output path
255
252
 
256
253
  ```
257
- docs/features/{PREFIX}-{slug}/feature.md
254
+ {featuresRoot}/{PREFIX}-{slug}/feature.md
258
255
  ```
259
256
 
260
- Example: `docs/features/FTR-003-supplier-onboarding/feature.md`
257
+ Example: `{featuresRoot}/FTR-003-supplier-onboarding/feature.md`
261
258
 
262
- The directory was created in Phase 1c. If for any reason it doesn't exist, create it now.
259
+ The directory was created in Phase 1d using `featuresRoot`. If for any reason it doesn't exist, create it now using the same `featuresRoot` path — do not substitute a different root.
263
260
 
264
261
  ### feature.md template
265
262
 
@@ -352,34 +349,13 @@ For each entity: name, key fields (name, type, constraints), relationships.
352
349
 
353
350
  ### 4b. Finalize ledger entry
354
351
 
355
- After writing `feature.md`, update the ledger entry to record successful completion:
352
+ After writing `feature.md`, close the ledger entry via the facade. Run the following command via Bash (quote the `--dir` path to handle spaces):
356
353
 
357
- 1. Capture the current UTC timestamp via Bash:
358
- ```bash
359
- completed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && echo $completed_at
360
- ```
354
+ ```bash
355
+ ai-toolkit ledger close --dir "{featureDir}" --prefix {PREFIX} --agent define-feature:define --attempt 1
356
+ ```
361
357
 
362
- 2. Read the ledger file at `{featureDir}/{PREFIX}-token-ledger.json` using the Read tool.
363
-
364
- 3. Write the updated ledger file using the Write tool, replacing the entry's fields:
365
- - `"status"`: `"done"`
366
- - `"completed_at"`: the timestamp captured in step 1
367
- - `"phase_delta_tokens"`: leave as `0` — token measurement is not available to the agent directly
368
-
369
- Example final ledger content:
370
- ```json
371
- [
372
- {
373
- "agent": "define-feature:define",
374
- "phase": "define",
375
- "model": "sonnet",
376
- "status": "done",
377
- "phase_delta_tokens": 0,
378
- "started_at": "<original started_at>",
379
- "completed_at": "<completed_at from step 1>"
380
- }
381
- ]
382
- ```
358
+ **Important:** `--tokens` is intentionally omitted. The ledger facade records `phase_delta_tokens` as `null` when `--tokens` is not supplied. `define-feature` is an interactive/LLM activity whose token consumption cannot be observed from within the agent itself — do **not** pass `0` or any estimate.
383
359
 
384
360
  ---
385
361
 
@@ -392,7 +368,7 @@ After writing `feature.md`:
392
368
  3. Tell the user the next step:
393
369
 
394
370
  ```
395
- ✅ feature.md written at docs/features/{PREFIX}-{slug}/feature.md
371
+ ✅ feature.md written at {featuresRoot}/{PREFIX}-{slug}/feature.md
396
372
 
397
373
  Summary:
398
374
  Actors: N
@@ -400,7 +376,7 @@ Summary:
400
376
  Acceptance criteria: N (Must: N, Should: N, Could: N)
401
377
  Open questions: N
402
378
 
403
- Next step: run /implement-feature docs/features/{PREFIX}-{slug}/feature.md
379
+ Next step: run /implement-feature {featuresRoot}/{PREFIX}-{slug}/feature.md
404
380
  to start the full pipeline (requirements → spec → implementation → PR).
405
381
  ```
406
382
 
@@ -6,7 +6,7 @@ model: sonnet
6
6
 
7
7
  # Install Toolkit
8
8
 
9
- Copies the toolkit's agents, skills, commands, and procedures into a destination project so it can use `/implement-feature`, `/init-agents`, all developer agents, and all slash commands without needing the toolkit repo locally.
9
+ Installs the ai-toolkit into a destination project so it can use `/implement-feature`, `/init-agents`, all developer agents, and all slash commands without needing the toolkit repo locally.
10
10
 
11
11
  ---
12
12
 
@@ -15,130 +15,57 @@ Copies the toolkit's agents, skills, commands, and procedures into a destination
15
15
  The user provides:
16
16
  1. **Path to the destination project root** (optional) — defaults to the current working directory if not specified
17
17
  2. **`--force`** flag (optional) — overwrite files that already exist in the destination
18
+ 3. **`--global`** flag (optional) — install into the global Claude Code config directory (`~/.claude/`) instead of a project
18
19
 
19
20
  ---
20
21
 
21
- ## Step 1 — Validate inputs and check version
22
-
23
- 1. Determine the **toolkit root**: the current working directory (this agent runs from the toolkit repo).
24
- 2. Verify the toolkit root contains `.claude/agents/` — if not, abort: "Error: must be run from the ai-toolkit directory."
25
- 3. Verify the destination path exists — if not, abort: "Error: destination path not found: {path}"
26
- 4. Check if destination is a git repository (look for `.git/`) — warn if not, but continue.
27
- 5. **Version check**:
28
- - Read `package.json` from the toolkit root to get the current version (e.g. `0.2.0`)
29
- - Check if `.claude/.ai-toolkit-version` exists in the destination:
30
- - **File missing** → first install, no prompt needed, continue
31
- - **Same version** → inform the user and ask: "Toolkit v{x} is already installed. Re-install anyway?"
32
- - If No: abort cleanly
33
- - **Different version** → show both versions and ask:
34
- ```
35
- Version check:
36
- Installed : v{installed}
37
- Available : v{current}
38
-
39
- Proceed and update from v{installed} to v{current}?
40
- ```
41
- - If No: abort cleanly with "Your current installation was not changed."
22
+ ## Step 1 — Validate inputs
42
23
 
43
- ---
44
-
45
- ## Step 2 — Plan what to copy
46
-
47
- Six source directories to install:
48
-
49
- | Source (toolkit) | Destination | Purpose |
50
- |------------------|-------------|---------|
51
- | `.claude/agents/` | `{dest}/.claude/agents/` | All spawnable subagents |
52
- | `.claude/skills/` | `{dest}/.claude/skills/` | All user-invocable skills (except `install-toolkit/`) |
53
- | `.claude/commands/` | `{dest}/.claude/commands/` | All slash commands |
54
- | `.claude/workflows/` | `{dest}/.claude/workflows/` | Claude Code Workflow scripts for orchestrated pipelines |
55
- | `.claude/scripts/` | `{dest}/.claude/scripts/` | CLI scripts for work breakdown validation and rendering |
56
- | `docs/procedures/` | `{dest}/docs/procedures/` | Generic procedures (only if destination has no override) |
57
-
58
- For each file, compare source content against destination using an MD5/hash check and determine status:
59
- - **NEW** — file does not exist in destination → copy automatically, no prompt
60
- - **SAME** — file exists and content is identical → skip silently, no prompt
61
- - **MODIFIED** — file exists but content differs → show to user, ask per-file
62
-
63
- Build and display the plan before executing:
64
-
65
- ```
66
- 📦 Install Plan → {destination}
67
- ──────────────────────────────────────────────────
68
- .claude/agents/
69
- ✅ NEW generate-requirements.md
70
- ⚠️ MODIFIED developer-backend.md ← content differs
71
- ⏭ SAME review-solution.md
72
-
73
- .claude/skills/
74
- ✅ NEW implement-feature/SKILL.md
75
- ⏭ SAME init-agents/SKILL.md
76
-
77
- .claude/workflows/
78
- ✅ NEW pm-phase1.js
79
- ✅ NEW pm-phase2.js
80
- ✅ NEW pm-phase3.js
81
- ✅ NEW am-phase1.js
82
- ✅ NEW am-phase2.js
83
-
84
- .claude/scripts/
85
- ✅ NEW wb-validate.js
86
- ✅ NEW wb-render.js
87
-
88
- docs/procedures/
89
- ✅ NEW code-generation.md
90
- ⏭ SAME testing.md
91
- ──────────────────────────────────────────────────
92
- New: N | Modified: N | Unchanged: N
93
- ```
24
+ 1. Determine destination:
25
+ - If `--global` was passed: destination is `~/.claude/` (user-global config dir)
26
+ - Otherwise: destination is the path provided, or the current working directory if omitted
27
+ 2. Verify the destination path exists — if not, abort: "Error: destination path not found: {path}"
28
+ 3. Check if destination is a git repository (look for `.git/`) — warn if not, but continue.
94
29
 
95
30
  ---
96
31
 
97
- ## Step 3 — Execute
98
-
99
- **Phase A — New files** (no prompt needed): copy all NEW files, creating directories as needed.
32
+ ## Step 2 — Run installer
100
33
 
101
- **Phase B — Modified files** (per-file prompt):
34
+ Invoke the ai-toolkit CLI installer. The CLI handles source resolution (no CWD assumption), version checking, asset catalog enumeration, and file copy:
102
35
 
103
- For each MODIFIED file, show:
104
- ```
105
- ⚠️ MODIFIED: .claude/agents/developer-backend.md
106
- The toolkit version differs from the one already in your project.
107
- Overwrite? (y/N):
36
+ ```bash
37
+ ai-toolkit install --project "{dest}" # for a project install
38
+ # or
39
+ ai-toolkit install --global # for a global install
108
40
  ```
109
41
 
110
- Wait for the user's response before moving to the next file. If `--force` was passed, overwrite all modified files without prompting.
111
-
112
- **Phase C — Same files**: skip silently (no output for these).
113
-
114
- Use shell commands appropriate for the OS. On Windows with bash:
42
+ If `--force` was passed:
115
43
  ```bash
116
- mkdir -p "{dest_dir}"
117
- cp "{source_file}" "{dest_file}"
44
+ ai-toolkit install --project "{dest}" --force
118
45
  ```
119
46
 
120
- **Never copy the `install-toolkit/` skill directory** — this skill is toolkit-internal and has no use in a destination project.
47
+ The installer will:
48
+ - Compare installed version vs available version and ask for confirmation if already installed at the same version
49
+ - Enumerate all installable assets from the asset catalog (`src/claude/`)
50
+ - Copy NEW files automatically; prompt for MODIFIED files (or overwrite silently if `--force`)
51
+ - Skip SAME files silently
52
+ - Never copy `install-toolkit` skill or `settings.json` / `settings.local.json`
53
+ - Never clobber `docs/procedures/` overrides that exist in the destination
54
+ - Stamp the installed version in `{dest}/.claude/.ai-toolkit-version`
121
55
 
122
- **Never copy `.claude/settings.json` or `.claude/settings.local.json`** — these are user-owned configuration. Copying them would clobber the destination's existing settings. The toolkit's own `settings.json` exists only to configure this repo; it is verified and advised on (Step 4b), never installed.
123
-
124
- **For `docs/procedures/`**: copy only files that don't already exist in the destination (regardless of `--force`) — project-specific procedure overrides must never be clobbered.
56
+ Show the installer's output to the user without modification.
125
57
 
126
58
  ---
127
59
 
128
- ## Step 4 — Verify and stamp version
60
+ ## Step 3 — Verify result
129
61
 
130
- After copying:
131
- 1. Verify all expected files are present in the destination
132
- 2. Verify no files are zero bytes
133
- 3. Write the installed version to `.claude/.ai-toolkit-version` in the destination:
134
- ```bash
135
- echo "{current_version}" > "{dest}/.claude/.ai-toolkit-version"
136
- ```
137
- This file is read on future installs to show the version comparison prompt.
62
+ After the installer exits:
63
+ 1. Confirm exit code was 0 — if non-zero, report the error and stop
64
+ 2. Verify `.claude/.ai-toolkit-version` was written in the destination
138
65
 
139
66
  ---
140
67
 
141
- ## Step 4b — Verify subagent spawn depth (verify & advise ONLY — never write)
68
+ ## Step 4 — Verify subagent spawn depth (verify & advise ONLY — never write)
142
69
 
143
70
  The orchestrated pipelines (`/implement-feature`, `/assess-codebase`) require the
144
71
  environment variable `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` to be set to `2` or higher.
@@ -105,6 +105,38 @@ function sanitizeField(str) {
105
105
  .trim()
106
106
  }
107
107
 
108
+ // ── Verification-command helpers (LOSSLESS — never sanitized) ─────────────────
109
+ //
110
+ // Verification commands are executable and MUST survive verbatim: operators like
111
+ // `||`, shell pipes `|`, and regex alternations `grep -E 'a|b|c'` are destroyed by
112
+ // sanitizeField (which maps `|` → space to protect table/CSV column structure).
113
+ // These commands are therefore rendered in a dedicated section as fenced code
114
+ // blocks whose content is byte-equivalent to the source JSON. The task table shows
115
+ // only a reference (a count + anchor link), never a sanitized copy of a command.
116
+
117
+ function getVerificationCommands(task) {
118
+ const v = task && (task.verificationCommands ?? task.verification?.commands)
119
+ return Array.isArray(v) ? v : []
120
+ }
121
+
122
+ // Stable, deterministic HTML anchor id for a task's authoritative detail block.
123
+ // The summary table links here; the "Task Details" section renders every field.
124
+ function taskAnchor(id) {
125
+ const s = typeof id === 'string' ? id : String(id ?? '')
126
+ return 'task-' + s.replace(/[^A-Za-z0-9_-]/g, '-')
127
+ }
128
+
129
+ // Render a single command inside a fenced code block, byte-equivalent to `cmd`.
130
+ // The fence length is chosen to exceed the longest backtick run in the content so a
131
+ // command that itself contains backticks can never prematurely close the fence.
132
+ function fencedCommandBlock(cmd) {
133
+ const s = typeof cmd === 'string' ? cmd : String(cmd ?? '')
134
+ const runs = s.match(/`+/g) || []
135
+ const maxRun = runs.reduce((m, r) => Math.max(m, r.length), 0)
136
+ const fence = '`'.repeat(Math.max(3, maxRun + 1))
137
+ return `${fence}\n${s}\n${fence}`
138
+ }
139
+
108
140
  function buildCommitSubject(task) {
109
141
  if (!task.commit || typeof task.commit !== 'object') {
110
142
  return 'chore: implement task'
@@ -189,8 +221,15 @@ function renderMarkdown(wb, prefix, phaseDepsMap) {
189
221
  const est = mins !== null ? String(mins) : '—'
190
222
  const depArr = Array.isArray(task.dependsOn) && task.dependsOn.length > 0 ? task.dependsOn : null
191
223
  const deps = depArr ? depArr.join(', ') : '—'
192
- const verifArr = task.verificationCommands ?? task.verification?.commands ?? []
193
- const verif = verifArr.length > 0 ? sanitizeField(verifArr.join('; ')) : '—'
224
+ // The summary table is a digest only. Verification commands are NOT rendered
225
+ // in-table (sanitizeField would destroy their operators). The cell links to the
226
+ // authoritative per-task "Task Details" section, which carries every field
227
+ // (outcome, acceptance criteria, estimates, grouping rationale, commit, and the
228
+ // lossless verification commands). See renderTaskDetails / fencedCommandBlock.
229
+ const verifCmds = getVerificationCommands(task)
230
+ const verif = verifCmds.length > 0
231
+ ? `${verifCmds.length} cmd — [details](#${taskAnchor(task.id)})`
232
+ : `[details](#${taskAnchor(task.id)})`
194
233
  return `| ${id} | ${title} | ${outcome} | ${domain} | ${est} | ${deps} | ${verif} |`
195
234
  }
196
235
 
@@ -263,6 +302,66 @@ function renderMarkdown(wb, prefix, phaseDepsMap) {
263
302
  }
264
303
  }
265
304
 
305
+ // ── Task Details (AUTHORITATIVE) ──
306
+ // The summary tables above are a digest. This section is the authoritative,
307
+ // self-contained representation of every task: it renders each field integrally so
308
+ // the Work Breakdown Markdown (plus the dispatch CSV) is a complete deliverable —
309
+ // the intermediate JSON is not required by any downstream consumer.
310
+ //
311
+ // Free-text fields (title, outcome, grouping rationale, commit subject) are rendered
312
+ // OUTSIDE any table and are therefore preserved verbatim — including pipes, which
313
+ // sanitizeField would otherwise map to spaces to protect table/CSV columns.
314
+ // Verification commands are emitted as fenced code blocks that are byte-equivalent
315
+ // to the source; the fence length exceeds any backtick run in the command so a
316
+ // command containing backticks can never break out of its block.
317
+ const raw = v => (typeof v === 'string' ? v : String(v ?? ''))
318
+ const getTokens = task => {
319
+ const t = task.estimate?.tokens ?? task.tokens
320
+ return (t !== undefined && t !== null) ? String(t) : '—'
321
+ }
322
+ const listOrDash = arr =>
323
+ (Array.isArray(arr) && arr.length > 0) ? arr.join(', ') : '—'
324
+
325
+ L.push('## Task Details')
326
+ L.push('')
327
+ L.push('> Authoritative per-task detail. Every field is rendered integrally so this document, together with the dispatch CSV, is a complete deliverable requiring no separate JSON. Verification commands are preserved **verbatim** in fenced code blocks — operators such as `||`, shell pipes `|`, and regex alternations (`grep -E \'a|b|c\'`) survive byte-for-byte. Each command is an independent fenced block.')
328
+ L.push('')
329
+ for (const phase of wb.phases) {
330
+ for (const task of (Array.isArray(phase.tasks) ? phase.tasks : [])) {
331
+ const cmds = getVerificationCommands(task)
332
+ const mins = getAgentMinutes(task)
333
+ L.push(`<a id="${taskAnchor(task.id)}"></a>`)
334
+ L.push(`### ${raw(task.id)}`)
335
+ L.push('')
336
+ L.push(`- **Task ID:** ${raw(task.id)}`)
337
+ L.push(`- **Title:** ${raw(task.title)}`)
338
+ L.push(`- **Outcome:** ${raw(task.outcome)}`)
339
+ L.push(`- **Domain:** ${raw(task.domain)}`)
340
+ L.push(`- **Agent type:** ${raw(task.agentType)}`)
341
+ L.push(`- **Dependencies:** ${listOrDash(task.dependsOn)}`)
342
+ L.push(`- **Acceptance criteria:** ${listOrDash(task.acceptanceCriteria)}`)
343
+ L.push(`- **Estimate — agent minutes:** ${mins !== null ? String(mins) : '—'}`)
344
+ L.push(`- **Estimate — tokens:** ${getTokens(task)}`)
345
+ L.push(`- **Output count:** ${task.outputCount != null ? String(task.outputCount) : '—'}`)
346
+ L.push(`- **Grouping rationale:** ${task.groupingRationale != null ? raw(task.groupingRationale) : '—'}`)
347
+ L.push(`- **Commit type:** ${task.commit && task.commit.type != null ? raw(task.commit.type) : '—'}`)
348
+ L.push(`- **Commit scope:** ${task.commit && task.commit.scope != null ? raw(task.commit.scope) : '—'}`)
349
+ L.push(`- **Commit subject:** ${task.commit && task.commit.subject != null ? raw(task.commit.subject) : '—'}`)
350
+ L.push('')
351
+ L.push('**Verification commands:**')
352
+ L.push('')
353
+ if (cmds.length === 0) {
354
+ L.push('_No verification commands._')
355
+ L.push('')
356
+ } else {
357
+ for (const cmd of cmds) {
358
+ L.push(fencedCommandBlock(cmd))
359
+ L.push('')
360
+ }
361
+ }
362
+ }
363
+ }
364
+
266
365
  // ── Statistics ──
267
366
  L.push('## Statistics')
268
367
  L.push('')
@@ -59,6 +59,36 @@ const ERRORS = {
59
59
  EMPTY_PHASE: 'empty_phase', // check 23
60
60
  }
61
61
 
62
+ // ── Markdown table row splitter ─────────────────────────────────────────────
63
+ // Splits a Markdown table row into cells on unescaped pipe separators only.
64
+ // A backslash-escaped pipe (\|) is treated as a literal pipe *inside* the cell
65
+ // rather than a column separator and is unescaped in the returned cell, so a
66
+ // criterion such as `ai-toolkit ledger open\|close\|fail\|skip` stays in a
67
+ // single cell and does not shift the "Related UC" column.
68
+ // Returns the raw split including the leading/trailing empty cells produced by
69
+ // the row's outer pipes, matching the previous `line.split('|')` contract so
70
+ // callers can keep using `.slice(1, -1)`.
71
+ function splitTableRow(line) {
72
+ const cells = []
73
+ let current = ''
74
+ for (let i = 0; i < line.length; i++) {
75
+ const ch = line[i]
76
+ if (ch === '\\' && line[i + 1] === '|') {
77
+ current += '|' // unescape: keep the literal pipe inside this cell
78
+ i++ // consume the escaped '|'
79
+ continue
80
+ }
81
+ if (ch === '|') {
82
+ cells.push(current)
83
+ current = ''
84
+ continue
85
+ }
86
+ current += ch
87
+ }
88
+ cells.push(current)
89
+ return cells
90
+ }
91
+
62
92
  // ── AC table parser (prerequisite for checks 19–21) ─────────────────────────
63
93
 
64
94
  function parseAcTable(requirementsPath) {
@@ -122,7 +152,7 @@ function parseAcTable(requirementsPath) {
122
152
  }
123
153
 
124
154
  // ── Validate column headers ────────────────────────────────────────────────
125
- const headerCells = lines[tableHeaderIdx].split('|').slice(1, -1).map(c => c.trim())
155
+ const headerCells = splitTableRow(lines[tableHeaderIdx]).slice(1, -1).map(c => c.trim())
126
156
  if (headerCells.length < 3) {
127
157
  process.stderr.write('Error: AC table header has fewer than 3 columns\n')
128
158
  process.exit(2)
@@ -143,7 +173,7 @@ function parseAcTable(requirementsPath) {
143
173
  if (!line.trim().startsWith('|')) break
144
174
 
145
175
  // Skip separator rows: every cell is only dashes, colons, or spaces
146
- const rawCells = line.split('|').slice(1, -1)
176
+ const rawCells = splitTableRow(line).slice(1, -1)
147
177
  if (rawCells.every(c => /^[\s\-:]+$/.test(c))) continue
148
178
 
149
179
  const cells = rawCells.map(c => c.trim())
@@ -204,6 +204,8 @@ From the `<usage>` block of the pm-phase3 result, read:
204
204
 
205
205
  Read `{PREFIX}-Token-Estimate.md`. Append the orchestrator row and grand total:
206
206
 
207
+ > **Null-compatibility — actuals reading and cost calculation:** Treat a token value of `null`, `0`, or `not_available` as **data unavailable** — never as a real, observable zero consumption. Render unavailable values as `—` in the Actual tokens and Actual cost columns; exclude them from sums, averages, and grand totals. Preventing resume clobber and legacy misinterpretation requires that no unavailable measurement is coerced into a real zero.
208
+
207
209
  ```markdown
208
210
  | project-manager/pm-phase3 (orchestrator) | — | sonnet | 80,000 (estimated) | {subagent_tokens} (actual) | ±{delta} | {duration} |
209
211
 
@@ -213,14 +215,14 @@ Read `{PREFIX}-Token-Estimate.md`. Append the orchestrator row and grand total:
213
215
 
214
216
  | Agent | Task / Scope | Model | Est. tokens | Actual tokens | Delta | Est. cost ($) | Actual cost ($) | Duration |
215
217
  |-------|-------------|-------|------------|---------------|-------|--------------|----------------|----------|
216
- {one row per entry in token_ledger — from phase 1, 2, and 3}
218
+ {one row per entry in token_ledger — from phase 1, 2, and 3; for any entry where actual tokens is null, 0, or not_available show — in Actual tokens and Actual cost and exclude that row from totals}
217
219
  | project-manager/pm-phase3 (orchestrator) | — | sonnet | 80,000 | {subagent_tokens} | ±{delta} | $0.4320 | ${actual_cost} | {duration} |
218
220
 
219
221
  ## Estimation accuracy by agent type
220
222
 
221
223
  | Model | Count | Avg est. tokens | Avg actual tokens | Avg delta | Trend |
222
224
  |-------|-------|----------------|------------------|-----------|-------|
223
- {one row per model tier (haiku, sonnet) — exclude N/A rows from averages}
225
+ {one row per model tier (haiku, sonnet) — exclude rows where actual tokens is null, 0, or not_available from averages}
224
226
 
225
227
  ## Grand Total
226
228
 
@@ -2,7 +2,7 @@ export const meta = {
2
2
  name: 'am-phase1',
3
3
  description: 'Assessment pipeline phase 1: discover assessment agents → scope filter → parallel assessment → intervention-documentation-standard → Token-Estimate → Effort-Estimate → findings_gate_payload.',
4
4
  phases: [
5
- { title: 'Discovery', detail: 'Scan .claude/agents/ for assessment agents, apply scope filter' },
5
+ { title: 'Discovery', detail: 'Run ai-toolkit list-assets --category agents --format json, apply scope filter' },
6
6
  { title: 'Assessment', detail: 'Dispatch all assessment agents in parallel' },
7
7
  { title: 'Interventions', detail: 'Run intervention-documentation-standard on all findings' },
8
8
  { title: 'Estimates', detail: 'Write Token-Estimate and Effort-Estimate files' },
@@ -66,17 +66,14 @@ const SCOPE_AGENT_MAP = {
66
66
  }
67
67
 
68
68
  const discovery = await agent(
69
- `Scan .claude/agents/ and list all assessment agents.
70
-
71
- An assessment agent is a .md file whose name or description contains one of: "assessment", "audit", "analysis".
72
- Exclude orchestrators (files containing "manager" in their name).
73
- Exclude remediation-only agents (files containing "refactoring", "hardening", "decomposition" in their name UNLESS they also contain "assessment" or "audit").
74
-
75
- For each matching agent, read its frontmatter and extract:
76
- - name (from the name: field in frontmatter)
77
- - description (from the description: field)
78
-
79
- Return { "assessment_agents": [ { "name": "...", "description": "..." }, ... ] }`,
69
+ `Run this command and parse its output:\n` +
70
+ `ai-toolkit list-assets --category agents --format json\n\n` +
71
+ `The command outputs a JSON array of installed agent file paths (e.g. [".claude/agents/foo.md", ...]).\n` +
72
+ `For each path in the array, read the file and extract the YAML frontmatter fields: name, description.\n\n` +
73
+ `An assessment agent is one whose file name or description contains one of: "assessment", "audit", "analysis".\n` +
74
+ `Exclude orchestrators (files containing "manager" in their name).\n` +
75
+ `Exclude remediation-only agents (files containing "refactoring", "hardening", "decomposition" in their name UNLESS they also contain "assessment" or "audit").\n\n` +
76
+ `Return { "assessment_agents": [ { "name": "...", "description": "..." }, ... ] }`,
80
77
  {
81
78
  label: 'discover-assessment-agents',
82
79
  phase: 'Discovery',