@jaimevalasek/aioson 1.33.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/docs/en/1-understand/ecosystem-map.md +45 -29
  3. package/docs/en/1-understand/glossary.md +6 -6
  4. package/docs/en/1-understand/what-is-aioson.md +5 -5
  5. package/docs/en/2-start/existing-project.md +7 -7
  6. package/docs/en/2-start/first-project.md +33 -39
  7. package/docs/en/2-start/initial-decisions.md +22 -19
  8. package/docs/en/3-recipes/README.md +2 -2
  9. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  10. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +13 -3
  11. package/docs/en/3-recipes/full-feature-with-sheldon.md +329 -338
  12. package/docs/en/4-agents/README.md +33 -16
  13. package/docs/en/4-agents/briefing-refiner.md +146 -0
  14. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  15. package/docs/en/4-agents/forge-run.md +3 -2
  16. package/docs/en/5-reference/README.md +1 -0
  17. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  18. package/docs/en/5-reference/cli-reference.md +57 -48
  19. package/docs/en/5-reference/executable-verification.md +10 -7
  20. package/docs/en/5-reference/parallel.md +2 -0
  21. package/docs/en/5-reference/qa-browser.md +2 -2
  22. package/docs/en/README.md +3 -3
  23. package/docs/pt/1-entender/glossario.md +5 -5
  24. package/docs/pt/1-entender/mapa-do-ecossistema.md +33 -22
  25. package/docs/pt/2-comecar/decisoes-iniciais.md +20 -18
  26. package/docs/pt/2-comecar/primeiro-projeto.md +2 -2
  27. package/docs/pt/3-receitas/README.md +1 -1
  28. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  29. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  30. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +291 -338
  31. package/docs/pt/4-agentes/README.md +13 -11
  32. package/docs/pt/4-agentes/analyst.md +9 -5
  33. package/docs/pt/4-agentes/architect.md +9 -5
  34. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  35. package/docs/pt/4-agentes/briefing.md +6 -1
  36. package/docs/pt/4-agentes/dev.md +38 -8
  37. package/docs/pt/4-agentes/deyvin.md +4 -0
  38. package/docs/pt/4-agentes/discover.md +4 -0
  39. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  40. package/docs/pt/4-agentes/neo.md +4 -0
  41. package/docs/pt/4-agentes/orache.md +6 -0
  42. package/docs/pt/4-agentes/orchestrator.md +176 -118
  43. package/docs/pt/4-agentes/pentester.md +11 -5
  44. package/docs/pt/4-agentes/pm.md +15 -7
  45. package/docs/pt/4-agentes/product.md +19 -1
  46. package/docs/pt/4-agentes/qa.md +10 -2
  47. package/docs/pt/4-agentes/setup.md +3 -1
  48. package/docs/pt/4-agentes/sheldon.md +178 -99
  49. package/docs/pt/4-agentes/tester.md +6 -0
  50. package/docs/pt/4-agentes/ux-ui.md +9 -5
  51. package/docs/pt/5-referencia/README.md +1 -1
  52. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  53. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  54. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  55. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  56. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  57. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  58. package/docs/pt/5-referencia/skills.md +2 -0
  59. package/docs/pt/README.md +4 -4
  60. package/docs/pt/agentes.md +50 -50
  61. package/package.json +2 -2
  62. package/src/agents.js +1 -1
  63. package/src/artifact-kinds.js +111 -0
  64. package/src/autopilot-signal.js +71 -0
  65. package/src/cli.js +112 -81
  66. package/src/commands/agent-epilogue.js +251 -186
  67. package/src/commands/agents.js +122 -50
  68. package/src/commands/audit-code.js +344 -0
  69. package/src/commands/briefing.js +337 -1
  70. package/src/commands/classify.js +389 -389
  71. package/src/commands/feature-close.js +122 -4
  72. package/src/commands/harness-check.js +259 -175
  73. package/src/commands/live.js +71 -11
  74. package/src/commands/review-feature.js +189 -0
  75. package/src/commands/runtime.js +81 -66
  76. package/src/commands/sync-agents-copy.js +115 -0
  77. package/src/commands/update.js +5 -1
  78. package/src/commands/verification-plan.js +143 -0
  79. package/src/commands/verify-artifact.js +593 -0
  80. package/src/commands/workflow-execute.js +434 -316
  81. package/src/commands/workflow-next.js +193 -20
  82. package/src/commands/workflow-plan.js +5 -5
  83. package/src/constants.js +2 -0
  84. package/src/doctor.js +4 -2
  85. package/src/gateway-pointer-merge.js +7 -1
  86. package/src/handoff-contract.js +267 -172
  87. package/src/harness/contract-integrity-gate.js +172 -0
  88. package/src/harness/contract-integrity.js +111 -0
  89. package/src/harness/contract-schema.js +377 -332
  90. package/src/harness/detect-runtime-feature.js +90 -0
  91. package/src/harness/static-criteria.js +192 -0
  92. package/src/i18n/messages/en.js +10 -5
  93. package/src/i18n/messages/es.js +10 -5
  94. package/src/i18n/messages/fr.js +10 -5
  95. package/src/i18n/messages/pt-BR.js +10 -5
  96. package/src/install-wizard.js +8 -8
  97. package/src/installer.js +13 -6
  98. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  99. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  100. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  101. package/src/lib/briefing-refiner/review-html.js +388 -68
  102. package/src/parser.js +12 -0
  103. package/src/preflight-engine.js +12 -12
  104. package/src/verification/policy-engine.js +95 -95
  105. package/src/verification-policy.js +398 -0
  106. package/src/version.js +52 -1
  107. package/template/.aioson/agents/architect.md +34 -5
  108. package/template/.aioson/agents/briefing-refiner.md +91 -48
  109. package/template/.aioson/agents/briefing.md +4 -0
  110. package/template/.aioson/agents/committer.md +2 -1
  111. package/template/.aioson/agents/copywriter.md +30 -21
  112. package/template/.aioson/agents/design-hybrid-forge.md +10 -1
  113. package/template/.aioson/agents/dev.md +35 -23
  114. package/template/.aioson/agents/deyvin.md +4 -0
  115. package/template/.aioson/agents/discover.md +16 -3
  116. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  117. package/template/.aioson/agents/genome.md +19 -10
  118. package/template/.aioson/agents/neo.md +34 -30
  119. package/template/.aioson/agents/orache.md +24 -11
  120. package/template/.aioson/agents/orchestrator.md +100 -7
  121. package/template/.aioson/agents/pentester.md +4 -0
  122. package/template/.aioson/agents/pm.md +8 -8
  123. package/template/.aioson/agents/product.md +25 -1
  124. package/template/.aioson/agents/profiler-enricher.md +20 -11
  125. package/template/.aioson/agents/profiler-forge.md +20 -11
  126. package/template/.aioson/agents/profiler-researcher.md +20 -11
  127. package/template/.aioson/agents/qa.md +96 -31
  128. package/template/.aioson/agents/setup.md +12 -1
  129. package/template/.aioson/agents/sheldon.md +103 -13
  130. package/template/.aioson/agents/site-forge.md +11 -2
  131. package/template/.aioson/agents/squad.md +20 -5
  132. package/template/.aioson/agents/tester.md +4 -0
  133. package/template/.aioson/agents/ux-ui.md +2 -1
  134. package/template/.aioson/agents/validator.md +33 -1
  135. package/template/.aioson/config/verification.json +61 -0
  136. package/template/.aioson/config.md +13 -9
  137. package/template/.aioson/docs/LAYERS.md +2 -2
  138. package/template/.aioson/docs/agent-help.md +126 -0
  139. package/template/.aioson/docs/autopilot-handoff.md +30 -20
  140. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  141. package/template/.aioson/docs/dev/phase-loop.md +50 -0
  142. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  143. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  144. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  145. package/template/.aioson/docs/product/prd-contract.md +12 -12
  146. package/template/.aioson/docs/prototype-contract.md +21 -4
  147. package/template/.aioson/docs/reference-identity.md +94 -0
  148. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  149. package/template/.aioson/docs/verification-config.md +82 -0
  150. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  151. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  152. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  153. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  154. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  155. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  156. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  157. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  158. package/template/.aioson/skills/process/prototype-forge/SKILL.md +6 -0
  159. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  160. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  161. package/template/AGENTS.md +36 -36
  162. package/template/CLAUDE.md +15 -11
@@ -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;
@@ -262,6 +278,24 @@ function spawnExecutable(binaryPath) {
262
278
  return process.platform === 'win32' ? `"${binaryPath}"` : binaryPath;
263
279
  }
264
280
 
281
+ function isWindowsPidAlive(pid) {
282
+ // process.kill(pid, 0) is not authoritative on Windows: it throws EPERM both
283
+ // for "process exists but no access" AND (commonly) for PIDs that are already
284
+ // gone. Probe the real process table so a dead PID reconciles instead of
285
+ // looking 'unknown' forever (which blocks live:start with "session already
286
+ // active"). Returns true/false when known, null when the probe itself failed.
287
+ try {
288
+ const out = require('node:child_process').execFileSync(
289
+ 'tasklist',
290
+ ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'],
291
+ { encoding: 'utf8', windowsHide: true, timeout: 3000 }
292
+ );
293
+ return out.includes(`"${pid}"`);
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+
265
299
  function detectProcessState(pid) {
266
300
  if (!pid) return 'not_tracked';
267
301
  try {
@@ -271,6 +305,12 @@ function detectProcessState(pid) {
271
305
  if (error && error.code === 'ESRCH') {
272
306
  return 'dead';
273
307
  }
308
+ if (process.platform === 'win32') {
309
+ const alive = isWindowsPidAlive(Number(pid));
310
+ if (alive === true) return 'alive';
311
+ if (alive === false) return 'dead';
312
+ // probe inconclusive → fall through to 'unknown'
313
+ }
274
314
  return 'unknown';
275
315
  }
276
316
  }
@@ -649,10 +689,30 @@ function createLiveEventRecord(context, options = {}) {
649
689
  };
650
690
  }
651
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
+
652
709
  function waitForChild(child) {
653
710
  return new Promise((resolve, reject) => {
711
+ if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
712
+ if (child.aiosonExit) return resolve(child.aiosonExit);
654
713
  child.once('error', reject);
655
714
  child.once('close', (code, signal) => {
715
+ if (child.aiosonSpawnError) return reject(child.aiosonSpawnError);
656
716
  resolve({ code: Number(code || 0), signal: signal || null });
657
717
  });
658
718
  });
@@ -1295,12 +1355,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1295
1355
  let attachResult = null;
1296
1356
 
1297
1357
  if (attach && !noLaunch) {
1298
- attachChild = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1358
+ attachChild = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1299
1359
  cwd: targetDir,
1300
1360
  env: process.env,
1301
1361
  stdio: 'inherit',
1302
1362
  shell: process.platform === 'win32'
1303
- });
1363
+ }));
1304
1364
  state.child_pid = attachChild.pid || null;
1305
1365
  if (existing.task?.task_key) {
1306
1366
  const taskMeta = parseTaskMeta(existing.task);
@@ -1404,12 +1464,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1404
1464
  });
1405
1465
  } else {
1406
1466
  // Fallback to normal spawn if tmux not available
1407
- child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1467
+ child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1408
1468
  cwd: targetDir,
1409
1469
  env: process.env,
1410
1470
  stdio: 'inherit',
1411
1471
  shell: process.platform === 'win32'
1412
- });
1472
+ }));
1413
1473
  taskMeta.child_pid = child.pid || null;
1414
1474
  updateTask(db, {
1415
1475
  taskKey,
@@ -1417,12 +1477,12 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1417
1477
  });
1418
1478
  }
1419
1479
  } else {
1420
- child = spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1480
+ child = trackChild(spawn(spawnExecutable(binaryPath), buildLaunchArgs(options, tool), {
1421
1481
  cwd: targetDir,
1422
1482
  env: process.env,
1423
1483
  stdio: 'inherit',
1424
1484
  shell: process.platform === 'win32'
1425
- });
1485
+ }));
1426
1486
  taskMeta.child_pid = child.pid || null;
1427
1487
  updateTask(db, {
1428
1488
  taskKey,
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson review:feature [path] [--feature=<slug>] [--scope=<target>]
5
+ * [--skip-audit] [--out-dir=<dir>] [--tool=<tool>] [--json]
6
+ *
7
+ * One-shot "review an already-implemented feature" helper. It does NOT replace
8
+ * the workflow gates — it is the post-close / standalone path for re-running the
9
+ * adversarial + test passes against a feature:
10
+ *
11
+ * 1. runs the deterministic `security:audit` for the slug (writes/refreshes
12
+ * security-findings-{slug}.json),
13
+ * 2. prepares the `@pentester` and `@tester` activation prompts (read-only,
14
+ * headless — no runtime session, no state mutation),
15
+ * 3. prints a copy-pasteable plan (or saves the prompts to files / emits JSON).
16
+ *
17
+ * Slug resolution: explicit `--feature`/`--slug` wins; otherwise the active
18
+ * feature (pulse / unique in_progress). After `feature:close` there is no active
19
+ * feature, so the caller must pass `--feature` — the command says so explicitly.
20
+ */
21
+
22
+ const path = require('node:path');
23
+ const fs = require('node:fs');
24
+
25
+ const { runSecurityAudit } = require('./security-audit');
26
+ const { runAgentPrompt } = require('./agents');
27
+ const { resolveActiveFeature } = require('./feature-current');
28
+
29
+ function silentLogger() {
30
+ return { log() {}, error() {} };
31
+ }
32
+
33
+ // Build an agent activation prompt without side effects (headless skips workflow
34
+ // routing, runtime bootstrap and telemetry). Returns the prompt string, or an
35
+ // `{ error }` object when the prompt could not be built.
36
+ async function buildAgentPromptText(agentName, targetDir, agentOptions, t) {
37
+ try {
38
+ const res = await runAgentPrompt({
39
+ args: [agentName, targetDir],
40
+ options: { ...agentOptions, headless: true },
41
+ logger: silentLogger(),
42
+ t
43
+ });
44
+ return res && res.ok && typeof res.prompt === 'string' ? res.prompt : { error: 'prompt_unavailable' };
45
+ } catch (err) {
46
+ return { error: err.message };
47
+ }
48
+ }
49
+
50
+ async function runReviewFeature({ args, options = {}, logger, t }) {
51
+ const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
52
+ const tool = options.tool || 'claude';
53
+
54
+ // 1. Resolve the feature slug.
55
+ let slug = String(options.feature || options.slug || '').trim();
56
+ let slugSource = slug ? 'explicit' : null;
57
+ if (!slug) {
58
+ const active = await resolveActiveFeature(targetDir);
59
+ if (active.ambiguous) {
60
+ const msg = `review:feature: ${active.candidates.length} features are in_progress (${active.candidates.join(', ')}). Pass --feature=<slug>.`;
61
+ if (options.json) return { ok: false, reason: 'ambiguous_feature', candidates: active.candidates };
62
+ logger.error(msg);
63
+ return { ok: false, reason: 'ambiguous_feature' };
64
+ }
65
+ if (!active.slug) {
66
+ const msg = 'review:feature: no active feature found — pass --feature=<slug> (a closed feature is no longer the active one).';
67
+ if (options.json) return { ok: false, reason: 'missing_feature' };
68
+ logger.error(msg);
69
+ return { ok: false, reason: 'missing_feature' };
70
+ }
71
+ slug = active.slug;
72
+ slugSource = active.source;
73
+ }
74
+
75
+ // 2. Deterministic security audit (non-fatal — the review still produces the
76
+ // agent prompts even when the audit cannot run, e.g. archived artifacts).
77
+ let audit = null;
78
+ const skipAudit = Boolean(options['skip-audit'] || options.skipAudit);
79
+ if (!skipAudit) {
80
+ // security:audit sets process.exitCode by finding severity. review:feature
81
+ // is a prep/report command — surfacing findings is its job, not a failure —
82
+ // so capture the audit's verdict in the result but do not let it set this
83
+ // command's exit code.
84
+ const prevExitCode = process.exitCode;
85
+ audit = await runSecurityAudit({
86
+ args: [targetDir],
87
+ options: { feature: slug, json: true, ...(options.now ? { now: options.now } : {}) },
88
+ logger: silentLogger()
89
+ });
90
+ process.exitCode = prevExitCode;
91
+ }
92
+
93
+ // 3. Prepare the @pentester + @tester prompts.
94
+ const scope = String(options.scope || '').trim();
95
+ const pentesterPrompt = await buildAgentPromptText('pentester', targetDir, {
96
+ tool,
97
+ feature: slug,
98
+ ...(scope ? { mode: 'app_target', scope } : {})
99
+ }, t);
100
+ const testerPrompt = await buildAgentPromptText('tester', targetDir, { tool, feature: slug }, t);
101
+
102
+ // optional: persist the prompts to files
103
+ const outDirOpt = options['out-dir'] || options.outDir;
104
+ const saved = {};
105
+ if (outDirOpt) {
106
+ const dir = path.resolve(targetDir, String(outDirOpt));
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ if (typeof pentesterPrompt === 'string') {
109
+ const p = path.join(dir, `review-pentester-${slug}.prompt.md`);
110
+ fs.writeFileSync(p, pentesterPrompt, 'utf8');
111
+ saved.pentester = p;
112
+ }
113
+ if (typeof testerPrompt === 'string') {
114
+ const p = path.join(dir, `review-tester-${slug}.prompt.md`);
115
+ fs.writeFileSync(p, testerPrompt, 'utf8');
116
+ saved.tester = p;
117
+ }
118
+ }
119
+
120
+ const result = {
121
+ ok: true,
122
+ slug,
123
+ slug_source: slugSource,
124
+ audit: audit
125
+ ? {
126
+ ok: audit.ok,
127
+ exitCode: audit.exitCode,
128
+ classification: audit.classification || null,
129
+ summary: audit.summary || null,
130
+ findingsCount: audit.findingsCount ?? null,
131
+ artifactPath: audit.artifactPath || null,
132
+ reason: audit.reason || null
133
+ }
134
+ : null,
135
+ prompts: {
136
+ pentester: typeof pentesterPrompt === 'string' ? pentesterPrompt : null,
137
+ tester: typeof testerPrompt === 'string' ? testerPrompt : null
138
+ },
139
+ prompt_errors: {
140
+ pentester: pentesterPrompt && pentesterPrompt.error ? pentesterPrompt.error : null,
141
+ tester: testerPrompt && testerPrompt.error ? testerPrompt.error : null
142
+ },
143
+ saved,
144
+ commands: {
145
+ pentester: `aioson agent:prompt pentester . --tool=${tool} --mode=app_target --feature=${slug} --scope=<target>`,
146
+ tester: `aioson agent:prompt tester . --tool=${tool} --feature=${slug}`
147
+ }
148
+ };
149
+
150
+ if (options.json) return result;
151
+
152
+ // ── human output ──
153
+ const resolvedNote = slugSource && slugSource !== 'explicit' ? ` (resolved from ${slugSource})` : '';
154
+ logger.log(`Review plan — feature: ${slug}${resolvedNote}`);
155
+ logger.log('');
156
+
157
+ if (!skipAudit && audit) {
158
+ const s = audit.summary;
159
+ if (s) {
160
+ logger.log(`1. security:audit (deterministic) — ${audit.classification || '?'}: ${s.critical || 0} critical, ${s.high || 0} high, ${s.medium || 0} medium, ${s.low || 0} low, ${s.inconclusive || 0} inconclusive`);
161
+ if (audit.artifactPath) logger.log(` findings → ${path.relative(targetDir, audit.artifactPath)}`);
162
+ } else {
163
+ logger.log(`1. security:audit — could not run (${audit.reason || 'unknown'}); proceeding with the agent prompts.`);
164
+ }
165
+ } else {
166
+ logger.log('1. security:audit — skipped (--skip-audit)');
167
+ }
168
+ logger.log('');
169
+
170
+ logger.log('2. @pentester — adversarial review (vulnerabilities, threat surface):');
171
+ if (saved.pentester) logger.log(` saved → ${path.relative(targetDir, saved.pentester)}`);
172
+ if (!scope) logger.log(' tip: pass --scope=<target> for a precise app_target review contract');
173
+ logger.log('');
174
+ logger.log(typeof pentesterPrompt === 'string'
175
+ ? pentesterPrompt
176
+ : ` [could not build @pentester prompt: ${pentesterPrompt.error}]`);
177
+ logger.log('');
178
+
179
+ logger.log('3. @tester — engineering test pass (missing tests, regressions):');
180
+ if (saved.tester) logger.log(` saved → ${path.relative(targetDir, saved.tester)}`);
181
+ logger.log('');
182
+ logger.log(typeof testerPrompt === 'string'
183
+ ? testerPrompt
184
+ : ` [could not build @tester prompt: ${testerPrompt.error}]`);
185
+
186
+ return result;
187
+ }
188
+
189
+ module.exports = { runReviewFeature };
@@ -1171,6 +1171,12 @@ async function runRuntimeLog({ args, options = {}, logger, t }) {
1171
1171
  *
1172
1172
  * Intended to be called ONCE at the very end of an agent session, after delivering the main artifact.
1173
1173
  */
1174
+ function logVerifyArtifactLine(logger, va) {
1175
+ if (!va) return;
1176
+ const marker = va.skipped ? 'hint' : va.ok ? 'ok' : 'advisory';
1177
+ logger.log(`agent:done — verify:artifact (${va.kind}): ${marker}${va.reason ? ` — ${va.reason}` : ''}`);
1178
+ }
1179
+
1174
1180
  async function runAgentDone({ args, options = {}, logger, t }) {
1175
1181
  const targetDir = resolveTargetDir(args);
1176
1182
  const agentName = String(options.agent || '').trim();
@@ -1187,6 +1193,13 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1187
1193
  ? String(options.artifacts).split(',').map((p) => p.trim()).filter(Boolean)
1188
1194
  : [];
1189
1195
 
1196
+ // Build-free artifact done-gate (advisory): for the peripheral agents that
1197
+ // produce a non-code artifact, prove it is complete/well-formed at the same
1198
+ // session-end call they always make, instead of relying on each agent to run
1199
+ // its `## Done gate` line. Resolved once here; surfaced on every return path.
1200
+ const { verifyAgentArtifact } = require('../artifact-kinds');
1201
+ const verifyArtifact = await verifyAgentArtifact({ targetDir, agent: normalizedAgent, options });
1202
+
1190
1203
  const { db, dbPath, runtimeDir } = await openRuntimeDb(targetDir);
1191
1204
 
1192
1205
  try {
@@ -1224,7 +1237,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1224
1237
  }
1225
1238
 
1226
1239
  // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1227
- const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1240
+ const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1228
1241
 
1229
1242
  if (isDocCreatingAgent(normalizedAgent)) {
1230
1243
  backupAiosonDocs(targetDir).catch(() => {});
@@ -1252,7 +1265,8 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1252
1265
  });
1253
1266
  } catch { /* ignore — never blocks agent_done */ }
1254
1267
 
1255
- return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey, auto_advance: autoAdvance };
1268
+ if (!options.json) logVerifyArtifactLine(logger, verifyArtifact);
1269
+ return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'live_event', runKey: session.runKey, auto_advance: autoAdvance, verify_artifact: verifyArtifact };
1256
1270
  }
1257
1271
 
1258
1272
  // No active session — create a standalone task+run and immediately complete it.
@@ -1297,7 +1311,7 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1297
1311
  }
1298
1312
 
1299
1313
  // F2 (workflow-handoff-integrity v1.9.5) — best-effort auto-advance workflow pointer
1300
- const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1314
+ const autoAdvance = await maybeAutoAdvanceWorkflow({ targetDir, normalizedAgent, options, logger, t });
1301
1315
 
1302
1316
  if (isDocCreatingAgent(normalizedAgent)) {
1303
1317
  backupAiosonDocs(targetDir).catch(() => {});
@@ -1325,7 +1339,8 @@ async function runAgentDone({ args, options = {}, logger, t }) {
1325
1339
  });
1326
1340
  } catch { /* ignore — never blocks agent_done */ }
1327
1341
 
1328
- return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey, auto_advance: autoAdvance };
1342
+ if (!options.json) logVerifyArtifactLine(logger, verifyArtifact);
1343
+ return { ok: true, targetDir, dbPath, agent: normalizedAgent, mode: 'standalone', runKey, taskKey, auto_advance: autoAdvance, verify_artifact: verifyArtifact };
1329
1344
  } finally {
1330
1345
  db.close();
1331
1346
  }
@@ -2026,9 +2041,9 @@ function parseDurationMs(value, defaultHours = 24) {
2026
2041
  *
2027
2042
  * Sources checked:
2028
2043
  * 1. Session files in .aioson/.sessions/ with finished=false older than threshold.
2029
- * 2. agent_runs rows with status='running'/'queued' older than threshold
2030
- * that have no corresponding live session file (orphaned DB records).
2031
- * 3. workflow tasks/runs left running after a workflow finished or was abandoned.
2044
+ * 2. agent_runs rows with status='running'/'queued' older than threshold
2045
+ * that have no corresponding live session file (orphaned DB records).
2046
+ * 3. workflow tasks/runs left running after a workflow finished or was abandoned.
2032
2047
  *
2033
2048
  * --older-than Duration threshold. Accepts: 24h (default), 8h, 30m, 7d.
2034
2049
  * --dry-run Report what would be recovered without making any changes.
@@ -2109,31 +2124,31 @@ async function runAgentRecover({ args, options = {}, logger }) {
2109
2124
  recovered.push({ source: 'session_file', agent: agentName, runKey, taskKey, startedAt: session.startedAt });
2110
2125
  }
2111
2126
 
2112
- // ── 2. Scan DB for orphaned running runs (no session file) ────────────────
2113
- const orphanedRuns = db.prepare(`
2114
- SELECT run_key, task_key, agent_name, source, started_at
2115
- FROM agent_runs
2116
- WHERE status IN ('running', 'queued')
2117
- AND COALESCE(started_at, updated_at, '') < ?
2118
- `).all(cutoffIso);
2119
-
2120
- for (const run of orphanedRuns) {
2121
- // Skip if already recovered via session file
2122
- if (recovered.some((r) => r.runKey === run.run_key)) continue;
2123
- const source = run.source === 'workflow' ? 'workflow_run' : 'orphaned_run';
2124
- const summary = source === 'workflow_run'
2125
- ? 'Recovered: stale workflow run left running'
2126
- : 'Recovered: orphaned run with no session file';
2127
-
2128
- if (!dryRun) {
2129
- db.prepare(`
2130
- UPDATE agent_runs
2131
- SET status = 'abandoned', summary = ?, updated_at = ?, finished_at = ?
2132
- WHERE run_key = ?
2133
- `).run(summary, now, now, run.run_key);
2134
-
2135
- if (run.task_key) {
2136
- const taskRow = db.prepare('SELECT task_key, status FROM tasks WHERE task_key = ?').get(run.task_key);
2127
+ // ── 2. Scan DB for orphaned running runs (no session file) ────────────────
2128
+ const orphanedRuns = db.prepare(`
2129
+ SELECT run_key, task_key, agent_name, source, started_at
2130
+ FROM agent_runs
2131
+ WHERE status IN ('running', 'queued')
2132
+ AND COALESCE(started_at, updated_at, '') < ?
2133
+ `).all(cutoffIso);
2134
+
2135
+ for (const run of orphanedRuns) {
2136
+ // Skip if already recovered via session file
2137
+ if (recovered.some((r) => r.runKey === run.run_key)) continue;
2138
+ const source = run.source === 'workflow' ? 'workflow_run' : 'orphaned_run';
2139
+ const summary = source === 'workflow_run'
2140
+ ? 'Recovered: stale workflow run left running'
2141
+ : 'Recovered: orphaned run with no session file';
2142
+
2143
+ if (!dryRun) {
2144
+ db.prepare(`
2145
+ UPDATE agent_runs
2146
+ SET status = 'abandoned', summary = ?, updated_at = ?, finished_at = ?
2147
+ WHERE run_key = ?
2148
+ `).run(summary, now, now, run.run_key);
2149
+
2150
+ if (run.task_key) {
2151
+ const taskRow = db.prepare('SELECT task_key, status FROM tasks WHERE task_key = ?').get(run.task_key);
2137
2152
  if (taskRow && (taskRow.status === 'running' || taskRow.status === 'queued')) {
2138
2153
  db.prepare(`
2139
2154
  UPDATE tasks
@@ -2141,40 +2156,40 @@ async function runAgentRecover({ args, options = {}, logger }) {
2141
2156
  WHERE task_key = ?
2142
2157
  `).run(now, now, run.task_key);
2143
2158
  }
2144
- }
2145
- }
2146
-
2147
- recovered.push({ source, agent: run.agent_name, runKey: run.run_key, taskKey: run.task_key, startedAt: run.started_at });
2148
- }
2149
-
2150
- // ── 3. Scan DB for stale workflow tasks without a recovered run ───────────
2151
- const staleWorkflowTasks = db.prepare(`
2152
- SELECT task_key, title, created_by, session_key, created_at, updated_at
2153
- FROM tasks
2154
- WHERE status IN ('running', 'queued')
2155
- AND (created_by = '@workflow' OR session_key LIKE 'workflow:%')
2156
- AND COALESCE(updated_at, created_at, '') < ?
2157
- `).all(cutoffIso);
2158
-
2159
- for (const task of staleWorkflowTasks) {
2160
- if (recovered.some((r) => r.taskKey === task.task_key)) continue;
2161
-
2162
- if (!dryRun) {
2163
- db.prepare(`
2164
- UPDATE tasks
2165
- SET status = 'abandoned', updated_at = ?, finished_at = ?
2166
- WHERE task_key = ?
2167
- `).run(now, now, task.task_key);
2168
- }
2169
-
2170
- recovered.push({
2171
- source: 'workflow_task',
2172
- agent: task.created_by || '@workflow',
2173
- runKey: null,
2174
- taskKey: task.task_key,
2175
- startedAt: task.updated_at || task.created_at
2176
- });
2177
- }
2159
+ }
2160
+ }
2161
+
2162
+ recovered.push({ source, agent: run.agent_name, runKey: run.run_key, taskKey: run.task_key, startedAt: run.started_at });
2163
+ }
2164
+
2165
+ // ── 3. Scan DB for stale workflow tasks without a recovered run ───────────
2166
+ const staleWorkflowTasks = db.prepare(`
2167
+ SELECT task_key, title, created_by, session_key, created_at, updated_at
2168
+ FROM tasks
2169
+ WHERE status IN ('running', 'queued')
2170
+ AND (created_by = '@workflow' OR session_key LIKE 'workflow:%')
2171
+ AND COALESCE(updated_at, created_at, '') < ?
2172
+ `).all(cutoffIso);
2173
+
2174
+ for (const task of staleWorkflowTasks) {
2175
+ if (recovered.some((r) => r.taskKey === task.task_key)) continue;
2176
+
2177
+ if (!dryRun) {
2178
+ db.prepare(`
2179
+ UPDATE tasks
2180
+ SET status = 'abandoned', updated_at = ?, finished_at = ?
2181
+ WHERE task_key = ?
2182
+ `).run(now, now, task.task_key);
2183
+ }
2184
+
2185
+ recovered.push({
2186
+ source: 'workflow_task',
2187
+ agent: task.created_by || '@workflow',
2188
+ runKey: null,
2189
+ taskKey: task.task_key,
2190
+ startedAt: task.updated_at || task.created_at
2191
+ });
2192
+ }
2178
2193
 
2179
2194
  // ── Output ────────────────────────────────────────────────────────────────
2180
2195
  const olderThanLabel = options['older-than'] || options.olderThan || '24h';
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ // Cross-platform replacement for the legacy `rsync -av template/ ./` step in the
4
+ // `sync:agents` npm script. rsync is not available on Windows, which silently
5
+ // left the workspace un-synced. This mirrors template/ into the repo root using
6
+ // Node's fs, replicating the original rsync excludes:
7
+ // --exclude='config.md' --exclude='runtime/' --exclude='backups/' --exclude='mcp/servers.local.json'
8
+ // rsync matched those names at any depth; we replicate that (basename / dir
9
+ // segment / path suffix).
10
+ //
11
+ // We also exclude the live project-state files (project-pulse.md, project-map.md,
12
+ // learning-loop.json, git-guard.json): the dogfooding workspace owns its own
13
+ // evolving state, so a template sync must never overwrite it with the template
14
+ // seeds. External project installs (`aioson update`) are a separate code path and
15
+ // still receive the seeds.
16
+ //
17
+ // Gateway-pointer files (CLAUDE.md / AGENTS.md / OPENCODE.md) carry an AIOSON
18
+ // managed block (<!-- AIOSON:BEGIN --> … <!-- AIOSON:END -->) when a project was
19
+ // installed/updated. A blind copy of the raw template body STRIPS that block (and
20
+ // any project content around it). So when the destination already has a managed
21
+ // block, we refresh it in place via mergeGatewayPointer (same path the installer
22
+ // uses) instead of clobbering it. When the destination has no block we keep the
23
+ // plain copy — that leaves the framework repo's own raw CLAUDE.md untouched.
24
+
25
+ const fs = require('node:fs/promises');
26
+ const path = require('node:path');
27
+ const {
28
+ isGatewayPointerPath,
29
+ findBlockRange,
30
+ mergeGatewayPointer
31
+ } = require('../gateway-pointer-merge');
32
+
33
+ const EXCLUDE_BASENAMES = new Set([
34
+ 'config.md',
35
+ // live project-state — owned by the workspace, never clobbered by template seeds
36
+ 'project-pulse.md',
37
+ 'project-map.md',
38
+ 'learning-loop.json',
39
+ 'git-guard.json'
40
+ ]);
41
+ const EXCLUDE_DIR_NAMES = new Set(['runtime', 'backups']);
42
+ const EXCLUDE_SUFFIXES = [path.join('mcp', 'servers.local.json')];
43
+
44
+ // `relPath` is relative to template/ and uses the OS separator.
45
+ function isExcluded(relPath) {
46
+ const segments = relPath.split(path.sep).filter(Boolean);
47
+ const base = segments[segments.length - 1];
48
+ if (EXCLUDE_BASENAMES.has(base)) return true;
49
+ // a directory named runtime/ or backups/ anywhere in the path (or the dir itself)
50
+ if (segments.some((seg) => EXCLUDE_DIR_NAMES.has(seg))) return true;
51
+ if (EXCLUDE_SUFFIXES.some((suf) => relPath === suf || relPath.endsWith(path.sep + suf))) return true;
52
+ return false;
53
+ }
54
+
55
+ // Refresh an AIOSON-managed block in place when the destination already has one;
56
+ // otherwise fall back to a plain copy. Keeps markers + any project-authored
57
+ // content around the block intact (the blind copy would have stripped them).
58
+ async function copyGatewayPointer(srcChild, destChild) {
59
+ await fs.mkdir(path.dirname(destChild), { recursive: true });
60
+ let hasBlock = false;
61
+ try {
62
+ const existing = await fs.readFile(destChild, 'utf8');
63
+ hasBlock = findBlockRange(existing) !== null;
64
+ } catch {
65
+ hasBlock = false; // destination missing/unreadable → plain copy
66
+ }
67
+ if (hasBlock) {
68
+ await mergeGatewayPointer({ templatePath: srcChild, targetPath: destChild });
69
+ } else {
70
+ await fs.copyFile(srcChild, destChild);
71
+ }
72
+ }
73
+
74
+ async function copyTree(srcRoot, destRoot, relPath = '') {
75
+ const entries = await fs.readdir(path.join(srcRoot, relPath), { withFileTypes: true });
76
+ let copied = 0;
77
+ for (const entry of entries) {
78
+ const childRel = path.join(relPath, entry.name);
79
+ if (isExcluded(childRel)) continue;
80
+ const srcChild = path.join(srcRoot, childRel);
81
+ const destChild = path.join(destRoot, childRel);
82
+ if (entry.isDirectory()) {
83
+ await fs.mkdir(destChild, { recursive: true });
84
+ copied += await copyTree(srcRoot, destRoot, childRel);
85
+ } else if (entry.isFile()) {
86
+ if (isGatewayPointerPath(childRel)) {
87
+ await copyGatewayPointer(srcChild, destChild);
88
+ } else {
89
+ await fs.mkdir(path.dirname(destChild), { recursive: true });
90
+ await fs.copyFile(srcChild, destChild);
91
+ }
92
+ copied += 1;
93
+ }
94
+ }
95
+ return copied;
96
+ }
97
+
98
+ async function syncAgentsCopy(rootDir = process.cwd()) {
99
+ const templateDir = path.join(rootDir, 'template');
100
+ const copied = await copyTree(templateDir, rootDir);
101
+ return { copied };
102
+ }
103
+
104
+ module.exports = { syncAgentsCopy, isExcluded };
105
+
106
+ if (require.main === module) {
107
+ syncAgentsCopy(process.cwd())
108
+ .then(({ copied }) => {
109
+ console.log(`[sync:agents] mirrored template/ -> ./ (${copied} files, cross-platform copy; rsync not required)`);
110
+ })
111
+ .catch((err) => {
112
+ console.error(`[sync:agents] copy failed: ${err.message}`);
113
+ process.exitCode = 1;
114
+ });
115
+ }