@jaimevalasek/aioson 1.30.2 → 1.36.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 (158) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +19 -6
  3. package/docs/en/1-understand/ecosystem-map.md +45 -29
  4. package/docs/en/1-understand/glossary.md +5 -5
  5. package/docs/en/1-understand/what-is-aioson.md +5 -5
  6. package/docs/en/2-start/existing-project.md +7 -7
  7. package/docs/en/2-start/first-project.md +32 -38
  8. package/docs/en/2-start/initial-decisions.md +18 -17
  9. package/docs/en/3-recipes/README.md +2 -2
  10. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  11. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
  12. package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
  13. package/docs/en/4-agents/README.md +28 -14
  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/cli-reference.md +51 -48
  17. package/docs/en/5-reference/executable-verification.md +10 -7
  18. package/docs/en/5-reference/parallel.md +2 -0
  19. package/docs/en/5-reference/qa-browser.md +2 -2
  20. package/docs/en/README.md +3 -3
  21. package/docs/pt/1-entender/glossario.md +4 -4
  22. package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
  23. package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
  24. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  25. package/docs/pt/3-receitas/README.md +1 -1
  26. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  27. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
  28. package/docs/pt/4-agentes/analyst.md +9 -5
  29. package/docs/pt/4-agentes/architect.md +9 -5
  30. package/docs/pt/4-agentes/dev.md +19 -7
  31. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  32. package/docs/pt/4-agentes/orchestrator.md +164 -118
  33. package/docs/pt/4-agentes/pentester.md +5 -5
  34. package/docs/pt/4-agentes/pm.md +15 -7
  35. package/docs/pt/4-agentes/sheldon.md +166 -99
  36. package/docs/pt/4-agentes/ux-ui.md +7 -4
  37. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  38. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  39. package/docs/pt/5-referencia/harness-retro.md +2 -1
  40. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  41. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  42. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  43. package/docs/pt/README.md +4 -4
  44. package/docs/pt/agentes.md +48 -50
  45. package/package.json +2 -2
  46. package/src/artifact-kinds.js +110 -0
  47. package/src/cli.js +82 -42
  48. package/src/commands/agent-epilogue.js +251 -186
  49. package/src/commands/agents.js +104 -48
  50. package/src/commands/audit-code.js +344 -0
  51. package/src/commands/classify.js +75 -13
  52. package/src/commands/feature-close.js +43 -18
  53. package/src/commands/harness-check.js +259 -175
  54. package/src/commands/harness-retro-promote.js +387 -0
  55. package/src/commands/live.js +24 -0
  56. package/src/commands/prototype-check.js +163 -0
  57. package/src/commands/review-feature.js +189 -0
  58. package/src/commands/runtime.js +81 -66
  59. package/src/commands/sync-agents-copy.js +115 -0
  60. package/src/commands/verification-plan.js +116 -0
  61. package/src/commands/verify-artifact.js +530 -0
  62. package/src/commands/verify-implementation.js +428 -0
  63. package/src/commands/workflow-execute.js +309 -309
  64. package/src/commands/workflow-next.js +361 -19
  65. package/src/commands/workflow-plan.js +5 -5
  66. package/src/constants.js +4 -0
  67. package/src/gateway-pointer-merge.js +7 -1
  68. package/src/handoff-contract.js +267 -172
  69. package/src/harness/contract-integrity-gate.js +172 -0
  70. package/src/harness/contract-integrity.js +111 -0
  71. package/src/harness/contract-schema.js +377 -332
  72. package/src/harness/detect-runtime-feature.js +90 -0
  73. package/src/harness/static-criteria.js +192 -0
  74. package/src/i18n/messages/en.js +8 -4
  75. package/src/i18n/messages/es.js +8 -4
  76. package/src/i18n/messages/fr.js +8 -4
  77. package/src/i18n/messages/pt-BR.js +8 -4
  78. package/src/install-wizard.js +8 -8
  79. package/src/installer.js +13 -6
  80. package/src/lib/retro/retro-render.js +10 -1
  81. package/src/lib/retro/retro-sources.js +45 -27
  82. package/src/lib/retro/verification-reports.js +230 -0
  83. package/src/parser.js +6 -0
  84. package/src/preflight-engine.js +12 -12
  85. package/src/runtime-store.js +13 -9
  86. package/src/verification/evidence-bundle.js +251 -0
  87. package/src/verification/ledger-store.js +221 -0
  88. package/src/verification/path-policy.js +74 -0
  89. package/src/verification/policy-engine.js +95 -0
  90. package/src/verification/prompt-package.js +314 -0
  91. package/src/verification/redaction.js +77 -0
  92. package/src/verification/report-parser.js +132 -0
  93. package/src/verification/report-store.js +97 -0
  94. package/src/verification/result.js +16 -0
  95. package/src/verification/runners/index.js +319 -0
  96. package/src/verification/runtime-telemetry.js +144 -0
  97. package/src/verification/schema.js +276 -0
  98. package/src/verification/source-discovery.js +153 -0
  99. package/src/verification-policy.js +398 -0
  100. package/src/version.js +52 -1
  101. package/template/.aioson/agents/analyst.md +9 -6
  102. package/template/.aioson/agents/architect.md +34 -5
  103. package/template/.aioson/agents/briefing-refiner.md +25 -0
  104. package/template/.aioson/agents/briefing.md +69 -12
  105. package/template/.aioson/agents/committer.md +2 -1
  106. package/template/.aioson/agents/copywriter.md +30 -21
  107. package/template/.aioson/agents/design-hybrid-forge.md +28 -15
  108. package/template/.aioson/agents/dev.md +28 -10
  109. package/template/.aioson/agents/deyvin.md +3 -2
  110. package/template/.aioson/agents/discover.md +12 -3
  111. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  112. package/template/.aioson/agents/genome.md +19 -10
  113. package/template/.aioson/agents/neo.md +30 -30
  114. package/template/.aioson/agents/orache.md +20 -11
  115. package/template/.aioson/agents/orchestrator.md +84 -7
  116. package/template/.aioson/agents/pm.md +8 -8
  117. package/template/.aioson/agents/product.md +15 -11
  118. package/template/.aioson/agents/profiler-enricher.md +20 -11
  119. package/template/.aioson/agents/profiler-forge.md +20 -11
  120. package/template/.aioson/agents/profiler-researcher.md +20 -11
  121. package/template/.aioson/agents/qa.md +87 -17
  122. package/template/.aioson/agents/scope-check.md +19 -5
  123. package/template/.aioson/agents/setup.md +12 -1
  124. package/template/.aioson/agents/sheldon.md +92 -9
  125. package/template/.aioson/agents/site-forge.md +11 -2
  126. package/template/.aioson/agents/squad.md +20 -5
  127. package/template/.aioson/agents/ux-ui.md +4 -2
  128. package/template/.aioson/agents/validator.md +33 -1
  129. package/template/.aioson/config/verification.json +61 -0
  130. package/template/.aioson/config.md +13 -9
  131. package/template/.aioson/docs/LAYERS.md +2 -2
  132. package/template/.aioson/docs/autopilot-handoff.md +10 -10
  133. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  134. package/template/.aioson/docs/dev/phase-loop.md +47 -0
  135. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  136. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  137. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  138. package/template/.aioson/docs/product/prd-contract.md +12 -12
  139. package/template/.aioson/docs/prototype-contract.md +98 -0
  140. package/template/.aioson/docs/reference-identity.md +94 -0
  141. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  142. package/template/.aioson/docs/verification-config.md +82 -0
  143. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  144. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  145. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  146. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  147. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  148. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  149. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  150. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  151. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  152. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  153. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  154. package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
  155. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  156. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
  157. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  158. package/template/CLAUDE.md +15 -11
@@ -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
+ }
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // `aioson verification:plan` — deterministic bridge between verification.json and
4
+ // the agents. Given a slug + trigger + run context, it resolves WHICH verification
5
+ // sub-agents should run, on WHICH host/model, and the phase-loop settings — so
6
+ // @dev (per-phase) and the post-dev cycle (end-of-feature) follow a computed plan
7
+ // instead of re-interpreting the config in prose. Read-only; never mutates state.
8
+
9
+ const fs = require('node:fs/promises');
10
+ const path = require('node:path');
11
+ const {
12
+ readVerificationConfig,
13
+ resolveHost,
14
+ getAgentDispatch,
15
+ shouldRunForTrigger,
16
+ resolveAgentReportPath,
17
+ getCrossCheck,
18
+ getBudget,
19
+ getPhaseLoop,
20
+ VERIFICATION_AGENTS,
21
+ TRIGGERS
22
+ } = require('../verification-policy');
23
+
24
+ const BAR = '─'.repeat(60);
25
+ const CLASSIFICATIONS = ['MICRO', 'SMALL', 'MEDIUM'];
26
+
27
+ async function readFileSafe(filePath) {
28
+ try {
29
+ return await fs.readFile(filePath, 'utf8');
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // Best-effort classification: prd-{slug}.md frontmatter wins, then project.context.md.
36
+ async function detectClassification(targetDir, slug) {
37
+ const dir = path.join(targetDir, '.aioson', 'context');
38
+ const candidates = [];
39
+ if (slug) candidates.push(path.join(dir, `prd-${slug}.md`));
40
+ candidates.push(path.join(dir, 'project.context.md'));
41
+ for (const file of candidates) {
42
+ const text = await readFileSafe(file);
43
+ if (!text) continue;
44
+ const m = text.match(/^\s*classification:\s*["']?([A-Za-z]+)["']?\s*$/m);
45
+ if (m && CLASSIFICATIONS.includes(m[1].toUpperCase())) return m[1].toUpperCase();
46
+ }
47
+ return null;
48
+ }
49
+
50
+ async function runVerificationPlan({ args, options = {}, logger }) {
51
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
52
+ const slug = (options.feature || options.slug || '').toString().trim() || null;
53
+
54
+ const triggerRaw = (options.trigger || 'per-phase').toString().trim().toLowerCase();
55
+ const trigger = TRIGGERS.includes(triggerRaw) ? triggerRaw : 'per-phase';
56
+
57
+ const config = await readVerificationConfig(targetDir);
58
+ const host = resolveHost(config, options.host);
59
+
60
+ let classification = (options.classification || '').toString().trim().toUpperCase();
61
+ if (!CLASSIFICATIONS.includes(classification)) {
62
+ classification = (await detectClassification(targetDir, slug)) || 'SMALL';
63
+ }
64
+ const sensitiveSurface = Boolean(options.sensitive);
65
+
66
+ const context = { trigger, classification, sensitiveSurface };
67
+ const agents = [];
68
+ for (const agentId of VERIFICATION_AGENTS) {
69
+ const dispatch = getAgentDispatch(config, agentId, host);
70
+ const entry = {
71
+ agent: agentId,
72
+ 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}')
76
+ };
77
+ if (agentId === 'validator') {
78
+ const cc = getCrossCheck(config, agentId);
79
+ if (cc) entry.cross_check = cc;
80
+ }
81
+ agents.push(entry);
82
+ }
83
+
84
+ const result = {
85
+ ok: true,
86
+ feature_slug: slug,
87
+ host,
88
+ trigger,
89
+ classification,
90
+ sensitive_surface: sensitiveSurface,
91
+ agents,
92
+ phase_loop: getPhaseLoop(config),
93
+ budget: getBudget(config)
94
+ };
95
+
96
+ if (options.json) return result;
97
+
98
+ logger.log('');
99
+ logger.log(`Verification plan${slug ? ` — ${slug}` : ''}`);
100
+ logger.log(BAR);
101
+ logger.log(`Host: ${host} Trigger: ${trigger} Class: ${classification}${sensitiveSurface ? ' [sensitive]' : ''}`);
102
+ logger.log(BAR);
103
+ for (const a of agents) {
104
+ const mark = a.run ? 'RUN ' : 'skip';
105
+ const cc = a.cross_check && a.cross_check.enabled ? ` + cross_check(${a.cross_check.tool}/${a.cross_check.model})` : '';
106
+ logger.log(` [${mark}] ${a.agent.padEnd(10)} ${a.mode}/${a.model}${a.run ? ` → ${a.report}` : ''}${cc}`);
107
+ }
108
+ logger.log(BAR);
109
+ const pl = result.phase_loop;
110
+ 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}`);
111
+ logger.log('');
112
+
113
+ return result;
114
+ }
115
+
116
+ module.exports = { runVerificationPlan };