@jaimevalasek/aioson 1.36.0 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/docs/en/1-understand/ecosystem-map.md +1 -1
- package/docs/en/1-understand/glossary.md +1 -1
- package/docs/en/2-start/first-project.md +1 -1
- package/docs/en/2-start/initial-decisions.md +4 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
- package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
- package/docs/en/4-agents/README.md +6 -3
- package/docs/en/4-agents/briefing-refiner.md +146 -0
- package/docs/en/5-reference/README.md +1 -0
- package/docs/en/5-reference/autopilot-handoff.md +286 -0
- package/docs/en/5-reference/cli-reference.md +6 -0
- package/docs/pt/1-entender/glossario.md +1 -1
- package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
- package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
- package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
- package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
- package/docs/pt/4-agentes/README.md +13 -11
- package/docs/pt/4-agentes/briefing-refiner.md +64 -40
- package/docs/pt/4-agentes/briefing.md +6 -1
- package/docs/pt/4-agentes/dev.md +19 -1
- package/docs/pt/4-agentes/deyvin.md +4 -0
- package/docs/pt/4-agentes/discover.md +4 -0
- package/docs/pt/4-agentes/neo.md +4 -0
- package/docs/pt/4-agentes/orache.md +6 -0
- package/docs/pt/4-agentes/orchestrator.md +12 -0
- package/docs/pt/4-agentes/pentester.md +6 -0
- package/docs/pt/4-agentes/product.md +19 -1
- package/docs/pt/4-agentes/qa.md +10 -2
- package/docs/pt/4-agentes/setup.md +3 -1
- package/docs/pt/4-agentes/sheldon.md +12 -0
- package/docs/pt/4-agentes/tester.md +6 -0
- package/docs/pt/4-agentes/ux-ui.md +2 -1
- package/docs/pt/5-referencia/README.md +1 -1
- package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
- package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
- package/docs/pt/5-referencia/comandos-cli.md +16 -7
- package/docs/pt/5-referencia/skills.md +2 -0
- package/docs/pt/agentes.md +3 -1
- package/package.json +1 -1
- package/src/agent-execution/adapters/base.js +15 -0
- package/src/agent-execution/adapters/claude.js +3 -0
- package/src/agent-execution/adapters/codex.js +3 -0
- package/src/agent-execution/adapters/opencode.js +3 -0
- package/src/agent-execution/capabilities.js +9 -0
- package/src/agent-execution/dispatcher.js +72 -0
- package/src/agent-execution/executable-resolver.js +7 -0
- package/src/agent-execution/manifest.js +62 -0
- package/src/agent-execution/model-catalog.js +80 -0
- package/src/agent-execution/model-resolver.js +132 -0
- package/src/agent-execution/reports.js +9 -0
- package/src/agent-execution/schema.js +48 -0
- package/src/agent-execution/telemetry-bridge.js +15 -0
- package/src/agents.js +1 -1
- package/src/artifact-kinds.js +2 -1
- package/src/autopilot-signal.js +71 -0
- package/src/cli.js +31 -5
- package/src/commands/agent-execution.js +36 -0
- package/src/commands/agents.js +18 -2
- package/src/commands/briefing.js +337 -1
- package/src/commands/feature-close.js +136 -43
- package/src/commands/live.js +47 -11
- package/src/commands/op-capture.js +33 -3
- package/src/commands/op-reinforce.js +10 -22
- package/src/commands/update.js +5 -1
- package/src/commands/verification-plan.js +56 -12
- package/src/commands/verify-artifact.js +64 -1
- package/src/commands/workflow-execute.js +168 -36
- package/src/commands/workflow-next.js +60 -16
- package/src/doctor.js +4 -2
- package/src/harness/criteria-runner.js +4 -1
- package/src/i18n/messages/en.js +2 -1
- package/src/i18n/messages/es.js +2 -1
- package/src/i18n/messages/fr.js +2 -1
- package/src/i18n/messages/pt-BR.js +2 -1
- package/src/lib/briefing-refiner/apply-feedback.js +18 -4
- package/src/lib/briefing-refiner/feedback-schema.js +73 -4
- package/src/lib/briefing-refiner/refinement-report.js +11 -0
- package/src/lib/briefing-refiner/review-html.js +388 -68
- package/src/operator-memory/decision.js +41 -0
- package/src/parser.js +6 -0
- package/src/runtime-store.js +167 -8
- package/template/.aioson/agents/briefing-refiner.md +87 -47
- package/template/.aioson/agents/briefing.md +4 -0
- package/template/.aioson/agents/dev.md +9 -2
- package/template/.aioson/agents/deyvin.md +4 -0
- package/template/.aioson/agents/discover.md +4 -0
- package/template/.aioson/agents/neo.md +4 -0
- package/template/.aioson/agents/orache.md +4 -0
- package/template/.aioson/agents/orchestrator.md +16 -0
- package/template/.aioson/agents/pentester.md +4 -0
- package/template/.aioson/agents/product.md +26 -1
- package/template/.aioson/agents/qa.md +5 -1
- package/template/.aioson/agents/sheldon.md +9 -1
- package/template/.aioson/agents/tester.md +4 -0
- package/template/.aioson/agents/ux-ui.md +1 -1
- package/template/.aioson/docs/agent-help.md +126 -0
- package/template/.aioson/docs/autopilot-handoff.md +32 -16
- package/template/.aioson/docs/dev/phase-loop.md +9 -7
- package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
- package/template/.aioson/schemas/agent-execution.schema.json +28 -0
- package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
- package/template/AGENTS.md +36 -36
- package/template/CLAUDE.md +1 -1
package/src/commands/live.js
CHANGED
|
@@ -235,14 +235,30 @@ async function resolveExecutablePath(command) {
|
|
|
235
235
|
.map((entry) => entry.trim())
|
|
236
236
|
.filter(Boolean);
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
// No Windows NUNCA testamos o arquivo sem extensão: o npm instala um shim
|
|
239
|
+
// extensionless (script sh) ao lado do .cmd, e ele não é executável via
|
|
240
|
+
// CreateProcess — spawná-lo dá ENOENT mesmo com o arquivo existindo.
|
|
241
|
+
// filter(Boolean) descarta segmentos vazios de um PATHEXT malformado
|
|
242
|
+
// (";.COM;..."), que viravam candidato sem extensão.
|
|
243
|
+
let extensions;
|
|
244
|
+
if (process.platform === 'win32') {
|
|
245
|
+
extensions = Array.from(new Set(
|
|
246
|
+
String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
247
|
+
.split(';')
|
|
248
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
));
|
|
251
|
+
if (extensions.length === 0) {
|
|
252
|
+
extensions = ['.exe', '.cmd', '.bat', '.com'];
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
extensions = [''];
|
|
256
|
+
}
|
|
241
257
|
|
|
242
258
|
for (const dir of pathEntries) {
|
|
243
259
|
for (const ext of extensions) {
|
|
244
|
-
const candidate = process.platform === 'win32'
|
|
245
|
-
? path.join(dir, `${binary}${ext}`)
|
|
260
|
+
const candidate = process.platform === 'win32'
|
|
261
|
+
? (binary.toLowerCase().endsWith(ext) ? path.join(dir, binary) : path.join(dir, `${binary}${ext}`))
|
|
246
262
|
: path.join(dir, binary);
|
|
247
263
|
if (await exists(candidate)) {
|
|
248
264
|
return candidate;
|
|
@@ -673,10 +689,30 @@ function createLiveEventRecord(context, options = {}) {
|
|
|
673
689
|
};
|
|
674
690
|
}
|
|
675
691
|
|
|
692
|
+
// Latch de spawn: entre o spawn() e o await de waitForChild existem awaits
|
|
693
|
+
// (writeLiveState etc.). Se o spawn falhar nessa janela, o 'error' emitia sem
|
|
694
|
+
// listener e DERRUBAVA o node ("Unhandled 'error' event"). Guardamos o
|
|
695
|
+
// desfecho no próprio child pra waitForChild resolver/rejeitar mesmo quando o
|
|
696
|
+
// evento já passou.
|
|
697
|
+
function trackChild(child) {
|
|
698
|
+
child.aiosonSpawnError = null;
|
|
699
|
+
child.aiosonExit = null;
|
|
700
|
+
child.on('error', (err) => {
|
|
701
|
+
if (!child.aiosonSpawnError) child.aiosonSpawnError = err;
|
|
702
|
+
});
|
|
703
|
+
child.on('close', (code, signal) => {
|
|
704
|
+
child.aiosonExit = { code: Number(code || 0), signal: signal || null };
|
|
705
|
+
});
|
|
706
|
+
return child;
|
|
707
|
+
}
|
|
708
|
+
|
|
676
709
|
function waitForChild(child) {
|
|
677
710
|
return new Promise((resolve, reject) => {
|
|
711
|
+
if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
|
|
712
|
+
if (child.aiosonExit) return resolve(child.aiosonExit);
|
|
678
713
|
child.once('error', reject);
|
|
679
714
|
child.once('close', (code, signal) => {
|
|
715
|
+
if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
|
|
680
716
|
resolve({ code: Number(code || 0), signal: signal || null });
|
|
681
717
|
});
|
|
682
718
|
});
|
|
@@ -1319,12 +1355,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1319
1355
|
let attachResult = null;
|
|
1320
1356
|
|
|
1321
1357
|
if (attach && !noLaunch) {
|
|
1322
|
-
attachChild = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1358
|
+
attachChild = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1323
1359
|
cwd: targetDir,
|
|
1324
1360
|
env: process.env,
|
|
1325
1361
|
stdio: 'inherit',
|
|
1326
1362
|
shell: process.platform === 'win32'
|
|
1327
|
-
});
|
|
1363
|
+
}));
|
|
1328
1364
|
state.child_pid = attachChild.pid || null;
|
|
1329
1365
|
if (existing.task?.task_key) {
|
|
1330
1366
|
const taskMeta = parseTaskMeta(existing.task);
|
|
@@ -1428,12 +1464,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1428
1464
|
});
|
|
1429
1465
|
} else {
|
|
1430
1466
|
// Fallback to normal spawn if tmux not available
|
|
1431
|
-
child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1467
|
+
child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1432
1468
|
cwd: targetDir,
|
|
1433
1469
|
env: process.env,
|
|
1434
1470
|
stdio: 'inherit',
|
|
1435
1471
|
shell: process.platform === 'win32'
|
|
1436
|
-
});
|
|
1472
|
+
}));
|
|
1437
1473
|
taskMeta.child_pid = child.pid || null;
|
|
1438
1474
|
updateTask(db, {
|
|
1439
1475
|
taskKey,
|
|
@@ -1441,12 +1477,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
|
|
|
1441
1477
|
});
|
|
1442
1478
|
}
|
|
1443
1479
|
} else {
|
|
1444
|
-
child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1480
|
+
child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
|
|
1445
1481
|
cwd: targetDir,
|
|
1446
1482
|
env: process.env,
|
|
1447
1483
|
stdio: 'inherit',
|
|
1448
1484
|
shell: process.platform === 'win32'
|
|
1449
|
-
});
|
|
1485
|
+
}));
|
|
1450
1486
|
taskMeta.child_pid = child.pid || null;
|
|
1451
1487
|
updateTask(db, {
|
|
1452
1488
|
taskKey,
|
|
@@ -23,14 +23,27 @@ const path = require('node:path');
|
|
|
23
23
|
const { resolveIdentity } = require('../operator-memory/identity');
|
|
24
24
|
const { ensureStorageTree, recordIdentityActivity, openIndexDb, getStorageRoot } = require('../operator-memory/storage');
|
|
25
25
|
const { deriveSlug, fingerprintProposal } = require('../operator-memory/slug');
|
|
26
|
-
const { captureSignal, readProposal, VALID_SIGNAL_TYPES } = require('../operator-memory/proposal');
|
|
27
|
-
const { promoteProposal } = require('../operator-memory/decision');
|
|
26
|
+
const { captureSignal, readProposal, deleteProposal, VALID_SIGNAL_TYPES } = require('../operator-memory/proposal');
|
|
27
|
+
const { promoteProposal, readDecision, reinforceDecision } = require('../operator-memory/decision');
|
|
28
28
|
const { emitDossierEvent } = require('../lib/dossier-telemetry');
|
|
29
29
|
|
|
30
30
|
const CONFIRMATIONS_JSONL = '.aioson/runtime/session-confirmations.jsonl';
|
|
31
31
|
|
|
32
32
|
const PROMOTION_THRESHOLD = 2;
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Detections required before a signal promotes to a decision, by signal type.
|
|
36
|
+
*
|
|
37
|
+
* `confirmation` needs 2x — the user accepting the same non-obvious approach twice
|
|
38
|
+
* is what turns it into a signal. `authorization` / `exclusion` / `correction` are
|
|
39
|
+
* single explicit standing decisions ("pode sempre X", "nunca X", "pare de X"), so
|
|
40
|
+
* they promote on first detection. Mirrors the taxonomy in
|
|
41
|
+
* agents/_shared/memory-capture-directive.md.
|
|
42
|
+
*/
|
|
43
|
+
function promotionThresholdFor(signalType) {
|
|
44
|
+
return signalType === 'confirmation' ? PROMOTION_THRESHOLD : 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
function existsCheckFactory(identity) {
|
|
35
48
|
return (slug) => {
|
|
36
49
|
const existing = readProposal(identity, slug);
|
|
@@ -130,7 +143,23 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
|
|
|
130
143
|
|
|
131
144
|
const count = result.proposal.detected_count;
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
// Idempotent re-detection: a signal already promoted to a decision is reinforced,
|
|
147
|
+
// not re-promoted — re-promotion would duplicate the FTS row and reset promoted_at.
|
|
148
|
+
const existingDecision = readDecision(resolved.identity, slug);
|
|
149
|
+
if (existingDecision) {
|
|
150
|
+
let reinforced = null;
|
|
151
|
+
try { reinforced = reinforceDecision(resolved.identity, slug); } catch { /* best-effort */ }
|
|
152
|
+
// captureSignal re-created a stray proposal (the decision had none) — drop it.
|
|
153
|
+
try { deleteProposal(resolved.identity, slug); } catch { /* best-effort */ }
|
|
154
|
+
const auditLine = `✔ Memory reforçada: '${proposal}'. aioson op:forget ${slug} p/ desfazer.`;
|
|
155
|
+
if (options.json) {
|
|
156
|
+
return { ok: true, promoted: false, reinforced: true, slug, identity: resolved.identity, reinforcement_count: reinforced ? reinforced.reinforcement_count : undefined };
|
|
157
|
+
}
|
|
158
|
+
if (logger) logger.log(auditLine);
|
|
159
|
+
return { ok: true, promoted: false, reinforced: true, slug };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (count >= promotionThresholdFor(signal)) {
|
|
134
163
|
// Promote to decision
|
|
135
164
|
let decision;
|
|
136
165
|
try {
|
|
@@ -166,6 +195,7 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
|
|
|
166
195
|
|
|
167
196
|
module.exports = {
|
|
168
197
|
runOpCapture,
|
|
198
|
+
promotionThresholdFor,
|
|
169
199
|
PROMOTION_THRESHOLD,
|
|
170
200
|
CONFIRMATIONS_JSONL
|
|
171
201
|
};
|
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* Phase 5 (v1.16.0). User-driven action when decay prompt fires.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const fs = require('node:fs');
|
|
9
8
|
const { resolveIdentity } = require('../operator-memory/identity');
|
|
10
9
|
const { ensureStorageTree } = require('../operator-memory/storage');
|
|
11
|
-
const {
|
|
12
|
-
const { regenerateIndex } = require('../operator-memory/index-md');
|
|
10
|
+
const { reinforceDecision } = require('../operator-memory/decision');
|
|
13
11
|
const { emitDossierEvent } = require('../lib/dossier-telemetry');
|
|
14
12
|
|
|
15
13
|
async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
@@ -31,30 +29,20 @@ async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
|
31
29
|
|
|
32
30
|
const resolved = resolveIdentity();
|
|
33
31
|
ensureStorageTree(resolved.identity);
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
|
|
33
|
+
// Surgical in-place reinforce: bumps reinforcement_count + refreshes
|
|
34
|
+
// last_reinforced and regenerates the index, preserving title/body/trigger-quotes
|
|
35
|
+
// byte-for-byte. (The old re-serialize path double-wrapped them, because the merged
|
|
36
|
+
// body from readDecision already contained the "# title" and "## Trigger quotes".)
|
|
37
|
+
const reinforced = reinforceDecision(resolved.identity, slug);
|
|
38
|
+
if (!reinforced) {
|
|
36
39
|
const err = `op:reinforce — decision '${slug}' not found for identity ${resolved.identity}.`;
|
|
37
40
|
if (options.json) return { ok: false, error: err };
|
|
38
41
|
if (logger && logger.error) logger.error(err);
|
|
39
42
|
return { ok: false, exitCode: 1, error: err };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
const now =
|
|
43
|
-
const previous = decision.last_reinforced;
|
|
44
|
-
const updated = {
|
|
45
|
-
...decision,
|
|
46
|
-
slug,
|
|
47
|
-
last_reinforced: now,
|
|
48
|
-
reinforcement_count: Number(decision.reinforcement_count || 0) + 1
|
|
49
|
-
};
|
|
50
|
-
// serialize keeps quotes + body + frontmatter intact
|
|
51
|
-
const out = serializeDecision({ ...updated, body: decision.body, title: decision.body?.split('\n')[0]?.replace(/^# /, '') || slug });
|
|
52
|
-
const filePath = decisionPath(resolved.identity, slug);
|
|
53
|
-
const tmp = `${filePath}.tmp`;
|
|
54
|
-
fs.writeFileSync(tmp, out, 'utf8');
|
|
55
|
-
fs.renameSync(tmp, filePath);
|
|
56
|
-
|
|
57
|
-
try { regenerateIndex(resolved.identity); } catch { /* non-fatal */ }
|
|
45
|
+
const { last_reinforced: now, previous, reinforcement_count } = reinforced;
|
|
58
46
|
|
|
59
47
|
await emitDossierEvent(targetDir, {
|
|
60
48
|
agent: 'op-reinforce',
|
|
@@ -64,7 +52,7 @@ async function runOpReinforce({ args = [], options = {}, logger }) {
|
|
|
64
52
|
});
|
|
65
53
|
|
|
66
54
|
if (options.json) {
|
|
67
|
-
return { ok: true, slug, last_reinforced: now, previous, reinforcement_count
|
|
55
|
+
return { ok: true, slug, last_reinforced: now, previous, reinforcement_count };
|
|
68
56
|
}
|
|
69
57
|
if (logger) logger.log(`op:reinforce — '${slug}' last_reinforced refreshed to ${now}.`);
|
|
70
58
|
return { ok: true, slug };
|
package/src/commands/update.js
CHANGED
|
@@ -5,6 +5,7 @@ const { detectFramework } = require('../detector');
|
|
|
5
5
|
const { updateInstallation } = require('../updater');
|
|
6
6
|
const { validateProjectContextFile, getInteractionLanguage } = require('../context');
|
|
7
7
|
const { applyAgentLocale } = require('../locales');
|
|
8
|
+
const { getCliVersionLabelSync } = require('../version');
|
|
8
9
|
|
|
9
10
|
async function runUpdate({ args, options, logger, t }) {
|
|
10
11
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
@@ -36,10 +37,13 @@ async function runUpdate({ args, options, logger, t }) {
|
|
|
36
37
|
(result.savedProfile && result.savedProfile.locale
|
|
37
38
|
? result.savedProfile.locale
|
|
38
39
|
: 'en');
|
|
39
|
-
localeSync = await applyAgentLocale(targetDir, language, { dryRun, selectiveUpdate: selective && !all });
|
|
40
|
+
localeSync = await applyAgentLocale(targetDir, language, { dryRun, selectiveUpdate: selective && !all });
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
logger.log(t('update.done_at', { targetDir }));
|
|
44
|
+
// Surface WHICH template landed — stale-template updates used to be silent
|
|
45
|
+
// (the copy comes from the installed CLI's own bundle, not from npm latest).
|
|
46
|
+
logger.log(t('update.template_version', { version: getCliVersionLabelSync() }));
|
|
43
47
|
logger.log(t('update.files_updated', { count: result.copied.length }));
|
|
44
48
|
logger.log(t('update.backups_created', { count: result.backedUp.length }));
|
|
45
49
|
if (result.migrations && result.migrations.profileRename && result.migrations.profileRename.changed) {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// instead of re-interpreting the config in prose. Read-only; never mutates state.
|
|
8
8
|
|
|
9
9
|
const fs = require('node:fs/promises');
|
|
10
|
-
const path = require('node:path');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { loadManifest, resolveAgentExecution } = require('../agent-execution/manifest');
|
|
11
12
|
const {
|
|
12
13
|
readVerificationConfig,
|
|
13
14
|
resolveHost,
|
|
@@ -24,6 +25,22 @@ const {
|
|
|
24
25
|
const BAR = '─'.repeat(60);
|
|
25
26
|
const CLASSIFICATIONS = ['MICRO', 'SMALL', 'MEDIUM'];
|
|
26
27
|
|
|
28
|
+
// A loud, deterministic instruction the phase loop echoes back to @dev at every
|
|
29
|
+
// phase boundary. On a per-phase trigger with auto_continue it is the fresh
|
|
30
|
+
// tool-result that stops the agent from ending its turn to "report" after a
|
|
31
|
+
// phase — the exact failure the phase loop exists to prevent. It never tells the
|
|
32
|
+
// agent to /compact: context is shed transparently by the host's auto-compact,
|
|
33
|
+
// without ending the turn.
|
|
34
|
+
function buildContinuationDirective(trigger, phaseLoop) {
|
|
35
|
+
if (trigger === 'end-of-feature') {
|
|
36
|
+
return 'END-OF-FEATURE — last phase: run the full runtime smoke (build + migrate + boot + Core happy-path), then hand off through the post-dev review cycle. Do not close/publish — that is the human gate.';
|
|
37
|
+
}
|
|
38
|
+
if (phaseLoop && phaseLoop.auto_continue) {
|
|
39
|
+
return 'CONTINUE-NOW — a clean report is a checkpoint, not a stop. Go DIRECTLY into the next phase in this same turn: do not stop, do not ask "continue?", do not summarize-and-end, and never self-issue /compact (the host auto-compacts transparently). Halt only on a failing gate/verification after retries or a genuine hard stop.';
|
|
40
|
+
}
|
|
41
|
+
return 'PAUSE — auto_continue is off: after this phase report, stop and wait for confirmation before the next phase.';
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
async function readFileSafe(filePath) {
|
|
28
45
|
try {
|
|
29
46
|
return await fs.readFile(filePath, 'utf8');
|
|
@@ -47,15 +64,28 @@ async function detectClassification(targetDir, slug) {
|
|
|
47
64
|
return null;
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
async function runVerificationPlan({ args, options = {}, logger }) {
|
|
67
|
+
async function runVerificationPlan({ args, options = {}, logger, catalogLoader }) {
|
|
51
68
|
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
52
69
|
const slug = (options.feature || options.slug || '').toString().trim() || null;
|
|
53
70
|
|
|
54
71
|
const triggerRaw = (options.trigger || 'per-phase').toString().trim().toLowerCase();
|
|
55
|
-
|
|
72
|
+
// An unknown trigger must fail loudly. Silently coercing it to per-phase used
|
|
73
|
+
// to emit an imperative CONTINUE-NOW directive on what was actually a typo'd
|
|
74
|
+
// end-of-feature call — steering @dev into a phase that does not exist.
|
|
75
|
+
if (!TRIGGERS.includes(triggerRaw)) {
|
|
76
|
+
const failure = { ok: false, reason: 'invalid_trigger', trigger: triggerRaw, valid_triggers: [...TRIGGERS] };
|
|
77
|
+
if (options.json) return failure;
|
|
78
|
+
logger.error(`Invalid --trigger=${triggerRaw}. Valid triggers: ${TRIGGERS.join(', ')}.`);
|
|
79
|
+
return failure;
|
|
80
|
+
}
|
|
81
|
+
const trigger = triggerRaw;
|
|
56
82
|
|
|
57
|
-
const config = await readVerificationConfig(targetDir);
|
|
58
|
-
const host = resolveHost(config, options.host);
|
|
83
|
+
const config = await readVerificationConfig(targetDir);
|
|
84
|
+
const host = resolveHost(config, options.host);
|
|
85
|
+
const executionManifest = slug ? await loadManifest(targetDir, slug) : { exists: false, legacy: true, ok: true };
|
|
86
|
+
if (executionManifest.exists && !executionManifest.ok) {
|
|
87
|
+
return { ok: false, reason: 'agent_execution_manifest_invalid', errors: executionManifest.errors, manifest_path: executionManifest.path };
|
|
88
|
+
}
|
|
59
89
|
|
|
60
90
|
let classification = (options.classification || '').toString().trim().toUpperCase();
|
|
61
91
|
if (!CLASSIFICATIONS.includes(classification)) {
|
|
@@ -66,13 +96,24 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
66
96
|
const context = { trigger, classification, sensitiveSurface };
|
|
67
97
|
const agents = [];
|
|
68
98
|
for (const agentId of VERIFICATION_AGENTS) {
|
|
69
|
-
const dispatch = getAgentDispatch(config, agentId, host);
|
|
70
|
-
const
|
|
99
|
+
const dispatch = getAgentDispatch(config, agentId, host);
|
|
100
|
+
const execution = executionManifest.exists ? await resolveAgentExecution(executionManifest.manifest, agentId, {}, { catalogLoader }) : null;
|
|
101
|
+
if (execution && !execution.ok) {
|
|
102
|
+
return { ok: false, reason: 'model_resolution_failed', agent: agentId, resolution: execution };
|
|
103
|
+
}
|
|
104
|
+
const entry = {
|
|
71
105
|
agent: agentId,
|
|
72
106
|
run: shouldRunForTrigger(config, agentId, context),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
host: execution ? execution.host : (dispatch ? dispatch.host : host),
|
|
108
|
+
mode: execution ? execution.mode : (dispatch ? dispatch.mode : 'native'),
|
|
109
|
+
model: execution ? execution.model : (dispatch ? dispatch.model : 'configured-default'),
|
|
110
|
+
model_requested: execution ? execution.model_requested : (dispatch ? dispatch.model : 'configured-default'),
|
|
111
|
+
model_resolved: execution ? execution.model_resolved : (dispatch ? dispatch.model : 'configured-default'),
|
|
112
|
+
model_resolution_strategy: execution ? execution.model_resolution_strategy : 'legacy',
|
|
113
|
+
reasoning_effort: execution ? (execution.reasoning_effort || null) : null,
|
|
114
|
+
report: execution ? execution.report.replace('{run_id}', '{run_id}') : resolveAgentReportPath(config, agentId, slug || '{slug}'),
|
|
115
|
+
execution_source: execution ? 'manifest' : 'legacy',
|
|
116
|
+
execution: execution || undefined
|
|
76
117
|
};
|
|
77
118
|
if (agentId === 'validator') {
|
|
78
119
|
const cc = getCrossCheck(config, agentId);
|
|
@@ -90,8 +131,10 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
90
131
|
sensitive_surface: sensitiveSurface,
|
|
91
132
|
agents,
|
|
92
133
|
phase_loop: getPhaseLoop(config),
|
|
93
|
-
budget: getBudget(config)
|
|
94
|
-
};
|
|
134
|
+
budget: getBudget(config)
|
|
135
|
+
};
|
|
136
|
+
result.agent_execution = executionManifest.exists ? { source: 'manifest', path: executionManifest.path, digest: executionManifest.digest } : { source: 'legacy' };
|
|
137
|
+
result.continuation_directive = buildContinuationDirective(trigger, result.phase_loop);
|
|
95
138
|
|
|
96
139
|
if (options.json) return result;
|
|
97
140
|
|
|
@@ -108,6 +151,7 @@ async function runVerificationPlan({ args, options = {}, logger }) {
|
|
|
108
151
|
logger.log(BAR);
|
|
109
152
|
const pl = result.phase_loop;
|
|
110
153
|
logger.log(`Phase loop: auto_continue=${pl.auto_continue} compact_between_phases=${pl.compact_between_phases} max_fix_retries=${pl.max_fix_retries_per_phase}`);
|
|
154
|
+
logger.log(`▶ ${result.continuation_directive}`);
|
|
111
155
|
logger.log('');
|
|
112
156
|
|
|
113
157
|
return result;
|
|
@@ -179,7 +179,7 @@ const RULESETS = {
|
|
|
179
179
|
|
|
180
180
|
// Kinds whose target file path is keyed by --slug; without it we cannot resolve
|
|
181
181
|
// the artifact, so fail with a clear usage error instead of a `null/` path.
|
|
182
|
-
const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy']);
|
|
182
|
+
const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy', 'review']);
|
|
183
183
|
|
|
184
184
|
// Kinds whose artifact has a date-stamped / caller-known path — resolved via
|
|
185
185
|
// --file=<path> rather than derived from a slug.
|
|
@@ -374,6 +374,69 @@ const ADAPTERS = {
|
|
|
374
374
|
return { ok: issues.length === 0, issues, warnings, checks: [{ id: 'site', ok: issues.length === 0, detail: issues.join('; ') || null }] };
|
|
375
375
|
},
|
|
376
376
|
|
|
377
|
+
// briefing-refiner — the review surface must be the deterministic, fully
|
|
378
|
+
// self-contained artifact `aioson briefing:review` renders: export fallbacks
|
|
379
|
+
// present, no external resources, an embedded source_hash, and a feedback
|
|
380
|
+
// JSON that passes the canonical schema. Staleness (briefings.md changed
|
|
381
|
+
// after generation, e.g. right after an apply) is a warning, not a failure.
|
|
382
|
+
review: async (ctx) => {
|
|
383
|
+
if (!ctx.slug) {
|
|
384
|
+
return { ok: false, issues: ['kind=review requires --slug=<briefing-slug>'], warnings: [], checks: [] };
|
|
385
|
+
}
|
|
386
|
+
const { hashText } = require('../lib/briefing-refiner/briefing-sections');
|
|
387
|
+
const { validateFeedback } = require('../lib/briefing-refiner/feedback-schema');
|
|
388
|
+
const briefingDir = path.resolve(ctx.targetDir, '.aioson', 'briefings', ctx.slug);
|
|
389
|
+
const issues = [];
|
|
390
|
+
const warnings = [];
|
|
391
|
+
|
|
392
|
+
let html = null;
|
|
393
|
+
try {
|
|
394
|
+
html = fs.readFileSync(path.join(briefingDir, 'review.html'), 'utf8');
|
|
395
|
+
} catch {
|
|
396
|
+
issues.push(`review.html not found for briefing "${ctx.slug}" — run: aioson briefing:review . --slug=${ctx.slug}`);
|
|
397
|
+
}
|
|
398
|
+
if (html) {
|
|
399
|
+
if (!html.includes('aioson:review')) {
|
|
400
|
+
issues.push('review.html lacks the aioson:review marker (hand-rolled surface) — regenerate with `aioson briefing:review`');
|
|
401
|
+
}
|
|
402
|
+
if (!/id="download"/.test(html)) issues.push('review.html has no download fallback button (id="download")');
|
|
403
|
+
if (!/id="copy"/.test(html)) issues.push('review.html has no copy-JSON fallback button (id="copy")');
|
|
404
|
+
// User content is HTML-escaped by the generator (quotes become "),
|
|
405
|
+
// so a real external reference can only be generator-emitted markup —
|
|
406
|
+
// match those forms only, or briefing text quoting HTML/CSS would
|
|
407
|
+
// false-positive the gate.
|
|
408
|
+
if (/<script[^>]+\bsrc=|<link[^>]+href=["']https?:|\bsrc=["']https?:/i.test(html)) {
|
|
409
|
+
issues.push('review.html references external resources — it must be fully self-contained');
|
|
410
|
+
}
|
|
411
|
+
const hashMatch = html.match(/source_hash=([0-9a-f]{64})/);
|
|
412
|
+
if (!hashMatch) {
|
|
413
|
+
issues.push('review.html has no embedded source_hash');
|
|
414
|
+
} else {
|
|
415
|
+
try {
|
|
416
|
+
const currentHash = hashText(fs.readFileSync(path.join(briefingDir, 'briefings.md'), 'utf8'));
|
|
417
|
+
if (hashMatch[1] !== currentHash) {
|
|
418
|
+
warnings.push('review.html is stale: source_hash differs from the current briefings.md — regenerate the review for the next round');
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
issues.push('briefings.md not found next to review.html');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const feedback = JSON.parse(fs.readFileSync(path.join(briefingDir, 'refinement-feedback.json'), 'utf8'));
|
|
428
|
+
const res = validateFeedback(feedback, { slug: ctx.slug, allowStale: true });
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
for (const err of res.errors) issues.push(`refinement-feedback.json: ${err}`);
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Absent right after an apply (it gets archived) — only a warning.
|
|
434
|
+
warnings.push('refinement-feedback.json missing or unreadable — expected right after review generation');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { ok: issues.length === 0, issues, warnings, checks: [{ id: `review:${ctx.slug}`, ok: issues.length === 0, detail: issues.join('; ') || null }] };
|
|
438
|
+
},
|
|
439
|
+
|
|
377
440
|
// committer — advisory subject-quality audit. Reads --file if given, else the
|
|
378
441
|
// HEAD commit message (post-commit, so the agent can amend before push).
|
|
379
442
|
'commit-message': async (ctx) => {
|