@dtlabs/ai-toolkit 0.14.1 → 0.14.3
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.
- package/CHANGELOG.md +19 -0
- package/docs/workflow-control-plane.md +17 -5
- package/lib/workflow-control.js +120 -12
- package/lib/workflow-runner.js +3 -1
- package/lib/workflow-worker-evidence.js +53 -0
- package/lib/workflow-worker-outputs.js +37 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-concurrency-safety-assessment.md +2 -2
- package/src/claude/agents/gaia-generate-requirements.md +5 -7
- package/src/claude/agents/gaia-generate-tech-spec.md +5 -7
- package/src/claude/agents/gaia-generate-work-breakdown.md +3 -3
- package/src/claude/agents/gaia-generic-software-assessment.md +2 -2
- package/src/claude/agents/gaia-intervention-documentation-standard.md +25 -2
- package/src/claude/agents/gaia-layered-architecture-assessment.md +2 -2
- package/src/claude/agents/gaia-validate-feature-docs.md +13 -12
- package/src/claude/workflows/am-phase1.js +21 -6
- package/src/claude/workflows/pm-phase1.js +8 -5
- package/src/claude/workflows/pm-phase2.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
## 0.1.4 (2026-07-21)
|
|
2
|
+
## [0.14.3](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.2...v0.14.3) (2026-10-01)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* **workflows:** accept a schema-valid <structured_output> text block when the CLI field is empty ([6814d96](https://github.com/dtlab-tech/ai-toolkit/commit/6814d96dd10dc5807b8c20417c5a2767cce05216))
|
|
8
|
+
* **workflows:** handle multiple model-chosen delimiters for the structured-output text fallback ([7d97758](https://github.com/dtlab-tech/ai-toolkit/commit/7d977589e10480e6bce66a4ca68a5dbc89b372b1))
|
|
9
|
+
* **workflows:** schema-validated text fallback for empty structured_output ([f2bd76f](https://github.com/dtlab-tech/ai-toolkit/commit/f2bd76fcba554c1b30a2c84629bfd2224a721d13))
|
|
10
|
+
* **workflows:** schema-validated text fallback for empty structured_output ([daa1411](https://github.com/dtlab-tech/ai-toolkit/commit/daa1411d15c919254ce948a785b31d56e6e4468c))
|
|
11
|
+
|
|
12
|
+
## [0.14.2](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.1...v0.14.2) (2026-10-01)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **workflows:** host persists worker-declared outputs, no Write tool on the agent ([1109fc0](https://github.com/dtlab-tech/ai-toolkit/commit/1109fc0943ad21eedbbf859a185e3a49c654c06d))
|
|
18
|
+
* **workflows:** host persists worker-declared outputs, no Write tool on the agent ([56f062c](https://github.com/dtlab-tech/ai-toolkit/commit/56f062c232110d2399d88ee011fa165b43336d12))
|
|
19
|
+
* **workflows:** host persists worker-declared outputs, no Write tool on the agent ([7c1b8dc](https://github.com/dtlab-tech/ai-toolkit/commit/7c1b8dca4ba9715809b6417f5081bcca0edb09e3))
|
|
20
|
+
|
|
2
21
|
## [0.14.1](https://github.com/dtlab-tech/ai-toolkit/compare/v0.14.0...v0.14.1) (2026-09-30)
|
|
3
22
|
|
|
4
23
|
|
|
@@ -91,11 +91,23 @@ negative semantic verdict, unknown scopes, paths with spaces and explicit approv
|
|
|
91
91
|
The live paid Claude model/permission flow has not been exercised by these tests.
|
|
92
92
|
Document validation requires both a written Validation Report and a structured
|
|
93
93
|
verdict (`valid: boolean`, `findings: string[]`). The validator performs one pass;
|
|
94
|
-
the host owns revisions.
|
|
95
|
-
`structured_output` from `--json-schema`.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
the host owns revisions. Bare plain-text JSON, with no delimiter, never substitutes
|
|
95
|
+
for `structured_output` from `--json-schema`. However, the CLI's own structured-output
|
|
96
|
+
extraction is turn-bound: it only populates `structured_output` when the model's final
|
|
97
|
+
turn ends in a tool call carrying the schema payload, and a long multi-phase agent
|
|
98
|
+
response (full report text, then a closing JSON object) ends that same turn in plain
|
|
99
|
+
text instead — so `structured_output` can come back empty even though the model
|
|
100
|
+
emitted a schema-valid payload in a `<structured_output>...</structured_output>`
|
|
101
|
+
block, exactly as its own agent definition instructs. The host falls back to
|
|
102
|
+
extracting and schema-validating that one delimited block from the plain-text result
|
|
103
|
+
when `structured_output` is absent; this is a deterministic, strictly-delimited parse
|
|
104
|
+
(never "guess the JSON somewhere in the text"), so it does not reintroduce LLM-driven
|
|
105
|
+
I/O. A missing field, a missing/malformed delimited block, or a block that fails
|
|
106
|
+
schema validation all still stop the workflow with the original error naming the
|
|
107
|
+
worker and required fields. Regression tests cover missing/malformed verdicts, a
|
|
108
|
+
clean verdict without a report, the delimited-text fallback (both the accepted and
|
|
109
|
+
the schema-rejected case), and a negative verdict followed by host revision and a
|
|
110
|
+
clean result; model responses are mocked.
|
|
99
111
|
The subprocess adapter reuses FTR-018's existing qualification; no new cross-platform
|
|
100
112
|
process-supervision guarantee is claimed. On Windows, the full executor test suite
|
|
101
113
|
requires Git Bash ahead of the WSL launcher in PATH. User-level installations are
|
package/lib/workflow-control.js
CHANGED
|
@@ -8,6 +8,8 @@ const { spawnSync } = require('child_process');
|
|
|
8
8
|
const registry = require('./agent-registry');
|
|
9
9
|
const ledger = require('./execution-ledger');
|
|
10
10
|
const { spawnClaudeAgent } = require('./task-executor/claude-process');
|
|
11
|
+
const { createEvidence } = require('./workflow-worker-evidence');
|
|
12
|
+
const { resolveOutputPath } = require('./workflow-worker-outputs');
|
|
11
13
|
const CLI = path.join(__dirname, '..', 'bin', 'cli.js');
|
|
12
14
|
|
|
13
15
|
|
|
@@ -66,7 +68,16 @@ function deriveFeatureDir(featurePath) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function createControl(options) {
|
|
69
|
-
|
|
71
|
+
if (options.workerMaxBudgetUsd != null && (!Number.isFinite(options.workerMaxBudgetUsd) || options.workerMaxBudgetUsd <= 0)) throw new Error('Worker budget must be a positive finite number');
|
|
72
|
+
// Windows can alias any long/dotted path segment to an 8.3 short name (e.g. the
|
|
73
|
+
// TEMP env var itself is often short-form: C:\Users\TOMADA~1\...). The spawned
|
|
74
|
+
// CLI resolves a tool call's absolute path in long form regardless, so an
|
|
75
|
+
// --allowedTools pattern built from a short-form root would silently never
|
|
76
|
+
// match — confirmed empirically (tests/manual/workflow-worker-smoke.js) against
|
|
77
|
+
// a real CLI run under a short-form TEMP path. realpath.native (unlike the pure-JS
|
|
78
|
+
// fs.realpathSync) resolves 8.3 aliases on Windows, and is a harmless plain
|
|
79
|
+
// symlink-resolving realpath on every other platform.
|
|
80
|
+
const root = fs.realpathSync.native(path.resolve(options.projectDir));
|
|
70
81
|
const runId = crypto.randomUUID();
|
|
71
82
|
let context, spent = 0, sequence = 0;
|
|
72
83
|
const tokenLedger = [];
|
|
@@ -81,6 +92,13 @@ function createControl(options) {
|
|
|
81
92
|
try { fs.writeFileSync(tmp, content, 'utf8'); fs.renameSync(tmp, dest); }
|
|
82
93
|
finally { if (fs.existsSync(tmp)) fs.unlinkSync(tmp); }
|
|
83
94
|
};
|
|
95
|
+
// Validates a worker-declared (or, for a dynamic multi-file deliverable like
|
|
96
|
+
// intervention-documentation-standard, model-declared) output path the same way
|
|
97
|
+
// worker()'s single-output contract does — inside the project, outside any tool-config
|
|
98
|
+
// directory, never through a symlink — then writes it. The one path through which any
|
|
99
|
+
// LLM response ever reaches disk in this workflow runtime; see worker()'s own comment
|
|
100
|
+
// for why that's the host's job and never the agent's.
|
|
101
|
+
const writeOutput = (p, content) => write(resolveOutputPath(root, { path: p }), content);
|
|
84
102
|
async function resolve(id, kind = 'agent') {
|
|
85
103
|
const record = await registry.resolveAgent({ projectDir: root, homeDir: options.home,
|
|
86
104
|
agentId: id, requireVerified: true });
|
|
@@ -123,13 +141,70 @@ function createControl(options) {
|
|
|
123
141
|
return activity(opts.label || id, async () => {
|
|
124
142
|
const cliArgs = ['--print', '--output-format', 'json', '--agent', identity.nativeName];
|
|
125
143
|
if (opts.schema) cliArgs.push('--json-schema', JSON.stringify(opts.schema));
|
|
144
|
+
if (options.workerMaxBudgetUsd != null) cliArgs.push('--max-budget-usd', String(options.workerMaxBudgetUsd));
|
|
145
|
+
const outputs = opts.outputs || [];
|
|
146
|
+
for (const output of outputs) {
|
|
147
|
+
if (!output || typeof output.path !== 'string' || !Array.isArray(output.inputs || [])) throw new Error('Invalid worker output contract');
|
|
148
|
+
}
|
|
149
|
+
// Never ask an LLM to do I/O: a worker with a declared output has no Write tool at
|
|
150
|
+
// all (see its agent frontmatter) and returns the document as plain response text
|
|
151
|
+
// (or, under --json-schema, as a `report` field) instead. The host resolves and
|
|
152
|
+
// validates the real target path up front — fail before dispatch, not after paying
|
|
153
|
+
// for a model turn — and writes the file itself once the response is in hand. This
|
|
154
|
+
// sidesteps the CLI's own permission system for Write entirely, which empirically
|
|
155
|
+
// (tests/manual/workflow-worker-smoke.js) does not support a non-interactive mode
|
|
156
|
+
// that both skips prompting AND honors a scoped, per-file allow pattern: `dontAsk`
|
|
157
|
+
// ignores runtime-supplied allow rules for paths with no pre-existing persisted
|
|
158
|
+
// grant, while `acceptEdits`/`auto` grant Write (and every other tool, unscoped)
|
|
159
|
+
// anywhere in the project regardless of any allowlist passed alongside them.
|
|
160
|
+
if (outputs.length > 1) throw new Error('Worker output contract supports exactly one output today: ' + id);
|
|
161
|
+
const outputTarget = outputs.length ? resolveOutputPath(root, outputs[0]) : null;
|
|
162
|
+
const evidence = createEvidence(options, runId, identity, {
|
|
163
|
+
executable: options.claudePath || null, cwd: root, args: cliArgs, outputs,
|
|
164
|
+
});
|
|
165
|
+
(options.log || (() => {}))('Worker evidence: ' + evidence.file);
|
|
166
|
+
try {
|
|
126
167
|
const result = await (options.dispatch || spawnClaudeAgent)({
|
|
127
168
|
claudePath: options.claudePath, cwd: root, args: cliArgs, prompt,
|
|
128
169
|
taskTimeoutMs: options.taskTimeoutMs || 1800000,
|
|
129
170
|
});
|
|
171
|
+
// Count usage before any business/protocol check, including denied tools.
|
|
172
|
+
const usage = result.result?.usage;
|
|
173
|
+
const counts = usage && [usage.input_tokens, usage.output_tokens,
|
|
174
|
+
usage.cache_read_input_tokens ?? 0, usage.cache_creation_input_tokens ?? 0];
|
|
175
|
+
const tokens = counts && counts.every(n => Number.isInteger(n) && n >= 0)
|
|
176
|
+
? counts.reduce((a, b) => a + b, 0) : null;
|
|
177
|
+
measuredTokens = Number.isSafeInteger(tokens) && tokens > 0 ? tokens : null;
|
|
178
|
+
spent += measuredTokens || 0;
|
|
179
|
+
tokenLedger.push({ agent: identity.nativeName, model: definition.model,
|
|
180
|
+
phase_delta_tokens: measuredTokens, usage: usage || null, evidence_path: evidence.file });
|
|
181
|
+
evidence.save({ response: result });
|
|
182
|
+
const denials = result.result?.permission_denials;
|
|
183
|
+
if (Array.isArray(denials) && denials.length) {
|
|
184
|
+
const tools = [...new Set(denials.map(d => d.tool_name || 'unknown'))].join(', ');
|
|
185
|
+
throw new Error('Worker permission denied: ' + id + '; tools: ' + tools);
|
|
186
|
+
}
|
|
130
187
|
if (result.exitCode !== 0 || result.parseError || result.timedOut ||
|
|
131
188
|
!result.result || result.result.is_error !== false) throw new Error('Worker failed: ' + id);
|
|
132
|
-
|
|
189
|
+
let payload = opts.schema ? result.result.structured_output : result.result.result;
|
|
190
|
+
let usedTextFallback = false;
|
|
191
|
+
// The CLI's structured-output extraction is turn-bound: it only populates
|
|
192
|
+
// `structured_output` when the model's final turn ends in a tool call carrying the
|
|
193
|
+
// schema payload. A long, multi-phase agent response (visible report text, then a
|
|
194
|
+
// closing JSON object) ends that same turn in plain text instead, so `structured_output`
|
|
195
|
+
// comes back empty even though the model faithfully emitted a schema-valid payload in
|
|
196
|
+
// the plain-text result. Observed empirically (not a transient flake) with
|
|
197
|
+
// gaia-validate-feature-docs across multiple toolkit versions and multiple runs: the
|
|
198
|
+
// model never misses or mangles the payload, only the CLI's own field never gets
|
|
199
|
+
// populated — but the model is NOT consistent about how it delimits that payload inside
|
|
200
|
+
// its text (an `<structured_output>` tag one run, a ```json fence the next, same agent,
|
|
201
|
+
// same schema). Falling back to a strictly-delimited, schema-validated extraction that
|
|
202
|
+
// tries each known convention — never a bare "parse the whole result as JSON" guess —
|
|
203
|
+
// keeps the host's parse deterministic rather than asking the LLM to do I/O.
|
|
204
|
+
if (opts.schema && payload == null && typeof result.result.result === 'string') {
|
|
205
|
+
const candidate = extractDelimitedStructuredOutput(result.result.result, opts.schema);
|
|
206
|
+
if (candidate != null) { payload = candidate; usedTextFallback = true; }
|
|
207
|
+
}
|
|
133
208
|
if (payload == null || (typeof payload === 'string' && !payload.trim())) {
|
|
134
209
|
if (opts.schema) {
|
|
135
210
|
const fields = (opts.schema.required || []).join(', ') || '(see requested schema)';
|
|
@@ -138,13 +213,18 @@ function createControl(options) {
|
|
|
138
213
|
throw new Error('Worker returned no result: ' + id);
|
|
139
214
|
}
|
|
140
215
|
if (opts.schema) validateSchema(payload, opts.schema);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
216
|
+
if (usedTextFallback) evidence.save({ usedTextFallback: true });
|
|
217
|
+
if (outputTarget) {
|
|
218
|
+
const content = opts.schema ? payload.report : payload;
|
|
219
|
+
if (typeof content !== 'string' || !content.trim()) throw new Error('Worker output content missing or empty: ' + id);
|
|
220
|
+
writeOutput(outputTarget, content);
|
|
221
|
+
}
|
|
222
|
+
evidence.save({ outcome: 'done', completedAt: new Date().toISOString() });
|
|
147
223
|
return payload;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
evidence.save({ outcome: 'failed', error: String(error), completedAt: new Date().toISOString() });
|
|
226
|
+
throw new Error(error.message + '; evidence: ' + evidence.file, { cause: error });
|
|
227
|
+
}
|
|
148
228
|
}, definition.model, () => measuredTokens, { agentId: id, nativeAgentName: identity.nativeName, platform: 'claude', definitionHash: identity.sha256, resolutionScope: identity.scope, toolkitVersion: identity.toolkitVersion });
|
|
149
229
|
}
|
|
150
230
|
async function parallel(fns) {
|
|
@@ -189,13 +269,20 @@ function createControl(options) {
|
|
|
189
269
|
}
|
|
190
270
|
function feature(p) {
|
|
191
271
|
if (!p) throw new Error('Feature path required');
|
|
192
|
-
const file = full(p)
|
|
272
|
+
const file = full(p);
|
|
273
|
+
read(file); // confirms existence (with a clear error) before realpath.native requires it
|
|
274
|
+
// Canonicalize once, here, so every output path derived from featureDir downstream
|
|
275
|
+
// (pm-phase1.js/pm-phase2.js's `file()` helpers) shares the same 8.3-alias-free form
|
|
276
|
+
// as `root` — resolveOutputPath() can then do a plain, non-realpath string comparison
|
|
277
|
+
// instead of reconciling mismatched forms (which would have to silently follow
|
|
278
|
+
// symlinks to do so, defeating its own symlink-escape check).
|
|
279
|
+
const canonicalFile = fs.realpathSync.native(file);
|
|
280
|
+
const dir = deriveFeatureDir(canonicalFile);
|
|
193
281
|
const match = path.basename(dir).match(/^([A-Z]+-\d+)(?:-|$)/);
|
|
194
282
|
if (!match) throw new Error('Cannot derive canonical feature prefix');
|
|
195
|
-
|
|
196
|
-
return { featurePath: file, featureDir: dir, prefix: match[1] };
|
|
283
|
+
return { featurePath: canonicalFile, featureDir: dir, prefix: match[1] };
|
|
197
284
|
}
|
|
198
|
-
const api = { root, runId, read, exists, write, resolve, worker, parallel, run, command, activity,
|
|
285
|
+
const api = { root, runId, read, exists, write, writeOutput, resolve, worker, parallel, run, command, activity,
|
|
199
286
|
feature, parseArgs, tokenLedger, spent: () => spent,
|
|
200
287
|
mtime: p => fs.statSync(full(p)).mtimeMs,
|
|
201
288
|
json: p => JSON.parse(read(p)),
|
|
@@ -209,6 +296,27 @@ function createControl(options) {
|
|
|
209
296
|
return api;
|
|
210
297
|
}
|
|
211
298
|
|
|
299
|
+
// Tries each known convention a model has been observed using to delimit its final
|
|
300
|
+
// structured answer inside otherwise-free text, in order of explicitness, and returns the
|
|
301
|
+
// first candidate that is both valid JSON and schema-conformant. Returns null (never throws)
|
|
302
|
+
// if no candidate qualifies, so the caller's original "no structured output" error still fires
|
|
303
|
+
// unchanged when the text holds nothing usable.
|
|
304
|
+
function extractDelimitedStructuredOutput(text, schema) {
|
|
305
|
+
const candidates = [];
|
|
306
|
+
const tagMatch = text.match(/<structured_output>([\s\S]*?)<\/structured_output>/);
|
|
307
|
+
if (tagMatch) candidates.push(tagMatch[1]);
|
|
308
|
+
const fenceMatches = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)];
|
|
309
|
+
if (fenceMatches.length) candidates.push(fenceMatches[fenceMatches.length - 1][1]);
|
|
310
|
+
for (const raw of candidates) {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(raw.trim());
|
|
313
|
+
validateSchema(parsed, schema);
|
|
314
|
+
return parsed;
|
|
315
|
+
} catch (e) { /* try the next candidate, or fall through to null */ }
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
212
320
|
function validateSchema(value, schema) {
|
|
213
321
|
if (schema.type === 'object') {
|
|
214
322
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid structured object');
|
package/lib/workflow-runner.js
CHANGED
|
@@ -26,11 +26,13 @@ async function main(argv) {
|
|
|
26
26
|
const split = argv.indexOf('--');
|
|
27
27
|
if (split < 0) throw new Error('Workflow arguments must follow --');
|
|
28
28
|
const options = { projectDir: process.cwd(), log: s => process.stderr.write(s + '\n') };
|
|
29
|
-
const keys = { '--project': 'projectDir', '--home': 'home', '--claude-path': 'claudePath'
|
|
29
|
+
const keys = { '--project': 'projectDir', '--home': 'home', '--claude-path': 'claudePath',
|
|
30
|
+
'--diagnostics-dir': 'diagnosticsDir', '--worker-max-budget-usd': 'workerMaxBudgetUsd' };
|
|
30
31
|
for (let i = 2; i < split; i += 2) {
|
|
31
32
|
if (!keys[argv[i]] || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error('Invalid workflow option: ' + argv[i]);
|
|
32
33
|
options[keys[argv[i]]] = argv[i + 1];
|
|
33
34
|
}
|
|
35
|
+
if (options.workerMaxBudgetUsd != null) options.workerMaxBudgetUsd = Number(options.workerMaxBudgetUsd);
|
|
34
36
|
if (!options.claudePath && argv[1] !== 'am-phase2') throw new Error('--claude-path is required');
|
|
35
37
|
const result = await runWorkflow(argv[1], argv.slice(split + 1), options);
|
|
36
38
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
// Do not capture the parent environment or stdin prompt. Diagnostic responses
|
|
9
|
+
// can contain document text; redact common credential forms before persistence.
|
|
10
|
+
function redact(value) {
|
|
11
|
+
const secretKey = /^(?:authorization|proxy.authorization|.*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret|credential))$/i;
|
|
12
|
+
const secrets = Object.entries(process.env)
|
|
13
|
+
.filter(([key, val]) => secretKey.test(key) && val && val.length >= 8)
|
|
14
|
+
.map(([, val]) => val);
|
|
15
|
+
function cleanString(text) {
|
|
16
|
+
for (const secret of secrets) text = text.split(secret).join('[REDACTED]');
|
|
17
|
+
return text
|
|
18
|
+
.replace(/\b(Bearer\s+)[^\s"'<>]+/gi, '$1[REDACTED]')
|
|
19
|
+
.replace(/\b((?:api[_-]?key|token|password|secret)\s*[=:]\s*)[^\s,"'<>]+/gi, '$1[REDACTED]')
|
|
20
|
+
.replace(/\bsk-[A-Za-z0-9_-]+/g, '[REDACTED]');
|
|
21
|
+
}
|
|
22
|
+
function walk(item) {
|
|
23
|
+
if (typeof item === 'string') {
|
|
24
|
+
// stdout is often a JSON string: redact credential keys there too.
|
|
25
|
+
try { return JSON.stringify(walk(JSON.parse(item))); } catch (_) { return cleanString(item); }
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(item)) return item.map(walk);
|
|
28
|
+
if (item && typeof item === 'object') return Object.fromEntries(Object.entries(item)
|
|
29
|
+
.map(([key, val]) => [key, secretKey.test(key) ? '[REDACTED]' : walk(val)]));
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
return walk(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createEvidence(options, runId, identity, invocation) {
|
|
36
|
+
const dir = path.join(path.resolve(options.diagnosticsDir || path.join(os.tmpdir(), 'ai-toolkit-workflows')), runId);
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
38
|
+
const file = path.join(dir, crypto.randomUUID() + '.json');
|
|
39
|
+
const record = { version: 1, runId, agentId: identity.agentId, definitionPath: identity.path,
|
|
40
|
+
definitionHash: identity.sha256, invocation, startedAt: new Date().toISOString(), outcome: 'running' };
|
|
41
|
+
function save(patch) {
|
|
42
|
+
Object.assign(record, patch);
|
|
43
|
+
const temp = file + '.tmp';
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(temp, JSON.stringify(redact(record), null, 2) + '\n', { mode: 0o600 });
|
|
46
|
+
fs.renameSync(temp, file);
|
|
47
|
+
} finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
|
|
48
|
+
}
|
|
49
|
+
save({});
|
|
50
|
+
return { file, save };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { createEvidence, redact };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Validates that a worker's declared output stays inside the project, outside any
|
|
6
|
+
// tool-config directory, and is never reached through a symlink/junction — then
|
|
7
|
+
// returns its canonical absolute path. The workflow host (never the LLM) writes the
|
|
8
|
+
// worker's returned content to this path with fs.writeFileSync; there is no CLI
|
|
9
|
+
// permission surface to construct here ("Never ask an LLM to do I/O").
|
|
10
|
+
//
|
|
11
|
+
// Assumes `root` and `output.path` already share the same 8.3-alias form (createControl
|
|
12
|
+
// realpath.native's `root` once at startup; feature() realpath.native's featurePath/
|
|
13
|
+
// featureDir the same way, and every output.path in this codebase is built from
|
|
14
|
+
// featureDir) — do NOT realpath-resolve anything in here to "reconcile" a mismatch: that
|
|
15
|
+
// would have to follow symlinks to do it, silently defeating the symlink check below.
|
|
16
|
+
function resolveOutputPath(root, output) {
|
|
17
|
+
const target = path.resolve(root, output.path);
|
|
18
|
+
const relative = path.relative(root, target);
|
|
19
|
+
if (!relative || path.isAbsolute(relative) || relative === '..' || relative.startsWith('..' + path.sep)) {
|
|
20
|
+
throw new Error('Worker output must be inside the project: ' + target);
|
|
21
|
+
}
|
|
22
|
+
const segments = relative.split(path.sep);
|
|
23
|
+
if (segments.some(s => ['.git', '.claude', '.codex'].includes(s.toLowerCase()))) {
|
|
24
|
+
throw new Error('Unsupported worker output path: ' + target);
|
|
25
|
+
}
|
|
26
|
+
// Never automatically write a target reached through a symlink/junction.
|
|
27
|
+
let cursor = root;
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
cursor = path.join(cursor, segment);
|
|
30
|
+
try {
|
|
31
|
+
if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error('Worker output traverses a symlink: ' + cursor);
|
|
32
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
33
|
+
}
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { resolveOutputPath };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
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
|
-
tools: Read, Grep, Glob, Bash
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
5
|
model: haiku
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -120,7 +120,7 @@ Blocking: Yes / No
|
|
|
120
120
|
|
|
121
121
|
## Output
|
|
122
122
|
|
|
123
|
-
Write to `{ASSESS_PREFIX}-Concurrency-Assessment.md
|
|
123
|
+
Return the assessment as your final response — you do not have a Write tool; the host persists your response verbatim to `{ASSESS_PREFIX}-Concurrency-Assessment.md`. Your entire final response must be this document's content, and nothing else (no preamble, no Markdown code fence wrapping the whole thing):
|
|
124
124
|
|
|
125
125
|
```markdown
|
|
126
126
|
# Concurrency Safety Assessment — {ASSESS_PREFIX}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
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
|
-
tools: Read, Glob, Grep
|
|
5
|
+
tools: Read, Glob, Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Generate Functional Requirements Document
|
|
@@ -20,21 +20,19 @@ The user provides the path to a `feature.md` file. Read it in full before procee
|
|
|
20
20
|
3. **Identify** all functional areas described
|
|
21
21
|
3. **Extract** actors, use cases, business rules, acceptance criteria
|
|
22
22
|
4. **Organize** into a structured requirements document
|
|
23
|
-
5. **
|
|
23
|
+
5. **Return** the complete document as your final response — you do not have a Write tool; the host persists your response to `{PREFIX}-Requirements.md` verbatim. Do not attempt to write any file yourself.
|
|
24
24
|
|
|
25
|
-
## Output Filename
|
|
25
|
+
## Output Filename (for your reference only — you do not write this file)
|
|
26
26
|
|
|
27
27
|
Extract the **feature prefix** from the folder name containing `feature.md`:
|
|
28
28
|
- Folder: `FTR-001-user-management` → prefix: `FTR-001`
|
|
29
29
|
- Folder: `FTR-042-Search-Engine` → prefix: `FTR-042`
|
|
30
30
|
|
|
31
|
-
The prefix is everything up to and including the second hyphen-separated segment (pattern: `[A-Z]+-[0-9]+`).
|
|
32
|
-
|
|
33
|
-
Output file: `{PREFIX}-Requirements.md` in the same directory as `feature.md`.
|
|
31
|
+
The prefix is everything up to and including the second hyphen-separated segment (pattern: `[A-Z]+-[0-9]+`). Use it in the document's own "Document Info" section; the host, not you, decides the actual output filename.
|
|
34
32
|
|
|
35
33
|
## Output Structure
|
|
36
34
|
|
|
37
|
-
Generate the document in **English** following this template:
|
|
35
|
+
Generate the document in **English** following this template. Your entire final response must be this document's content, and nothing else (no preamble, no "here is the document", no Markdown code fence wrapping the whole thing) — the host writes your response verbatim to disk:
|
|
38
36
|
|
|
39
37
|
```markdown
|
|
40
38
|
# Functional Requirements — [Feature Title]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
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
|
-
tools: Read, Glob, Grep
|
|
5
|
+
tools: Read, Glob, Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Generate Technical Specification
|
|
@@ -37,21 +37,19 @@ The tech spec MUST use the project's actual stack and patterns. Do not assume an
|
|
|
37
37
|
3. **Read** `docs/features/REGISTRY.md` if it exists — identify features that touch the same entities, endpoints, or services as this feature. Note architectural dependencies and integration points in Section 5 (External Integrations) or Section 12 (Risks & Mitigations) of the output document. If a prior feature introduced shared infrastructure this feature reuses (e.g. auth middleware, base service, shared DTO), reference it explicitly.
|
|
38
38
|
4. **Explore** the existing codebase areas affected (models, controllers/handlers, pages, services)
|
|
39
39
|
4. **Design** the technical solution aligned with existing patterns from AGENTS.md
|
|
40
|
-
5. **
|
|
40
|
+
5. **Return** the complete document as your final response — you do not have a Write tool; the host persists your response to `{PREFIX}-Tech-Spec.md` verbatim. Do not attempt to write any file yourself.
|
|
41
41
|
|
|
42
|
-
## Output Filename
|
|
42
|
+
## Output Filename (for your reference only — you do not write this file)
|
|
43
43
|
|
|
44
44
|
Extract the **feature prefix** from the folder name containing `feature.md`:
|
|
45
45
|
- Folder: `FTR-001-user-management` → prefix: `FTR-001`
|
|
46
46
|
- Folder: `FTR-042-Search-Engine` → prefix: `FTR-042`
|
|
47
47
|
|
|
48
|
-
The prefix is everything up to and including the second hyphen-separated segment (pattern: `[A-Z]+-[0-9]+`).
|
|
49
|
-
|
|
50
|
-
Output file: `{PREFIX}-Tech-Spec.md` in the same directory as `feature.md`.
|
|
48
|
+
The prefix is everything up to and including the second hyphen-separated segment (pattern: `[A-Z]+-[0-9]+`). Use it in the document's own "Document Info" section; the host, not you, decides the actual output filename.
|
|
51
49
|
|
|
52
50
|
## Output Structure
|
|
53
51
|
|
|
54
|
-
Generate the document in **English** following this template. Adapt section content to the project's actual tech stack (from AGENTS.md):
|
|
52
|
+
Generate the document in **English** following this template. Adapt section content to the project's actual tech stack (from AGENTS.md). Your entire final response must be this document's content, and nothing else (no preamble, no "here is the spec", no Markdown code fence wrapping the whole thing) — the host writes your response verbatim to disk:
|
|
55
53
|
|
|
56
54
|
```markdown
|
|
57
55
|
# Technical Specification — [Feature Title]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
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
|
-
tools: Read, Glob, Grep
|
|
5
|
+
tools: Read, Glob, Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Generate Work Breakdown
|
|
@@ -186,9 +186,9 @@ Duration guidance:
|
|
|
186
186
|
|
|
187
187
|
- `estimate.tokens`: integer; rough estimate of token consumption (input + output) for the agent invocation; use project norms if available, otherwise estimate based on task complexity (small: 10 000–25 000, medium: 25 000–60 000)
|
|
188
188
|
|
|
189
|
-
### Step 7 —
|
|
189
|
+
### Step 7 — Return the JSON Output
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
Return `{PREFIX}-Work-Breakdown.json`'s content as your final response, and nothing else (no preamble, no explanatory prose, no Markdown code fence wrapping it) — you do not have a Write tool; the host persists your response verbatim to disk. The content must be:
|
|
192
192
|
- Valid JSON
|
|
193
193
|
- UTF-8 encoded
|
|
194
194
|
- 2-space indentation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
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
|
-
tools: Read, Grep, Glob, Bash
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
5
|
model: haiku
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -112,7 +112,7 @@ Report a score (1–5) for each KPI, where 5 = excellent, 1 = critical issues:
|
|
|
112
112
|
|
|
113
113
|
## Output
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
Return the assessment as your final response — you do not have a Write tool; the host persists your response verbatim to `{ASSESS_PREFIX}-Generic-Assessment.md` in the assessment directory. Your entire final response must be this document's content, and nothing else (no preamble, no "here is the assessment", no Markdown code fence wrapping the whole thing):
|
|
116
116
|
|
|
117
117
|
```markdown
|
|
118
118
|
# Generic Software Assessment — {ASSESS_PREFIX}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
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
|
-
tools: Read, Grep, Glob, Bash
|
|
5
|
+
tools: Read, Grep, Glob, Bash
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Intervention Documentation Standard
|
|
@@ -117,7 +117,7 @@ References to assessment findings (`{ASSESS_PREFIX}-G-NNN`), related ADRs, relat
|
|
|
117
117
|
|
|
118
118
|
## Interventions Index
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Alongside the intervention documents, compose one more file, `{ASSESS_PREFIX}-Interventions-Index.md`:
|
|
121
121
|
|
|
122
122
|
```markdown
|
|
123
123
|
# Interventions Index — {ASSESS_PREFIX}
|
|
@@ -130,3 +130,26 @@ After writing all intervention documents, produce `{ASSESS_PREFIX}-Interventions
|
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
Order by criticality (CRITICAL first), then by dependency (prerequisites before dependents).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Output Contract (MANDATORY — your final response)
|
|
137
|
+
|
|
138
|
+
You do not have a Write tool. You produce a **variable number of files** (one per intervention, plus the index) whose names you yourself decide (the `{area-slug}` in each filename), so the host cannot know their paths in advance the way it does for a single fixed deliverable — you must declare each file's path and content yourself, and the host validates and persists every one of them.
|
|
139
|
+
|
|
140
|
+
Return a single JSON object with exactly this shape, without Markdown fences or explanatory prose:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"files": [
|
|
145
|
+
{ "path": "{ASSESS_PREFIX}-INT-001-sql-injection-hardening.md", "content": "# ...full document content..." },
|
|
146
|
+
{ "path": "{ASSESS_PREFIX}-INT-002-god-class-decomposition.md", "content": "# ...full document content..." },
|
|
147
|
+
{ "path": "{ASSESS_PREFIX}-Interventions-Index.md", "content": "# Interventions Index — {ASSESS_PREFIX}\n\n| ID | ... |\n..." }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `files` is a non-empty array. Every intervention document you decided to write, plus the index, must appear here — there is no separate Write step and nothing you do not include in this array reaches disk.
|
|
153
|
+
- Each `path` is a bare filename (no directory components) — the host resolves it against the assessment output directory itself; never include `docs/`, `..`, or an absolute path.
|
|
154
|
+
- Each `content` is that file's complete, final content, verbatim — exactly as the host will write it byte-for-byte.
|
|
155
|
+
- The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this object in `structured_output`. Plain-text JSON alone does not satisfy that contract.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
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
|
-
tools: Read, Grep, Glob, Bash
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
5
|
model: haiku
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -135,7 +135,7 @@ Report the current state and recommend the appropriate option based on team size
|
|
|
135
135
|
|
|
136
136
|
## Output
|
|
137
137
|
|
|
138
|
-
Write to `{ASSESS_PREFIX}-Layer-Assessment.md
|
|
138
|
+
Return the assessment as your final response — you do not have a Write tool; the host persists your response verbatim to `{ASSESS_PREFIX}-Layer-Assessment.md`. Your entire final response must be this document's content, and nothing else (no preamble, no Markdown code fence wrapping the whole thing):
|
|
139
139
|
|
|
140
140
|
```markdown
|
|
141
141
|
# Layered Architecture Assessment — {ASSESS_PREFIX}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gaia-validate-feature-docs
|
|
3
|
-
description: "Validates Requirements and Tech-Spec documents against feature.md.
|
|
3
|
+
description: "Validates Requirements and Tech-Spec documents against feature.md. Returns the validation report content and a structured verdict; the host persists the report and owns revisions. Input: path to feature.md"
|
|
4
4
|
model: haiku
|
|
5
|
-
tools: Read, Glob, Grep
|
|
5
|
+
tools: Read, Glob, Grep
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Validate Feature Docs
|
|
@@ -111,13 +111,13 @@ Use the same remaining gaps in the report and the structured verdict. Record a g
|
|
|
111
111
|
|
|
112
112
|
---
|
|
113
113
|
|
|
114
|
-
## Phase 6 —
|
|
114
|
+
## Phase 6 — Compose the Validation Report Content (MANDATORY — before returning the verdict)
|
|
115
115
|
|
|
116
|
-
**This is not optional and not the same as the Phase 4 gap report.** The coverage report you produced in Phase 4 is on-screen text; it is NOT the deliverable.
|
|
116
|
+
**This is not optional and not the same as the Phase 4 gap report.** The coverage report you produced in Phase 4 is on-screen text; it is NOT the deliverable. You do not have a Write tool — the deliverable is the `report` field of your Phase 7 structured output, which the host persists to `{PREFIX}-Validation-Report.md` verbatim. Returning a verdict without a complete `report` field is a failure.
|
|
117
117
|
|
|
118
|
-
Execute this phase regardless of the validation outcome, then complete Phase 7. Downstream agents read
|
|
118
|
+
Execute this phase regardless of the validation outcome, then complete Phase 7. Downstream agents read the persisted file as a hard precondition; if the `report` field is missing or empty, the entire pipeline aborts.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Compose `{PREFIX}-Validation-Report.md`'s full content following this template:
|
|
121
121
|
|
|
122
122
|
```markdown
|
|
123
123
|
# Validation Report — {PREFIX}
|
|
@@ -146,22 +146,23 @@ Write `{PREFIX}-Validation-Report.md` in the same directory:
|
|
|
146
146
|
|
|
147
147
|
## Phase 7 — Structured Completion (MANDATORY — your final response)
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
Return a single JSON object with exactly these fields, without Markdown fences or explanatory prose:
|
|
150
150
|
|
|
151
151
|
```json
|
|
152
|
-
{"valid": true, "findings": []}
|
|
152
|
+
{"valid": true, "findings": [], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
For remaining gaps:
|
|
156
156
|
|
|
157
157
|
```json
|
|
158
|
-
{"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"]}
|
|
158
|
+
{"valid": false, "findings": ["Requirements: UC-01 lacks an acceptance criterion", "Tech-Spec: POST /api/users lacks a response definition"], "report": "# Validation Report — FTR-001\n\n## Summary\n..."}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
- `valid` is a boolean: `true` only when all required inputs were read, coverage is complete, and no unresolved gaps remain.
|
|
162
162
|
- `findings` is an array of strings, empty only for a clean validation. Missing inputs and ambiguous claims are unresolved findings.
|
|
163
|
-
-
|
|
164
|
-
-
|
|
163
|
+
- `report` is a non-empty string: the complete Phase 6 document content, verbatim, as the host will write it byte-for-byte to `{PREFIX}-Validation-Report.md`.
|
|
164
|
+
- The caller supplies `--json-schema`; use the runtime's structured-output mechanism to return this object in `structured_output`. Plain-text JSON alone does not satisfy that contract.
|
|
165
|
+
- There is only one deliverable now: the structured verdict, with `report` inside it. There is no separate Write step.
|
|
165
166
|
|
|
166
167
|
## Clarification Protocol
|
|
167
168
|
|
|
@@ -171,7 +172,7 @@ If a claim is contradictory or its correct resolution is ambiguous, do not guess
|
|
|
171
172
|
|
|
172
173
|
## Guidelines
|
|
173
174
|
|
|
174
|
-
- **Always
|
|
175
|
+
- **Always include the complete report content in the `report` field of your structured verdict** (Phases 6 and 7), clean or not — there is no Write tool to fall back on.
|
|
175
176
|
- **Validate only** — the host owns document revisions and agent dispatch
|
|
176
177
|
- **Be specific about gaps** — point to the exact section, claim, and what is missing
|
|
177
178
|
- **Perform one validation pass per invocation** — return remaining gaps to the host
|
|
@@ -29,20 +29,35 @@ return c.run(meta.name, outputDir, prefix, async () => {
|
|
|
29
29
|
const id = SCOPE_AGENT_MAP[area]
|
|
30
30
|
groups.set(id, [...(groups.get(id) || []), area])
|
|
31
31
|
}
|
|
32
|
+
// Neither assessor below has a Write tool: each returns its report as plain response
|
|
33
|
+
// text, which the host persists to its declared `outputs` path (see worker()'s own
|
|
34
|
+
// comment in lib/workflow-control.js for why — "Never ask an LLM to do I/O").
|
|
32
35
|
const assessmentResults = await c.parallel([...groups].map(([id, areas]) => async () => {
|
|
36
|
+
const suffix = { 'gaia.agent.assessment.generic': 'Generic', 'gaia.agent.assessment.layered-architecture': 'Layer', 'gaia.agent.assessment.concurrency': 'Concurrency' }[id]
|
|
37
|
+
const output_file = `${outputDir}/${prefix}-${suffix}-Assessment.md`
|
|
33
38
|
const result = await c.worker(id,
|
|
34
39
|
`${targetPath} --prefix ${prefix} --output-dir ${outputDir}\n` +
|
|
35
40
|
`Read-only assessment. Explicit scope: ${areas.join(', ')}. Inspect risks, evidence, severity and recommendations for EACH listed scope. ` +
|
|
36
41
|
'For security inspect trust boundaries and authorization; devops: build/deploy/observability; domain-model: invariants and ownership; dependencies: versions, provenance and supply-chain risks. ' +
|
|
37
|
-
'Do not remediate code or dispatch remediation agents.
|
|
38
|
-
|
|
39
|
-
const output_file = `${outputDir}/${prefix}-${suffix}-Assessment.md`
|
|
40
|
-
if (!c.read(output_file).trim()) throw new Error('Assessment report empty: ' + output_file)
|
|
42
|
+
'Do not remediate code or dispatch remediation agents. Return your assessment report as your final response.',
|
|
43
|
+
{ outputs: [{ path: output_file }] })
|
|
41
44
|
return { agent: id, scopes: areas, result, output_file }
|
|
42
45
|
}))
|
|
43
|
-
|
|
46
|
+
// intervention-documentation-standard writes a variable number of files (one per
|
|
47
|
+
// intervention plus an index) whose names it decides itself, so it cannot use the
|
|
48
|
+
// single static `outputs` contract above — it declares each {path, content} pair in
|
|
49
|
+
// its own structured_output, and the host validates and persists every one of them.
|
|
50
|
+
const docs = await c.worker('gaia.agent.assessment.intervention-documentation',
|
|
44
51
|
`--prefix ${prefix} --output-dir ${outputDir} --target ${targetPath}\nUse only the reports from these assessors: ${[...groups.keys()].join(', ')}.\n` +
|
|
45
|
-
assessmentResults.map(r => `${r.agent}: ${typeof r.result === 'string' ? r.result : JSON.stringify(r.result)}`).join('\n')
|
|
52
|
+
assessmentResults.map(r => `${r.agent}: ${typeof r.result === 'string' ? r.result : JSON.stringify(r.result)}`).join('\n'),
|
|
53
|
+
{ schema: { type: 'object', properties: { files: { type: 'array', items: { type: 'object',
|
|
54
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } }, required: ['files'] } })
|
|
55
|
+
if (!docs.files.length) throw new Error('Intervention documentation produced no files')
|
|
56
|
+
for (const f of docs.files) {
|
|
57
|
+
if (!f.path || /[\\/]/.test(f.path) || f.path.includes('..')) throw new Error('Invalid intervention document path: ' + f.path)
|
|
58
|
+
if (!f.content || !f.content.trim()) throw new Error('Empty intervention document: ' + f.path)
|
|
59
|
+
c.writeOutput(`${outputDir}/${f.path}`, f.content)
|
|
60
|
+
}
|
|
46
61
|
const metrics = c.assessmentEstimates(outputDir, prefix)
|
|
47
62
|
return { prefix, output_dir: outputDir, target_path: targetPath,
|
|
48
63
|
assessment_summaries: assessmentResults.map(({ agent, scopes, output_file }) => ({ agent, scopes, output_file })),
|
|
@@ -14,21 +14,24 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
14
14
|
const reqId = 'gaia.agent.planner.requirements'
|
|
15
15
|
const specId = 'gaia.agent.planner.tech-spec'
|
|
16
16
|
const valId = 'gaia.agent.planner.validate-feature-docs'
|
|
17
|
+
const output = (suffix, inputs = [featurePath]) => ({ path: file(suffix), inputs })
|
|
18
|
+
const requirements = () => c.worker(reqId, featurePath, { outputs: [output('Requirements')] })
|
|
19
|
+
const techSpec = () => c.worker(specId, featurePath, { outputs: [output('Tech-Spec', [featurePath, file('Requirements')])] })
|
|
17
20
|
let changed = false
|
|
18
21
|
if (force || !fresh(file('Requirements'))) {
|
|
19
|
-
await
|
|
22
|
+
await requirements()
|
|
20
23
|
if (!c.exists(file('Requirements'))) throw new Error('Requirements output missing')
|
|
21
24
|
changed = true
|
|
22
25
|
}
|
|
23
26
|
if (force || changed || !fresh(file('Tech-Spec'), [featurePath, file('Requirements')])) {
|
|
24
|
-
await
|
|
27
|
+
await techSpec()
|
|
25
28
|
if (!c.exists(file('Tech-Spec'))) throw new Error('Tech-Spec output missing')
|
|
26
29
|
changed = true
|
|
27
30
|
}
|
|
28
31
|
let validationSummary = 'skipped (fresh)'
|
|
29
32
|
if (force || changed || !fresh(file('Validation-Report'), [featurePath, file('Requirements'), file('Tech-Spec')])) {
|
|
30
33
|
for (let cycle = 1; cycle <= 3; cycle++) {
|
|
31
|
-
const result = await c.worker(valId, featurePath + '\nValidate only; the host owns the revision loop.
|
|
34
|
+
const result = await c.worker(valId, featurePath + '\nValidate only; the host owns the revision loop. Return valid, a findings array (one string per remaining gap), and the full validation report content. Do not dispatch other agents.', { label: `validation:${cycle}`, outputs: [output('Validation-Report', [featurePath, file('Requirements'), file('Tech-Spec')])], schema: { type: 'object', properties: { valid: { type: 'boolean' }, findings: { type: 'array', items: { type: 'string' } }, report: { type: 'string' } }, required: ['valid', 'findings', 'report'] } })
|
|
32
35
|
const text = result.findings.join('\n')
|
|
33
36
|
if (result.valid && result.findings.length === 0) {
|
|
34
37
|
if (!fresh(file('Validation-Report'), [featurePath, file('Requirements'), file('Tech-Spec')])) throw new Error('Validation agent did not produce its report')
|
|
@@ -36,8 +39,8 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
36
39
|
break
|
|
37
40
|
}
|
|
38
41
|
if (cycle === 3) throw new Error('Validation gaps remain after 3 cycles')
|
|
39
|
-
if (!text.includes('Tech-Spec') || text.includes('Requirements')) await
|
|
40
|
-
if (!text.includes('Requirements') || text.includes('Tech-Spec')) await
|
|
42
|
+
if (!text.includes('Tech-Spec') || text.includes('Requirements')) await requirements()
|
|
43
|
+
if (!text.includes('Requirements') || text.includes('Tech-Spec')) await techSpec()
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
c.append(`${featureDir}/${prefix}-process-log.txt`, `[${new Date().toISOString()}] pm-phase1: ${validationSummary}; Gate 1 approval requested\n`)
|
|
@@ -14,8 +14,10 @@ return c.run(meta.name, featureDir, prefix, async () => {
|
|
|
14
14
|
const approvals = c.read(file('Approvals.md'))
|
|
15
15
|
const gate = approvals.match(/^## Gate 1\b[^\n]*\n([\s\S]*?)(?=^## |$(?![\s\S]))/m)
|
|
16
16
|
if (!gate || !/✅\s*Approved/.test(gate[1])) throw new Error('Gate 1 approval evidence missing')
|
|
17
|
-
await c.worker('gaia.agent.planner.work-breakdown', featurePath)
|
|
18
17
|
const jsonPath = file('Work-Breakdown.json')
|
|
18
|
+
await c.worker('gaia.agent.planner.work-breakdown', featurePath, {
|
|
19
|
+
outputs: [{ path: jsonPath, inputs: [featurePath, file('Requirements.md'), file('Tech-Spec.md')] }],
|
|
20
|
+
})
|
|
19
21
|
const report = await c.activity('wb-validate', async () => {
|
|
20
22
|
const res = c.command(['run-asset', 'scripts/wb-validate.js', '--project', c.root,
|
|
21
23
|
'--', jsonPath, file('Requirements.md')], [0, 1])
|