@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/1-understand/glossary.md +1 -1
  4. package/docs/en/2-start/first-project.md +1 -1
  5. package/docs/en/2-start/initial-decisions.md +4 -2
  6. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
  7. package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
  8. package/docs/en/4-agents/README.md +6 -3
  9. package/docs/en/4-agents/briefing-refiner.md +146 -0
  10. package/docs/en/5-reference/README.md +1 -0
  11. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  12. package/docs/en/5-reference/cli-reference.md +6 -0
  13. package/docs/pt/1-entender/glossario.md +1 -1
  14. package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
  15. package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
  16. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  17. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  18. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
  19. package/docs/pt/4-agentes/README.md +13 -11
  20. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  21. package/docs/pt/4-agentes/briefing.md +6 -1
  22. package/docs/pt/4-agentes/dev.md +19 -1
  23. package/docs/pt/4-agentes/deyvin.md +4 -0
  24. package/docs/pt/4-agentes/discover.md +4 -0
  25. package/docs/pt/4-agentes/neo.md +4 -0
  26. package/docs/pt/4-agentes/orache.md +6 -0
  27. package/docs/pt/4-agentes/orchestrator.md +12 -0
  28. package/docs/pt/4-agentes/pentester.md +6 -0
  29. package/docs/pt/4-agentes/product.md +19 -1
  30. package/docs/pt/4-agentes/qa.md +10 -2
  31. package/docs/pt/4-agentes/setup.md +3 -1
  32. package/docs/pt/4-agentes/sheldon.md +12 -0
  33. package/docs/pt/4-agentes/tester.md +6 -0
  34. package/docs/pt/4-agentes/ux-ui.md +2 -1
  35. package/docs/pt/5-referencia/README.md +1 -1
  36. package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
  37. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  38. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  39. package/docs/pt/5-referencia/skills.md +2 -0
  40. package/docs/pt/agentes.md +3 -1
  41. package/package.json +1 -1
  42. package/src/agent-execution/adapters/base.js +15 -0
  43. package/src/agent-execution/adapters/claude.js +3 -0
  44. package/src/agent-execution/adapters/codex.js +3 -0
  45. package/src/agent-execution/adapters/opencode.js +3 -0
  46. package/src/agent-execution/capabilities.js +9 -0
  47. package/src/agent-execution/dispatcher.js +72 -0
  48. package/src/agent-execution/executable-resolver.js +7 -0
  49. package/src/agent-execution/manifest.js +62 -0
  50. package/src/agent-execution/model-catalog.js +80 -0
  51. package/src/agent-execution/model-resolver.js +132 -0
  52. package/src/agent-execution/reports.js +9 -0
  53. package/src/agent-execution/schema.js +48 -0
  54. package/src/agent-execution/telemetry-bridge.js +15 -0
  55. package/src/agents.js +1 -1
  56. package/src/artifact-kinds.js +2 -1
  57. package/src/autopilot-signal.js +71 -0
  58. package/src/cli.js +31 -5
  59. package/src/commands/agent-execution.js +36 -0
  60. package/src/commands/agents.js +18 -2
  61. package/src/commands/briefing.js +337 -1
  62. package/src/commands/feature-close.js +136 -43
  63. package/src/commands/live.js +47 -11
  64. package/src/commands/op-capture.js +33 -3
  65. package/src/commands/op-reinforce.js +10 -22
  66. package/src/commands/update.js +5 -1
  67. package/src/commands/verification-plan.js +56 -12
  68. package/src/commands/verify-artifact.js +64 -1
  69. package/src/commands/workflow-execute.js +168 -36
  70. package/src/commands/workflow-next.js +60 -16
  71. package/src/doctor.js +4 -2
  72. package/src/harness/criteria-runner.js +4 -1
  73. package/src/i18n/messages/en.js +2 -1
  74. package/src/i18n/messages/es.js +2 -1
  75. package/src/i18n/messages/fr.js +2 -1
  76. package/src/i18n/messages/pt-BR.js +2 -1
  77. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  78. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  79. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  80. package/src/lib/briefing-refiner/review-html.js +388 -68
  81. package/src/operator-memory/decision.js +41 -0
  82. package/src/parser.js +6 -0
  83. package/src/runtime-store.js +167 -8
  84. package/template/.aioson/agents/briefing-refiner.md +87 -47
  85. package/template/.aioson/agents/briefing.md +4 -0
  86. package/template/.aioson/agents/dev.md +9 -2
  87. package/template/.aioson/agents/deyvin.md +4 -0
  88. package/template/.aioson/agents/discover.md +4 -0
  89. package/template/.aioson/agents/neo.md +4 -0
  90. package/template/.aioson/agents/orache.md +4 -0
  91. package/template/.aioson/agents/orchestrator.md +16 -0
  92. package/template/.aioson/agents/pentester.md +4 -0
  93. package/template/.aioson/agents/product.md +26 -1
  94. package/template/.aioson/agents/qa.md +5 -1
  95. package/template/.aioson/agents/sheldon.md +9 -1
  96. package/template/.aioson/agents/tester.md +4 -0
  97. package/template/.aioson/agents/ux-ui.md +1 -1
  98. package/template/.aioson/docs/agent-help.md +126 -0
  99. package/template/.aioson/docs/autopilot-handoff.md +32 -16
  100. package/template/.aioson/docs/dev/phase-loop.md +9 -7
  101. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  102. package/template/.aioson/schemas/agent-execution.schema.json +28 -0
  103. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  104. package/template/AGENTS.md +36 -36
  105. package/template/CLAUDE.md +1 -1
@@ -235,14 +235,30 @@ async function resolveExecutablePath(command) {
235
235
  .map((entry) => entry.trim())
236
236
  .filter(Boolean);
237
237
 
238
- const extensions = process.platform === 'win32'
239
- ? Array.from(new Set([...String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').map((entry) => entry.toLowerCase())]))
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' && ext && !binary.toLowerCase().endsWith(ext)
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
- if (count >= PROMOTION_THRESHOLD) {
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 { readDecision, decisionPath, serializeDecision } = require('../operator-memory/decision');
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
- const decision = readDecision(resolved.identity, slug);
35
- if (!decision) {
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 = new Date().toISOString();
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: updated.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 };
@@ -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
- const trigger = TRIGGERS.includes(triggerRaw) ? triggerRaw : 'per-phase';
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 entry = {
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
- mode: dispatch ? dispatch.mode : 'native',
74
- model: dispatch ? dispatch.model : 'configured-default',
75
- report: resolveAgentReportPath(config, agentId, slug || '{slug}')
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 &quot;),
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) => {