@dtlabs/ai-toolkit 0.11.0 → 0.13.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 (38) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/bin/cli.js +757 -68
  3. package/lib/agent-registry.js +1018 -0
  4. package/lib/execution-ledger.js +609 -0
  5. package/package.json +1 -1
  6. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  7. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +37 -61
  8. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  9. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  10. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  11. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  12. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  13. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  14. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  15. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  16. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  17. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  18. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  19. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  20. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  21. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +1 -1
  22. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  23. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  24. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  25. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  26. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  27. package/src/claude/commands/next-task.md +4 -4
  28. package/src/claude/scripts/wb-render.js +101 -2
  29. package/src/claude/scripts/wb-validate.js +32 -2
  30. package/src/claude/skills/assess-codebase/SKILL.md +1 -1
  31. package/src/claude/skills/define-feature/SKILL.md +2 -2
  32. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  33. package/src/claude/skills/implement-feature/SKILL.md +190 -10
  34. package/src/claude/skills/init-agents/SKILL.md +2 -2
  35. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  36. package/src/claude/workflows/pm-phase1.js +94 -73
  37. package/src/claude/workflows/pm-phase2.js +118 -108
  38. package/src/claude/workflows/pm-phase3.js +219 -75
@@ -7,42 +7,6 @@ export const meta = {
7
7
  ],
8
8
  }
9
9
 
10
- // ── Ledger helper functions ───────────────────────────────────────────────────
11
- // Route all ledger I/O through agent() — fs is not available in the workflow runtime.
12
- // Mirrors appendLedgerEntry / updateLedgerEntry from bin/cli.js (same contract).
13
-
14
- async function appendLedgerEntry(featureDir, prefix, entry) {
15
- const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
16
- // Defaults come first so the caller can override started_at/completed_at.
17
- // Skipped entries pass completed_at: '__TS__' to get a real timestamp; running entries
18
- // pass completed_at: null to keep it null. Both are replaced/kept by the agent below.
19
- const entryWithoutTs = JSON.stringify({ started_at: '__TS__', completed_at: null, ...entry })
20
- await agent(
21
- `Append a JSON object to the ledger array at: ${ledgerPath}\n\n` +
22
- `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
23
- `2. Read the file. If it does not exist or cannot be parsed as a JSON array, start with [].\n` +
24
- `3. Push this object onto the array, replacing every "__TS__" string value in the object with NOW: ${entryWithoutTs}\n` +
25
- `4. Write the full array back (JSON, 2-space indent). Return no output.`,
26
- { label: 'append-ledger', phase: 'Work Breakdown', model: 'haiku' }
27
- )
28
- }
29
-
30
- async function updateLedgerEntry(featureDir, prefix, agentKey, updates) {
31
- const ledgerPath = `${featureDir}/${prefix}-token-ledger.json`
32
- // completed_at is generated via Bash inside the agent to get a real UTC timestamp with time
33
- const updatesWithoutTs = JSON.stringify({ ...updates, completed_at: '__TS__' })
34
- await agent(
35
- `Update an entry in the ledger array at: ${ledgerPath}\n\n` +
36
- `1. Run: date -u +"%Y-%m-%dT%H:%M:%SZ" and capture the output as NOW.\n` +
37
- `2. Read the file. If it does not exist or cannot be parsed as a JSON array, do nothing.\n` +
38
- `3. Search from the end for the last entry where agent === "${agentKey}".\n` +
39
- `4. If found, merge these fields into that entry, replacing "__TS__" in completed_at with NOW: ${updatesWithoutTs}\n` +
40
- `5. Write the full array back (JSON, 2-space indent). Return no output.\n` +
41
- `6. If not found, do nothing.`,
42
- { label: 'update-ledger', phase: 'Work Breakdown', model: 'haiku' }
43
- )
44
- }
45
-
46
10
  // Output schemas for wb-validate and wb-render agent invocations (used by US-05)
47
11
  const WB_WRAPPER_SCHEMA = {
48
12
  type: 'object',
@@ -85,43 +49,105 @@ function normalizeError(err) {
85
49
  try { return JSON.stringify(err) } catch (_) { return '<unknown error>' }
86
50
  }
87
51
 
52
+ // ── Tier 2 resolution guard ───────────────────────────────────────────────────
53
+ // All worker agents must be resolved via the CLI before dispatch.
54
+ // No direct require('lib/agent-registry') — resolution goes through the CLI facade.
55
+ const RESOLVE_SCHEMA = {
56
+ type: 'object',
57
+ properties: {
58
+ nativeName: { type: 'string' },
59
+ status: { type: 'string' },
60
+ agentId: { type: 'string' },
61
+ exitNonZero: { type: 'boolean' },
62
+ error: { type: 'string' },
63
+ },
64
+ }
65
+
66
+ async function resolveAgentTier2(agentId, label, phaseName) {
67
+ const result = await agent(
68
+ `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}"}.`,
69
+ { label, phase: phaseName, model: 'haiku', schema: RESOLVE_SCHEMA }
70
+ )
71
+ if (!result || result.exitNonZero || !result.nativeName) {
72
+ throw new Error(`HARD STOP — Tier 2 resolution failed for ${agentId}: ${result && result.error ? result.error : 'no nativeName returned'}`)
73
+ }
74
+ return result.nativeName
75
+ }
76
+
88
77
  // ── Parse args ────────────────────────────────────────────────────────────────
89
78
 
90
79
  // args: "<path-to-feature.md>"
91
80
  const featurePath = (typeof args === 'string' ? args : '').trim().split(/\s+/)[0]
92
81
 
82
+ // Feature directory = parent of the feature.md path. Separator-agnostic: the workflow
83
+ // runtime provides no `path` module, so we cannot use path.dirname. Handle BOTH POSIX
84
+ // "/" and Windows "\": strip a trailing "<sep>feature.md" tail when present; if the
85
+ // input is already a directory (no feature.md tail), strip only a trailing separator.
86
+ // This replaces the previous POSIX-only regex (/\/[^/]+$/), which left "\feature.md"
87
+ // attached on Windows paths and produced malformed "…\feature.md/…json" paths that only
88
+ // "worked" because an LLM wrapper agent silently repaired them.
89
+ function deriveFeatureDir(featurePath) {
90
+ return /[/\\]feature\.md$/i.test(featurePath)
91
+ ? featurePath.replace(/[/\\]feature\.md$/i, '')
92
+ : featurePath.replace(/[/\\]+$/, '')
93
+ }
94
+
93
95
  // Derive feature directory and prefix early (needed for ledger writes before metrics agent runs)
94
- const featureDir = featurePath.replace(/\/[^/]+$/, '')
96
+ const featureDir = deriveFeatureDir(featurePath)
95
97
  const prefixMatch = featureDir.match(/([A-Z]+-\d+)/)
96
98
  const prefix = prefixMatch ? prefixMatch[1] : 'FTR-000'
97
99
 
100
+ // ── Tier 3 — self-ledger helper ───────────────────────────────────────────────
101
+ const SELF_LEDGER_SCHEMA = {
102
+ type: 'object',
103
+ properties: {
104
+ exitCode: { type: 'number' },
105
+ stdout: { type: 'string' },
106
+ stderr: { type: 'string' },
107
+ },
108
+ required: ['exitCode'],
109
+ }
110
+
111
+ async function selfLedgerOp(cmd, label, phaseName) {
112
+ const result = await agent(
113
+ `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.`,
114
+ { label, phase: phaseName, model: 'haiku', schema: SELF_LEDGER_SCHEMA }
115
+ )
116
+ const status = (result && typeof result.exitCode === 'number') ? result.exitCode : 1
117
+ if (status !== 0) {
118
+ throw new Error(`HARD STOP — self-ledger operation failed. Exit code: ${status}. Command: ${cmd}`)
119
+ }
120
+ }
121
+
122
+ // ── Tier 3 self-registration (open BEFORE any main logic, fail-closed) ────────
123
+ await selfLedgerOp(
124
+ `ai-toolkit ledger open --prefix ${prefix} --agent pm-phase2:self --phase phase2 --dir "${featureDir}" --attempt 1`,
125
+ 'ledger-open-pm-phase2-self', 'Work Breakdown'
126
+ )
127
+
98
128
  // ── generate-work-breakdown ───────────────────────────────────────────────────
99
129
  phase('Work Breakdown')
100
130
 
101
131
  const tokenLedger = []
102
132
 
103
133
  log(`Running generate-work-breakdown for ${featurePath}`)
104
- await appendLedgerEntry(featureDir, prefix, {
105
- agent: 'generate-work-breakdown:phase2',
106
- phase: 'phase2',
107
- model: 'haiku',
108
- status: 'running',
109
- phase_delta_tokens: 0,
110
- started_at: '__TS__',
111
- completed_at: null,
112
- })
134
+ const wbNativeName = await resolveAgentTier2('gaia.agent.planner.work-breakdown', 'resolve-generate-work-breakdown', 'Work Breakdown')
135
+ const wbKey = 'generate-work-breakdown:phase2'
136
+ await agent(
137
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${wbKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
138
+ { label: 'ledger-open-generate-work-breakdown', phase: 'Work Breakdown', model: 'haiku' }
139
+ )
113
140
  const beforeWB = budget.spent()
114
141
  await agent(featurePath, {
115
- agentType: 'generate-work-breakdown',
142
+ agentType: wbNativeName,
116
143
  label: 'generate-work-breakdown',
117
144
  phase: 'Work Breakdown',
118
145
  })
119
146
  const wbTokens = budget.spent() - beforeWB
120
- await updateLedgerEntry(featureDir, prefix, 'generate-work-breakdown:phase2', {
121
- status: 'done',
122
- completed_at: '__TS__',
123
- phase_delta_tokens: wbTokens,
124
- })
147
+ await agent(
148
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbKey} --tokens ${wbTokens} --attempt 1\n\nReturn no output.`,
149
+ { label: 'ledger-close-generate-work-breakdown', phase: 'Work Breakdown', model: 'haiku' }
150
+ )
125
151
  tokenLedger.push({ agent: 'generate-work-breakdown', model: 'haiku', phase_delta_tokens: wbTokens })
126
152
  log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
127
153
 
@@ -129,11 +155,11 @@ log(`generate-work-breakdown done — phase delta: ${wbTokens} tokens`)
129
155
  let wbValidatorReport = null
130
156
  let validateFailed = false
131
157
  let validateExitCode = null
132
- await appendLedgerEntry(featureDir, prefix, {
133
- agent: 'wb-validate:phase2',
134
- phase: 'phase2', model: 'haiku', status: 'running',
135
- phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
136
- })
158
+ const wbValidateKey = 'wb-validate:phase2'
159
+ await agent(
160
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${wbValidateKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
161
+ { label: 'ledger-open-wb-validate', phase: 'Work Breakdown', model: 'haiku' }
162
+ )
137
163
  const beforeValidate = budget.spent()
138
164
  try {
139
165
  const validateWrapper = await agent(
@@ -173,20 +199,14 @@ try {
173
199
  if (validateWrapper.exitCode === 1 && wbValidatorReport.valid) {
174
200
  throw new Error('Inconsistent wb-validate result: exit 1 with valid=true')
175
201
  }
176
- await updateLedgerEntry(featureDir, prefix, 'wb-validate:phase2', {
177
- status: 'done', completed_at: '__TS__',
178
- phase_delta_tokens: budget.spent() - beforeValidate,
179
- })
180
202
  } catch (err) {
181
203
  validateFailed = true
182
- await updateLedgerEntry(featureDir, prefix, 'wb-validate:phase2', {
183
- status: 'failed', completed_at: '__TS__',
184
- phase_delta_tokens: budget.spent() - beforeValidate,
185
- error_summary: normalizeError(err),
186
- exit_code: err._exitCode ?? validateExitCode ?? null,
187
- })
188
204
  }
189
205
  const validateTokens = budget.spent() - beforeValidate
206
+ await agent(
207
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbValidateKey} --tokens ${validateTokens} --attempt 1\n\nReturn no output.`,
208
+ { label: 'ledger-close-wb-validate', phase: 'Work Breakdown', model: 'haiku' }
209
+ )
190
210
  const wbValidatorPassed = !validateFailed && wbValidatorReport !== null
191
211
  && wbValidatorReport.valid === true && wbValidatorReport.errors.length === 0
192
212
  log(`wb-validate: ${wbValidatorPassed ? 'passed' : 'failed/errors'} — ${validateTokens} tokens`)
@@ -195,18 +215,15 @@ log(`wb-validate: ${wbValidatorPassed ? 'passed' : 'failed/errors'} — ${valida
195
215
  let semanticResult = null
196
216
  let semanticFailed = false
197
217
  let semanticTokens = 0
218
+ const semanticKey = 'validate-work-breakdown-semantic:phase2'
198
219
  if (!wbValidatorPassed) {
199
- await appendLedgerEntry(featureDir, prefix, {
200
- agent: 'validate-work-breakdown-semantic:phase2',
201
- phase: 'phase2', model: 'sonnet', status: 'skipped',
202
- phase_delta_tokens: 0, started_at: '__TS__', completed_at: '__TS__',
203
- })
220
+ log('validate-work-breakdown-semantic: skipped (wb-validate did not pass)')
204
221
  } else {
205
- await appendLedgerEntry(featureDir, prefix, {
206
- agent: 'validate-work-breakdown-semantic:phase2',
207
- phase: 'phase2', model: 'sonnet', status: 'running',
208
- phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
209
- })
222
+ const semanticNativeName = await resolveAgentTier2('gaia.agent.planner.validate-work-breakdown', 'resolve-validate-work-breakdown-semantic', 'Work Breakdown')
223
+ await agent(
224
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${semanticKey} --phase phase2 --model sonnet --attempt 1\n\nReturn no output.`,
225
+ { label: 'ledger-open-semantic-validator', phase: 'Work Breakdown', model: 'haiku' }
226
+ )
210
227
  const beforeSemantic = budget.spent()
211
228
  try {
212
229
  semanticResult = await agent(
@@ -214,22 +231,18 @@ if (!wbValidatorPassed) {
214
231
  {
215
232
  label: 'validate-work-breakdown-semantic',
216
233
  phase: 'Work Breakdown',
217
- agentType: 'validate-work-breakdown-semantic',
234
+ agentType: semanticNativeName,
218
235
  schema: WB_SEMANTIC_SCHEMA,
219
236
  }
220
237
  )
221
- semanticTokens = budget.spent() - beforeSemantic
222
- await updateLedgerEntry(featureDir, prefix, 'validate-work-breakdown-semantic:phase2', {
223
- status: 'done', completed_at: '__TS__', phase_delta_tokens: semanticTokens,
224
- })
225
238
  } catch (err) {
226
239
  semanticFailed = true
227
- semanticTokens = budget.spent() - beforeSemantic
228
- await updateLedgerEntry(featureDir, prefix, 'validate-work-breakdown-semantic:phase2', {
229
- status: 'failed', completed_at: '__TS__', phase_delta_tokens: semanticTokens,
230
- error_summary: normalizeError(err), exit_code: null,
231
- })
232
240
  }
241
+ semanticTokens = budget.spent() - beforeSemantic
242
+ await agent(
243
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${semanticKey} --tokens ${semanticTokens} --attempt 1\n\nReturn no output.`,
244
+ { label: 'ledger-close-semantic-validator', phase: 'Work Breakdown', model: 'haiku' }
245
+ )
233
246
  }
234
247
  log(`semantic validator: ${semanticFailed ? 'failed' : semanticResult ? 'done' : 'skipped'}`)
235
248
 
@@ -237,18 +250,14 @@ log(`semantic validator: ${semanticFailed ? 'failed' : semanticResult ? 'done' :
237
250
  const canRender = wbValidatorPassed && !semanticFailed
238
251
  let renderFailed = false
239
252
  let renderResult = null
253
+ const wbRenderKey = 'wb-render:phase2'
240
254
  if (!canRender) {
241
- await appendLedgerEntry(featureDir, prefix, {
242
- agent: 'wb-render:phase2',
243
- phase: 'phase2', model: 'haiku', status: 'skipped',
244
- phase_delta_tokens: 0, started_at: '__TS__', completed_at: '__TS__',
245
- })
255
+ log('wb-render: skipped (wb-validate or semantic validator did not pass)')
246
256
  } else {
247
- await appendLedgerEntry(featureDir, prefix, {
248
- agent: 'wb-render:phase2',
249
- phase: 'phase2', model: 'haiku', status: 'running',
250
- phase_delta_tokens: 0, started_at: '__TS__', completed_at: null,
251
- })
257
+ await agent(
258
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger open --dir ${featureDir} --prefix ${prefix} --agent ${wbRenderKey} --phase phase2 --model haiku --attempt 1\n\nReturn no output.`,
259
+ { label: 'ledger-open-wb-render', phase: 'Work Breakdown', model: 'haiku' }
260
+ )
252
261
  const beforeRender = budget.spent()
253
262
  try {
254
263
  renderResult = await agent(
@@ -291,20 +300,15 @@ if (!canRender) {
291
300
  { _exitCode: renderResult.exitCode }
292
301
  )
293
302
  }
294
- await updateLedgerEntry(featureDir, prefix, 'wb-render:phase2', {
295
- status: 'done', completed_at: '__TS__',
296
- phase_delta_tokens: budget.spent() - beforeRender,
297
- })
298
303
  } catch (err) {
299
304
  renderFailed = true
300
- await updateLedgerEntry(featureDir, prefix, 'wb-render:phase2', {
301
- status: 'failed', completed_at: '__TS__',
302
- phase_delta_tokens: budget.spent() - beforeRender,
303
- error_summary: normalizeError(err),
304
- exit_code: err._exitCode ?? renderResult?.exitCode ?? null,
305
- })
306
305
  }
307
- tokenLedger.push({ agent: 'wb-render', model: 'haiku', phase_delta_tokens: budget.spent() - beforeRender })
306
+ const renderTokens = budget.spent() - beforeRender
307
+ await agent(
308
+ `Run this shell command via Bash. If the --dir path contains spaces, enclose it in double quotes.\n\nai-toolkit ledger close --dir ${featureDir} --prefix ${prefix} --agent ${wbRenderKey} --tokens ${renderTokens} --attempt 1\n\nReturn no output.`,
309
+ { label: 'ledger-close-wb-render', phase: 'Work Breakdown', model: 'haiku' }
310
+ )
311
+ tokenLedger.push({ agent: 'wb-render', model: 'haiku', phase_delta_tokens: renderTokens })
308
312
  }
309
313
 
310
314
  tokenLedger.push({ agent: 'wb-validate', model: 'haiku', phase_delta_tokens: validateTokens })
@@ -546,6 +550,12 @@ ${phase2Events}`,
546
550
  { label: 'append-process-log', phase: 'Effort Estimate' }
547
551
  )
548
552
 
553
+ // ── Tier 3 self-registration — close on successful completion ─────────────────
554
+ await selfLedgerOp(
555
+ `ai-toolkit ledger close --prefix ${prefix} --agent pm-phase2:self --dir "${featureDir}" --attempt 1`,
556
+ 'ledger-close-pm-phase2-self', 'Effort Estimate'
557
+ )
558
+
549
559
  return {
550
560
  prefix: metrics.prefix || prefix,
551
561
  feature_path: featurePath,