@dtlabs/ai-toolkit 0.12.0 → 0.14.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 (50) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/bin/cli.js +851 -9
  3. package/docs/task-executor-bootstrap.md +195 -0
  4. package/docs/task-executor-cli.md +319 -0
  5. package/docs/workflow-control-plane.md +95 -0
  6. package/lib/agent-registry.js +1018 -0
  7. package/lib/execution-ledger.js +283 -7
  8. package/lib/task-executor/claude-process.js +563 -0
  9. package/lib/task-executor/git.js +1362 -0
  10. package/lib/task-executor/index.js +3729 -0
  11. package/lib/task-executor/ownership.js +590 -0
  12. package/lib/task-executor/plan.js +725 -0
  13. package/lib/task-executor/store.js +1164 -0
  14. package/lib/workflow-artifacts.js +102 -0
  15. package/lib/workflow-control.js +219 -0
  16. package/lib/workflow-runner.js +39 -0
  17. package/package.json +1 -1
  18. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  19. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +1 -1
  20. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  21. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  22. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  23. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  24. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  25. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  26. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  27. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  28. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  29. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  30. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  31. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  32. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  33. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +10 -10
  34. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  35. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  36. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  37. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  38. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  39. package/src/claude/commands/next-task.md +4 -4
  40. package/src/claude/skills/assess-codebase/SKILL.md +9 -103
  41. package/src/claude/skills/define-feature/SKILL.md +2 -2
  42. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  43. package/src/claude/skills/implement-feature/SKILL.md +131 -38
  44. package/src/claude/skills/init-agents/SKILL.md +2 -2
  45. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  46. package/src/claude/workflows/am-phase1.js +43 -294
  47. package/src/claude/workflows/am-phase2.js +10 -164
  48. package/src/claude/workflows/pm-phase1.js +45 -287
  49. package/src/claude/workflows/pm-phase2.js +51 -511
  50. package/src/claude/workflows/pm-phase3.js +5 -767
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ function cell(value) { return String(value ?? '—').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' '); }
4
+ function table(headers, rows) {
5
+ return [headers, headers.map(() => '---'), ...rows].map(row => '| ' + row.map(cell).join(' | ') + ' |').join('\n') + '\n';
6
+ }
7
+ function pricing(control) {
8
+ if (!control.exists('docs/token-pricing.json')) return null;
9
+ const value = control.json('docs/token-pricing.json');
10
+ if (!value || !value.models || !Number.isFinite(value.usd_to_eur) || value.usd_to_eur <= 0) throw new Error('Invalid token pricing');
11
+ return value;
12
+ }
13
+ function cost(tokens, model, rates) {
14
+ if (!rates || !Number.isFinite(tokens)) return null;
15
+ const rate = rates.models[model];
16
+ if (!rate || ![rate.input_per_1m_usd, rate.output_per_1m_usd].every(v => Number.isFinite(v) && v >= 0)) throw new Error('Invalid model pricing: ' + model);
17
+ return tokens * (0.8 * rate.input_per_1m_usd + 0.2 * rate.output_per_1m_usd) / 1000000 * rates.usd_to_eur;
18
+ }
19
+ function tokensDocument(c, prefix) {
20
+ const rates = pricing(c);
21
+ return `# Token Estimate — ${prefix}\n\nActuals for this invocation only; previous phases remain in the ledger. Missing usage is unavailable, not zero. Costs use an estimated 80/20 input/output split.\n\n` +
22
+ table(['Agent', 'Model', 'Tokens Actual', 'Estimated cost EUR'], c.tokenLedger.map(e => [e.agent, e.model, e.phase_delta_tokens, cost(e.phase_delta_tokens, e.model, rates)?.toFixed(4)]));
23
+ }
24
+ function estimates(c, dir, prefix, wb) {
25
+ if (!wb || !Array.isArray(wb.phases) || !wb.phases.length) throw new Error('Invalid Work Breakdown phases');
26
+ const tasks = wb.phases.flatMap(p => { if (!Array.isArray(p.tasks)) throw new Error('Invalid phase tasks'); return p.tasks; });
27
+ const rates = { BE: 120, TEST: 60, INFRA: 30, FE: 90, DB: 90, DevOps: 30 };
28
+ const domains = {};
29
+ for (const t of tasks) { if (!Object.hasOwn(rates, t.domain)) throw new Error('Unknown task domain'); domains[t.domain] = (domains[t.domain] || 0) + 1; }
30
+ const human = list => list.reduce((n, t) => n + rates[t.domain], 0);
31
+ const minutes = human(tasks);
32
+ const human_estimate = `~${Math.floor(minutes / 60)}h ${minutes % 60}min`;
33
+ const agentMinutes = tasks.every(t => Number.isFinite(t.estimate?.agentMinutes)) ? tasks.reduce((n, t) => n + t.estimate.agentMinutes, 0) : null;
34
+ const agent_estimate = agentMinutes == null ? 'unavailable' : `${agentMinutes}min (sequential estimate)`;
35
+ const base = `${dir}/${prefix}`;
36
+ const metrics = { prefix, feature_dir: dir, feature_title: wb.title || wb.feature || prefix,
37
+ total_tasks: tasks.length, user_stories: wb.phases.filter(p => p.id.startsWith('US-')).length,
38
+ domain_breakdown: Object.entries(domains).map(([d, n]) => `${d}: ${n}`).join(', '),
39
+ implementation_phases: wb.phases.length, human_estimate, agent_estimate,
40
+ work_breakdown_path: `${base}-Work-Breakdown.json`, tasks_csv_path: `${base}-Work-Breakdown.csv`,
41
+ effort_estimate_path: `${base}-Effort-Estimate.md`, token_estimate_path: `${base}-Token-Estimate.md` };
42
+ c.write(metrics.effort_estimate_path, `# Effort Estimate — ${prefix} — ${cell(metrics.feature_title)}\n\n` +
43
+ table(['Metric', 'Value'], [['User Stories', metrics.user_stories], ['Total tasks', tasks.length], ['Human estimate', human_estimate], ['Agent estimate', agent_estimate]]) +
44
+ '\n## Per-Phase Breakdown\n\n' + table(['Phase', 'Title', 'Tasks', 'Est. Human minutes', 'Actual Human', 'Actual Agent'],
45
+ wb.phases.map(p => [p.id, p.title, p.tasks.length, human(p.tasks), '—', '—'])));
46
+ c.write(metrics.token_estimate_path, tokensDocument(c, prefix));
47
+ return metrics;
48
+ }
49
+ function interventions(c, dir, prefix) {
50
+ const raw = c.read(`${dir}/${prefix}-Interventions-Index.md`);
51
+ const lines = raw.split(/\r?\n/).filter(l => l.trim().startsWith('|'));
52
+ const split = line => line.trim().replace(/^\||\|$/g, '').split(/(?<!\\)\|/).map(s => s.trim());
53
+ const headerAt = lines.findIndex(l => split(l).some(v => /^(Criticality|Severity)$/i.test(v)));
54
+ if (headerAt < 0) throw new Error('Interventions index missing Criticality column');
55
+ const headers = split(lines[headerAt]), severityAt = headers.findIndex(v => /^(Criticality|Severity)$/i.test(v));
56
+ const rows = [];
57
+ for (const line of lines.slice(headerAt + 1)) {
58
+ const values = split(line);
59
+ if (values.every(v => /^:?-+:?$/.test(v))) continue;
60
+ const id = line.match(/\bINT-\d+\b/);
61
+ if (!id) continue;
62
+ const severity = values[severityAt]?.replace(/\*/g, '').toUpperCase();
63
+ if (!['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].includes(severity)) throw new Error('Unknown intervention severity: ' + severity);
64
+ if (rows.some(r => r.id === id[0])) throw new Error('Duplicate intervention ID: ' + id[0]);
65
+ rows.push({ id: id[0], severity });
66
+ }
67
+ return rows;
68
+ }
69
+ function assessmentEstimates(c, dir, prefix) {
70
+ const rows = interventions(c, dir, prefix);
71
+ const severity_counts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
72
+ const rates = { CRITICAL: 8, HIGH: 4, MEDIUM: 2, LOW: 1 };
73
+ rows.forEach(r => severity_counts[r.severity]++);
74
+ const hours = rows.reduce((n, r) => n + rates[r.severity], 0);
75
+ const base = `${dir}/${prefix}`;
76
+ c.write(`${base}-Token-Estimate.md`, tokensDocument(c, prefix));
77
+ c.write(`${base}-Effort-Estimate.md`, `# Effort Estimate — ${prefix}\n\n` +
78
+ table(['Severity', 'Count', 'Hours each', 'Subtotal hours'], Object.entries(severity_counts).map(([s, n]) => [s, n, rates[s], n * rates[s]])) + `\nTotal: ${hours} human hours (estimate).\n`);
79
+ return { severity_counts, total_interventions: rows.length, remediation_hours_est: hours,
80
+ interventions_index_path: `${base}-Interventions-Index.md`, effort_estimate_path: `${base}-Effort-Estimate.md`, token_estimate_path: `${base}-Token-Estimate.md` };
81
+ }
82
+ function recordAssessmentApproval(c, dir, prefix, ack, flagged) {
83
+ const rows = interventions(c, dir, prefix);
84
+ const selected = flagged === 'none' ? [] : flagged.split(',').map(s => s.trim());
85
+ for (const id of selected) if (!rows.some(r => r.id === id)) throw new Error('Unknown flagged intervention: ' + id);
86
+ const today = new Date().toISOString().slice(0, 10), approvals_path = `${dir}/${prefix}-Approvals.md`;
87
+ const content = `# Assessment Approvals — ${prefix}\n\n## Findings Gate Acknowledgement\n\n` +
88
+ table(['Field', 'Value'], [['Acknowledged by', 'Toolkit user'], ['Date', today], ['Acknowledgement', ack]]) +
89
+ '\n## Interventions Flagged for Feature Delivery\n\n' + table(['Intervention', 'Flagged', 'Date', 'Notes'], rows.map(r => [r.id, selected.includes(r.id) ? 'Yes' : 'No', today, selected.includes(r.id) ? '—' : 'Not selected']));
90
+ c.write(approvals_path, content);
91
+ if (c.read(approvals_path) !== content) throw new Error('Approval readback mismatch');
92
+ const registryPath = 'docs/assessments/registry.md';
93
+ const header = '# Assessment Registry\n\n' + table(['Date', 'Prefix', 'Total', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'Flagged'], []);
94
+ const old = c.exists(registryPath) ? c.read(registryPath) : header;
95
+ const row = `| ${today} | [${prefix}](${prefix}/) | ${rows.length} | ` +
96
+ ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].map(s => rows.filter(r => r.severity === s).length).join(' | ') + ` | ${selected.length} |`;
97
+ c.write(registryPath, old.split('\n').filter(l => !l.includes(`[${prefix}](`)).join('\n').trimEnd() + '\n' + row + '\n');
98
+ c.read(registryPath);
99
+ return { approvals_path, registry_updated: registryPath, flagged_count: selected.length, summary: `${prefix}: ${selected.length} interventions selected` };
100
+ }
101
+
102
+ module.exports = { estimates, assessmentEstimates, recordAssessmentApproval, interventions };
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ // Node host for workflow control-plane operations. Never ask an LLM to do I/O.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { spawnSync } = require('child_process');
8
+ const registry = require('./agent-registry');
9
+ const ledger = require('./execution-ledger');
10
+ const { spawnClaudeAgent } = require('./task-executor/claude-process');
11
+ const CLI = path.join(__dirname, '..', 'bin', 'cli.js');
12
+
13
+
14
+ // Runtime payload has no third-party dependencies. Only these two canonical
15
+ // scalar fields are needed; reject ambiguous/duplicate declarations.
16
+ function definitionFields(source) {
17
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
18
+ if (!match) throw new Error('Agent frontmatter missing');
19
+ const fields = {};
20
+ for (const key of ['name', 'model']) {
21
+ const lines = match[1].split(/\r?\n/).filter(l => l.startsWith(key + ':'));
22
+ if (lines.length !== 1) throw new Error('Ambiguous agent field: ' + key);
23
+ fields[key] = lines[0].slice(key.length + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
24
+ }
25
+ if (!/^[a-z][a-z0-9-]*$/.test(fields.name) || !['haiku', 'sonnet', 'opus'].includes(fields.model)) throw new Error('Invalid agent name/model');
26
+ return fields;
27
+ }
28
+
29
+ function validateIdentity(id, record, kind = 'agent') {
30
+ const entry = registry.CATALOG.find(e => e.agentId === id);
31
+ if (!entry || (entry.role === 'orchestrator') !== (kind === 'workflow') ||
32
+ !record || record.agentId !== id || record.status !== 'verified' ||
33
+ record.nativeName !== entry.nativeNames.claude ||
34
+ !record.path || !/^sha256:[a-f0-9]{64}$/.test(record.sha256 || '')) {
35
+ throw new Error('Invalid verified ' + kind + ' identity: ' + id);
36
+ }
37
+ const bytes = fs.readFileSync(record.path);
38
+ if ('sha256:' + crypto.createHash('sha256').update(bytes).digest('hex') !== record.sha256) {
39
+ throw new Error('Definition hash changed: ' + id);
40
+ }
41
+ if (kind === 'agent' && definitionFields(bytes.toString()).name !== record.nativeName) {
42
+ throw new Error('Agent frontmatter/nativeName mismatch: ' + id);
43
+ }
44
+ return record;
45
+ }
46
+
47
+ function parseArgs(input) {
48
+ if (Array.isArray(input)) return input.slice();
49
+ const tokens = [];
50
+ let value = '', quote = null, started = false;
51
+ for (const ch of String(input || '')) {
52
+ if (quote) { if (ch === quote) quote = null; else value += ch; }
53
+ else if (ch === '"' || ch === "'") { quote = ch; started = true; }
54
+ else if (/\s/.test(ch)) { if (started) { tokens.push(value); value = ''; started = false; } }
55
+ else { value += ch; started = true; }
56
+ }
57
+ if (quote) throw new Error('Unterminated argument quote');
58
+ if (started) tokens.push(value);
59
+ return tokens;
60
+ }
61
+
62
+ function deriveFeatureDir(featurePath) {
63
+ return /[/\\]feature\.md$/i.test(featurePath)
64
+ ? featurePath.replace(/[/\\]feature\.md$/i, '')
65
+ : featurePath.replace(/[/\\]+$/, '')
66
+ }
67
+
68
+ function createControl(options) {
69
+ const root = path.resolve(options.projectDir);
70
+ const runId = crypto.randomUUID();
71
+ let context, spent = 0, sequence = 0;
72
+ const tokenLedger = [];
73
+ const pending = new Set();
74
+ const full = p => path.resolve(root, p);
75
+ const read = p => fs.readFileSync(full(p), 'utf8');
76
+ const exists = p => { try { return fs.statSync(full(p)).isFile(); } catch (e) { if (e.code === 'ENOENT') return false; throw e; } };
77
+ const write = (p, content) => {
78
+ const dest = full(p);
79
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
80
+ const tmp = dest + '.tmp-' + crypto.randomUUID();
81
+ try { fs.writeFileSync(tmp, content, 'utf8'); fs.renameSync(tmp, dest); }
82
+ finally { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); }
83
+ };
84
+ async function resolve(id, kind = 'agent') {
85
+ const record = await registry.resolveAgent({ projectDir: root, homeDir: options.home,
86
+ agentId: id, requireVerified: true });
87
+ return validateIdentity(id, record, kind);
88
+ }
89
+ function command(argv, accepted = [0]) {
90
+ if (!Array.isArray(argv) || argv.some(a => typeof a !== 'string')) throw new Error('CLI argv array required');
91
+ if (options.home && argv[0] === 'run-asset') {
92
+ argv = argv.slice();
93
+ const split = argv.indexOf('--');
94
+ argv.splice(split < 0 ? argv.length : split, 0, '--home', options.home);
95
+ }
96
+ const result = spawnSync(process.execPath, [CLI, ...argv], {
97
+ cwd: root, encoding: 'utf8', shell: false, windowsHide: true,
98
+ timeout: 120000, maxBuffer: 20 * 1024 * 1024,
99
+ });
100
+ if (result.error || !Number.isInteger(result.status) || !accepted.includes(result.status)) {
101
+ throw new Error('Deterministic command failed: ' + (result.error?.message || result.stderr || result.status));
102
+ }
103
+ return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr };
104
+ }
105
+ async function activity(label, fn, model, usage = () => null, metadata) {
106
+ if (!context) throw new Error('Workflow ownership not established');
107
+ const key = `${context.name}:${label}:${runId}:${++sequence}`;
108
+ ledger.openOwned(context.dir, context.prefix, key, context.name, model, runId, metadata);
109
+ try {
110
+ const result = await fn();
111
+ ledger.finalizeOwned(context.dir, context.prefix, key, runId, 'done', undefined, usage());
112
+ return result;
113
+ } catch (error) {
114
+ try { ledger.finalizeOwned(context.dir, context.prefix, key, runId, 'failed', String(error), usage()); }
115
+ catch (persistError) { throw new AggregateError([error, persistError], 'Activity failed and ledger finalization failed'); }
116
+ throw error;
117
+ }
118
+ }
119
+ async function worker(id, prompt, opts = {}) {
120
+ let measuredTokens = null;
121
+ const identity = await resolve(id);
122
+ const definition = definitionFields(read(identity.path));
123
+ return activity(opts.label || id, async () => {
124
+ const cliArgs = ['--print', '--output-format', 'json', '--agent', identity.nativeName];
125
+ if (opts.schema) cliArgs.push('--json-schema', JSON.stringify(opts.schema));
126
+ const result = await (options.dispatch || spawnClaudeAgent)({
127
+ claudePath: options.claudePath, cwd: root, args: cliArgs, prompt,
128
+ taskTimeoutMs: options.taskTimeoutMs || 1800000,
129
+ });
130
+ if (result.exitCode !== 0 || result.parseError || result.timedOut ||
131
+ !result.result || result.result.is_error !== false) throw new Error('Worker failed: ' + id);
132
+ const payload = opts.schema ? result.result.structured_output : result.result.result;
133
+ if (payload == null || (typeof payload === 'string' && !payload.trim())) throw new Error('Worker returned no result: ' + id);
134
+ if (opts.schema) validateSchema(payload, opts.schema);
135
+ const usage = result.result.usage;
136
+ const tokens = usage && [usage.input_tokens, usage.output_tokens].every(Number.isFinite)
137
+ ? usage.input_tokens + usage.output_tokens + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0) : null;
138
+ measuredTokens = Number.isInteger(tokens) && tokens > 0 ? tokens : null;
139
+ spent += measuredTokens || 0;
140
+ tokenLedger.push({ agent: identity.nativeName, model: definition.model, phase_delta_tokens: measuredTokens });
141
+ return payload;
142
+ }, definition.model, () => measuredTokens, { agentId: id, nativeAgentName: identity.nativeName, platform: 'claude', definitionHash: identity.sha256, resolutionScope: identity.scope, toolkitVersion: identity.toolkitVersion });
143
+ }
144
+ async function parallel(fns) {
145
+ // Drain every sibling before finalizing the owner, even if one rejects.
146
+ const work = Promise.allSettled(fns.map(fn => Promise.resolve().then(fn)));
147
+ pending.add(work);
148
+ try {
149
+ const results = await work;
150
+ const failed = results.filter(r => r.status === 'rejected');
151
+ if (failed.length) throw new AggregateError(failed.map(r => r.reason), 'Parallel workers failed');
152
+ return results.map(r => r.value);
153
+ } finally { pending.delete(work); }
154
+ }
155
+ async function run(name, dir, prefix, fn) {
156
+ if (context) throw new Error('Nested workflow ownership forbidden');
157
+ if (!/^(?:FTR|ASSESS|[A-Z]+)-\d+$/.test(prefix)) throw new Error('Invalid workflow prefix');
158
+ context = { name, dir: full(dir), prefix };
159
+ fs.mkdirSync(context.dir, { recursive: true });
160
+ const key = `${name}:self:${runId}`;
161
+ const runLockPath = path.join(context.dir, prefix + '-workflow.lock');
162
+ const runLock = ledger._acquireLock(runLockPath);
163
+ try {
164
+ ledger.openOwned(context.dir, prefix, key, name, null, runId);
165
+ try {
166
+ const result = await fn();
167
+ await Promise.allSettled([...pending]);
168
+ if (!result || result.error || result.errors?.length || result.gate2_payload?.gate2_blocked) {
169
+ throw new Error('Workflow returned unsuccessful result: ' + JSON.stringify(result));
170
+ }
171
+ // All work, output validation and serialization precede the terminal commit.
172
+ const serialized = JSON.stringify(result);
173
+ const committedResult = JSON.parse(serialized);
174
+ ledger.finalizeOwned(context.dir, prefix, key, runId, 'done');
175
+ return committedResult;
176
+ } catch (error) {
177
+ await Promise.allSettled([...pending]);
178
+ try { ledger.finalizeOwned(context.dir, prefix, key, runId, 'failed', String(error)); }
179
+ catch (persistError) { throw new AggregateError([error, persistError], 'Workflow failed; ledger finalization failed'); }
180
+ throw error;
181
+ }
182
+ } finally { ledger._releaseLock(runLockPath, runLock); }
183
+ }
184
+ function feature(p) {
185
+ if (!p) throw new Error('Feature path required');
186
+ const file = full(p), dir = deriveFeatureDir(file);
187
+ const match = path.basename(dir).match(/^([A-Z]+-\d+)(?:-|$)/);
188
+ if (!match) throw new Error('Cannot derive canonical feature prefix');
189
+ read(file);
190
+ return { featurePath: file, featureDir: dir, prefix: match[1] };
191
+ }
192
+ const api = { root, runId, read, exists, write, resolve, worker, parallel, run, command, activity,
193
+ feature, parseArgs, tokenLedger, spent: () => spent,
194
+ mtime: p => fs.statSync(full(p)).mtimeMs,
195
+ json: p => JSON.parse(read(p)),
196
+ log: text => (options.log || (() => {}))(String(text)),
197
+ append: (p, text) => { fs.mkdirSync(path.dirname(full(p)), { recursive: true }); fs.appendFileSync(full(p), text); },
198
+ };
199
+ const artifacts = require('./workflow-artifacts');
200
+ api.estimates = (...args) => artifacts.estimates(api, ...args);
201
+ api.assessmentEstimates = (...args) => artifacts.assessmentEstimates(api, ...args);
202
+ api.recordAssessmentApproval = (...args) => artifacts.recordAssessmentApproval(api, ...args);
203
+ return api;
204
+ }
205
+
206
+ function validateSchema(value, schema) {
207
+ if (schema.type === 'object') {
208
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
209
+ for (const key of schema.required || []) if (!(key in value)) throw new Error('Missing structured field: ' + key);
210
+ for (const [key, sub] of Object.entries(schema.properties || {})) if (key in value) validateSchema(value[key], sub);
211
+ } else if (schema.type === 'array') {
212
+ if (!Array.isArray(value)) throw new Error('Invalid structured array');
213
+ if (schema.items) value.forEach(v => validateSchema(v, schema.items));
214
+ } else if (schema.type && (typeof value !== schema.type || (schema.type === 'number' && !Number.isFinite(value)))) {
215
+ throw new Error('Invalid structured ' + schema.type);
216
+ }
217
+ }
218
+
219
+ module.exports = { createControl, validateIdentity, validateSchema, parseArgs, deriveFeatureDir };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { createControl, parseArgs } = require('./workflow-control');
5
+ const IDS = {
6
+ 'pm-phase1': 'gaia.orchestrator.feature.phase1',
7
+ 'pm-phase2': 'gaia.orchestrator.feature.phase2',
8
+ 'am-phase1': 'gaia.orchestrator.assessment.phase1',
9
+ 'am-phase2': 'gaia.orchestrator.assessment.phase2',
10
+ };
11
+
12
+ async function runWorkflow(name, args, options) {
13
+ if (!IDS[name]) throw new Error('Unsupported workflow: ' + name + '. For implementation use ai-toolkit tasks start/run (FTR-018).');
14
+ const control = createControl(options);
15
+ const identity = await control.resolve(IDS[name], 'workflow');
16
+ const source = fs.readFileSync(identity.path, 'utf8');
17
+ if (!/controlPlaneVersion:\s*1\b/.test(source)) throw new Error('Installed workflow does not support deterministic control-plane v1; reinstall toolkit assets.');
18
+ // Only verified toolkit scripts are evaluated. This host is not a security sandbox.
19
+ const body = source.replace(/^export const meta\s*=/m, 'const meta =');
20
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
21
+ return new AsyncFunction('control', 'args', body)(control, parseArgs(args));
22
+ }
23
+
24
+ async function main(argv) {
25
+ if (argv[0] !== 'run') throw new Error('Usage: ai-toolkit workflow run NAME --project DIR --claude-path EXE -- ARGS');
26
+ const split = argv.indexOf('--');
27
+ if (split < 0) throw new Error('Workflow arguments must follow --');
28
+ const options = { projectDir: process.cwd(), log: s => process.stderr.write(s + '\n') };
29
+ const keys = { '--project': 'projectDir', '--home': 'home', '--claude-path': 'claudePath' };
30
+ for (let i = 2; i < split; i += 2) {
31
+ if (!keys[argv[i]] || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error('Invalid workflow option: ' + argv[i]);
32
+ options[keys[argv[i]]] = argv[i + 1];
33
+ }
34
+ if (!options.claudePath && argv[1] !== 'am-phase2') throw new Error('--claude-path is required');
35
+ const result = await runWorkflow(argv[1], argv.slice(split + 1), options);
36
+ process.stdout.write(JSON.stringify(result) + '\n');
37
+ }
38
+
39
+ module.exports = { runWorkflow, main };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: concurrency-safety-assessment
2
+ name: gaia-concurrency-safety-assessment
3
3
  description: "Concurrency Safety Assessment — identifies race conditions, shared mutable state corruption, and non-deterministic behaviour in concurrent or event-driven systems. Language-agnostic; produces structured findings with severity, evidence, and mitigation patterns. Output: {ASSESS_PREFIX}-Concurrency-Assessment.md"
4
4
  tools: Read, Grep, Glob, Bash, Write
5
5
  model: haiku
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: define-feature
2
+ name: gaia-define-feature
3
3
  description: "Define Feature — grills the user with targeted questions to define a new feature, then writes feature.md in docs/features/FTR-XXX-slug/. Output: feature.md ready for /implement-feature"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: dependency-injection-refactoring
2
+ name: gaia-dependency-injection-refactoring
3
3
  description: "Dependency Injection Refactoring — guides the incremental conversion of tightly coupled or statically accessed dependencies to constructor-injected services, preserving build integrity at every step. Language-agnostic; reads AGENTS.md for framework-specific DI patterns. Input: target classes from intervention document"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: dependency-supply-chain-security
2
+ name: gaia-dependency-supply-chain-security
3
3
  description: "Dependency Supply Chain Security — guides the implementation of dependency integrity controls: trusted package sources, deterministic restore via lock files, unused dependency audit, and SCA integration in CI/CD pipelines. Works with npm, pip, NuGet, Maven, and other package managers; reads AGENTS.md for project stack. Output: remediated configuration files + CI/CD pipeline changes"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: developer-backend
2
+ name: gaia-developer-backend
3
3
  description: "Senior Backend Developer — implements backend tasks (DB, BE, INFRA domains) from the Work Breakdown following project conventions. Input: path to feature.md + task IDs"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: developer-frontend
2
+ name: gaia-developer-frontend
3
3
  description: "Senior Frontend Developer — implements frontend tasks (FE domain) from the Work Breakdown following project conventions. Input: path to feature.md + task IDs"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: developer-testing
2
+ name: gaia-developer-testing
3
3
  description: "Senior Test Engineer — creates unit/integration tests for implemented features (TEST domain). Input: path to feature.md + task IDs"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: domain-model-refactoring
2
+ name: gaia-domain-model-refactoring
3
3
  description: "Domain Model Refactoring — guides the decomposition of monolithic domain model files, introduction of type hierarchies for structurally similar types, and alignment of model names with domain ubiquitous language. Language-agnostic; reads AGENTS.md for project conventions. Input: target model directory from intervention document"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: generate-requirements
2
+ name: gaia-generate-requirements
3
3
  description: "Generates a functional requirements document from a feature description. Input: path to feature.md"
4
4
  model: haiku
5
5
  tools: Read, Glob, Grep, Write
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: generate-tech-spec
2
+ name: gaia-generate-tech-spec
3
3
  description: "Generates a technical specification document from a feature description and requirements. Input: path to feature.md"
4
4
  model: haiku
5
5
  tools: Read, Glob, Grep, Write
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: generate-work-breakdown
2
+ name: gaia-generate-work-breakdown
3
3
  description: "Generates a structured work breakdown (User Stories + Tasks) from validated and approved Requirements and Tech-Spec. Input: path to feature.md"
4
4
  model: haiku
5
5
  tools: Read, Glob, Grep, Write
@@ -24,7 +24,7 @@ The user provides the path to a `feature.md` file. From the same directory, read
24
24
  ```
25
25
  Cannot generate work breakdown: `{file}` not found.
26
26
  The work breakdown requires validated and approved Requirements + Tech-Spec.
27
- Run /agent-project-manager to orchestrate the full pipeline (including validation and approval gates).
27
+ Run /implement-feature to orchestrate the full pipeline (including validation and approval gates).
28
28
  ```
29
29
 
30
30
  ---
@@ -63,7 +63,7 @@ Every task must be the **smallest independently implementable, verifiable, and c
63
63
 
64
64
  - **One observable outcome** — the task produces exactly one concrete artifact or behavioral change (one file, one endpoint, one migration, one test suite for one check). State it in the `outcome` field as a factual sentence describing what exists after the task is done.
65
65
  - **One domain** — choose exactly one: `BE`, `FE`, `DB`, `DevOps`, `INFRA`, or `TEST`.
66
- - **One agent type** — choose exactly one: `developer-backend`, `developer-frontend`, `developer-testing`, `developer-database`, or `review-solution`.
66
+ - **One agent type** — choose exactly one: `developer-backend`, `developer-frontend`, `developer-testing`, or `review-solution`. (`developer-database` is deprecated — use `developer-backend` for DB domain tasks.)
67
67
  - **One commit** — supply a single `commit.subject` describing the change.
68
68
  - **Estimated ≤ 15 minutes of agent time** — this is the target. If a task cannot be scoped to ≤ 15 minutes, split it further. Tasks up to 20 minutes are acceptable but above target. Tasks over 30 minutes are invalid and will be rejected by the validator.
69
69
 
@@ -126,7 +126,7 @@ Decomposition strategy by domain:
126
126
  | `title` | string | Short descriptive title; must not contain `\|`, CR, or LF |
127
127
  | `outcome` | string | Single observable outcome — what exists after this task is done |
128
128
  | `domain` | string | One of: `BE`, `FE`, `DB`, `DevOps`, `INFRA`, `TEST` |
129
- | `agentType` | string | One of: `developer-backend`, `developer-frontend`, `developer-testing`, `developer-database`, `review-solution` |
129
+ | `agentType` | string | One of: `developer-backend`, `developer-frontend`, `developer-testing`, `review-solution` (deprecated: `developer-database` — maps to `developer-backend`) |
130
130
  | `dependsOn` | array of strings | Task IDs this task depends on; may be empty `[]`; all IDs must be defined within this same JSON |
131
131
  | `acceptanceCriteria` | array of strings | AC IDs from Requirements that this task covers; may be empty `[]` |
132
132
  | `verification` | object | `{ "commands": ["..."] }` — one or more shell commands that verify the task output; array must be non-empty |
@@ -136,7 +136,7 @@ Decomposition strategy by domain:
136
136
  | `commit` | object | `{ "type": "feat", "subject": "..." }` — subject must not contain `\|`, CR, or LF |
137
137
 
138
138
  **Domain → agentType mapping (default):**
139
- - `DB` → `developer-database`
139
+ - `DB` → `developer-backend` (note: `developer-database` is a deprecated alias that still resolves — do NOT use it in new Work Breakdowns)
140
140
  - `BE`, `INFRA`, `DevOps` → `developer-backend`
141
141
  - `FE` → `developer-frontend`
142
142
  - `TEST` → `developer-testing`
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: generic-software-assessment
2
+ name: gaia-generic-software-assessment
3
3
  description: "Generic Software Assessment — performs a broad quality analysis across architecture, code quality, security, testability, observability, and DevOps. Reads AGENTS.md for project conventions. Output: {ASSESS_PREFIX}-Generic-Assessment.md"
4
4
  tools: Read, Grep, Glob, Bash, Write
5
5
  model: haiku
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: god-class-decomposition
2
+ name: gaia-god-class-decomposition
3
3
  description: "God Class Decomposition — guides the safe, incremental decomposition of oversized classes and methods using responsibility cluster analysis, extract-method, extract-class, and parameter object refactoring. Language-agnostic; reads AGENTS.md for project conventions. Input: target class(es) from intervention document"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: init-agents-md
2
+ name: gaia-init-agents-md
3
3
  description: "Analyzes a project codebase and generates a complete AGENTS.md file with tech stack, conventions, real code patterns, build/test commands, and hard constraints. Input: (optional) path to project root — defaults to current working directory."
4
4
  model: sonnet
5
5
  tools: Read, Glob, Grep, Bash, Write
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: install-toolkit
2
+ name: gaia-install-toolkit
3
3
  description: "Installs the ai-toolkit into a destination project by copying agents, skills, commands, and procedures. Input: [path-to-destination] [--force] — defaults to current working directory"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: intervention-documentation-standard
2
+ name: gaia-intervention-documentation-standard
3
3
  description: "Intervention Documentation Standard — generates structured, self-contained intervention documents from assessment findings. Each document is independently actionable by an architect or developer agent. Input: assessment findings; Output: {ASSESS_PREFIX}-INT-NNN-{slug}.md per finding + Interventions-Index.md"
4
4
  model: sonnet
5
5
  tools: Read, Grep, Glob, Bash, Write
@@ -89,12 +89,12 @@ Other interventions that must be completed first (blocking) or should be coordin
89
89
  What to do if the intervention fails mid-way: rollback strategy, branch isolation, partial revert steps.
90
90
 
91
91
  ### 17. Suggested Agent Assignment
92
- Which agent type should execute this intervention:
93
- - `developer-backend` — backend/infrastructure implementation
94
- - `developer-frontend` — frontend/UI implementation
95
- - `developer-testing` — test writing
96
- - `review-solution` — post-intervention validation
97
- - Specialised agent (e.g. `security-hardening`) if available in `.claude/agents/`
92
+ Which verified native agent should implement this intervention (recommendations only, never assessment dispatch):
93
+ - `gaia-developer-backend` — backend/infrastructure implementation
94
+ - `gaia-developer-frontend` — frontend/UI implementation
95
+ - `gaia-developer-testing` — test writing
96
+ - `gaia-review-solution` — post-intervention validation
97
+ - Specialised agent (e.g. `gaia-security-hardening`) if available in `.claude/agents/`
98
98
 
99
99
  ### 18. Expected Outputs
100
100
  List of files to be created or modified. Each entry: `file path` → what changes.
@@ -124,9 +124,9 @@ After writing all intervention documents, produce `{ASSESS_PREFIX}-Interventions
124
124
 
125
125
  | ID | Title | Area | Criticality | Depends on | Suggested Agent |
126
126
  |---|---|---|---|---|---|
127
- | INT-001 | ... | Security | CRITICAL | — | developer-backend |
128
- | INT-002 | ... | Code Quality | HIGH | INT-003 | developer-backend |
129
- | INT-003 | ... | Architecture | HIGH | — | developer-backend |
127
+ | INT-001 | ... | Security | CRITICAL | — | gaia-developer-backend |
128
+ | INT-002 | ... | Code Quality | HIGH | INT-003 | gaia-developer-backend |
129
+ | INT-003 | ... | Architecture | HIGH | — | gaia-developer-backend |
130
130
  ```
131
131
 
132
132
  Order by criticality (CRITICAL first), then by dependency (prerequisites before dependents).
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: layered-architecture-assessment
2
+ name: gaia-layered-architecture-assessment
3
3
  description: "Layered Architecture Assessment — audits a codebase for layer boundary violations, missing separation of concerns, and namespace/package misalignment. Produces a violation inventory and a recommended layer model. Language-agnostic; reads AGENTS.md for project conventions. Output: {ASSESS_PREFIX}-Layer-Assessment.md"
4
4
  tools: Read, Grep, Glob, Bash, Write
5
5
  model: haiku
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: review-solution
2
+ name: gaia-review-solution
3
3
  description: "Senior Architect — full solution review across quality, reuse, architecture, and security dimensions. Reports findings without auto-fixing. Input: scope (files, US-XX, or git diff)"
4
4
  model: opus
5
5
  tools: Read, Grep, Glob, Bash
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: security-hardening
2
+ name: gaia-security-hardening
3
3
  description: "Security Hardening — guides the implementation of targeted security fixes: input validation at system boundaries, parameterised queries, secret management, log sanitisation, and TLS/transport configuration. Language-agnostic; reads AGENTS.md for project-specific patterns. Input: security findings from assessment"
4
4
  model: sonnet
5
5
  ---
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: validate-feature-docs
2
+ name: gaia-validate-feature-docs
3
3
  description: "Validates Requirements and Tech-Spec documents against feature.md. Triggers targeted revision of failing documents if gaps are found. Input: path to feature.md"
4
4
  model: haiku
5
5
  tools: Read, Glob, Grep, Write
@@ -18,7 +18,7 @@ A QA agent that cross-references `feature.md` against `{PREFIX}-Requirements.md`
18
18
  - `feature.md` — source of truth
19
19
  - `{PREFIX}-Requirements.md`
20
20
  - `{PREFIX}-Tech-Spec.md`
21
- 3. If either output document is missing, abort and report: "Cannot validate: `{file}` not found. Run /agent-project-manager first."
21
+ 3. If either output document is missing, abort and report: "Cannot validate: `{file}` not found. Run /implement-feature first."
22
22
 
23
23
  ---
24
24
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: validate-work-breakdown-semantic
2
+ name: gaia-validate-work-breakdown-semantic
3
3
  description: "Semantic validator for Work Breakdown JSON — analyzes task coherence, scope alignment, and estimate realism. Runs after wb-validate.js exits 0. Input: path to {PREFIX}-Work-Breakdown.json"
4
4
  model: sonnet
5
5
  tools: Read, Glob, Grep