@dtlabs/ai-toolkit 0.13.0 → 0.14.1

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.
@@ -1,364 +1,51 @@
1
1
  export const meta = {
2
+ controlPlaneVersion: 1,
2
3
  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
- // ── Tier 2 resolution guard ───────────────────────────────────────────────────
13
- // All worker agents must be resolved via the CLI before dispatch.
14
- // No direct require('lib/agent-registry') — resolution goes through the CLI facade.
15
- const RESOLVE_SCHEMA = {
16
- type: 'object',
17
- properties: {
18
- nativeName: { type: 'string' },
19
- status: { type: 'string' },
20
- agentId: { type: 'string' },
21
- exitNonZero: { type: 'boolean' },
22
- error: { type: 'string' },
23
- },
24
- }
25
-
26
- async function resolveAgentTier2(agentId, label, phaseName) {
27
- const result = await agent(
28
- `Run this command via Bash:\n\nai-toolkit agents resolve --project . --id ${agentId} --require-verified\n\nIf the command exits 0: parse the JSON from stdout and return it with exitNonZero: false.\nIf the command exits non-zero: return {"exitNonZero": true, "error": "<stderr text>", "agentId": "${agentId}"}.`,
29
- { label, phase: phaseName, model: 'haiku', schema: RESOLVE_SCHEMA }
30
- )
31
- if (!result || result.exitNonZero || !result.nativeName) {
32
- throw new Error(`HARD STOP — Tier 2 resolution failed for ${agentId}: ${result && result.error ? result.error : 'no nativeName returned'}`)
33
- }
34
- return result.nativeName
35
- }
36
-
37
- // ── Tier 3 — self-ledger helper ───────────────────────────────────────────────
38
- const SELF_LEDGER_SCHEMA = {
39
- type: 'object',
40
- properties: {
41
- exitCode: { type: 'number' },
42
- stdout: { type: 'string' },
43
- stderr: { type: 'string' },
44
- },
45
- required: ['exitCode'],
46
- }
47
-
48
- async function selfLedgerOp(cmd, label, phaseName) {
49
- const result = await agent(
50
- `Run this shell command via Bash and return the exit code as structured output.\n\nCommand: ${cmd}\n\nCapture: exitCode (integer), stdout (string), stderr (string). Return all three.`,
51
- { label, phase: phaseName, model: 'haiku', schema: SELF_LEDGER_SCHEMA }
52
- )
53
- const status = (result && typeof result.exitCode === 'number') ? result.exitCode : 1
54
- if (status !== 0) {
55
- throw new Error(`HARD STOP — self-ledger operation failed. Exit code: ${status}. Command: ${cmd}`)
4
+ description: 'Deterministic documentation workflow; returns Gate 1 evidence.',
5
+ phases: [{ title: 'Discovery' }, { title: 'Requirements' }, { title: 'Tech-Spec' }, { title: 'Validation' }],
6
+ }
7
+
8
+ const c = control
9
+ const { featurePath, featureDir, prefix } = c.feature(args[0])
10
+ const force = args.includes('--force')
11
+ return c.run(meta.name, featureDir, prefix, async () => {
12
+ const file = suffix => `${featureDir}/${prefix}-${suffix}.md`
13
+ const fresh = (output, inputs = [featurePath]) => c.exists(output) && c.read(output).trim().length > 0 && inputs.every(input => c.exists(input) && c.mtime(output) >= c.mtime(input))
14
+ const reqId = 'gaia.agent.planner.requirements'
15
+ const specId = 'gaia.agent.planner.tech-spec'
16
+ const valId = 'gaia.agent.planner.validate-feature-docs'
17
+ let changed = false
18
+ if (force || !fresh(file('Requirements'))) {
19
+ await c.worker(reqId, featurePath)
20
+ if (!c.exists(file('Requirements'))) throw new Error('Requirements output missing')
21
+ changed = true
56
22
  }
57
- }
58
-
59
- // ── Parse args ────────────────────────────────────────────────────────────────
60
- // args is the raw prompt string: "<path-to-feature.md> [--force]"
61
- const featurePath = args.split(/\s+/)[0]
62
- const force = typeof args === 'string' && args.includes('--force')
63
-
64
- // Derive prefix and feature dir early (before Discovery agent) for self-registration
65
- const featureDirEarly = featurePath.replace(/[/\\][^/\\]+$/, '')
66
- const prefixEarlyMatch = featureDirEarly.match(/([A-Z]+-\d+)/)
67
- const prefixEarly = prefixEarlyMatch ? prefixEarlyMatch[1] : 'FTR-000'
68
-
69
- // ── Tier 3 self-registration (open BEFORE any main logic, fail-closed) ────────
70
- await selfLedgerOp(
71
- `ai-toolkit ledger open --prefix ${prefixEarly} --agent pm-phase1:self --phase phase1 --dir "${featureDirEarly}" --attempt 1`,
72
- 'ledger-open-pm-phase1-self', 'Discovery'
73
- )
74
-
75
- // ── Discovery ─────────────────────────────────────────────────────────────────
76
- phase('Discovery')
77
-
78
- const discoveryResult = await agent(
79
- `You are a discovery agent for the feature delivery pipeline.
80
-
81
- Read the feature.md file at: ${featurePath}
82
-
83
- Extract:
84
- 1. The feature PREFIX from the directory name (pattern [A-Z]+-[0-9]+, e.g. FTR-009)
85
- 2. The feature directory path (parent of feature.md)
86
- 3. Whether these files exist and are NOT stale vs feature.md (stale = older mtime):
87
- - {PREFIX}-Requirements.md
88
- - {PREFIX}-Tech-Spec.md
89
- - {PREFIX}-Validation-Report.md
90
-
91
- Return a JSON object:
92
- {
93
- "prefix": "FTR-009",
94
- "feature_dir": "internal_docs/features/FTR-009-workflow-orchestrators",
95
- "needs_requirements": true,
96
- "needs_tech_spec": true,
97
- "needs_validation": true,
98
- "force": ${force}
99
- }
100
-
101
- If force=true, set all needs_* to true regardless of file state.
102
- If feature.md does not exist, return { "error": "feature.md not found: ${featurePath}" }.`,
103
- {
104
- label: 'discovery',
105
- phase: 'Discovery',
106
- schema: {
107
- type: 'object',
108
- properties: {
109
- prefix: { type: 'string' },
110
- feature_dir: { type: 'string' },
111
- needs_requirements: { type: 'boolean' },
112
- needs_tech_spec: { type: 'boolean' },
113
- needs_validation: { type: 'boolean' },
114
- force: { type: 'boolean' },
115
- error: { type: 'string' },
116
- },
117
- required: ['prefix', 'feature_dir'],
118
- },
23
+ if (force || changed || !fresh(file('Tech-Spec'), [featurePath, file('Requirements')])) {
24
+ await c.worker(specId, featurePath)
25
+ if (!c.exists(file('Tech-Spec'))) throw new Error('Tech-Spec output missing')
26
+ changed = true
119
27
  }
120
- )
121
-
122
- if (discoveryResult.error) {
123
- return { error: discoveryResult.error }
124
- }
125
-
126
- const { prefix, feature_dir } = discoveryResult
127
-
128
- log(`Prefix: ${prefix} | Dir: ${feature_dir}`)
129
- log(`needs_requirements=${discoveryResult.needs_requirements} needs_tech_spec=${discoveryResult.needs_tech_spec}`)
130
-
131
- // ── Ensure ledger file exists (US-02-T02) ─────────────────────────────────────
132
- // If define-feature was not used, the ledger file will not exist yet.
133
- // Touch it with an empty array so the ledger facade can always assume a valid base.
134
- const ledgerFilePath = `${feature_dir}/${prefix}-token-ledger.json`
135
- await agent(
136
- `Check whether the file ${ledgerFilePath} exists.\n\n1. Try to read the file using the Read tool.\n2. If the file does NOT exist: write it now using the Write tool with contents: []\n3. If the file already exists and is a valid JSON array: do nothing.\n4. If the file exists but is not valid JSON: overwrite it with: []\n5. Return no output.`,
137
- { label: 'ensure-ledger', phase: 'Discovery', model: 'haiku' }
138
- )
139
- log(`Ledger ensured at ${ledgerFilePath}`)
140
-
141
- // ── generate-requirements ─────────────────────────────────────────────────────
142
- phase('Requirements')
143
-
144
- const tokenLedger = []
145
- const errors = []
146
-
147
- if (discoveryResult.needs_requirements) {
148
- const reqNativeName = await resolveAgentTier2('gaia.agent.planner.requirements', 'resolve-generate-requirements', 'Requirements')
149
- const reqKey = 'generate-requirements:phase1'
150
- await agent(
151
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${feature_dir} --prefix ${prefix} --agent ${reqKey} --phase phase1 --model haiku --attempt 1\n\nReturn no output.`,
152
- { label: 'ledger-open-requirements', phase: 'Requirements', model: 'haiku' }
153
- )
154
- const beforeReq = budget.spent()
155
- const reqResult = await agent(featurePath, { agentType: reqNativeName, label: 'generate-requirements', phase: 'Requirements' })
156
- const reqTokens = budget.spent() - beforeReq
157
- await agent(
158
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${feature_dir} --prefix ${prefix} --agent ${reqKey} --tokens ${reqTokens} --attempt 1\n\nReturn no output.`,
159
- { label: 'ledger-close-requirements', phase: 'Requirements', model: 'haiku' }
160
- )
161
- tokenLedger.push({ agent: 'generate-requirements', model: 'haiku', phase_delta_tokens: reqTokens })
162
- log(`generate-requirements done — phase delta: ${reqTokens} tokens`)
163
- } else {
164
- log('generate-requirements: fresh — skipped')
165
- }
166
-
167
- // ── generate-tech-spec ────────────────────────────────────────────────────────
168
- phase('Tech-Spec')
169
-
170
- if (discoveryResult.needs_tech_spec) {
171
- const specNativeName = await resolveAgentTier2('gaia.agent.planner.tech-spec', 'resolve-generate-tech-spec', 'Tech-Spec')
172
- const specKey = 'generate-tech-spec:phase1'
173
- await agent(
174
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${feature_dir} --prefix ${prefix} --agent ${specKey} --phase phase1 --model haiku --attempt 1\n\nReturn no output.`,
175
- { label: 'ledger-open-tech-spec', phase: 'Tech-Spec', model: 'haiku' }
176
- )
177
- const beforeSpec = budget.spent()
178
- const specResult = await agent(featurePath, { agentType: specNativeName, label: 'generate-tech-spec', phase: 'Tech-Spec' })
179
- const specTokens = budget.spent() - beforeSpec
180
- await agent(
181
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${feature_dir} --prefix ${prefix} --agent ${specKey} --tokens ${specTokens} --attempt 1\n\nReturn no output.`,
182
- { label: 'ledger-close-tech-spec', phase: 'Tech-Spec', model: 'haiku' }
183
- )
184
- tokenLedger.push({ agent: 'generate-tech-spec', model: 'haiku', phase_delta_tokens: specTokens })
185
- log(`generate-tech-spec done — phase delta: ${specTokens} tokens`)
186
- } else {
187
- log('generate-tech-spec: fresh — skipped')
188
- }
189
-
190
- // ── validate-feature-docs (revision loop, max 3 cycles) ──────────────────────
191
- phase('Validation')
192
-
193
- // Resolve all validation-phase agents once before the loop (Tier 2 guard)
194
- const valNativeName = await resolveAgentTier2('gaia.agent.planner.validate-feature-docs', 'resolve-validate-feature-docs', 'Validation')
195
- const reqRevNativeName = await resolveAgentTier2('gaia.agent.planner.requirements', 'resolve-generate-requirements-rev', 'Validation')
196
- const specRevNativeName = await resolveAgentTier2('gaia.agent.planner.tech-spec', 'resolve-generate-tech-spec-rev', 'Validation')
197
-
198
- let validationSummary = 'skipped'
199
- let lastValText = ''
200
- const MAX_CYCLES = 3
201
-
202
- for (let cycle = 1; cycle <= MAX_CYCLES; cycle++) {
203
- log(`validate-feature-docs cycle ${cycle}`)
204
- const valKey = `validate-feature-docs:phase1:cycle${cycle}`
205
- await agent(
206
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${feature_dir} --prefix ${prefix} --agent ${valKey} --phase phase1 --model haiku --attempt 1\n\nReturn no output.`,
207
- { label: `ledger-open-validate-cycle-${cycle}`, phase: 'Validation', model: 'haiku' }
208
- )
209
- const beforeVal = budget.spent()
210
- const valResult = await agent(featurePath, { agentType: valNativeName, label: `validate-feature-docs (cycle ${cycle})`, phase: 'Validation' })
211
- const valTokens = budget.spent() - beforeVal
212
- await agent(
213
- `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${feature_dir} --prefix ${prefix} --agent ${valKey} --tokens ${valTokens} --attempt 1\n\nReturn no output.`,
214
- { label: `ledger-close-validate-cycle-${cycle}`, phase: 'Validation', model: 'haiku' }
215
- )
216
- tokenLedger.push({ agent: `validate-feature-docs (cycle ${cycle})`, model: 'haiku', phase_delta_tokens: valTokens })
217
-
218
- // valResult is the agent's text output — check for gap indicators
219
- // Use only 'MISSING:' as the gap signal: 'gaps found' also matches 'ZERO GAPS FOUND'
220
- const resultText = typeof valResult === 'string' ? valResult : JSON.stringify(valResult)
221
- lastValText = resultText
222
- const hasGaps = resultText.includes('MISSING:')
223
-
224
- if (!hasGaps) {
225
- validationSummary = `0 gaps (clean on cycle ${cycle})`
226
- log(`Validation clean on cycle ${cycle}`)
227
- break
228
- }
229
-
230
- log(`Validation cycle ${cycle}: gaps found`)
231
-
232
- if (cycle < MAX_CYCLES) {
233
- // Re-run failing docs before next validation cycle
234
- if (resultText.includes('Requirements')) {
235
- await agent(featurePath, { agentType: reqRevNativeName, label: `generate-requirements (revision ${cycle})`, phase: 'Validation' })
28
+ let validationSummary = 'skipped (fresh)'
29
+ if (force || changed || !fresh(file('Validation-Report'), [featurePath, file('Requirements'), file('Tech-Spec')])) {
30
+ for (let cycle = 1; cycle <= 3; cycle++) {
31
+ const result = await c.worker(valId, featurePath + '\nValidate only; the host owns the revision loop. Write the validation report and return valid plus a findings array (one string per remaining gap). Do not dispatch other agents.', { label: `validation:${cycle}`, schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } } }, required: ['valid', 'findings'] } })
32
+ const text = result.findings.join('\n')
33
+ if (result.valid && result.findings.length === 0) {
34
+ if (!fresh(file('Validation-Report'), [featurePath, file('Requirements'), file('Tech-Spec')])) throw new Error('Validation agent did not produce its report')
35
+ validationSummary = `0 gaps (clean on cycle ${cycle})`
36
+ break
37
+ }
38
+ if (cycle === 3) throw new Error('Validation gaps remain after 3 cycles')
39
+ if (!text.includes('Tech-Spec') || text.includes('Requirements')) await c.worker(reqId, featurePath)
40
+ if (!text.includes('Requirements') || text.includes('Tech-Spec')) await c.worker(specId, featurePath)
236
41
  }
237
- if (resultText.includes('Tech-Spec')) {
238
- await agent(featurePath, { agentType: specRevNativeName, label: `generate-tech-spec (revision ${cycle})`, phase: 'Validation' })
239
- }
240
- } else {
241
- validationSummary = 'gaps remain after 3 cycles'
242
- errors.push('validate-feature-docs: gaps remain after 3 revision cycles')
243
42
  }
244
- }
245
-
246
- // ── Guarantee the Validation Report exists on disk ───────────────────────────
247
- // The validate-feature-docs agent (haiku) is instructed to write the report in its
248
- // Phase 6, but this is LLM-dependent and unreliable when validation is clean. Downstream
249
- // pm-phase2 (generate-work-breakdown) treats the report as a hard precondition, so its
250
- // absence aborts the whole pipeline. This step deterministically ensures the file exists.
251
- if (validationSummary !== 'skipped') {
252
- const validationReportPath = `${feature_dir}/${prefix}-Validation-Report.md`
253
- await agent(
254
- `Ensure the Validation Report file exists on disk for this feature delivery run.
255
-
256
- File path: ${validationReportPath}
257
-
258
- Steps:
259
- 1. Check whether the file already exists (use Read or Glob).
260
- 2. If it ALREADY EXISTS: do nothing, return "exists".
261
- 3. If it DOES NOT EXIST: write it now using the Write tool, based on the validation outcome below.
262
- Get the current date via Bash: run \`date -u +"%Y-%m-%d"\`.
263
-
264
- Validation outcome summary: ${validationSummary}
265
-
266
- Validation agent output (source of truth for gaps found/resolved):
267
- ──────────────────────────────────────────────────
268
- ${lastValText.slice(0, 4000)}
269
- ──────────────────────────────────────────────────
270
-
271
- File format to write (fill from the outcome above; if the run was clean, mark both documents Clean with 0 gaps):
272
-
273
- # Validation Report — ${prefix}
274
-
275
- ## Summary
276
- | Document | Gaps found | Gaps resolved | Status |
277
- |----------|-----------|--------------|--------|
278
- | ${prefix}-Requirements.md | N | N | Clean / Gaps remain |
279
- | ${prefix}-Tech-Spec.md | N | N | Clean / Gaps remain |
280
-
281
- ## Gaps found and resolved
282
- (derive from the validation output above; write "(none)" if clean)
283
-
284
- ## Remaining gaps (if any)
285
- (list any unresolved gaps, or "(none)")
286
-
287
- ## Validation date
288
- {date from Bash}
289
-
290
- Return "written" if you created the file, or "exists" if it was already present.`,
291
- { label: 'ensure-validation-report', phase: 'Validation' }
292
- )
293
- log(`Validation report ensured at ${validationReportPath}`)
294
- }
295
-
296
- // ── Write process-log (phase 1 snapshot) ─────────────────────────────────────
297
- const reqEntry = tokenLedger.find(e => e.agent === 'generate-requirements')
298
- const specEntry = tokenLedger.find(e => e.agent === 'generate-tech-spec')
299
- const valEntries = tokenLedger.filter(e => e.agent.startsWith('validate-feature-docs'))
300
- const phase1Events = [
301
- `pm-phase1 START — documentation phase`,
302
- reqEntry
303
- ? `Agent DONE: generate-requirements — tokens: ${reqEntry.phase_delta_tokens}`
304
- : `generate-requirements: skipped (fresh)`,
305
- specEntry
306
- ? `Agent DONE: generate-tech-spec — tokens: ${specEntry.phase_delta_tokens}`
307
- : `generate-tech-spec: skipped (fresh)`,
308
- ...valEntries.map(e => `Agent DONE: ${e.agent} — tokens: ${e.phase_delta_tokens}`),
309
- `Validation result: ${validationSummary}`,
310
- `APPROVAL REQUESTED — Gate 1`,
311
- ].join('\n')
312
-
313
- await agent(
314
- `Create the process-log file for this feature delivery run.
315
-
316
- File path: ${feature_dir}/${prefix}-process-log.txt
317
-
318
- Steps:
319
- 1. Get current UTC datetime via Bash: run \`date -u +"%Y-%m-%dT%H:%M:%S"\`
320
- 2. Read feature.md at: ${featurePath} — extract the feature title (first H1 heading after the frontmatter).
321
- 3. Write the file using the Write tool. Use the datetime from step 1 for ALL timestamps.
322
- Format each event line as: [{datetime}] {event text}
323
-
324
- File structure:
325
- ════════════════════════════════════════════════════════
326
- RUN STARTED — {datetime}
327
- Feature: ${prefix} — {title from step 2}
328
- ════════════════════════════════════════════════════════
329
- {one line per event below, each prefixed with [{datetime}]}
330
-
331
- Events to log:
332
- ${phase1Events}`,
333
- { label: 'write-process-log', phase: 'Validation' }
334
- )
335
-
336
- // ── Build gate1_payload ───────────────────────────────────────────────────────
337
- const gate1Payload = {
338
- prefix,
339
- feature_dir,
340
- feature_path: featurePath,
341
- requirements: {
342
- path: `${feature_dir}/${prefix}-Requirements.md`,
343
- summary: `${prefix}-Requirements.md — generated`,
344
- },
345
- tech_spec: {
346
- path: `${feature_dir}/${prefix}-Tech-Spec.md`,
347
- summary: `${prefix}-Tech-Spec.md — generated`,
348
- },
349
- validation: {
350
- path: `${feature_dir}/${prefix}-Validation-Report.md`,
351
- summary: validationSummary,
352
- },
353
- token_ledger: tokenLedger,
354
- errors,
355
- }
356
-
357
- // ── Tier 3 self-registration — close on successful completion ─────────────────
358
- await selfLedgerOp(
359
- `ai-toolkit ledger close --prefix ${prefix} --agent pm-phase1:self --dir "${feature_dir}" --attempt 1`,
360
- 'ledger-close-pm-phase1-self', 'Validation'
361
- )
362
-
363
- log(`pm-phase1 complete — gate1_payload ready`)
364
- return gate1Payload
43
+ c.append(`${featureDir}/${prefix}-process-log.txt`, `[${new Date().toISOString()}] pm-phase1: ${validationSummary}; Gate 1 approval requested\n`)
44
+ return {
45
+ prefix, feature_dir: featureDir, feature_path: featurePath,
46
+ requirements: { path: file('Requirements'), summary: 'Requirements ready' },
47
+ tech_spec: { path: file('Tech-Spec'), summary: 'Tech-Spec ready' },
48
+ validation: { path: file('Validation-Report'), summary: validationSummary },
49
+ token_ledger: c.tokenLedger, errors: [],
50
+ }
51
+ })