@jaimevalasek/aioson 1.36.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/docs/en/1-understand/ecosystem-map.md +1 -1
  3. package/docs/en/1-understand/glossary.md +1 -1
  4. package/docs/en/2-start/first-project.md +1 -1
  5. package/docs/en/2-start/initial-decisions.md +4 -2
  6. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +8 -1
  7. package/docs/en/3-recipes/full-feature-with-sheldon.md +3 -1
  8. package/docs/en/4-agents/README.md +6 -3
  9. package/docs/en/4-agents/briefing-refiner.md +146 -0
  10. package/docs/en/5-reference/README.md +1 -0
  11. package/docs/en/5-reference/autopilot-handoff.md +286 -0
  12. package/docs/en/5-reference/cli-reference.md +6 -0
  13. package/docs/pt/1-entender/glossario.md +1 -1
  14. package/docs/pt/1-entender/mapa-do-ecossistema.md +1 -1
  15. package/docs/pt/2-comecar/decisoes-iniciais.md +4 -2
  16. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  17. package/docs/pt/3-receitas/da-ideia-ao-prd-via-briefing.md +8 -1
  18. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +3 -1
  19. package/docs/pt/4-agentes/README.md +13 -11
  20. package/docs/pt/4-agentes/briefing-refiner.md +64 -40
  21. package/docs/pt/4-agentes/briefing.md +6 -1
  22. package/docs/pt/4-agentes/dev.md +19 -1
  23. package/docs/pt/4-agentes/deyvin.md +4 -0
  24. package/docs/pt/4-agentes/discover.md +4 -0
  25. package/docs/pt/4-agentes/neo.md +4 -0
  26. package/docs/pt/4-agentes/orache.md +6 -0
  27. package/docs/pt/4-agentes/orchestrator.md +12 -0
  28. package/docs/pt/4-agentes/pentester.md +6 -0
  29. package/docs/pt/4-agentes/product.md +19 -1
  30. package/docs/pt/4-agentes/qa.md +10 -2
  31. package/docs/pt/4-agentes/setup.md +3 -1
  32. package/docs/pt/4-agentes/sheldon.md +12 -0
  33. package/docs/pt/4-agentes/tester.md +6 -0
  34. package/docs/pt/4-agentes/ux-ui.md +2 -1
  35. package/docs/pt/5-referencia/README.md +1 -1
  36. package/docs/pt/5-referencia/agent-chain-continuity.md +1 -1
  37. package/docs/pt/5-referencia/autopilot-handoff.md +191 -74
  38. package/docs/pt/5-referencia/comandos-cli.md +16 -7
  39. package/docs/pt/5-referencia/skills.md +2 -0
  40. package/docs/pt/agentes.md +3 -1
  41. package/package.json +1 -1
  42. package/src/agent-execution/adapters/base.js +15 -0
  43. package/src/agent-execution/adapters/claude.js +3 -0
  44. package/src/agent-execution/adapters/codex.js +3 -0
  45. package/src/agent-execution/adapters/opencode.js +3 -0
  46. package/src/agent-execution/capabilities.js +9 -0
  47. package/src/agent-execution/dispatcher.js +72 -0
  48. package/src/agent-execution/executable-resolver.js +7 -0
  49. package/src/agent-execution/manifest.js +62 -0
  50. package/src/agent-execution/model-catalog.js +80 -0
  51. package/src/agent-execution/model-resolver.js +132 -0
  52. package/src/agent-execution/reports.js +9 -0
  53. package/src/agent-execution/schema.js +48 -0
  54. package/src/agent-execution/telemetry-bridge.js +15 -0
  55. package/src/agents.js +1 -1
  56. package/src/artifact-kinds.js +2 -1
  57. package/src/autopilot-signal.js +71 -0
  58. package/src/cli.js +31 -5
  59. package/src/commands/agent-execution.js +36 -0
  60. package/src/commands/agents.js +18 -2
  61. package/src/commands/briefing.js +337 -1
  62. package/src/commands/feature-close.js +136 -43
  63. package/src/commands/live.js +47 -11
  64. package/src/commands/op-capture.js +33 -3
  65. package/src/commands/op-reinforce.js +10 -22
  66. package/src/commands/update.js +5 -1
  67. package/src/commands/verification-plan.js +56 -12
  68. package/src/commands/verify-artifact.js +64 -1
  69. package/src/commands/workflow-execute.js +168 -36
  70. package/src/commands/workflow-next.js +60 -16
  71. package/src/doctor.js +4 -2
  72. package/src/harness/criteria-runner.js +4 -1
  73. package/src/i18n/messages/en.js +2 -1
  74. package/src/i18n/messages/es.js +2 -1
  75. package/src/i18n/messages/fr.js +2 -1
  76. package/src/i18n/messages/pt-BR.js +2 -1
  77. package/src/lib/briefing-refiner/apply-feedback.js +18 -4
  78. package/src/lib/briefing-refiner/feedback-schema.js +73 -4
  79. package/src/lib/briefing-refiner/refinement-report.js +11 -0
  80. package/src/lib/briefing-refiner/review-html.js +388 -68
  81. package/src/operator-memory/decision.js +41 -0
  82. package/src/parser.js +6 -0
  83. package/src/runtime-store.js +167 -8
  84. package/template/.aioson/agents/briefing-refiner.md +87 -47
  85. package/template/.aioson/agents/briefing.md +4 -0
  86. package/template/.aioson/agents/dev.md +9 -2
  87. package/template/.aioson/agents/deyvin.md +4 -0
  88. package/template/.aioson/agents/discover.md +4 -0
  89. package/template/.aioson/agents/neo.md +4 -0
  90. package/template/.aioson/agents/orache.md +4 -0
  91. package/template/.aioson/agents/orchestrator.md +16 -0
  92. package/template/.aioson/agents/pentester.md +4 -0
  93. package/template/.aioson/agents/product.md +26 -1
  94. package/template/.aioson/agents/qa.md +5 -1
  95. package/template/.aioson/agents/sheldon.md +9 -1
  96. package/template/.aioson/agents/tester.md +4 -0
  97. package/template/.aioson/agents/ux-ui.md +1 -1
  98. package/template/.aioson/docs/agent-help.md +126 -0
  99. package/template/.aioson/docs/autopilot-handoff.md +32 -16
  100. package/template/.aioson/docs/dev/phase-loop.md +9 -7
  101. package/template/.aioson/docs/play/llm-data-and-bindings.md +70 -7
  102. package/template/.aioson/schemas/agent-execution.schema.json +28 -0
  103. package/template/.aioson/skills/design/interface-design/SKILL.md +17 -0
  104. package/template/AGENTS.md +36 -36
  105. package/template/CLAUDE.md +1 -1
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  const fs = require('node:fs/promises');
11
- const path = require('node:path');
11
+ const path = require('node:path');
12
+ const { initManifest, loadManifest } = require('../agent-execution/manifest');
12
13
  const {
13
14
  detectClassification,
14
15
  scanArtifacts,
@@ -27,6 +28,7 @@ const { readHandoff, readHandoffProtocol } = require('../session-handoff');
27
28
  const {
28
29
  STATE_RELATIVE_PATH,
29
30
  buildDefaultWorkflowConfig,
31
+ detectWorkflowMode,
30
32
  readWorkflowConfig,
31
33
  runWorkflowNext
32
34
  } = require('./workflow-next');
@@ -110,11 +112,26 @@ function isAgenticRequested(options = {}) {
110
112
  options.agentic ||
111
113
  options['agentic-run'] ||
112
114
  options.autopilot === 'agentic' ||
113
- options.autopilot === 'runtime'
115
+ options.autopilot === 'runtime' ||
116
+ // Seeding the scheme IS turning on autopilot — the whole point of --seed is to
117
+ // persist an enabled agentic_policy the interactive agents then follow.
118
+ options.seed ||
119
+ options['seed-only']
114
120
  );
115
121
  }
116
122
 
117
123
  function buildAgenticPolicy(options = {}, classification = 'SMALL') {
124
+ // --seed --step: the per-feature "step by step" choice. Writes an explicitly
125
+ // DISARMED scheme — resolveAutopilotSignal and the agent triggers treat it as
126
+ // the feature-level override that wins over a project-wide auto_handoff: true.
127
+ if (options.step) {
128
+ return {
129
+ enabled: false,
130
+ mode: 'step_by_step',
131
+ source: 'workflow:execute --step'
132
+ };
133
+ }
134
+
118
135
  const enabled = isAgenticRequested(options);
119
136
  if (!enabled) return null;
120
137
 
@@ -229,6 +246,18 @@ function inferCompletedStagesFromArtifacts(sequence, artifacts, gates) {
229
246
  inferred = Boolean(artifacts.requirements && artifacts.requirements.exists && gates.requirements === 'approved');
230
247
  } else if (normalized === 'architect') {
231
248
  inferred = Boolean(artifacts.architecture && artifacts.architecture.exists && gates.design === 'approved');
249
+ } else if (normalized === 'sheldon' || normalized === 'orchestrator') {
250
+ // Single spec authorities (lean @sheldon / maestro @orchestrator) collapse
251
+ // Gates A/B/C into one hop — done once the spec package exists with the
252
+ // collapsed gates approved. Without this, a seed run AFTER the spec stage
253
+ // finished reported `next: sheldon|orchestrator` and pointed the whole
254
+ // agentic scheme backwards.
255
+ inferred = Boolean(
256
+ artifacts.spec && artifacts.spec.exists &&
257
+ gates.requirements === 'approved' &&
258
+ gates.design === 'approved' &&
259
+ gates.plan === 'approved'
260
+ );
232
261
  } else if (normalized === 'pm') {
233
262
  inferred = Boolean(artifacts.implementation_plan && artifacts.implementation_plan.exists && gates.plan === 'approved');
234
263
  } else if (normalized === 'qa') {
@@ -244,9 +273,17 @@ function inferCompletedStagesFromArtifacts(sequence, artifacts, gates) {
244
273
  return completed;
245
274
  }
246
275
 
247
- async function seedFeatureWorkflowState(targetDir, slug, classification, startFrom) {
276
+ /**
277
+ * Reads the persisted workflow state for a seed/preview of `slug`, applying the
278
+ * SAME staleness guard as loadOrCreateState: a state whose feature is no longer
279
+ * the features.md-active feature is stale and gets discarded (the loader would
280
+ * throw it away one command later anyway — hard-failing the seed on it just
281
+ * silently disarmed autopilot). The refusal survives only for a genuinely
282
+ * active different feature.
283
+ */
284
+ async function resolveExistingFeatureState(targetDir, slug) {
248
285
  const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
249
- const existing = await readJsonIfExists(statePath);
286
+ let existing = await readJsonIfExists(statePath);
250
287
  const focusStage = getFocusStage(existing);
251
288
 
252
289
  if (
@@ -256,14 +293,41 @@ async function seedFeatureWorkflowState(targetDir, slug, classification, startFr
256
293
  existing.featureSlug !== slug &&
257
294
  focusStage
258
295
  ) {
259
- return {
260
- ok: false,
261
- reason: 'different_active_feature',
262
- active_feature: existing.featureSlug,
263
- active_stage: focusStage
264
- };
296
+ let modeInfo = null;
297
+ try {
298
+ modeInfo = await detectWorkflowMode(targetDir);
299
+ } catch {
300
+ modeInfo = null;
301
+ }
302
+ const existingIsActive = Boolean(
303
+ modeInfo && modeInfo.mode === 'feature' && modeInfo.featureSlug === existing.featureSlug
304
+ );
305
+ const requestedIsActive = Boolean(
306
+ modeInfo && modeInfo.mode === 'feature' && modeInfo.featureSlug === slug
307
+ );
308
+ if (!existingIsActive || requestedIsActive) {
309
+ existing = null; // stale pointer — reseed for the requested feature
310
+ } else {
311
+ return {
312
+ existing: null,
313
+ refusal: {
314
+ ok: false,
315
+ reason: 'different_active_feature',
316
+ active_feature: existing.featureSlug,
317
+ active_stage: focusStage
318
+ }
319
+ };
320
+ }
265
321
  }
266
322
 
323
+ return { existing, refusal: null };
324
+ }
325
+
326
+ async function seedFeatureWorkflowState(targetDir, slug, classification, startFrom) {
327
+ const statePath = path.join(targetDir, STATE_RELATIVE_PATH);
328
+ const { existing, refusal } = await resolveExistingFeatureState(targetDir, slug);
329
+ if (refusal) return refusal;
330
+
267
331
  if (existing && existing.mode === 'feature' && existing.featureSlug === slug) {
268
332
  return {
269
333
  ok: true,
@@ -312,23 +376,8 @@ async function seedFeatureWorkflowState(targetDir, slug, classification, startFr
312
376
  }
313
377
 
314
378
  async function previewFeatureWorkflowState(targetDir, slug, classification, startFrom) {
315
- const existing = await readJsonIfExists(path.join(targetDir, STATE_RELATIVE_PATH));
316
- const focusStage = getFocusStage(existing);
317
-
318
- if (
319
- existing &&
320
- existing.mode === 'feature' &&
321
- existing.featureSlug &&
322
- existing.featureSlug !== slug &&
323
- focusStage
324
- ) {
325
- return {
326
- ok: false,
327
- reason: 'different_active_feature',
328
- active_feature: existing.featureSlug,
329
- active_stage: focusStage
330
- };
331
- }
379
+ const { existing, refusal } = await resolveExistingFeatureState(targetDir, slug);
380
+ if (refusal) return refusal;
332
381
 
333
382
  if (existing && existing.mode === 'feature' && existing.featureSlug === slug) {
334
383
  return {
@@ -539,7 +588,11 @@ function buildCheckpointPayload(activation, handoff, handoffProtocol) {
539
588
  async function writeExecutionCheckpoint(targetDir, payload) {
540
589
  const execPath = path.join(targetDir, EXECUTION_STATE_RELATIVE_PATH);
541
590
  const existing = await readJsonIfExists(execPath);
542
- const history = Array.isArray(existing && existing.history) ? [...existing.history] : [];
591
+ // History is per-feature: never carry a previous feature's checkpoints into
592
+ // a new feature's scheme.
593
+ const history = existing && existing.feature === payload.feature && Array.isArray(existing.history)
594
+ ? [...existing.history]
595
+ : [];
543
596
  if (payload.checkpoint) {
544
597
  history.push({
545
598
  at: new Date().toISOString(),
@@ -699,9 +752,12 @@ async function performRunnerTransition(targetDir, suggestion, tool, requestedMod
699
752
  async function runWorkflowExecute({ args, options = {}, logger }) {
700
753
  const targetDir = path.resolve(process.cwd(), args[0] || '.');
701
754
  const slug = options.feature ? String(options.feature).trim() : null;
702
- const tool = options.tool ? String(options.tool).trim() : 'claude';
755
+ const tool = options.tool ? String(options.tool).trim() : 'claude';
703
756
  const requestedMode = options.mode ? String(options.mode).trim() : null;
704
757
  const dryRun = Boolean(options['dry-run'] || options.dry);
758
+ // --step (disarm) is by definition record-only: it writes the disarmed scheme
759
+ // and never drives stage transitions, with or without an explicit --seed.
760
+ const seedOnly = Boolean(options.seed || options['seed-only'] || options.step);
705
761
  const startFrom = options['start-from'] ? String(options['start-from']).trim() : null;
706
762
  const skipOptional = Boolean(options['skip-optional']);
707
763
  const parsedMaxCheckpoints = Number.parseInt(String(options['max-checkpoints'] || '1'), 10);
@@ -733,9 +789,20 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
733
789
  return failure;
734
790
  }
735
791
 
736
- const parallelGuard = laneIndex !== null
792
+ const parallelGuard = laneIndex !== null
737
793
  ? await runLaneGuardPreflight(targetDir, laneIndex)
738
- : null;
794
+ : null;
795
+
796
+ let agentExecution = await loadManifest(targetDir, slug);
797
+ if (!dryRun && !agentExecution.exists) {
798
+ await initManifest(targetDir, slug, tool);
799
+ agentExecution = await loadManifest(targetDir, slug);
800
+ }
801
+ if (agentExecution.exists && !agentExecution.ok) {
802
+ const failure = { ok: false, reason: 'agent_execution_manifest_invalid', errors: agentExecution.errors, manifest_path: agentExecution.path };
803
+ if (!options.json) logger.error(`Invalid agent execution manifest: ${agentExecution.path}`);
804
+ return failure;
805
+ }
739
806
 
740
807
  if (parallelGuard && !parallelGuard.ok && !parallelGuard.skipped) {
741
808
  if (parallelGuard.reason === 'lane_not_found') {
@@ -792,14 +859,18 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
792
859
  `--tool=${quoteCliArg(tool)}`,
793
860
  ...(requestedMode ? [`--mode=${quoteCliArg(requestedMode)}`] : []),
794
861
  ...(maxCheckpoints !== 1 ? [`--max-checkpoints=${quoteCliArg(maxCheckpoints)}`] : []),
795
- ...(agenticPolicy ? ['--agentic'] : []),
796
- ...(agenticPolicy && agenticPolicy.review_cycle.max_dev_qa_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
862
+ // A seed run must resume as a seed run — `--agentic` (the CLI-advancing
863
+ // runner) is the opposite of the seed-only contract. A disarm run keeps
864
+ // its --step so replaying it never re-arms the scheme.
865
+ ...(seedOnly ? ['--seed'] : agenticPolicy && agenticPolicy.enabled ? ['--agentic'] : []),
866
+ ...(options.step ? ['--step'] : []),
867
+ ...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_dev_qa_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
797
868
  ? [`--max-dev-qa-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_dev_qa_cycles)}`]
798
869
  : []),
799
- ...(agenticPolicy && agenticPolicy.review_cycle.max_tester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
870
+ ...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_tester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
800
871
  ? [`--max-tester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_tester_correction_cycles)}`]
801
872
  : []),
802
- ...(agenticPolicy && agenticPolicy.review_cycle.max_pentester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
873
+ ...(agenticPolicy && agenticPolicy.review_cycle && agenticPolicy.review_cycle.max_pentester_correction_cycles !== DEFAULT_AGENTIC_MAX_CYCLES
803
874
  ? [`--max-pentester-cycles=${quoteCliArg(agenticPolicy.review_cycle.max_pentester_correction_cycles)}`]
804
875
  : [])
805
876
  ].join(' ');
@@ -823,7 +894,8 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
823
894
  status_snapshot: statusSnapshot,
824
895
  suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
825
896
  resume_command: resumeCommand,
826
- agentic_policy: agenticPolicy,
897
+ agentic_policy: agenticPolicy,
898
+ agent_execution: agentExecution.exists ? { path: agentExecution.path, digest: agentExecution.digest } : { source: 'legacy' },
827
899
  parallel_guard: parallelGuard
828
900
  };
829
901
 
@@ -854,6 +926,66 @@ async function runWorkflowExecute({ args, options = {}, logger }) {
854
926
  return result;
855
927
  }
856
928
 
929
+ // --seed: persist the workflow.state.json + workflow-execute.json (with an
930
+ // enabled agentic_policy) and STOP. Unlike a full run, it does not drive stage
931
+ // transitions from the CLI — in an interactive Claude Code session the agents
932
+ // themselves follow the scheme. This is what the spec->dev handoff calls so a
933
+ // feature built the normal way carries the autopilot contract without the user
934
+ // running anything. Idempotent: re-seeding the same slug reuses existing state.
935
+ if (seedOnly) {
936
+ const handoff = await readHandoff(targetDir);
937
+ const handoffProtocol = await readHandoffProtocol(targetDir);
938
+ const nextStage = seeded.state ? (seeded.state.current || seeded.state.next || null) : null;
939
+ const executionState = await writeExecutionCheckpoint(targetDir, {
940
+ feature: slug,
941
+ classification,
942
+ tool,
943
+ requestedMode,
944
+ resumed: seeded.resumed,
945
+ status: nextStage ? 'active' : 'completed',
946
+ checkpoint: buildCheckpointPayload(null, handoff, handoffProtocol),
947
+ statusSnapshot,
948
+ suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
949
+ resumeCommand,
950
+ agenticPolicy
951
+ });
952
+
953
+ const result = {
954
+ ok: true,
955
+ feature: slug,
956
+ classification,
957
+ tool,
958
+ requested_mode: requestedMode,
959
+ seeded: true,
960
+ resumed: seeded.resumed,
961
+ state_path: seeded.statePath,
962
+ execution_state_path: EXECUTION_STATE_RELATIVE_PATH,
963
+ next_stage: nextStage,
964
+ checkpoint: executionState.checkpoint,
965
+ execution_state: executionState,
966
+ status_snapshot: statusSnapshot,
967
+ suggestion: statusSnapshot && statusSnapshot.suggestion ? statusSnapshot.suggestion : null,
968
+ resume_command: resumeCommand,
969
+ agentic_policy: agenticPolicy,
970
+ agent_execution: { path: agentExecution.path, digest: agentExecution.digest },
971
+ parallel_guard: parallelGuard
972
+ };
973
+
974
+ if (options.json) return result;
975
+
976
+ logger.log('');
977
+ logger.log(`Agentic workflow scheme seeded → ${EXECUTION_STATE_RELATIVE_PATH}`);
978
+ logger.log(`Feature: ${slug} (${classification}) Next stage: @${nextStage || 'none'}`);
979
+ logger.log(
980
+ `Autopilot: ${agenticPolicy && agenticPolicy.enabled
981
+ ? 'enabled — interactive agents run the chain to feature:close (human gate)'
982
+ : 'disabled'}`
983
+ );
984
+ for (const line of formatAgenticPolicyLines(agenticPolicy)) logger.log(line);
985
+ logger.log('');
986
+ return result;
987
+ }
988
+
857
989
  const executionTransitions = [];
858
990
  let activation = null;
859
991
  let currentStatus = statusSnapshot;
@@ -16,6 +16,7 @@ const { logError, buildHealingPrompt } = require('../self-healing');
16
16
  const { validateHandoffProtocol } = require('../handoff-validator');
17
17
  const { readAutonomyProtocol, resolveEffectiveMode } = require('../autonomy-policy');
18
18
  const { readAgentManifest, buildAgentCapabilitySummary } = require('../agent-manifests');
19
+ const { resolveAutopilotSignal } = require('../autopilot-signal');
19
20
  const { runMemoryReflectPrepare } = require('./memory-reflect-prepare');
20
21
  const { inspectStagedChanges } = require('../lib/git-commit-guard');
21
22
  const { emitSecurityRuntimeEvent } = require('../lib/security/runtime-events');
@@ -61,19 +62,18 @@ const DEFAULT_FEATURE_WORKFLOW_BY_CLASSIFICATION = {
61
62
  MEDIUM: ['product', 'orchestrator', 'dev', 'pentester', 'qa']
62
63
  };
63
64
 
64
- // Stages eligible for autopilot handoff (auto_handoff: true in project.context.md).
65
- // Two segments — see .aioson/docs/autopilot-handoff.md:
66
- // 1. analyst -> dev: deterministic pre-dev chain. Prompt-only clients stop
67
- // before the first @dev entry; workflow:execute --agentic may resume it
68
- // through a fresh checkpointed activation.
69
- // 2. post-dev review cycle: @dev @qa @tester/@pentester (when their @qa triggers
70
- // fire) @validator STOPS before feature:close (human approves the close).
71
- // @product and @sheldon are intentionally absent: per config.md, upstream agents
72
- // (@briefing/@product/@sheldon) always hand off MANUALLY. In the lean lane
73
- // (productsheldondev qa) this means auto_handoff is a deliberate no-op
74
- // pre-dev — @sheldon is the only pre-dev agent and hands off by hand — and is
75
- // active only on the post-dev cycle (dev → qa). This is by design, not an omission.
65
+ // Stages eligible for autopilot handoff the FULL feature chain (see
66
+ // .aioson/docs/autopilot-handoff.md). Activation = auto_handoff: true in
67
+ // project.context.md OR the seeded scheme (resolveAutopilotSignal). Two segments:
68
+ // 1. spec dev chain: @product seeds the agentic scheme and invokes the spec
69
+ // authority (@sheldon lean / @orchestrator maestro), which crosses into
70
+ // @dev via the dev-state.md cold-start packet once its own gates/decisions
71
+ // are settled. Detour agents (analyst/architect/pm/...) chain only when an
72
+ // opt-in detour adds them to the active sequence.
73
+ // 2. post-dev review cycle: @dev @qa @tester/@pentester (when their @qa
74
+ // triggers fire)@validatorSTOPS before feature:close (human gate).
76
75
  const AUTOPILOT_HANDOFF_STAGES = new Set([
76
+ 'product', 'sheldon', 'orchestrator',
77
77
  'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'pm',
78
78
  'dev', 'qa', 'tester', 'pentester', 'validator'
79
79
  ]);
@@ -99,6 +99,14 @@ function isMaestroOrchestratorState(state) {
99
99
  return idx !== -1 && sequence[idx + 1] === 'dev';
100
100
  }
101
101
 
102
+ // Lean lane (SMALL, sheldon → dev): @sheldon is the single spec authority.
103
+ // Mirrors isLeanSheldonState in handoff-contract.js.
104
+ function isLeanSheldonState(state) {
105
+ const sequence = Array.isArray(state && state.sequence) ? state.sequence.map(normalizeAgentName) : [];
106
+ const idx = sequence.indexOf('sheldon');
107
+ return idx !== -1 && sequence[idx + 1] === 'dev';
108
+ }
109
+
102
110
  function buildDefaultWorkflowConfig() {
103
111
  return {
104
112
  version: 1,
@@ -376,6 +384,29 @@ async function validateStageArtifacts(targetDir, state, stage) {
376
384
  return true;
377
385
  }
378
386
 
387
+ if (stage === 'sheldon') {
388
+ // Lean lane (SMALL, sheldon → dev): @sheldon is the single spec authority —
389
+ // "done" once the collapsed spec package exists (mirrors the orchestrator
390
+ // maestro branch below; the handoff contract re-checks Gates A/B/C). Without
391
+ // this branch nothing ever marks the sheldon stage resolved, and a later
392
+ // `--complete=dev` computes `next: sheldon` — the state machine walking
393
+ // BACKWARDS into the spec agent after implementation.
394
+ if (state.mode === 'feature' && slug && isLeanSheldonState(state)) {
395
+ const designDoc = [path.join(base, `design-doc-${slug}.md`), path.join(base, 'design-doc.md')];
396
+ const readiness = [path.join(base, `readiness-${slug}.md`), path.join(base, 'readiness.md')];
397
+ return (await exists(path.join(base, `sheldon-enrichment-${slug}.md`)))
398
+ && (await exists(path.join(base, `requirements-${slug}.md`)))
399
+ && (await exists(path.join(base, `spec-${slug}.md`)))
400
+ && (await exists(path.join(base, `implementation-plan-${slug}.md`)))
401
+ && (await anyExists(designDoc))
402
+ && (await anyExists(readiness));
403
+ }
404
+ if (state.mode === 'feature' && slug) {
405
+ return await exists(path.join(base, `sheldon-enrichment-${slug}.md`));
406
+ }
407
+ return await exists(path.join(base, 'sheldon-enrichment.md'));
408
+ }
409
+
379
410
  if (stage === 'orchestrator') {
380
411
  // Maestro lane (MEDIUM, orchestrator → dev): the orchestrator is the single
381
412
  // spec authority — "done" once the gated spec package exists (mirrors how the
@@ -519,7 +550,10 @@ function isInferableStage(stage) {
519
550
  // could never infer scope-check as completed during stale-state recovery.
520
551
  // pm is inferable from implementation-plan-{slug}.md for the same reason:
521
552
  // it sits before scope-check in the MEDIUM feature sequence.
522
- return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'orchestrator'].includes(
553
+ // sheldon (lean lane) and orchestrator (maestro lane) are the single spec
554
+ // authorities — both inferable from their collapsed spec package so stale-state
555
+ // recovery and mid-lane seeding never re-activate a finished spec stage.
556
+ return ['setup', 'product', 'analyst', 'scope-check', 'architect', 'discovery-design-doc', 'ux-ui', 'pm', 'sheldon', 'orchestrator'].includes(
523
557
  normalizeAgentName(stage)
524
558
  );
525
559
  }
@@ -993,8 +1027,15 @@ async function finalizeCurrentStage(targetDir, config, state, stageName) {
993
1027
  detour: null
994
1028
  });
995
1029
 
1030
+ // Reconcile eagerly: completing a later stage must never leave `next` pointing
1031
+ // at an earlier unresolved stage (e.g. lean-lane sheldon that only chained via
1032
+ // prompt). Without this, `--complete=dev` re-activated the spec agent and the
1033
+ // reconcile only healed on the NEXT load — after the backwards activation had
1034
+ // already been printed and persisted.
1035
+ const reconciled = reconcileWorkflowState(nextState);
1036
+
996
1037
  return {
997
- state: nextState,
1038
+ state: reconciled.changed ? reconciled.state : nextState,
998
1039
  completedStage: normalizedStage,
999
1040
  ...(auditCodeSummary ? { auditCode: auditCodeSummary } : {})
1000
1041
  };
@@ -1560,9 +1601,12 @@ async function activateStage(
1560
1601
  state.mode === 'feature' &&
1561
1602
  (state.classification === 'SMALL' || state.classification === 'MEDIUM')
1562
1603
  ) {
1604
+ // Frontmatter flag OR the seeded scheme (slug-scoped) — the per-feature
1605
+ // "Autopilot" choice only seeds workflow-execute.json and never writes
1606
+ // auto_handoff, so reading the frontmatter alone silently disabled it.
1563
1607
  try {
1564
- const projectContext = await validateProjectContextFile(targetDir);
1565
- autoHandoff = Boolean(projectContext && projectContext.data && projectContext.data.auto_handoff === true);
1608
+ const signal = await resolveAutopilotSignal(targetDir, { slug: state.featureSlug });
1609
+ autoHandoff = signal.enabled;
1566
1610
  } catch {
1567
1611
  autoHandoff = false;
1568
1612
  }
package/src/doctor.js CHANGED
@@ -228,8 +228,10 @@ async function runDoctor(targetDir) {
228
228
  }
229
229
 
230
230
  // Autopilot handoff: protocol doc installed but flag never declared in the
231
- // context frontmatter autopilot stays silently inactive (absent = manual
232
- // handoffs). An explicit true/false is a deliberate choice and passes.
231
+ // context frontmatter. Absent is no longer "silently manual" @product asks
232
+ // the run mode on screen at each feature kickoff. Declaring an explicit
233
+ // true/false sets a project default (skip the kickoff question) and passes;
234
+ // the advisory just surfaces that a default can be set.
233
235
  const autopilotDocExists = await exists(path.join(targetDir, '.aioson/docs/autopilot-handoff.md'));
234
236
  if (autopilotDocExists && contextValidation.exists && contextValidation.data) {
235
237
  const autoHandoffDeclared = Object.prototype.hasOwnProperty.call(contextValidation.data, 'auto_handoff');
@@ -16,7 +16,10 @@
16
16
 
17
17
  const crypto = require('node:crypto');
18
18
 
19
- const DEFAULT_CHECK_TIMEOUT_MS = 120000;
19
+ // Repository-wide quality gates can legitimately exceed two minutes on
20
+ // Windows. Keep the CLI --timeout override, but give deterministic contract
21
+ // checks enough headroom to avoid classifying a healthy full suite as failed.
22
+ const DEFAULT_CHECK_TIMEOUT_MS = 300000;
20
23
 
21
24
  /**
22
25
  * Normaliza uma linha de erro para assinatura estável (D7):
@@ -412,6 +412,7 @@ module.exports = {
412
412
  update: {
413
413
  not_installed: 'No AIOSON installation found in {targetDir}.',
414
414
  done_at: 'Update completed at: {targetDir}',
415
+ template_version: 'Template version applied: {version}',
415
416
  files_updated: 'Files updated: {count}',
416
417
  backups_created: 'Backups created: {count}',
417
418
  profile_renamed: 'i Profile `beginner` renamed to `creator` in project.context.md to better describe the user. Behavior unchanged. Edit the file to switch to `developer` if desired.',
@@ -491,7 +492,7 @@ module.exports = {
491
492
  features_dir_present: 'Features directory present (.aioson/context/features/)',
492
493
  features_dir_present_hint: 'Create .aioson/context/features/ to host per-feature dossiers (doctor --fix will create it).',
493
494
  auto_handoff_declared: 'Autopilot handoff flag declared (auto_handoff in project.context.md)',
494
- auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed but auto_handoff is not set in project.context.md frontmatter autopilot stays inactive. Set auto_handoff: true to enable it, or auto_handoff: false to silence this warning.',
495
+ auto_handoff_declared_hint: 'The autopilot-handoff protocol is installed and auto_handoff is not set @product asks the run mode on screen at each feature kickoff (Autopilot / Step by step / Always). Set auto_handoff: true to always autopilot and skip the question, or false to always go step-by-step.',
495
496
  claude_commands_present: 'Claude slash commands present ({missing} missing of {required})',
496
497
  claude_commands_present_hint: 'Missing: {paths}. Run `aioson doctor . --fix` to restore them from the template.',
497
498
  version_drift: 'CLI version matches project.context.md (context: {context}, CLI: {cli})',
@@ -280,6 +280,7 @@ module.exports = {
280
280
  update: {
281
281
  not_installed: 'No se encontro instalacion de AIOSON en {targetDir}.',
282
282
  done_at: 'Actualizacion completada en: {targetDir}',
283
+ template_version: 'Version de template aplicada: {version}',
283
284
  files_updated: 'Archivos actualizados: {count}',
284
285
  backups_created: 'Backups creados: {count}',
285
286
  profile_renamed: 'i Perfil `beginner` renombrado a `creator` en project.context.md para describir mejor al usuario. Comportamiento sin cambios. Edita el archivo para cambiar a `developer` si lo prefieres.',
@@ -365,7 +366,7 @@ module.exports = {
365
366
  features_dir_present: 'Directorio de features presente (.aioson/context/features/)',
366
367
  features_dir_present_hint: 'Cree .aioson/context/features/ para hospedar dossiers por feature (doctor --fix lo crea).',
367
368
  auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff en project.context.md)',
368
- auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado pero auto_handoff no esta definido en el frontmatter de project.context.md el autopilot queda inactivo. Defina auto_handoff: true para activarlo, o auto_handoff: false para silenciar este aviso.',
369
+ auto_handoff_declared_hint: 'El protocolo autopilot-handoff esta instalado y auto_handoff no esta definido @product pregunta el modo en pantalla al inicio de cada feature (Autopilot / Paso a paso / Siempre). Defina auto_handoff: true para siempre autopilot y omitir la pregunta, o false para siempre paso a paso.',
369
370
  claude_commands_present: 'Slash commands de Claude presentes ({missing} ausentes de {required})',
370
371
  claude_commands_present_hint: 'Ausentes: {paths}. Ejecute `aioson doctor . --fix` para restaurarlos.',
371
372
  version_drift: 'Version del CLI coincide con project.context.md (contexto: {context}, CLI: {cli})',
@@ -279,6 +279,7 @@ module.exports = {
279
279
  update: {
280
280
  not_installed: 'Aucune installation AIOSON trouvee dans {targetDir}.',
281
281
  done_at: 'Mise a jour terminee dans : {targetDir}',
282
+ template_version: 'Version du template appliquee : {version}',
282
283
  files_updated: 'Fichiers mis a jour : {count}',
283
284
  backups_created: 'Sauvegardes creees : {count}',
284
285
  profile_renamed: 'i Profil `beginner` renomme en `creator` dans project.context.md pour mieux decrire l utilisateur. Comportement inchange. Modifiez le fichier pour passer a `developer` si vous le souhaitez.',
@@ -364,7 +365,7 @@ module.exports = {
364
365
  features_dir_present: 'Repertoire features present (.aioson/context/features/)',
365
366
  features_dir_present_hint: 'Creez .aioson/context/features/ pour heberger les dossiers par feature (doctor --fix le cree).',
366
367
  auto_handoff_declared: 'Flag autopilot handoff declare (auto_handoff dans project.context.md)',
367
- auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe mais auto_handoff n\'est pas defini dans le frontmatter de project.context.md l\'autopilot reste inactif. Definissez auto_handoff: true pour l\'activer, ou auto_handoff: false pour faire taire cet avertissement.',
368
+ auto_handoff_declared_hint: 'Le protocole autopilot-handoff est installe et auto_handoff n\'est pas defini @product demande le mode a l\'ecran au debut de chaque feature (Autopilot / Pas a pas / Toujours). Definissez auto_handoff: true pour toujours autopilot et sauter la question, ou false pour toujours pas a pas.',
368
369
  claude_commands_present: 'Slash commands de Claude presents ({missing} absents sur {required})',
369
370
  claude_commands_present_hint: 'Absents : {paths}. Executez `aioson doctor . --fix` pour les restaurer.',
370
371
  version_drift: 'Version du CLI conforme a project.context.md (contexte : {context}, CLI : {cli})',
@@ -379,6 +379,7 @@ module.exports = {
379
379
  update: {
380
380
  not_installed: 'Nenhuma instalacao do AIOSON encontrada em {targetDir}.',
381
381
  done_at: 'Atualizacao concluida em: {targetDir}',
382
+ template_version: 'Versao do template aplicada: {version}',
382
383
  files_updated: 'Arquivos atualizados: {count}',
383
384
  backups_created: 'Backups criados: {count}',
384
385
  profile_renamed: 'i Perfil `beginner` renomeado para `creator` em project.context.md para descrever melhor o usuario. Comportamento inalterado. Edite o arquivo para mudar para `developer` se preferir.',
@@ -465,7 +466,7 @@ module.exports = {
465
466
  features_dir_present: 'Diretorio de features presente (.aioson/context/features/)',
466
467
  features_dir_present_hint: 'Crie .aioson/context/features/ para hospedar dossies por feature (doctor --fix cria automaticamente).',
467
468
  auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff no project.context.md)',
468
- auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado mas auto_handoff nao esta definido no frontmatter do project.context.md o autopilot fica inativo. Defina auto_handoff: true para ativar, ou auto_handoff: false para silenciar este aviso.',
469
+ auto_handoff_declared_hint: 'O protocolo autopilot-handoff esta instalado e auto_handoff nao esta definido — o @product pergunta o modo na tela no inicio de cada feature (Autopilot / Passo a passo / Sempre). Defina auto_handoff: true para sempre autopilot e pular a pergunta, ou false para sempre passo a passo.',
469
470
  claude_commands_present: 'Slash commands do Claude presentes ({missing} ausentes de {required})',
470
471
  claude_commands_present_hint: 'Ausentes: {paths}. Rode `aioson doctor . --fix` para restaurar a partir do template.',
471
472
  version_drift: 'Versao do CLI bate com project.context.md (contexto: {context}, CLI: {cli})',
@@ -39,7 +39,10 @@ function summarizeSkippedChanges(feedback) {
39
39
  return [...sectionChanges, ...decisionChanges];
40
40
  }
41
41
 
42
- async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale = false } = {}) {
42
+ // Declining never writes to briefings.md, so a stale source hash is irrelevant
43
+ // to safety — validating it would only dead-end the user whose briefing moved
44
+ // on. Hence allowStale defaults to TRUE here (and only here).
45
+ async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale = true } = {}) {
43
46
  const briefingPath = resolveBriefingPath(projectDir, slug, 'briefings.md');
44
47
  const currentMarkdown = await fs.readFile(briefingPath, 'utf8');
45
48
  const currentSourceHash = hashText(currentMarkdown);
@@ -57,10 +60,12 @@ async function applyDeclinedFeedback(projectDir, slug, feedback, { allowStale =
57
60
  source_hash: feedback.source_hash,
58
61
  applied_hash: currentSourceHash,
59
62
  status: 'declined',
63
+ round: feedback.round || 1,
60
64
  applied_changes: [],
61
65
  skipped_changes: skippedChanges,
62
66
  unresolved_comments: unresolvedComments,
63
67
  blocking_items: feedback.blocking_items || [],
68
+ findings: feedback.findings || [],
64
69
  next_action: 'rerun_review'
65
70
  });
66
71
 
@@ -104,15 +109,22 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
104
109
  if (returnedToDraft) {
105
110
  returnApprovedBriefingToDraft(registry, slug);
106
111
  }
112
+ // A blocking finding the user left pending blocks the PRD the same way an
113
+ // explicit blocking item does — even if the export step never mirrored it
114
+ // into blocking_items (hand-written feedback).
115
+ const pendingBlockingFindings = (feedback.findings || []).filter(
116
+ (finding) => finding.blocking && finding.status === 'pending'
117
+ );
118
+ const hasBlockers = (feedback.blocking_items || []).length > 0 || pendingBlockingFindings.length > 0;
107
119
  markRefinementState(registry, slug, {
108
- refinement_status: feedback.blocking_items.length > 0 ? 'blocked' : 'applied',
120
+ refinement_status: hasBlockers ? 'blocked' : 'applied',
109
121
  review_html: `.aioson/briefings/${slug}/review.html`,
110
122
  refinement_report: `.aioson/briefings/${slug}/refinement-report.md`
111
123
  });
112
124
  await writeBriefingRegistry(projectDir, registry);
113
125
 
114
126
  const unresolvedComments = (feedback.comments || []).filter((comment) => !comment.resolved);
115
- const nextAction = feedback.blocking_items.length > 0 ? 'resolve_blockers' : 'approve_briefing';
127
+ const nextAction = hasBlockers ? 'resolve_blockers' : 'approve_briefing';
116
128
  const report = buildRefinementReport({
117
129
  briefing_slug: slug,
118
130
  source_briefing_path: `.aioson/briefings/${slug}/briefings.md`,
@@ -120,15 +132,17 @@ async function applyConfirmedFeedback(projectDir, slug, feedback, { confirmed =
120
132
  source_hash: feedback.source_hash,
121
133
  applied_hash: appliedHash,
122
134
  status: 'applied',
135
+ round: feedback.round || 1,
123
136
  applied_changes: appliedChanges,
124
137
  skipped_changes: [],
125
138
  unresolved_comments: unresolvedComments,
126
139
  blocking_items: feedback.blocking_items || [],
140
+ findings: feedback.findings || [],
127
141
  next_action: nextAction
128
142
  });
129
143
  await fs.writeFile(resolveBriefingPath(projectDir, slug, 'refinement-report.md'), report, 'utf8');
130
144
 
131
- return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft };
145
+ return { ok: true, appliedChanges, nextAction, appliedHash, returnedToDraft, pendingBlockingFindings: pendingBlockingFindings.length };
132
146
  }
133
147
 
134
148
  module.exports = { applyConfirmedFeedback, applyDeclinedFeedback };