@dtlabs/ai-toolkit 0.1.6 → 0.2.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 (32) hide show
  1. package/.claude/agents/define-feature.md +1 -0
  2. package/.claude/agents/dependency-injection-refactoring.md +1 -0
  3. package/.claude/agents/dependency-supply-chain-security.md +1 -0
  4. package/.claude/agents/developer-backend.md +1 -0
  5. package/.claude/agents/developer-frontend.md +1 -0
  6. package/.claude/agents/developer-testing.md +1 -0
  7. package/.claude/agents/domain-model-refactoring.md +1 -0
  8. package/.claude/agents/god-class-decomposition.md +1 -0
  9. package/.claude/agents/init-agents-md.md +1 -0
  10. package/.claude/agents/install-toolkit.md +134 -3
  11. package/.claude/agents/intervention-documentation-standard.md +1 -0
  12. package/.claude/agents/review-solution.md +1 -0
  13. package/.claude/agents/security-hardening.md +1 -0
  14. package/.claude/commands/check-interventions.md +152 -0
  15. package/.claude/commands/next-intervention.md +113 -0
  16. package/.claude/settings.json +5 -0
  17. package/.claude/skills/assess-codebase/SKILL.md +139 -60
  18. package/.claude/skills/implement-feature/SKILL.md +208 -26
  19. package/.claude/workflows/am-phase1.js +304 -0
  20. package/.claude/workflows/am-phase2.js +166 -0
  21. package/.claude/workflows/pm-phase1.js +163 -0
  22. package/.claude/workflows/pm-phase2.js +108 -0
  23. package/.claude/workflows/pm-phase3.js +309 -0
  24. package/CHANGELOG.md +101 -3
  25. package/CLAUDE.md +34 -0
  26. package/README.md +26 -0
  27. package/bin/cli.js +64 -5
  28. package/docs/token-optimization-backlog.md +115 -0
  29. package/docs/token-optimization.md +274 -0
  30. package/package.json +1 -1
  31. package/.claude/agents/assessment-manager.md +0 -416
  32. package/.claude/agents/project-manager.md +0 -309
@@ -0,0 +1,304 @@
1
+ export const meta = {
2
+ name: 'am-phase1',
3
+ description: 'Assessment pipeline phase 1: discover assessment agents → scope filter → parallel assessment → intervention-documentation-standard → Token-Estimate → Effort-Estimate → findings_gate_payload.',
4
+ phases: [
5
+ { title: 'Discovery', detail: 'Scan .claude/agents/ for assessment agents, apply scope filter' },
6
+ { title: 'Assessment', detail: 'Dispatch all assessment agents in parallel' },
7
+ { title: 'Interventions', detail: 'Run intervention-documentation-standard on all findings' },
8
+ { title: 'Estimates', detail: 'Write Token-Estimate and Effort-Estimate files' },
9
+ ],
10
+ }
11
+
12
+ // args: "<target_path> [--scope=area1,area2] --prefix ASSESS-NNN [--force]"
13
+ const argStr = typeof args === 'string' ? args : '.'
14
+ const parts = argStr.trim().split(/\s+/)
15
+
16
+ let targetPath = '.'
17
+ let scope = null
18
+ let prefix = null
19
+ let force = false
20
+
21
+ for (const part of parts) {
22
+ if (part.startsWith('--scope=')) { scope = part.slice('--scope='.length).split(',').map(s => s.trim()) }
23
+ else if (part === '--force') { force = true }
24
+ else if (part === '--prefix') { /* next part is value */ }
25
+ else if (prefix === null && parts[parts.indexOf(part) - 1] === '--prefix') { prefix = part }
26
+ else if (!part.startsWith('--')) { targetPath = part }
27
+ }
28
+
29
+ // Re-parse prefix properly
30
+ const prefixMatch = argStr.match(/--prefix\s+(\S+)/)
31
+ if (prefixMatch) prefix = prefixMatch[1]
32
+
33
+ if (!prefix) prefix = 'ASSESS-001'
34
+
35
+ const outputDir = `docs/assessments/${prefix}`
36
+
37
+ // ── Discovery ─────────────────────────────────────────────────────────────────
38
+ phase('Discovery')
39
+
40
+ const DISCOVERY_SCHEMA = {
41
+ type: 'object',
42
+ properties: {
43
+ assessment_agents: {
44
+ type: 'array',
45
+ items: {
46
+ type: 'object',
47
+ properties: {
48
+ name: { type: 'string' },
49
+ description: { type: 'string' },
50
+ },
51
+ required: ['name'],
52
+ },
53
+ },
54
+ },
55
+ required: ['assessment_agents'],
56
+ }
57
+
58
+ const SCOPE_AGENT_MAP = {
59
+ architecture: ['layered-architecture-assessment'],
60
+ security: ['security-hardening'],
61
+ quality: ['generic-software-assessment'],
62
+ concurrency: ['concurrency-safety-assessment'],
63
+ devops: [],
64
+ 'domain-model': ['domain-model-refactoring'],
65
+ dependencies: ['dependency-supply-chain-security'],
66
+ }
67
+
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": "..." }, ... ] }`,
80
+ {
81
+ label: 'discover-assessment-agents',
82
+ phase: 'Discovery',
83
+ schema: DISCOVERY_SCHEMA,
84
+ }
85
+ )
86
+
87
+ let assessmentAgents = discovery.assessment_agents || []
88
+
89
+ // Apply scope filter
90
+ if (scope && scope.length > 0) {
91
+ const allowed = new Set(scope.flatMap(s => SCOPE_AGENT_MAP[s] || []))
92
+ assessmentAgents = assessmentAgents.filter(a => allowed.has(a.name))
93
+ log(`Scope filter applied (${scope.join(', ')}): ${assessmentAgents.length} agents selected`)
94
+ } else {
95
+ log(`No scope filter: ${assessmentAgents.length} assessment agents discovered`)
96
+ }
97
+
98
+ if (assessmentAgents.length === 0) {
99
+ log('WARNING: No assessment agents to run — proceeding with empty assessment')
100
+ }
101
+
102
+ // ── Assessment (parallel) ─────────────────────────────────────────────────────
103
+ phase('Assessment')
104
+
105
+ log(`Dispatching ${assessmentAgents.length} assessment agents in parallel`)
106
+
107
+ const tokenLedger = []
108
+ const beforeAssess = budget.spent()
109
+
110
+ const assessmentResults = await parallel(
111
+ assessmentAgents.map(a => () =>
112
+ agent(
113
+ `${targetPath} --prefix ${prefix} --output-dir ${outputDir}`,
114
+ {
115
+ agentType: a.name,
116
+ label: a.name,
117
+ phase: 'Assessment',
118
+ }
119
+ ).then(result => ({ agent: a.name, success: true, result }))
120
+ .catch(err => ({ agent: a.name, success: false, error: String(err) }))
121
+ )
122
+ )
123
+
124
+ const assessTokens = budget.spent() - beforeAssess
125
+ log(`Assessment phase complete — ${assessmentResults.filter(r => r?.success).length}/${assessmentAgents.length} succeeded | total phase delta: ${assessTokens} tokens`)
126
+
127
+ // Proportional token distribution across agents
128
+ const perAgentTokens = assessmentAgents.length > 0
129
+ ? Math.round(assessTokens / assessmentAgents.length)
130
+ : 0
131
+
132
+ for (const r of assessmentResults.filter(Boolean)) {
133
+ tokenLedger.push({
134
+ agent: r.agent,
135
+ model: 'sonnet',
136
+ phase_delta_tokens: perAgentTokens,
137
+ note: 'proportional distribution of assessment phase total',
138
+ success: r.success,
139
+ error: r.error || null,
140
+ })
141
+ }
142
+
143
+ const failedAgents = assessmentResults.filter(r => r && !r.success)
144
+ const completedAgents = assessmentResults.filter(r => r && r.success)
145
+
146
+ // ── Interventions ─────────────────────────────────────────────────────────────
147
+ phase('Interventions')
148
+
149
+ log('Running intervention-documentation-standard')
150
+
151
+ const beforeInt = budget.spent()
152
+ await agent(
153
+ `--prefix ${prefix} --output-dir ${outputDir} --target ${targetPath}`,
154
+ {
155
+ agentType: 'intervention-documentation-standard',
156
+ label: 'intervention-documentation-standard',
157
+ phase: 'Interventions',
158
+ }
159
+ )
160
+ const intTokens = budget.spent() - beforeInt
161
+ tokenLedger.push({
162
+ agent: 'intervention-documentation-standard',
163
+ model: 'sonnet',
164
+ phase_delta_tokens: intTokens,
165
+ note: 'exact phase delta',
166
+ })
167
+ log(`intervention-documentation-standard done — phase delta: ${intTokens} tokens`)
168
+
169
+ // ── Estimates ─────────────────────────────────────────────────────────────────
170
+ phase('Estimates')
171
+
172
+ const ESTIMATES_SCHEMA = {
173
+ type: 'object',
174
+ properties: {
175
+ severity_counts: {
176
+ type: 'object',
177
+ properties: {
178
+ CRITICAL: { type: 'number' },
179
+ HIGH: { type: 'number' },
180
+ MEDIUM: { type: 'number' },
181
+ LOW: { type: 'number' },
182
+ },
183
+ },
184
+ total_interventions: { type: 'number' },
185
+ interventions_index_path: { type: 'string' },
186
+ token_estimate_path: { type: 'string' },
187
+ effort_estimate_path: { type: 'string' },
188
+ remediation_hours_est: { type: 'number' },
189
+ },
190
+ required: ['severity_counts', 'total_interventions'],
191
+ }
192
+
193
+ const totalPhaseTokens = tokenLedger.reduce((s, e) => s + (e.phase_delta_tokens || 0), 0)
194
+
195
+ const estimatesResult = await agent(
196
+ `You are the estimates recorder for the assessment pipeline.
197
+
198
+ Prefix: ${prefix}
199
+ Output directory: ${outputDir}
200
+ Assessment agents that ran: ${assessmentAgents.map(a => a.name).join(', ') || 'none'}
201
+ Failed agents: ${failedAgents.map(r => r.agent).join(', ') || 'none'}
202
+
203
+ Token ledger (phase-level measurements):
204
+ ${JSON.stringify(tokenLedger, null, 2)}
205
+
206
+ TASK 1 — Read ${prefix}-Interventions-Index.md from ${outputDir}.
207
+ Count rows by Criticality column: CRITICAL, HIGH, MEDIUM, LOW.
208
+ Compute remediation_hours_est = CRITICAL×8 + HIGH×4 + MEDIUM×2 + LOW×1.
209
+
210
+ TASK 2 — Write Token-Estimate file at ${outputDir}/${prefix}-Token-Estimate.md.
211
+
212
+ Use this template (fill in all values):
213
+
214
+ # Token Estimate — ${prefix} — Assessment Pipeline
215
+
216
+ > Phase totals are exact measurements from budget tracking.
217
+ > Per-agent values within the assessment phase are proportional distributions of the phase total.
218
+ > Pricing model: docs/pricing.md (80% input / 20% output split).
219
+ > ⚠️ Per-agent values marked (proportional) are estimated distributions, not exact measurements.
220
+
221
+ ## Estimation model
222
+
223
+ | Parameter | Value |
224
+ |-----------|-------|
225
+ | Avg chars per token | 4 |
226
+ | Sonnet system prompt | ~3,000 tokens |
227
+ | Base overhead per call | ~5,000 tokens |
228
+ | Input/output split | 80% / 20% |
229
+
230
+ ## Assessment agents (Phase 1)
231
+
232
+ | Agent | Model | Phase delta tokens | Note | Status |
233
+ |-------|-------|--------------------|------|--------|
234
+ {one row per agent from token ledger — use phase_delta_tokens and note fields}
235
+
236
+ ## Phase subtotals
237
+
238
+ | Phase | Tokens (exact) |
239
+ |-------|---------------|
240
+ | Assessment agents (parallel) | {sum of assessment agent phase_delta_tokens} |
241
+ | intervention-documentation-standard | {its phase_delta_tokens} |
242
+ | Total am-phase1 | ${totalPhaseTokens} |
243
+
244
+ ## Grand total
245
+
246
+ > Updated at pipeline end by assess-codebase skill.
247
+
248
+ | Metric | Value |
249
+ |--------|-------|
250
+ | Total tokens (am-phase1) | ${totalPhaseTokens} (exact) |
251
+
252
+ TASK 3 — Write Effort-Estimate file at ${outputDir}/${prefix}-Effort-Estimate.md.
253
+
254
+ Use this template:
255
+
256
+ # Effort Estimate — ${prefix} — Assessment Pipeline
257
+
258
+ > Wall-clock effort tracking for the assessment pipeline.
259
+ > Remediation effort: CRITICAL=8h, HIGH=4h, MEDIUM=2h, LOW=1h (human hours, sequential).
260
+
261
+ ## Assessment phase
262
+
263
+ | Agent | Status |
264
+ |-------|--------|
265
+ {one row per assessment agent}
266
+ | intervention-documentation-standard | complete |
267
+
268
+ ## Remediation effort estimate
269
+
270
+ | Severity | Count | Rate | Subtotal |
271
+ |----------|-------|------|---------|
272
+ | CRITICAL | {count} | 8h | {subtotal}h |
273
+ | HIGH | {count} | 4h | {subtotal}h |
274
+ | MEDIUM | {count} | 2h | {subtotal}h |
275
+ | LOW | {count} | 1h | {subtotal}h |
276
+ | **Total** | **{total}** | — | **{total_hours}h** |
277
+
278
+ Human sequential total: {total_hours}h
279
+
280
+ Return the structured metrics.`,
281
+ {
282
+ label: 'write-estimates',
283
+ phase: 'Estimates',
284
+ schema: ESTIMATES_SCHEMA,
285
+ }
286
+ )
287
+
288
+ log(`Estimates written — ${estimatesResult.total_interventions} interventions: CRITICAL=${estimatesResult.severity_counts?.CRITICAL} HIGH=${estimatesResult.severity_counts?.HIGH} MEDIUM=${estimatesResult.severity_counts?.MEDIUM} LOW=${estimatesResult.severity_counts?.LOW}`)
289
+ log(`Remediation estimate: ${estimatesResult.remediation_hours_est}h human sequential`)
290
+
291
+ return {
292
+ prefix,
293
+ output_dir: outputDir,
294
+ target_path: targetPath,
295
+ assessment_summaries: completedAgents.map(r => ({ agent: r.agent })),
296
+ interventions_index_path: estimatesResult.interventions_index_path || `${outputDir}/${prefix}-Interventions-Index.md`,
297
+ severity_counts: estimatesResult.severity_counts || { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 },
298
+ total_interventions: estimatesResult.total_interventions || 0,
299
+ remediation_hours_est: estimatesResult.remediation_hours_est || 0,
300
+ effort_estimate_path: estimatesResult.effort_estimate_path || `${outputDir}/${prefix}-Effort-Estimate.md`,
301
+ token_estimate_path: estimatesResult.token_estimate_path || `${outputDir}/${prefix}-Token-Estimate.md`,
302
+ token_ledger: tokenLedger,
303
+ errors: failedAgents.map(r => ({ agent: r.agent, error: r.error })),
304
+ }
@@ -0,0 +1,166 @@
1
+ export const meta = {
2
+ name: 'am-phase2',
3
+ description: 'Assessment pipeline phase 2: write Approvals file → update Assessment Registry → return summary. Runs after Findings Gate is completed in the main loop.',
4
+ phases: [
5
+ { title: 'Approvals', detail: 'Write {PREFIX}-Approvals.md with gate acknowledgement and flagged interventions' },
6
+ { title: 'Registry', detail: 'Append row to docs/assessments/registry.md' },
7
+ { title: 'Summary', detail: 'Build and return run summary' },
8
+ ],
9
+ }
10
+
11
+ // args: "--prefix ASSESS-NNN --output-dir <path> --flagged INT-001,INT-003 --ack \"text\""
12
+ const argStr = typeof args === 'string' ? args : ''
13
+
14
+ const prefixMatch = argStr.match(/--prefix\s+(\S+)/)
15
+ const outputDirMatch = argStr.match(/--output-dir\s+(\S+)/)
16
+ const flaggedMatch = argStr.match(/--flagged\s+([^\s"]+)/)
17
+ const ackMatch = argStr.match(/--ack\s+"([^"]*)"/)
18
+
19
+ const prefix = prefixMatch ? prefixMatch[1] : 'ASSESS-001'
20
+ const outputDir = outputDirMatch ? outputDirMatch[1] : `docs/assessments/${prefix}`
21
+ const flaggedRaw = flaggedMatch ? flaggedMatch[1] : 'none'
22
+ const ackText = ackMatch ? ackMatch[1] : 'Acknowledged'
23
+
24
+ const flaggedIds = flaggedRaw.toLowerCase() === 'none'
25
+ ? []
26
+ : flaggedRaw.split(',').map(s => s.trim().toUpperCase())
27
+
28
+ // ── Approvals ─────────────────────────────────────────────────────────────────
29
+ phase('Approvals')
30
+
31
+ const APPROVALS_SCHEMA = {
32
+ type: 'object',
33
+ properties: {
34
+ approvals_path: { type: 'string' },
35
+ written: { type: 'boolean' },
36
+ },
37
+ required: ['approvals_path', 'written'],
38
+ }
39
+
40
+ log(`Writing Approvals file for ${prefix} — flagged: ${flaggedIds.join(', ') || 'none'}`)
41
+
42
+ const approvalsResult = await agent(
43
+ `Write the Approvals file for assessment ${prefix}.
44
+
45
+ Output path: ${outputDir}/${prefix}-Approvals.md
46
+
47
+ Read the Interventions Index at: ${outputDir}/${prefix}-Interventions-Index.md
48
+ Extract all INT-NNN identifiers and their titles.
49
+
50
+ Flagged interventions (to mark as Yes): ${flaggedIds.join(', ') || 'none'}
51
+ Acknowledgement text: "${ackText}"
52
+ Today's date: use the current date in YYYY-MM-DD format.
53
+
54
+ Write the file with this exact structure:
55
+
56
+ # Assessment Approvals — ${prefix}
57
+
58
+ ## Findings Gate Acknowledgement
59
+
60
+ | Field | Value |
61
+ |-------|-------|
62
+ | Acknowledged by | Toolkit user |
63
+ | Date | {today} |
64
+ | Acknowledgement | ${ackText} |
65
+
66
+ ## Interventions Flagged for Feature Delivery
67
+
68
+ | Intervention | Flagged | Date | Notes |
69
+ |---|---|---|---|
70
+ {one row per intervention: | INT-NNN — {title} | Yes/No | {today} | — or "Not selected" |}
71
+
72
+ Rules:
73
+ - Mark "Yes" for interventions whose ID is in: ${flaggedIds.join(', ') || '(none)'}
74
+ - Mark "No" for all others, Notes = "Not selected"
75
+ - "Flagged" column values must be exactly "Yes" or "No" (case-sensitive)
76
+
77
+ After writing, verify the file exists and is non-empty.
78
+ Return { "approvals_path": "<full path>", "written": true }.`,
79
+ {
80
+ label: 'write-approvals',
81
+ phase: 'Approvals',
82
+ schema: APPROVALS_SCHEMA,
83
+ }
84
+ )
85
+
86
+ log(`Approvals written: ${approvalsResult.approvals_path}`)
87
+
88
+ // ── Registry ──────────────────────────────────────────────────────────────────
89
+ phase('Registry')
90
+
91
+ const REGISTRY_SCHEMA = {
92
+ type: 'object',
93
+ properties: {
94
+ registry_result: { type: 'string' },
95
+ },
96
+ required: ['registry_result'],
97
+ }
98
+
99
+ log(`Updating Assessment Registry`)
100
+
101
+ const registryResult = await agent(
102
+ `Update the Assessment Registry at: docs/assessments/registry.md
103
+
104
+ Prefix: ${prefix}
105
+ Output directory: ${outputDir}
106
+
107
+ STEP 1 — Read severity counts from ${outputDir}/${prefix}-Interventions-Index.md.
108
+ Count rows by Criticality column: CRITICAL, HIGH, MEDIUM, LOW, compute total.
109
+
110
+ STEP 2 — Count flagged interventions from ${outputDir}/${prefix}-Approvals.md.
111
+ Find the "## Interventions Flagged for Feature Delivery" section.
112
+ Count rows where the Flagged column value is exactly "Yes" (case-sensitive).
113
+
114
+ STEP 3 — Build the registry row:
115
+ | {today-YYYY-MM-DD} | [${prefix}](${prefix}/) | {total} | {CRITICAL} | {HIGH} | {MEDIUM} | {LOW} | {flagged_count} |
116
+
117
+ STEP 4 — Write to docs/assessments/registry.md:
118
+ - If the file does not exist: create it with header + the new row:
119
+ # Assessment Registry
120
+ | Date | Prefix | Total | CRITICAL | HIGH | MEDIUM | LOW | Flagged |
121
+ |------|--------|-------|----------|------|--------|-----|---------|
122
+ {row}
123
+ - If the file exists: read it, strip trailing whitespace, append the new row, write back.
124
+
125
+ STEP 5 — Verify the file is readable after writing.
126
+
127
+ Return { "registry_result": "docs/assessments/registry.md [created]" }
128
+ or { "registry_result": "docs/assessments/registry.md [updated — row N appended]" }
129
+ or { "registry_result": "docs/assessments/registry.md [ERROR — {reason}]" }`,
130
+ {
131
+ label: 'update-registry',
132
+ phase: 'Registry',
133
+ schema: REGISTRY_SCHEMA,
134
+ }
135
+ )
136
+
137
+ log(`Registry: ${registryResult.registry_result}`)
138
+
139
+ // ── Summary ───────────────────────────────────────────────────────────────────
140
+ phase('Summary')
141
+
142
+ const summaryText = [
143
+ `Assessment Manager — Run Summary`,
144
+ `─────────────────────────────────────────────────────`,
145
+ `Target: ${argStr.split('--')[0].trim() || '.'} | Prefix: ${prefix}`,
146
+ `─────────────────────────────────────────────────────`,
147
+ `Approvals: ${approvalsResult.approvals_path}`,
148
+ `Registry: ${registryResult.registry_result}`,
149
+ `Effort Estimate: ${outputDir}/${prefix}-Effort-Estimate.md`,
150
+ `Token Estimate: ${outputDir}/${prefix}-Token-Estimate.md`,
151
+ `Process log: ${outputDir}/${prefix}-process-log.txt`,
152
+ `─────────────────────────────────────────────────────`,
153
+ `Flagged interventions (${flaggedIds.length}): ${flaggedIds.join(', ') || 'none'}`,
154
+ `Flagged interventions can be actioned via /define-feature`,
155
+ `referencing the INT-NNN document.`,
156
+ `─────────────────────────────────────────────────────`,
157
+ ].join('\n')
158
+
159
+ log('am-phase2 complete')
160
+
161
+ return {
162
+ approvals_path: approvalsResult.approvals_path,
163
+ registry_updated: registryResult.registry_result,
164
+ flagged_count: flaggedIds.length,
165
+ summary: summaryText,
166
+ }
@@ -0,0 +1,163 @@
1
+ export const meta = {
2
+ name: 'pm-phase1',
3
+ description: 'Feature delivery phase 1: discovery → generate-requirements → generate-tech-spec → validate-feature-docs (revision loop) → Effort-Estimate stub. Returns gate1_payload.',
4
+ phases: [
5
+ { title: 'Discovery', detail: 'Read feature.md, check existing outputs' },
6
+ { title: 'Requirements', detail: 'Run generate-requirements' },
7
+ { title: 'Tech-Spec', detail: 'Run generate-tech-spec' },
8
+ { title: 'Validation', detail: 'Run validate-feature-docs (revision loop, max 3×)' },
9
+ ],
10
+ }
11
+
12
+ // ── Parse args ────────────────────────────────────────────────────────────────
13
+ // args is the raw prompt string: "<path-to-feature.md> [--force]"
14
+ const featurePath = args.split(/\s+/)[0]
15
+ const force = typeof args === 'string' && args.includes('--force')
16
+
17
+ // ── Discovery ─────────────────────────────────────────────────────────────────
18
+ phase('Discovery')
19
+
20
+ const discoveryResult = await agent(
21
+ `You are a discovery agent for the feature delivery pipeline.
22
+
23
+ Read the feature.md file at: ${featurePath}
24
+
25
+ Extract:
26
+ 1. The feature PREFIX from the directory name (pattern [A-Z]+-[0-9]+, e.g. FTR-009)
27
+ 2. The feature directory path (parent of feature.md)
28
+ 3. Whether these files exist and are NOT stale vs feature.md (stale = older mtime):
29
+ - {PREFIX}-Requirements.md
30
+ - {PREFIX}-Tech-Spec.md
31
+ - {PREFIX}-Validation-Report.md
32
+
33
+ Return a JSON object:
34
+ {
35
+ "prefix": "FTR-009",
36
+ "feature_dir": "internal_docs/features/FTR-009-workflow-orchestrators",
37
+ "needs_requirements": true,
38
+ "needs_tech_spec": true,
39
+ "needs_validation": true,
40
+ "force": ${force}
41
+ }
42
+
43
+ If force=true, set all needs_* to true regardless of file state.
44
+ If feature.md does not exist, return { "error": "feature.md not found: ${featurePath}" }.`,
45
+ {
46
+ label: 'discovery',
47
+ phase: 'Discovery',
48
+ schema: {
49
+ type: 'object',
50
+ properties: {
51
+ prefix: { type: 'string' },
52
+ feature_dir: { type: 'string' },
53
+ needs_requirements: { type: 'boolean' },
54
+ needs_tech_spec: { type: 'boolean' },
55
+ needs_validation: { type: 'boolean' },
56
+ force: { type: 'boolean' },
57
+ error: { type: 'string' },
58
+ },
59
+ required: ['prefix', 'feature_dir'],
60
+ },
61
+ }
62
+ )
63
+
64
+ if (discoveryResult.error) {
65
+ return { error: discoveryResult.error }
66
+ }
67
+
68
+ const { prefix, feature_dir } = discoveryResult
69
+
70
+ log(`Prefix: ${prefix} | Dir: ${feature_dir}`)
71
+ log(`needs_requirements=${discoveryResult.needs_requirements} needs_tech_spec=${discoveryResult.needs_tech_spec}`)
72
+
73
+ // ── generate-requirements ─────────────────────────────────────────────────────
74
+ phase('Requirements')
75
+
76
+ const tokenLedger = []
77
+ const errors = []
78
+
79
+ if (discoveryResult.needs_requirements) {
80
+ const beforeReq = budget.spent()
81
+ const reqResult = await agent(featurePath, { agentType: 'generate-requirements', label: 'generate-requirements', phase: 'Requirements' })
82
+ const reqTokens = budget.spent() - beforeReq
83
+ tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
84
+ log(`generate-requirements done — phase delta: ${reqTokens} tokens`)
85
+ } else {
86
+ log('generate-requirements: fresh — skipped')
87
+ }
88
+
89
+ // ── generate-tech-spec ────────────────────────────────────────────────────────
90
+ phase('Tech-Spec')
91
+
92
+ if (discoveryResult.needs_tech_spec) {
93
+ const beforeSpec = budget.spent()
94
+ const specResult = await agent(featurePath, { agentType: 'generate-tech-spec', label: 'generate-tech-spec', phase: 'Tech-Spec' })
95
+ const specTokens = budget.spent() - beforeSpec
96
+ tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
97
+ log(`generate-tech-spec done — phase delta: ${specTokens} tokens`)
98
+ } else {
99
+ log('generate-tech-spec: fresh — skipped')
100
+ }
101
+
102
+ // ── validate-feature-docs (revision loop, max 3 cycles) ──────────────────────
103
+ phase('Validation')
104
+
105
+ let validationSummary = 'skipped'
106
+ const MAX_CYCLES = 3
107
+
108
+ for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
109
+ log(`validate-feature-docs cycle ${cycle}`)
110
+ const beforeVal = budget.spent()
111
+ const valResult = await agent(featurePath, { agentType: 'validate-feature-docs', label: `validate-feature-docs (cycle ${cycle})`, phase: 'Validation' })
112
+ const valTokens = budget.spent() - beforeVal
113
+ tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
114
+
115
+ // valResult is the agent's text output — check for gap indicators
116
+ const resultText = typeof valResult === 'string' ? valResult : JSON.stringify(valResult)
117
+ const hasGaps = resultText.includes('MISSING:') || resultText.toLowerCase().includes('gaps found')
118
+
119
+ if (!hasGaps) {
120
+ validationSummary = `0 gaps (clean on cycle ${cycle})`
121
+ log(`Validation clean on cycle ${cycle}`)
122
+ break
123
+ }
124
+
125
+ log(`Validation cycle ${cycle}: gaps found`)
126
+
127
+ if (cycle < MAX_CYCLES) {
128
+ // Re-run failing docs before next validation cycle
129
+ if (resultText.includes('Requirements')) {
130
+ await agent(featurePath, { agentType: 'generate-requirements', label: `generate-requirements (revision ${cycle})`, phase: 'Validation' })
131
+ }
132
+ if (resultText.includes('Tech-Spec')) {
133
+ await agent(featurePath, { agentType: 'generate-tech-spec', label: `generate-tech-spec (revision ${cycle})`, phase: 'Validation' })
134
+ }
135
+ } else {
136
+ validationSummary = 'gaps remain after 3 cycles'
137
+ errors.push('validate-feature-docs: gaps remain after 3 revision cycles')
138
+ }
139
+ }
140
+
141
+ // ── Build gate1_payload ───────────────────────────────────────────────────────
142
+ const gate1Payload = {
143
+ prefix,
144
+ feature_dir,
145
+ feature_path: featurePath,
146
+ requirements: {
147
+ path: `${feature_dir}/${prefix}-Requirements.md`,
148
+ summary: `${prefix}-Requirements.md — generated`,
149
+ },
150
+ tech_spec: {
151
+ path: `${feature_dir}/${prefix}-Tech-Spec.md`,
152
+ summary: `${prefix}-Tech-Spec.md — generated`,
153
+ },
154
+ validation: {
155
+ path: `${feature_dir}/${prefix}-Validation-Report.md`,
156
+ summary: validationSummary,
157
+ },
158
+ token_ledger: tokenLedger,
159
+ errors,
160
+ }
161
+
162
+ log(`pm-phase1 complete — gate1_payload ready`)
163
+ return gate1Payload