@jaimevalasek/aioson 1.21.7 → 1.22.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 (105) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/2-start/initial-decisions.md +1 -1
  4. package/docs/en/4-agents/README.md +8 -7
  5. package/docs/en/4-agents/discovery-design-doc.md +150 -0
  6. package/docs/en/5-reference/cli-reference.md +42 -16
  7. package/docs/en/README.md +2 -2
  8. package/docs/pt/4-agentes/README.md +8 -6
  9. package/docs/pt/4-agentes/briefing-refiner.md +122 -0
  10. package/docs/pt/4-agentes/discovery-design-doc.md +133 -74
  11. package/docs/pt/4-agentes/scope-check.md +65 -0
  12. package/docs/pt/5-referencia/README.md +1 -0
  13. package/docs/pt/5-referencia/comandos-cli.md +5 -4
  14. package/docs/pt/5-referencia/feature-archive.md +1 -0
  15. package/docs/pt/5-referencia/feature-export.md +155 -0
  16. package/docs/pt/README.md +2 -2
  17. package/docs/pt/agentes.md +3 -1
  18. package/package.json +1 -1
  19. package/src/agent-manifests.js +14 -3
  20. package/src/agents.js +21 -20
  21. package/src/cli.js +72 -52
  22. package/src/commands/briefing.js +28 -150
  23. package/src/commands/commit-prepare.js +5 -2
  24. package/src/commands/feature-archive.js +48 -12
  25. package/src/commands/feature-close.js +40 -0
  26. package/src/commands/feature-export.js +242 -0
  27. package/src/commands/gate-check.js +8 -3
  28. package/src/commands/git-guard.js +58 -0
  29. package/src/commands/harness-gate.js +120 -0
  30. package/src/commands/harness-status.js +157 -0
  31. package/src/commands/harness.js +18 -1
  32. package/src/commands/live.js +120 -115
  33. package/src/commands/parallel-doctor.js +2 -1
  34. package/src/commands/pulse-update.js +2 -2
  35. package/src/commands/scan-project.js +12 -2
  36. package/src/commands/self-implement-loop.js +305 -5
  37. package/src/commands/workflow-next.js +477 -425
  38. package/src/constants.js +21 -11
  39. package/src/context-search.js +3 -0
  40. package/src/doctor.js +24 -8
  41. package/src/dossier/schema.js +4 -3
  42. package/src/harness/active-contract.js +41 -0
  43. package/src/harness/attempt-artifacts.js +95 -0
  44. package/src/harness/budget-guard.js +127 -0
  45. package/src/harness/circuit-breaker.js +7 -0
  46. package/src/harness/contract-schema.js +324 -0
  47. package/src/harness/criteria-runner.js +136 -0
  48. package/src/harness/git-baseline.js +204 -0
  49. package/src/harness/glob-match.js +126 -0
  50. package/src/harness/guard-events.js +71 -0
  51. package/src/harness/human-gate.js +182 -0
  52. package/src/harness/scope-guard.js +115 -0
  53. package/src/i18n/messages/en.js +24 -21
  54. package/src/i18n/messages/es.js +11 -9
  55. package/src/i18n/messages/fr.js +11 -9
  56. package/src/i18n/messages/pt-BR.js +24 -21
  57. package/src/lib/briefing-refiner/apply-feedback.js +134 -0
  58. package/src/lib/briefing-refiner/briefing-paths.js +41 -0
  59. package/src/lib/briefing-refiner/briefing-registry.js +204 -0
  60. package/src/lib/briefing-refiner/briefing-sections.js +110 -0
  61. package/src/lib/briefing-refiner/feedback-schema.js +122 -0
  62. package/src/lib/briefing-refiner/refinement-report.js +39 -0
  63. package/src/lib/briefing-refiner/review-html.js +230 -0
  64. package/src/lib/dev-resume.js +94 -45
  65. package/src/parser.js +8 -5
  66. package/src/preflight-engine.js +88 -84
  67. package/src/runtime-store.js +2 -0
  68. package/src/sandbox.js +17 -3
  69. package/template/.aioson/agents/analyst.md +27 -23
  70. package/template/.aioson/agents/architect.md +7 -3
  71. package/template/.aioson/agents/briefing-refiner.md +121 -0
  72. package/template/.aioson/agents/briefing.md +83 -74
  73. package/template/.aioson/agents/committer.md +8 -0
  74. package/template/.aioson/agents/copywriter.md +19 -7
  75. package/template/.aioson/agents/design-hybrid-forge.md +16 -5
  76. package/template/.aioson/agents/dev.md +68 -66
  77. package/template/.aioson/agents/deyvin.md +97 -90
  78. package/template/.aioson/agents/discover.md +2 -2
  79. package/template/.aioson/agents/discovery-design-doc.md +34 -30
  80. package/template/.aioson/agents/genome.md +82 -71
  81. package/template/.aioson/agents/neo.md +11 -3
  82. package/template/.aioson/agents/orache.md +10 -0
  83. package/template/.aioson/agents/orchestrator.md +68 -68
  84. package/template/.aioson/agents/pentester.md +15 -6
  85. package/template/.aioson/agents/pm.md +30 -25
  86. package/template/.aioson/agents/product.md +108 -108
  87. package/template/.aioson/agents/profiler-enricher.md +10 -0
  88. package/template/.aioson/agents/profiler-forge.md +10 -0
  89. package/template/.aioson/agents/profiler-researcher.md +11 -0
  90. package/template/.aioson/agents/qa.md +28 -20
  91. package/template/.aioson/agents/scope-check.md +176 -164
  92. package/template/.aioson/agents/setup.md +11 -1
  93. package/template/.aioson/agents/sheldon.md +38 -38
  94. package/template/.aioson/agents/site-forge.md +15 -6
  95. package/template/.aioson/agents/squad.md +12 -0
  96. package/template/.aioson/agents/tester.md +209 -209
  97. package/template/.aioson/agents/ux-ui.md +2 -2
  98. package/template/.aioson/agents/validator.md +10 -2
  99. package/template/.aioson/config.md +31 -28
  100. package/template/.aioson/docs/autopilot-handoff.md +46 -0
  101. package/template/.aioson/docs/dossier/agent-templates.md +191 -0
  102. package/template/.aioson/docs/dossier/schema.md +218 -0
  103. package/template/.claude/commands/aioson/agent/briefing-refiner.md +17 -0
  104. package/template/AGENTS.md +50 -47
  105. package/template/CLAUDE.md +29 -27
@@ -26,16 +26,24 @@ const { emitDossierEvent } = require('../lib/dossier-telemetry');
26
26
 
27
27
  const STATE_RELATIVE_PATH = '.aioson/context/workflow.state.json';
28
28
  const CONFIG_RELATIVE_PATH = '.aioson/context/workflow.config.json';
29
- const EVENTS_RELATIVE_PATH = '.aioson/context/workflow.events.jsonl';
30
- const SCOPE_CHECK_MODES = new Set(['pre-dev', 'post-dev', 'post-fix', 'final']);
29
+ const EVENTS_RELATIVE_PATH = '.aioson/context/workflow.events.jsonl';
30
+ const SCOPE_CHECK_MODES = new Set(['pre-dev', 'post-dev', 'post-fix', 'final']);
31
+
32
+ const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
33
+ MICRO: ['product', 'dev', 'qa'],
34
+ SMALL: ['product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
35
+ // MEDIUM routes through @pm after discovery-design-doc (mirrors the
36
+ // project-mode position): Gate C requires implementation-plan-{slug}.md and
37
+ // @pm is its canonical owner (AC-SDLC-15/16) — without the stage, the
38
+ // sequence dead-ends at @dev preflight with no agent to produce the plan.
39
+ MEDIUM: ['product', 'analyst', 'architect', 'discovery-design-doc', 'pm', 'scope-check', 'dev', 'pentester', 'qa']
40
+ };
31
41
 
32
- const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
33
- MICRO: ['product', 'dev', 'qa'],
34
- SMALL: ['product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
35
- MEDIUM: ['product', 'analyst', 'architect', 'discovery-design-doc', 'scope-check', 'dev', 'pentester', 'qa']
36
- };
42
+ // Stages eligible for autopilot handoff (auto_handoff: true in project.context.md).
43
+ // The chain always breaks at the @dev handoff — see .aioson/docs/autopilot-handoff.md.
44
+ const AUTOPILOT_HANDOFF_STAGES = new Set(['analyst', 'scope-check', 'architect', 'discovery-design-doc', 'pm']);
37
45
 
38
- function normalizeAgentName(input) {
46
+ function normalizeAgentName(input) {
39
47
  return String(input || '')
40
48
  .trim()
41
49
  .toLowerCase()
@@ -51,11 +59,11 @@ function normalizeClassification(value, fallback = 'MICRO') {
51
59
  function buildDefaultWorkflowConfig() {
52
60
  return {
53
61
  version: 1,
54
- project: {
55
- MICRO: ['setup', 'dev'],
56
- SMALL: ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
57
- MEDIUM: ['setup', 'product', 'analyst', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator', 'scope-check', 'dev', 'qa']
58
- },
62
+ project: {
63
+ MICRO: ['setup', 'dev'],
64
+ SMALL: ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'dev', 'qa'],
65
+ MEDIUM: ['setup', 'product', 'analyst', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator', 'scope-check', 'dev', 'qa']
66
+ },
59
67
  feature: DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION,
60
68
  rules: {
61
69
  required: ['dev'],
@@ -64,9 +72,9 @@ function buildDefaultWorkflowConfig() {
64
72
  };
65
73
  }
66
74
 
67
- function parseFeaturesMarkdown(markdown) {
68
- return String(markdown || '')
69
- .split(/\r?\n/)
75
+ function parseFeaturesMarkdown(markdown) {
76
+ return String(markdown || '')
77
+ .split(/\r?\n/)
70
78
  .slice(3)
71
79
  .map((line) => line.trim())
72
80
  .filter(Boolean)
@@ -79,39 +87,39 @@ function parseFeaturesMarkdown(markdown) {
79
87
  started: parts[3],
80
88
  completed: parts[4]
81
89
  }))
82
- .filter((row) => row.slug && row.slug !== 'slug')
83
- .filter((row) => !/^-+$/ .test(row.slug));
84
- }
85
-
86
- function normalizeScopeCheckMode(input) {
87
- const mode = String(input || '').trim().toLowerCase();
88
- return SCOPE_CHECK_MODES.has(mode) ? mode : null;
89
- }
90
-
91
- function getScopeCheckModeOption(options = {}) {
92
- return normalizeScopeCheckMode(
93
- options.scopeMode ||
94
- options['scope-mode'] ||
95
- options.checkMode ||
96
- options['check-mode'] ||
97
- options.mode
98
- );
99
- }
100
-
101
- function chooseActiveFeature(features, preferredSlug = null) {
102
- const activeFeatures = (features || []).filter((feature) => feature.status === 'in_progress');
103
- if (preferredSlug) {
104
- const preferred = activeFeatures.find((feature) => feature.slug === preferredSlug);
105
- if (preferred) return preferred;
106
- }
107
- return activeFeatures.length > 0 ? activeFeatures[activeFeatures.length - 1] : null;
108
- }
109
-
110
- async function readJsonIfExists(filePath) {
111
- if (!(await exists(filePath))) return null;
112
- const content = await fs.readFile(filePath, 'utf8');
113
- return JSON.parse(content);
114
- }
90
+ .filter((row) => row.slug && row.slug !== 'slug')
91
+ .filter((row) => !/^-+$/ .test(row.slug));
92
+ }
93
+
94
+ function normalizeScopeCheckMode(input) {
95
+ const mode = String(input || '').trim().toLowerCase();
96
+ return SCOPE_CHECK_MODES.has(mode) ? mode : null;
97
+ }
98
+
99
+ function getScopeCheckModeOption(options = {}) {
100
+ return normalizeScopeCheckMode(
101
+ options.scopeMode ||
102
+ options['scope-mode'] ||
103
+ options.checkMode ||
104
+ options['check-mode'] ||
105
+ options.mode
106
+ );
107
+ }
108
+
109
+ function chooseActiveFeature(features, preferredSlug = null) {
110
+ const activeFeatures = (features || []).filter((feature) => feature.status === 'in_progress');
111
+ if (preferredSlug) {
112
+ const preferred = activeFeatures.find((feature) => feature.slug === preferredSlug);
113
+ if (preferred) return preferred;
114
+ }
115
+ return activeFeatures.length > 0 ? activeFeatures[activeFeatures.length - 1] : null;
116
+ }
117
+
118
+ async function readJsonIfExists(filePath) {
119
+ if (!(await exists(filePath))) return null;
120
+ const content = await fs.readFile(filePath, 'utf8');
121
+ return JSON.parse(content);
122
+ }
115
123
 
116
124
  async function writeJson(filePath, payload) {
117
125
  await ensureDir(path.dirname(filePath));
@@ -213,19 +221,19 @@ async function resolveExistingInstructionPath(targetDir, agent, locale) {
213
221
  return agent.path;
214
222
  }
215
223
 
216
- async function detectWorkflowMode(targetDir) {
217
- const prdPath = path.join(targetDir, '.aioson/context/prd.md');
218
- const featuresPath = path.join(targetDir, '.aioson/context/features.md');
219
- const handoffPath = path.join(targetDir, '.aioson/context/last-handoff.json');
220
- const hasProjectPrd = await exists(prdPath);
221
- const featuresMarkdown = await fs.readFile(featuresPath, 'utf8').catch(() => '');
222
- const features = parseFeaturesMarkdown(featuresMarkdown);
223
- const lastHandoff = await readJsonIfExists(handoffPath).catch(() => null);
224
- const preferredSlug = lastHandoff && lastHandoff.feature_slug ? lastHandoff.feature_slug : null;
225
- const activeFeature = chooseActiveFeature(features, preferredSlug);
226
-
227
- if (activeFeature) {
228
- return {
224
+ async function detectWorkflowMode(targetDir) {
225
+ const prdPath = path.join(targetDir, '.aioson/context/prd.md');
226
+ const featuresPath = path.join(targetDir, '.aioson/context/features.md');
227
+ const handoffPath = path.join(targetDir, '.aioson/context/last-handoff.json');
228
+ const hasProjectPrd = await exists(prdPath);
229
+ const featuresMarkdown = await fs.readFile(featuresPath, 'utf8').catch(() => '');
230
+ const features = parseFeaturesMarkdown(featuresMarkdown);
231
+ const lastHandoff = await readJsonIfExists(handoffPath).catch(() => null);
232
+ const preferredSlug = lastHandoff && lastHandoff.feature_slug ? lastHandoff.feature_slug : null;
233
+ const activeFeature = chooseActiveFeature(features, preferredSlug);
234
+
235
+ if (activeFeature) {
236
+ return {
229
237
  mode: 'feature',
230
238
  featureSlug: activeFeature.slug,
231
239
  features
@@ -245,15 +253,15 @@ function getSequenceForMode(config, mode, classification) {
245
253
  return Array.isArray(sequence) && sequence.length > 0 ? [...sequence] : [];
246
254
  }
247
255
 
248
- async function validateStageArtifacts(targetDir, state, stage) {
249
- const base = path.join(targetDir, '.aioson/context');
250
- const slug = state.featureSlug;
251
- const anyExists = async (candidates) => {
252
- for (const candidate of candidates) {
253
- if (await exists(candidate)) return true;
254
- }
255
- return false;
256
- };
256
+ async function validateStageArtifacts(targetDir, state, stage) {
257
+ const base = path.join(targetDir, '.aioson/context');
258
+ const slug = state.featureSlug;
259
+ const anyExists = async (candidates) => {
260
+ for (const candidate of candidates) {
261
+ if (await exists(candidate)) return true;
262
+ }
263
+ return false;
264
+ };
257
265
 
258
266
  if (stage === 'setup') {
259
267
  const context = await validateProjectContextFile(targetDir);
@@ -269,41 +277,51 @@ async function validateStageArtifacts(targetDir, state, stage) {
269
277
  return await exists(path.join(base, 'prd.md'));
270
278
  }
271
279
 
272
- if (stage === 'analyst') {
273
- if (state.mode === 'feature' && slug) {
274
- const requirements = path.join(base, `requirements-${slug}.md`);
275
- const spec = path.join(base, `spec-${slug}.md`);
276
- return (await exists(requirements)) && (await exists(spec));
277
- }
278
- return await exists(path.join(base, 'discovery.md'));
279
- }
280
-
281
- if (stage === 'scope-check') {
282
- if (state.mode === 'feature' && slug) {
283
- return await exists(path.join(base, `scope-check-${slug}.md`));
284
- }
285
- return await exists(path.join(base, 'scope-check.md'));
286
- }
287
-
288
- if (stage === 'architect') {
289
- return await exists(path.join(base, 'architecture.md'));
290
- }
291
-
292
- if (stage === 'ux-ui') {
293
- return await exists(path.join(base, 'ui-spec.md'));
294
- }
295
-
296
- if (stage === 'discovery-design-doc') {
297
- const designDocCandidates = slug
298
- ? [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')]
299
- : [path.join(base, 'design-doc.md')];
300
- const readinessCandidates = slug
301
- ? [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')]
302
- : [path.join(base, 'readiness.md')];
303
- return (await anyExists(designDocCandidates)) && (await anyExists(readinessCandidates));
304
- }
305
-
306
- if (stage === 'orchestrator') {
280
+ if (stage === 'analyst') {
281
+ if (state.mode === 'feature' && slug) {
282
+ const requirements = path.join(base, `requirements-${slug}.md`);
283
+ const spec = path.join(base, `spec-${slug}.md`);
284
+ return (await exists(requirements)) && (await exists(spec));
285
+ }
286
+ return await exists(path.join(base, 'discovery.md'));
287
+ }
288
+
289
+ if (stage === 'scope-check') {
290
+ if (state.mode === 'feature' && slug) {
291
+ return await exists(path.join(base, `scope-check-${slug}.md`));
292
+ }
293
+ return await exists(path.join(base, 'scope-check.md'));
294
+ }
295
+
296
+ if (stage === 'architect') {
297
+ return await exists(path.join(base, 'architecture.md'));
298
+ }
299
+
300
+ if (stage === 'ux-ui') {
301
+ return await exists(path.join(base, 'ui-spec.md'));
302
+ }
303
+
304
+ if (stage === 'discovery-design-doc') {
305
+ const designDocCandidates = slug
306
+ ? [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')]
307
+ : [path.join(base, 'design-doc.md')];
308
+ const readinessCandidates = slug
309
+ ? [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')]
310
+ : [path.join(base, 'readiness.md')];
311
+ return (await anyExists(designDocCandidates)) && (await anyExists(readinessCandidates));
312
+ }
313
+
314
+ if (stage === 'pm') {
315
+ // Feature mode: @pm's canonical artifact is the implementation plan
316
+ // (Gate C input). Project mode has no single canonical pm artifact —
317
+ // the handoff contract covers feature MEDIUM (AC-SDLC-16).
318
+ if (state.mode === 'feature' && slug) {
319
+ return await exists(path.join(base, `implementation-plan-${slug}.md`));
320
+ }
321
+ return true;
322
+ }
323
+
324
+ if (stage === 'orchestrator') {
307
325
  return await exists(path.join(base, 'parallel'));
308
326
  }
309
327
 
@@ -426,11 +444,17 @@ function reconcileWorkflowState(state) {
426
444
  };
427
445
  }
428
446
 
429
- function isInferableStage(stage) {
430
- return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'ux-ui', 'orchestrator'].includes(
431
- normalizeAgentName(stage)
432
- );
433
- }
447
+ function isInferableStage(stage) {
448
+ // discovery-design-doc is inferable from its design-doc + readiness artifacts
449
+ // (it has both a validateStageArtifacts branch and a handoff contract). Without
450
+ // it, MEDIUM sequences — where scope-check sits AFTER discovery-design-doc —
451
+ // could never infer scope-check as completed during stale-state recovery.
452
+ // pm is inferable from implementation-plan-{slug}.md for the same reason:
453
+ // it sits before scope-check in the MEDIUM feature sequence.
454
+ return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator'].includes(
455
+ normalizeAgentName(stage)
456
+ );
457
+ }
434
458
 
435
459
  function isSecurityGateBlocked(contractCheck, state, stageName) {
436
460
  if (normalizeAgentName(stageName) !== 'qa' || state.mode !== 'feature' || !state.featureSlug) {
@@ -465,58 +489,58 @@ function buildQaSecurityAuditBriefing(result, targetDir) {
465
489
  ].join('\n');
466
490
  }
467
491
 
468
- async function inferCompletedStages(targetDir, draftState) {
469
- const completed = [];
470
- for (const stage of draftState.sequence) {
471
- if (!isInferableStage(stage)) break;
472
- const valid = await validateStageArtifacts(targetDir, draftState, stage);
473
- if (!valid) break;
474
- const contractCheck = await validateHandoffContract(targetDir, draftState, normalizeAgentName(stage));
475
- if (!contractCheck.ok) break;
476
- completed.push(normalizeAgentName(stage));
477
- }
478
- return completed;
479
- }
480
-
481
- function mergeInferredCompletedStages(state, inferredCompleted) {
482
- if (!state || !Array.isArray(state.sequence) || !Array.isArray(inferredCompleted)) {
483
- return { state, changed: false };
484
- }
485
-
486
- const sequence = state.sequence.map(normalizeAgentName);
487
- const completedSet = new Set((state.completed || []).map(normalizeAgentName).filter(Boolean));
488
- const skippedSet = new Set((state.skipped || []).map(normalizeAgentName).filter(Boolean));
489
- let changed = false;
490
-
491
- for (const stage of inferredCompleted.map(normalizeAgentName).filter(Boolean)) {
492
- if (!sequence.includes(stage)) continue;
493
- if (!completedSet.has(stage)) {
494
- completedSet.add(stage);
495
- changed = true;
496
- }
497
- if (skippedSet.delete(stage)) {
498
- changed = true;
499
- }
500
- }
501
-
502
- if (!changed) return { state, changed: false };
503
-
504
- return {
505
- changed: true,
506
- state: buildStatePayload({
507
- ...state,
508
- sequence,
509
- completed: sequence.filter((stage) => completedSet.has(stage)),
510
- skipped: sequence.filter((stage) => skippedSet.has(stage))
511
- })
512
- };
513
- }
514
-
515
- // SF-project-18: cross-check workflow.state.json#completed against runtime
516
- // telemetry. Stages claimed as completed without a corresponding agent_done
517
- // event in .aioson/runtime/aios.sqlite are surfaced as a warning. Detection
518
- // is best-effort — if the runtime DB is unavailable, the check is silently
519
- // skipped (the framework still works in environments without telemetry).
492
+ async function inferCompletedStages(targetDir, draftState) {
493
+ const completed = [];
494
+ for (const stage of draftState.sequence) {
495
+ if (!isInferableStage(stage)) break;
496
+ const valid = await validateStageArtifacts(targetDir, draftState, stage);
497
+ if (!valid) break;
498
+ const contractCheck = await validateHandoffContract(targetDir, draftState, normalizeAgentName(stage));
499
+ if (!contractCheck.ok) break;
500
+ completed.push(normalizeAgentName(stage));
501
+ }
502
+ return completed;
503
+ }
504
+
505
+ function mergeInferredCompletedStages(state, inferredCompleted) {
506
+ if (!state || !Array.isArray(state.sequence) || !Array.isArray(inferredCompleted)) {
507
+ return { state, changed: false };
508
+ }
509
+
510
+ const sequence = state.sequence.map(normalizeAgentName);
511
+ const completedSet = new Set((state.completed || []).map(normalizeAgentName).filter(Boolean));
512
+ const skippedSet = new Set((state.skipped || []).map(normalizeAgentName).filter(Boolean));
513
+ let changed = false;
514
+
515
+ for (const stage of inferredCompleted.map(normalizeAgentName).filter(Boolean)) {
516
+ if (!sequence.includes(stage)) continue;
517
+ if (!completedSet.has(stage)) {
518
+ completedSet.add(stage);
519
+ changed = true;
520
+ }
521
+ if (skippedSet.delete(stage)) {
522
+ changed = true;
523
+ }
524
+ }
525
+
526
+ if (!changed) return { state, changed: false };
527
+
528
+ return {
529
+ changed: true,
530
+ state: buildStatePayload({
531
+ ...state,
532
+ sequence,
533
+ completed: sequence.filter((stage) => completedSet.has(stage)),
534
+ skipped: sequence.filter((stage) => skippedSet.has(stage))
535
+ })
536
+ };
537
+ }
538
+
539
+ // SF-project-18: cross-check workflow.state.json#completed against runtime
540
+ // telemetry. Stages claimed as completed without a corresponding agent_done
541
+ // event in .aioson/runtime/aios.sqlite are surfaced as a warning. Detection
542
+ // is best-effort — if the runtime DB is unavailable, the check is silently
543
+ // skipped (the framework still works in environments without telemetry).
520
544
  async function detectUnsubstantiatedCompletions(targetDir, completedStages, logger = null) {
521
545
  if (!Array.isArray(completedStages) || completedStages.length === 0) return [];
522
546
  let runtimeStore;
@@ -529,35 +553,48 @@ async function detectUnsubstantiatedCompletions(targetDir, completedStages, logg
529
553
  let dbExists;
530
554
  try { dbExists = await runtimeStore.runtimeStoreExists(targetDir); } catch { return []; }
531
555
  if (!dbExists) return [];
532
- let db;
556
+ let handle;
533
557
  try {
534
- db = await runtimeStore.openRuntimeDb(targetDir);
558
+ handle = await runtimeStore.openRuntimeDb(targetDir);
535
559
  } catch {
536
560
  return [];
537
561
  }
562
+ // openRuntimeDb resolves to { db, dbPath, runtimeDir } — the raw better-sqlite3
563
+ // handle lives on `.db`.
564
+ const db = handle && handle.db;
538
565
  if (!db || typeof db.prepare !== 'function') {
539
566
  try { if (db && typeof db.close === 'function') db.close(); } catch { /* ignore */ }
540
567
  return [];
541
568
  }
542
- const unsubstantiated = [];
569
+ let unsubstantiated = [];
543
570
  try {
544
571
  let stmt;
545
572
  try {
573
+ // agent identity lives on execution_events.agent_name (agent_events has no
574
+ // agent column). agent_done/stage_completed events are written there by
575
+ // appendRunEvent for every tracked run.
546
576
  stmt = db.prepare(
547
- "SELECT 1 FROM agent_events WHERE agent = ? AND event_type IN ('agent_done', 'stage_completed') LIMIT 1"
577
+ "SELECT 1 FROM execution_events WHERE agent_name = ? AND event_type IN ('agent_done', 'stage_completed') LIMIT 1"
548
578
  );
549
579
  } catch {
550
580
  // schema differences across versions — abort the cross-check.
551
581
  return [];
552
582
  }
583
+ const missing = [];
584
+ let substantiated = 0;
553
585
  for (const stage of completedStages) {
554
586
  try {
555
- const row = stmt.get(stage);
556
- if (!row) unsubstantiated.push(stage);
587
+ if (stmt.get(stage)) substantiated += 1;
588
+ else missing.push(stage);
557
589
  } catch {
558
590
  return [];
559
591
  }
560
592
  }
593
+ // Only treat missing stages as suspicious when the workflow demonstrably
594
+ // emits per-stage telemetry (≥1 completed stage has an agent_done event).
595
+ // Projects that never emit per-stage telemetry would otherwise warn on every
596
+ // run — keep the cross-check best-effort and silent for them.
597
+ unsubstantiated = substantiated > 0 ? missing : [];
561
598
  } finally {
562
599
  try { db.close(); } catch { /* ignore */ }
563
600
  }
@@ -574,39 +611,39 @@ async function loadOrCreateState(targetDir, options = {}) {
574
611
  const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
575
612
  let existing = await readJsonIfExists(statePath);
576
613
 
577
- // Mode/feature-transition guard: if the persisted state no longer matches
578
- // the current mode from features.md, it is stale. This covers both directions:
579
- // a feature was paused/closed and project mode should resume, or a new
580
- // feature was opened while a project workflow state still exists.
581
- if (existing) {
582
- const modeInfo = await detectWorkflowMode(targetDir);
583
- if (
584
- existing.mode !== modeInfo.mode ||
585
- (modeInfo.mode === 'feature' && existing.featureSlug !== modeInfo.featureSlug) ||
586
- (modeInfo.mode !== 'feature' && existing.featureSlug)
587
- ) {
588
- existing = null;
589
- }
590
- }
591
-
592
- if (existing && typeof existing === 'object' && Array.isArray(existing.sequence)) {
593
- // SF-project-18: warn-on-mismatch only, never refuse — preserves
594
- // backwards-compat with environments that lack runtime telemetry.
595
- if (Array.isArray(existing.completed) && existing.completed.length > 0 && options.logger) {
596
- await detectUnsubstantiatedCompletions(targetDir, existing.completed, options.logger);
597
- }
598
- const reconciled = reconcileWorkflowState(existing);
599
- const inferredCompleted = (reconciled.state.current || (reconciled.state.detour && reconciled.state.detour.active))
600
- ? []
601
- : await inferCompletedStages(targetDir, reconciled.state);
602
- const merged = mergeInferredCompletedStages(reconciled.state, inferredCompleted);
603
- const finalReconciled = merged.changed ? reconcileWorkflowState(merged.state) : reconciled;
604
- const changed = reconciled.changed || merged.changed || finalReconciled.changed;
605
- if (changed) {
606
- await writeJson(statePath, finalReconciled.state);
607
- }
608
- return { statePath, state: finalReconciled.state, created: false };
609
- }
614
+ // Mode/feature-transition guard: if the persisted state no longer matches
615
+ // the current mode from features.md, it is stale. This covers both directions:
616
+ // a feature was paused/closed and project mode should resume, or a new
617
+ // feature was opened while a project workflow state still exists.
618
+ if (existing) {
619
+ const modeInfo = await detectWorkflowMode(targetDir);
620
+ if (
621
+ existing.mode !== modeInfo.mode ||
622
+ (modeInfo.mode === 'feature' && existing.featureSlug !== modeInfo.featureSlug) ||
623
+ (modeInfo.mode !== 'feature' && existing.featureSlug)
624
+ ) {
625
+ existing = null;
626
+ }
627
+ }
628
+
629
+ if (existing && typeof existing === 'object' && Array.isArray(existing.sequence)) {
630
+ // SF-project-18: warn-on-mismatch only, never refuse — preserves
631
+ // backwards-compat with environments that lack runtime telemetry.
632
+ if (Array.isArray(existing.completed) && existing.completed.length > 0 && options.logger) {
633
+ await detectUnsubstantiatedCompletions(targetDir, existing.completed, options.logger);
634
+ }
635
+ const reconciled = reconcileWorkflowState(existing);
636
+ const inferredCompleted = (reconciled.state.current || (reconciled.state.detour && reconciled.state.detour.active))
637
+ ? []
638
+ : await inferCompletedStages(targetDir, reconciled.state);
639
+ const merged = mergeInferredCompletedStages(reconciled.state, inferredCompleted);
640
+ const finalReconciled = merged.changed ? reconcileWorkflowState(merged.state) : reconciled;
641
+ const changed = reconciled.changed || merged.changed || finalReconciled.changed;
642
+ if (changed) {
643
+ await writeJson(statePath, finalReconciled.state);
644
+ }
645
+ return { statePath, state: finalReconciled.state, created: false };
646
+ }
610
647
 
611
648
  const context = await validateProjectContextFile(targetDir);
612
649
  const modeInfo = await detectWorkflowMode(targetDir);
@@ -856,7 +893,7 @@ function applySkip(config, state, target) {
856
893
  });
857
894
  }
858
895
 
859
- async function ensureFeatureDossier(targetDir, state) {
896
+ async function ensureFeatureDossier(targetDir, state) {
860
897
  if (state.mode !== 'feature' || !state.featureSlug) return;
861
898
  const classification = String(state.classification || '').toUpperCase();
862
899
  if (classification !== 'SMALL' && classification !== 'MEDIUM') return;
@@ -912,205 +949,205 @@ async function ensureFeatureDossier(targetDir, state) {
912
949
  mode
913
950
  }
914
951
  });
915
- }
916
- }
917
-
918
- async function readTextIfExists(filePath) {
919
- try {
920
- return await fs.readFile(filePath, 'utf8');
921
- } catch (err) {
922
- if (err && err.code === 'ENOENT') return null;
923
- throw err;
924
- }
925
- }
926
-
927
- function parseDevStateContextPackage(raw) {
928
- if (!raw) return [];
929
- const section = raw.match(/## Context package\r?\n\r?\n([\s\S]*?)(?:\r?\n\r?\n## |\s*$)/);
930
- if (!section) return [];
931
- return section[1]
932
- .split(/\r?\n/)
933
- .map((line) => {
934
- const match = line.trim().match(/^\d+\.\s+(.+)$/);
935
- return match ? match[1].trim() : null;
936
- })
937
- .filter(Boolean);
938
- }
939
-
940
- function parseDevStateFrontmatter(raw) {
941
- if (!raw) return {};
942
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
943
- if (!fmMatch) return {};
944
- const fm = {};
945
- for (const line of fmMatch[1].split(/\r?\n/)) {
946
- const idx = line.indexOf(':');
947
- if (idx === -1) continue;
948
- const key = line.slice(0, idx).trim();
949
- const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
950
- if (key) fm[key] = value;
951
- }
952
- return fm;
953
- }
954
-
955
- function shouldUseDevStateForFeature(raw, featureSlug) {
956
- if (!raw) return false;
957
- const fm = parseDevStateFrontmatter(raw);
958
- if (!fm.active_feature) return false;
959
- const status = String(fm.status || '').toLowerCase();
960
- if (status === 'done' || status === 'abandoned') return false;
961
- if (fm.active_feature !== featureSlug) return false;
962
- return true;
963
- }
964
-
965
- function normalizeContextDependency(relPath) {
966
- const cleaned = String(relPath || '').trim().replace(/\\/g, '/');
967
- if (!cleaned) return null;
968
- if (cleaned.startsWith('.aioson/')) return cleaned;
969
- return `.aioson/context/${cleaned}`;
970
- }
971
-
972
- async function resolveStageDependencies(targetDir, state, stageName, agent) {
973
- if (stageName === 'scope-check') {
974
- const contextDir = path.join(targetDir, '.aioson', 'context');
975
- const slug = state.featureSlug;
976
- const candidates = [
977
- 'project.context.md',
978
- 'features.md',
979
- slug ? `prd-${slug}.md` : 'prd.md',
980
- slug ? `requirements-${slug}.md` : 'discovery.md',
981
- slug ? `spec-${slug}.md` : 'spec.md',
982
- slug ? `sheldon-enrichment-${slug}.md` : 'sheldon-enrichment.md',
983
- 'architecture.md',
984
- slug ? `design-doc-${slug}.md` : null,
985
- slug ? `readiness-${slug}.md` : null,
986
- 'design-doc.md',
987
- 'readiness.md',
988
- 'ui-spec.md',
989
- slug ? `implementation-plan-${slug}.md` : 'implementation-plan.md',
990
- 'dev-state.md',
991
- 'last-handoff.json',
992
- 'project-pulse.md'
993
- ].filter(Boolean);
994
- const existing = [];
995
- for (const candidate of candidates) {
996
- if (await exists(path.join(contextDir, candidate))) {
997
- existing.push(normalizeContextDependency(candidate));
998
- }
999
- }
1000
- return existing.length > 0 ? existing : agent.dependsOn;
1001
- }
1002
-
1003
- if (stageName === 'discovery-design-doc') {
1004
- const contextDir = path.join(targetDir, '.aioson', 'context');
1005
- const slug = state.featureSlug;
1006
- const candidates = [
1007
- 'project.context.md',
1008
- slug ? `prd-${slug}.md` : 'prd.md',
1009
- slug ? `requirements-${slug}.md` : 'discovery.md',
1010
- slug ? `spec-${slug}.md` : 'spec.md',
1011
- 'architecture.md',
1012
- slug ? `design-doc-${slug}.md` : null,
1013
- slug ? `readiness-${slug}.md` : null,
1014
- 'design-doc.md',
1015
- 'readiness.md',
1016
- 'project-map.md'
1017
- ].filter(Boolean);
1018
- const existing = [];
1019
- for (const candidate of candidates) {
1020
- if (await exists(path.join(contextDir, candidate))) {
1021
- existing.push(normalizeContextDependency(candidate));
1022
- }
1023
- }
1024
- return existing.length > 0 ? existing : agent.dependsOn;
1025
- }
1026
-
1027
- if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) {
1028
- return agent.dependsOn;
1029
- }
1030
-
1031
- const contextDir = path.join(targetDir, '.aioson', 'context');
1032
- const devStatePath = path.join(contextDir, 'dev-state.md');
1033
- const devStateRaw = await readTextIfExists(devStatePath);
1034
- const devStatePackage = shouldUseDevStateForFeature(devStateRaw, state.featureSlug)
1035
- ? parseDevStateContextPackage(devStateRaw)
1036
- .map(normalizeContextDependency)
1037
- .filter(Boolean)
1038
- : [];
1039
-
1040
- if (devStatePackage.length > 0) {
1041
- return Array.from(new Set(['.aioson/context/dev-state.md', ...devStatePackage]));
1042
- }
1043
-
1044
- const slug = state.featureSlug;
1045
- const candidates = [
1046
- 'project.context.md',
1047
- `prd-${slug}.md`,
1048
- `requirements-${slug}.md`,
1049
- `spec-${slug}.md`,
1050
- `design-doc-${slug}.md`,
1051
- `readiness-${slug}.md`,
1052
- 'design-doc.md',
1053
- 'readiness.md',
1054
- `scope-check-${slug}.md`,
1055
- 'scope-check.md',
1056
- `implementation-plan-${slug}.md`
1057
- ];
1058
- const existing = [];
1059
- for (const candidate of candidates) {
1060
- if (await exists(path.join(contextDir, candidate))) {
1061
- existing.push(normalizeContextDependency(candidate));
1062
- }
1063
- }
1064
- return existing.length > 0 ? existing : agent.dependsOn;
1065
- }
1066
-
1067
- function inferScopeCheckMode(state, requestedMode = null) {
1068
- if (requestedMode) return requestedMode;
1069
- const completed = Array.isArray(state.completed) ? state.completed.map(normalizeAgentName) : [];
1070
- const current = normalizeAgentName(state.current || state.next);
1071
- if (completed.includes('dev')) return 'post-dev';
1072
- if (completed.includes('qa') || completed.includes('tester') || completed.includes('pentester')) return 'post-fix';
1073
- if (current === 'scope-check') return 'pre-dev';
1074
- return 'pre-dev';
1075
- }
1076
-
1077
- function buildScopeCheckActivationContext(state, mode) {
1078
- const resolvedMode = inferScopeCheckMode(state, mode);
1079
- const lines = [
1080
- `Scope-check mode: ${resolvedMode}`,
1081
- `Workflow mode: ${state.mode || 'unknown'}`,
1082
- `Classification: ${state.classification || 'unknown'}`
1083
- ];
1084
- if (state.featureSlug) lines.push(`Feature slug: ${state.featureSlug}`);
1085
- if (resolvedMode === 'pre-dev') {
1086
- lines.push('Compare user intent against planning artifacts before implementation.');
1087
- } else if (resolvedMode === 'post-dev') {
1088
- lines.push('Compare the approved scope-check/design artifacts against the actual implementation diff and changed files before QA.');
1089
- } else if (resolvedMode === 'post-fix') {
1090
- lines.push('Compare approved scope, QA/tester/pentester findings, and the correction diff; confirm the fix did not change product intent.');
1091
- } else if (resolvedMode === 'final') {
1092
- lines.push('Reconcile intent, plan, delivered behavior, and remaining exclusions before close/commit/release.');
1093
- }
1094
- return lines.join('\n');
1095
- }
1096
-
1097
- function buildStageActivationContext(state, stageName, dependencies, scopeCheckMode = null) {
1098
- if (stageName === 'scope-check') {
1099
- return buildScopeCheckActivationContext(state, scopeCheckMode);
1100
- }
1101
-
1102
- if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) return '';
1103
- return [
1104
- `Feature slug: ${state.featureSlug}`,
1105
- `Workflow mode: ${state.mode}`,
1106
- `Classification: ${state.classification || 'unknown'}`,
1107
- dependencies.includes('.aioson/context/dev-state.md')
1108
- ? 'Resume source: .aioson/context/dev-state.md'
1109
- : 'Resume source: active feature artifacts'
1110
- ].join('\n');
1111
- }
1112
-
1113
- async function activateStage(targetDir, state, locale, tool, explicitAgent = null, requestedMode = null, scopeCheckMode = null) {
952
+ }
953
+ }
954
+
955
+ async function readTextIfExists(filePath) {
956
+ try {
957
+ return await fs.readFile(filePath, 'utf8');
958
+ } catch (err) {
959
+ if (err && err.code === 'ENOENT') return null;
960
+ throw err;
961
+ }
962
+ }
963
+
964
+ function parseDevStateContextPackage(raw) {
965
+ if (!raw) return [];
966
+ const section = raw.match(/## Context package\r?\n\r?\n([\s\S]*?)(?:\r?\n\r?\n## |\s*$)/);
967
+ if (!section) return [];
968
+ return section[1]
969
+ .split(/\r?\n/)
970
+ .map((line) => {
971
+ const match = line.trim().match(/^\d+\.\s+(.+)$/);
972
+ return match ? match[1].trim() : null;
973
+ })
974
+ .filter(Boolean);
975
+ }
976
+
977
+ function parseDevStateFrontmatter(raw) {
978
+ if (!raw) return {};
979
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
980
+ if (!fmMatch) return {};
981
+ const fm = {};
982
+ for (const line of fmMatch[1].split(/\r?\n/)) {
983
+ const idx = line.indexOf(':');
984
+ if (idx === -1) continue;
985
+ const key = line.slice(0, idx).trim();
986
+ const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
987
+ if (key) fm[key] = value;
988
+ }
989
+ return fm;
990
+ }
991
+
992
+ function shouldUseDevStateForFeature(raw, featureSlug) {
993
+ if (!raw) return false;
994
+ const fm = parseDevStateFrontmatter(raw);
995
+ if (!fm.active_feature) return false;
996
+ const status = String(fm.status || '').toLowerCase();
997
+ if (status === 'done' || status === 'abandoned') return false;
998
+ if (fm.active_feature !== featureSlug) return false;
999
+ return true;
1000
+ }
1001
+
1002
+ function normalizeContextDependency(relPath) {
1003
+ const cleaned = String(relPath || '').trim().replace(/\\/g, '/');
1004
+ if (!cleaned) return null;
1005
+ if (cleaned.startsWith('.aioson/')) return cleaned;
1006
+ return `.aioson/context/${cleaned}`;
1007
+ }
1008
+
1009
+ async function resolveStageDependencies(targetDir, state, stageName, agent) {
1010
+ if (stageName === 'scope-check') {
1011
+ const contextDir = path.join(targetDir, '.aioson', 'context');
1012
+ const slug = state.featureSlug;
1013
+ const candidates = [
1014
+ 'project.context.md',
1015
+ 'features.md',
1016
+ slug ? `prd-${slug}.md` : 'prd.md',
1017
+ slug ? `requirements-${slug}.md` : 'discovery.md',
1018
+ slug ? `spec-${slug}.md` : 'spec.md',
1019
+ slug ? `sheldon-enrichment-${slug}.md` : 'sheldon-enrichment.md',
1020
+ 'architecture.md',
1021
+ slug ? `design-doc-${slug}.md` : null,
1022
+ slug ? `readiness-${slug}.md` : null,
1023
+ 'design-doc.md',
1024
+ 'readiness.md',
1025
+ 'ui-spec.md',
1026
+ slug ? `implementation-plan-${slug}.md` : 'implementation-plan.md',
1027
+ 'dev-state.md',
1028
+ 'last-handoff.json',
1029
+ 'project-pulse.md'
1030
+ ].filter(Boolean);
1031
+ const existing = [];
1032
+ for (const candidate of candidates) {
1033
+ if (await exists(path.join(contextDir, candidate))) {
1034
+ existing.push(normalizeContextDependency(candidate));
1035
+ }
1036
+ }
1037
+ return existing.length > 0 ? existing : agent.dependsOn;
1038
+ }
1039
+
1040
+ if (stageName === 'discovery-design-doc') {
1041
+ const contextDir = path.join(targetDir, '.aioson', 'context');
1042
+ const slug = state.featureSlug;
1043
+ const candidates = [
1044
+ 'project.context.md',
1045
+ slug ? `prd-${slug}.md` : 'prd.md',
1046
+ slug ? `requirements-${slug}.md` : 'discovery.md',
1047
+ slug ? `spec-${slug}.md` : 'spec.md',
1048
+ 'architecture.md',
1049
+ slug ? `design-doc-${slug}.md` : null,
1050
+ slug ? `readiness-${slug}.md` : null,
1051
+ 'design-doc.md',
1052
+ 'readiness.md',
1053
+ 'project-map.md'
1054
+ ].filter(Boolean);
1055
+ const existing = [];
1056
+ for (const candidate of candidates) {
1057
+ if (await exists(path.join(contextDir, candidate))) {
1058
+ existing.push(normalizeContextDependency(candidate));
1059
+ }
1060
+ }
1061
+ return existing.length > 0 ? existing : agent.dependsOn;
1062
+ }
1063
+
1064
+ if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) {
1065
+ return agent.dependsOn;
1066
+ }
1067
+
1068
+ const contextDir = path.join(targetDir, '.aioson', 'context');
1069
+ const devStatePath = path.join(contextDir, 'dev-state.md');
1070
+ const devStateRaw = await readTextIfExists(devStatePath);
1071
+ const devStatePackage = shouldUseDevStateForFeature(devStateRaw, state.featureSlug)
1072
+ ? parseDevStateContextPackage(devStateRaw)
1073
+ .map(normalizeContextDependency)
1074
+ .filter(Boolean)
1075
+ : [];
1076
+
1077
+ if (devStatePackage.length > 0) {
1078
+ return Array.from(new Set(['.aioson/context/dev-state.md', ...devStatePackage]));
1079
+ }
1080
+
1081
+ const slug = state.featureSlug;
1082
+ const candidates = [
1083
+ 'project.context.md',
1084
+ `prd-${slug}.md`,
1085
+ `requirements-${slug}.md`,
1086
+ `spec-${slug}.md`,
1087
+ `design-doc-${slug}.md`,
1088
+ `readiness-${slug}.md`,
1089
+ 'design-doc.md',
1090
+ 'readiness.md',
1091
+ `scope-check-${slug}.md`,
1092
+ 'scope-check.md',
1093
+ `implementation-plan-${slug}.md`
1094
+ ];
1095
+ const existing = [];
1096
+ for (const candidate of candidates) {
1097
+ if (await exists(path.join(contextDir, candidate))) {
1098
+ existing.push(normalizeContextDependency(candidate));
1099
+ }
1100
+ }
1101
+ return existing.length > 0 ? existing : agent.dependsOn;
1102
+ }
1103
+
1104
+ function inferScopeCheckMode(state, requestedMode = null) {
1105
+ if (requestedMode) return requestedMode;
1106
+ const completed = Array.isArray(state.completed) ? state.completed.map(normalizeAgentName) : [];
1107
+ const current = normalizeAgentName(state.current || state.next);
1108
+ if (completed.includes('dev')) return 'post-dev';
1109
+ if (completed.includes('qa') || completed.includes('tester') || completed.includes('pentester')) return 'post-fix';
1110
+ if (current === 'scope-check') return 'pre-dev';
1111
+ return 'pre-dev';
1112
+ }
1113
+
1114
+ function buildScopeCheckActivationContext(state, mode) {
1115
+ const resolvedMode = inferScopeCheckMode(state, mode);
1116
+ const lines = [
1117
+ `Scope-check mode: ${resolvedMode}`,
1118
+ `Workflow mode: ${state.mode || 'unknown'}`,
1119
+ `Classification: ${state.classification || 'unknown'}`
1120
+ ];
1121
+ if (state.featureSlug) lines.push(`Feature slug: ${state.featureSlug}`);
1122
+ if (resolvedMode === 'pre-dev') {
1123
+ lines.push('Compare user intent against planning artifacts before implementation.');
1124
+ } else if (resolvedMode === 'post-dev') {
1125
+ lines.push('Compare the approved scope-check/design artifacts against the actual implementation diff and changed files before QA.');
1126
+ } else if (resolvedMode === 'post-fix') {
1127
+ lines.push('Compare approved scope, QA/tester/pentester findings, and the correction diff; confirm the fix did not change product intent.');
1128
+ } else if (resolvedMode === 'final') {
1129
+ lines.push('Reconcile intent, plan, delivered behavior, and remaining exclusions before close/commit/release.');
1130
+ }
1131
+ return lines.join('\n');
1132
+ }
1133
+
1134
+ function buildStageActivationContext(state, stageName, dependencies, scopeCheckMode = null) {
1135
+ if (stageName === 'scope-check') {
1136
+ return buildScopeCheckActivationContext(state, scopeCheckMode);
1137
+ }
1138
+
1139
+ if (stageName !== 'dev' || state.mode !== 'feature' || !state.featureSlug) return '';
1140
+ return [
1141
+ `Feature slug: ${state.featureSlug}`,
1142
+ `Workflow mode: ${state.mode}`,
1143
+ `Classification: ${state.classification || 'unknown'}`,
1144
+ dependencies.includes('.aioson/context/dev-state.md')
1145
+ ? 'Resume source: .aioson/context/dev-state.md'
1146
+ : 'Resume source: active feature artifacts'
1147
+ ].join('\n');
1148
+ }
1149
+
1150
+ async function activateStage(targetDir, state, locale, tool, explicitAgent = null, requestedMode = null, scopeCheckMode = null) {
1114
1151
  const stageName = normalizeAgentName(explicitAgent || state.current || state.next);
1115
1152
  if (!stageName) {
1116
1153
  return {
@@ -1209,17 +1246,32 @@ async function activateStage(targetDir, state, locale, tool, explicitAgent = nul
1209
1246
  requestedMode
1210
1247
  });
1211
1248
 
1212
- const instructionPath = await resolveExistingInstructionPath(targetDir, agent, locale);
1213
- const dependencies = await resolveStageDependencies(targetDir, state, stageName, agent);
1214
- let prompt = buildAgentPrompt(agent, tool, {
1215
- instructionPath,
1216
- targetDir,
1217
- interactionLanguage: locale,
1218
- autonomyMode: effectiveMode,
1219
- capabilitySummary: buildAgentCapabilitySummary(agentManifest, tool),
1220
- dependsOn: dependencies,
1221
- activationContext: buildStageActivationContext(state, stageName, dependencies, scopeCheckMode)
1222
- });
1249
+ let autoHandoff = false;
1250
+ if (
1251
+ AUTOPILOT_HANDOFF_STAGES.has(stageName) &&
1252
+ state.mode === 'feature' &&
1253
+ (state.classification === 'SMALL' || state.classification === 'MEDIUM')
1254
+ ) {
1255
+ try {
1256
+ const projectContext = await validateProjectContextFile(targetDir);
1257
+ autoHandoff = Boolean(projectContext && projectContext.data && projectContext.data.auto_handoff === true);
1258
+ } catch {
1259
+ autoHandoff = false;
1260
+ }
1261
+ }
1262
+
1263
+ const instructionPath = await resolveExistingInstructionPath(targetDir, agent, locale);
1264
+ const dependencies = await resolveStageDependencies(targetDir, state, stageName, agent);
1265
+ let prompt = buildAgentPrompt(agent, tool, {
1266
+ instructionPath,
1267
+ targetDir,
1268
+ interactionLanguage: locale,
1269
+ autonomyMode: effectiveMode,
1270
+ capabilitySummary: buildAgentCapabilitySummary(agentManifest, tool),
1271
+ dependsOn: dependencies,
1272
+ autoHandoff,
1273
+ activationContext: buildStageActivationContext(state, stageName, dependencies, scopeCheckMode)
1274
+ });
1223
1275
 
1224
1276
  if (testBriefing) {
1225
1277
  prompt += '\n\n' + testBriefing;
@@ -1489,10 +1541,10 @@ async function runWorkflowNext({ args, options, logger, t }) {
1489
1541
  requestedAgent = 'validator';
1490
1542
  }
1491
1543
 
1492
- const activationAgent = normalizeAgentName(requestedAgent || state.current || state.next);
1493
- const scopeCheckMode = activationAgent === 'scope-check' ? getScopeCheckModeOption(options) : null;
1494
- const requestedAutonomyMode = scopeCheckMode && activationAgent === 'scope-check' ? null : options.mode || null;
1495
- const activation = await activateStage(targetDir, state, locale, tool, requestedAgent, requestedAutonomyMode, scopeCheckMode);
1544
+ const activationAgent = normalizeAgentName(requestedAgent || state.current || state.next);
1545
+ const scopeCheckMode = activationAgent === 'scope-check' ? getScopeCheckModeOption(options) : null;
1546
+ const requestedAutonomyMode = scopeCheckMode && activationAgent === 'scope-check' ? null : options.mode || null;
1547
+ const activation = await activateStage(targetDir, state, locale, tool, requestedAgent, requestedAutonomyMode, scopeCheckMode);
1496
1548
  state = activation.state;
1497
1549
 
1498
1550
  // ── Living Memory: if a reflect manifest is pending (created above by the
@@ -1624,14 +1676,14 @@ module.exports = {
1624
1676
  EVENTS_RELATIVE_PATH,
1625
1677
  buildDefaultWorkflowConfig,
1626
1678
  parseFeaturesMarkdown,
1627
- readWorkflowConfig,
1628
- detectWorkflowMode,
1629
- loadOrCreateState,
1630
- persistState,
1631
- appendWorkflowEvent,
1632
- resolveLocaleForTarget,
1633
- reconcileWorkflowState,
1634
- finalizeCurrentStage,
1679
+ readWorkflowConfig,
1680
+ detectWorkflowMode,
1681
+ loadOrCreateState,
1682
+ persistState,
1683
+ appendWorkflowEvent,
1684
+ resolveLocaleForTarget,
1685
+ reconcileWorkflowState,
1686
+ finalizeCurrentStage,
1635
1687
  applySkip,
1636
1688
  activateStage,
1637
1689
  runWorkflowNext,