@jaimevalasek/aioson 1.21.8 → 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 (41) hide show
  1. package/CHANGELOG.md +18 -4
  2. package/package.json +1 -1
  3. package/src/agents.js +21 -20
  4. package/src/cli.js +15 -0
  5. package/src/commands/feature-close.js +40 -0
  6. package/src/commands/gate-check.js +8 -3
  7. package/src/commands/git-guard.js +58 -0
  8. package/src/commands/harness-gate.js +120 -0
  9. package/src/commands/harness-status.js +157 -0
  10. package/src/commands/harness.js +18 -1
  11. package/src/commands/self-implement-loop.js +305 -5
  12. package/src/commands/workflow-next.js +37 -2
  13. package/src/doctor.js +24 -8
  14. package/src/harness/active-contract.js +41 -0
  15. package/src/harness/attempt-artifacts.js +95 -0
  16. package/src/harness/budget-guard.js +127 -0
  17. package/src/harness/circuit-breaker.js +7 -0
  18. package/src/harness/contract-schema.js +324 -0
  19. package/src/harness/criteria-runner.js +136 -0
  20. package/src/harness/git-baseline.js +204 -0
  21. package/src/harness/glob-match.js +126 -0
  22. package/src/harness/guard-events.js +71 -0
  23. package/src/harness/human-gate.js +182 -0
  24. package/src/harness/scope-guard.js +115 -0
  25. package/src/i18n/messages/en.js +2 -0
  26. package/src/i18n/messages/es.js +11 -9
  27. package/src/i18n/messages/fr.js +11 -9
  28. package/src/i18n/messages/pt-BR.js +2 -0
  29. package/src/lib/dev-resume.js +94 -45
  30. package/src/preflight-engine.js +88 -84
  31. package/template/.aioson/agents/analyst.md +4 -0
  32. package/template/.aioson/agents/architect.md +4 -0
  33. package/template/.aioson/agents/dev.md +3 -1
  34. package/template/.aioson/agents/discovery-design-doc.md +4 -0
  35. package/template/.aioson/agents/pm.md +10 -5
  36. package/template/.aioson/agents/qa.md +22 -14
  37. package/template/.aioson/agents/scope-check.md +176 -172
  38. package/template/.aioson/config.md +31 -28
  39. package/template/.aioson/docs/autopilot-handoff.md +46 -0
  40. package/template/AGENTS.md +57 -57
  41. package/template/CLAUDE.md +33 -33
@@ -450,6 +450,8 @@ module.exports = {
450
450
  bootstrap_coverage_hint_seed: 'Run /discover to seed .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
451
451
  features_dir_present: 'Features directory present (.aioson/context/features/)',
452
452
  features_dir_present_hint: 'Create .aioson/context/features/ to host per-feature dossiers (doctor --fix will create it).',
453
+ auto_handoff_declared: 'Autopilot handoff flag declared (auto_handoff in project.context.md)',
454
+ 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.',
453
455
  claude_commands_present: 'Claude slash commands present ({missing} missing of {required})',
454
456
  claude_commands_present_hint: 'Missing: {paths}. Run `aioson doctor . --fix` to restore them from the template.',
455
457
  version_drift: 'CLI version matches project.context.md (context: {context}, CLI: {cli})',
@@ -160,12 +160,12 @@ module.exports = {
160
160
  'aioson squad:pipeline [path] [--sub=list|show|status] [--pipeline=<slug>] [--locale=es]',
161
161
  help_squad_investigate:
162
162
  'aioson squad:investigate [path] [--sub=list|show|score|link|register] [--investigation=<slug>] [--squad=<slug>] [--locale=es]',
163
- help_squad_learning:
164
- 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=es]',
165
- help_quality_audit:
166
- 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=es]',
167
- help_squad_dashboard:
168
- 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=es]',
163
+ help_squad_learning:
164
+ 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=es]',
165
+ help_quality_audit:
166
+ 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=es]',
167
+ help_squad_dashboard:
168
+ 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=es]',
169
169
  help_squad_worker:
170
170
  'aioson squad:worker [path] [--sub=list|run|test|logs|scaffold] [--squad=<slug>] [--worker=<slug>] [--input=<json>] [--locale=es]',
171
171
  help_squad_daemon:
@@ -179,7 +179,7 @@ module.exports = {
179
179
  help_commit_prepare:
180
180
  'aioson commit:prepare [path] [--staged-only] [--agent-safe] [--mode=guarded|trusted|headless] [--json] [--locale=es]',
181
181
  help_learning:
182
- 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=es]',
182
+ 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=es]',
183
183
  dashboard_moved:
184
184
  'El flujo `{command}` fue eliminado del CLI. El dashboard de AIOSON ahora se instala por separado. Abre la app del dashboard en tu computadora, crea o agrega un proyecto y selecciona la carpeta que ya contiene `.aioson/`.',
185
185
  dashboard_moved_line: '{message}\n',
@@ -324,6 +324,8 @@ module.exports = {
324
324
  bootstrap_coverage_hint_seed: 'Ejecute /discover para sembrar .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
325
325
  features_dir_present: 'Directorio de features presente (.aioson/context/features/)',
326
326
  features_dir_present_hint: 'Cree .aioson/context/features/ para hospedar dossiers por feature (doctor --fix lo crea).',
327
+ auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff en project.context.md)',
328
+ 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.',
327
329
  claude_commands_present: 'Slash commands de Claude presentes ({missing} ausentes de {required})',
328
330
  claude_commands_present_hint: 'Ausentes: {paths}. Ejecute `aioson doctor . --fix` para restaurarlos.',
329
331
  version_drift: 'Version del CLI coincide con project.context.md (contexto: {context}, CLI: {cli})',
@@ -609,7 +611,7 @@ module.exports = {
609
611
  note_product_optional:
610
612
  '@product es opcional para MICRO — omitelo y ve directo a @dev si la idea ya esta clara.',
611
613
  note_feature_flow:
612
- 'Flujo para nueva feature (tras la configuracion inicial): @product → @analyst → @scope-check → @dev → @qa. Sin @setup.'
614
+ 'Flujo para nueva feature (tras la configuracion inicial): @product → @analyst → @scope-check → @dev → @qa. Sin @setup.'
613
615
  },
614
616
  parallel_init: {
615
617
  context_missing:
@@ -1020,7 +1022,7 @@ module.exports = {
1020
1022
  folder_required_example_prompt:
1021
1023
  ' Prompt listo : aioson agent:prompt analyst --tool=codex',
1022
1024
  folder_required_example_next:
1023
- ' Flujo tras escaneo completo: @analyst -> @scope-check -> @architect -> @dev',
1025
+ ' Flujo tras escaneo completo: @analyst -> @scope-check -> @architect -> @dev',
1024
1026
  folder_not_found: 'La carpeta "{folder}" no existe en este proyecto. Directorios de nivel superior detectados: {available}',
1025
1027
  config_missing: '{file} no encontrado. Para usar el modo con LLM, copia aioson-models.json y completa tus claves de API.',
1026
1028
  config_invalid: 'JSON invalido en aioson-models.json: {error}',
@@ -160,12 +160,12 @@ module.exports = {
160
160
  'aioson squad:pipeline [path] [--sub=list|show|status] [--pipeline=<slug>] [--locale=fr]',
161
161
  help_squad_investigate:
162
162
  'aioson squad:investigate [path] [--sub=list|show|score|link|register] [--investigation=<slug>] [--squad=<slug>] [--locale=fr]',
163
- help_squad_learning:
164
- 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=fr]',
165
- help_quality_audit:
166
- 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=fr]',
167
- help_squad_dashboard:
168
- 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=fr]',
163
+ help_squad_learning:
164
+ 'aioson squad:learning [path] [--sub=list|stats|archive|promote|export] [--squad=<slug>] [--status=<status>] [--locale=fr]',
165
+ help_quality_audit:
166
+ 'aioson quality:audit [path] [--feature=<slug>] [--provider-output=<path>] [--baseline=<path>] [--changed=<path[,path]>] [--json] [--locale=fr]',
167
+ help_squad_dashboard:
168
+ 'aioson squad:dashboard [path] [--port=4180] [--squad=<slug>] [--locale=fr]',
169
169
  help_squad_worker:
170
170
  'aioson squad:worker [path] [--sub=list|run|test|logs|scaffold] [--squad=<slug>] [--worker=<slug>] [--input=<json>] [--locale=fr]',
171
171
  help_squad_daemon:
@@ -179,7 +179,7 @@ module.exports = {
179
179
  help_commit_prepare:
180
180
  'aioson commit:prepare [path] [--staged-only] [--agent-safe] [--mode=guarded|trusted|headless] [--json] [--locale=fr]',
181
181
  help_learning:
182
- 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=fr]',
182
+ 'aioson learning [path] [--sub=list|stats|promote|import-from-claude] [--status=<status>] [--id=<learning-id>] [--project-hash=<hash>] [--dry-run] [--select=<n[,n]|all>] [--locale=fr]',
183
183
  dashboard_moved:
184
184
  'Le flux `{command}` a été supprimé du CLI. Le dashboard AIOSON est désormais installé séparément. Ouvrez l application dashboard sur votre ordinateur, créez ou ajoutez un projet, puis sélectionnez le dossier qui contient déjà `.aioson/`.',
185
185
  dashboard_moved_line: '{message}\n',
@@ -323,6 +323,8 @@ module.exports = {
323
323
  bootstrap_coverage_hint_seed: 'Executez /discover pour creer .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
324
324
  features_dir_present: 'Repertoire features present (.aioson/context/features/)',
325
325
  features_dir_present_hint: 'Creez .aioson/context/features/ pour heberger les dossiers par feature (doctor --fix le cree).',
326
+ auto_handoff_declared: 'Flag autopilot handoff declare (auto_handoff dans project.context.md)',
327
+ 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.',
326
328
  claude_commands_present: 'Slash commands de Claude presents ({missing} absents sur {required})',
327
329
  claude_commands_present_hint: 'Absents : {paths}. Executez `aioson doctor . --fix` pour les restaurer.',
328
330
  version_drift: 'Version du CLI conforme a project.context.md (contexte : {context}, CLI : {cli})',
@@ -612,7 +614,7 @@ module.exports = {
612
614
  note_product_optional:
613
615
  '@product est optionnel pour MICRO — passez directement a @dev si l idee est deja claire.',
614
616
  note_feature_flow:
615
- 'Flux nouvelle feature (apres configuration initiale) : @product → @analyst → @scope-check → @dev → @qa. Pas de @setup.'
617
+ 'Flux nouvelle feature (apres configuration initiale) : @product → @analyst → @scope-check → @dev → @qa. Pas de @setup.'
616
618
  },
617
619
  parallel_init: {
618
620
  context_missing:
@@ -1028,7 +1030,7 @@ module.exports = {
1028
1030
  folder_required_example_prompt:
1029
1031
  ' Prompt pret : aioson agent:prompt analyst --tool=codex',
1030
1032
  folder_required_example_next:
1031
- ' Workflow apres scan complet : @analyst -> @scope-check -> @architect -> @dev',
1033
+ ' Workflow apres scan complet : @analyst -> @scope-check -> @architect -> @dev',
1032
1034
  folder_not_found: 'Le dossier "{folder}" est introuvable dans ce projet. Dossiers de premier niveau detectes : {available}',
1033
1035
  config_missing: '{file} introuvable. Pour utiliser le mode LLM, copiez aioson-models.json et renseignez vos cles API.',
1034
1036
  config_invalid: 'JSON invalide dans aioson-models.json : {error}',
@@ -424,6 +424,8 @@ module.exports = {
424
424
  bootstrap_coverage_hint_seed: 'Rode /discover para criar .aioson/context/bootstrap/{what-is,how-it-works,what-it-does,current-state}.md',
425
425
  features_dir_present: 'Diretorio de features presente (.aioson/context/features/)',
426
426
  features_dir_present_hint: 'Crie .aioson/context/features/ para hospedar dossies por feature (doctor --fix cria automaticamente).',
427
+ auto_handoff_declared: 'Flag de autopilot handoff declarada (auto_handoff no project.context.md)',
428
+ 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.',
427
429
  claude_commands_present: 'Slash commands do Claude presentes ({missing} ausentes de {required})',
428
430
  claude_commands_present_hint: 'Ausentes: {paths}. Rode `aioson doctor . --fix` para restaurar a partir do template.',
429
431
  version_drift: 'Versao do CLI bate com project.context.md (contexto: {context}, CLI: {cli})',
@@ -49,34 +49,34 @@ function readClassificationFromFrontmatters(rawList) {
49
49
  return null;
50
50
  }
51
51
 
52
- function extractDevStateFields(raw) {
53
- if (!raw) return { active_feature: null, active_phase: null, next_step: null, status: null };
54
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
55
- if (!fmMatch) return { active_feature: null, active_phase: null, next_step: null, status: null };
56
- const fm = {};
57
- for (const line of fmMatch[1].split(/\r?\n/)) {
52
+ function extractDevStateFields(raw) {
53
+ if (!raw) return { active_feature: null, active_phase: null, next_step: null, status: null };
54
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
55
+ if (!fmMatch) return { active_feature: null, active_phase: null, next_step: null, status: null };
56
+ const fm = {};
57
+ for (const line of fmMatch[1].split(/\r?\n/)) {
58
58
  const idx = line.indexOf(':');
59
59
  if (idx === -1) continue;
60
60
  const key = line.slice(0, idx).trim();
61
61
  const val = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
62
62
  fm[key] = val;
63
- }
64
- return {
65
- active_feature: fm.active_feature || null,
66
- active_phase: fm.active_phase || null,
67
- next_step: fm.next_step || null,
68
- status: fm.status || null
69
- };
70
- }
71
-
72
- function isCurrentDevStateForFeature(fields, featureSlug) {
73
- if (!fields) return false;
74
- if (!fields.active_feature) return false;
75
- const status = String(fields.status || '').toLowerCase();
76
- if (status === 'done' || status === 'abandoned') return false;
77
- if (fields.active_feature !== featureSlug) return false;
78
- return true;
79
- }
63
+ }
64
+ return {
65
+ active_feature: fm.active_feature || null,
66
+ active_phase: fm.active_phase || null,
67
+ next_step: fm.next_step || null,
68
+ status: fm.status || null
69
+ };
70
+ }
71
+
72
+ function isCurrentDevStateForFeature(fields, featureSlug) {
73
+ if (!fields) return false;
74
+ if (!fields.active_feature) return false;
75
+ const status = String(fields.status || '').toLowerCase();
76
+ if (status === 'done' || status === 'abandoned') return false;
77
+ if (fields.active_feature !== featureSlug) return false;
78
+ return true;
79
+ }
80
80
 
81
81
  function extractCodeMapPaths(dossierRaw) {
82
82
  if (!dossierRaw) return [];
@@ -91,6 +91,42 @@ function extractCodeMapPaths(dossierRaw) {
91
91
  return Array.from(new Set(paths));
92
92
  }
93
93
 
94
+ function readCorrectionsStatus(raw) {
95
+ const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
96
+ // No frontmatter or no status field: treat as open — a corrections plan
97
+ // must never be silently skipped because of a malformed header.
98
+ if (!fmMatch) return 'open';
99
+ for (const line of fmMatch[1].split(/\r?\n/)) {
100
+ const m = line.match(/^status:\s*([^#]*)/);
101
+ if (m) {
102
+ const val = m[1].trim().replace(/^["']|["']$/g, '').toLowerCase();
103
+ return val || 'open';
104
+ }
105
+ }
106
+ return 'open';
107
+ }
108
+
109
+ async function listOpenCorrections(targetDir, featureSlug) {
110
+ const planDir = path.join(targetDir, '.aioson', 'plans', featureSlug);
111
+ let entries;
112
+ try {
113
+ entries = await fs.readdir(planDir);
114
+ } catch (err) {
115
+ if (err && err.code === 'ENOENT') return [];
116
+ throw err;
117
+ }
118
+ const out = [];
119
+ for (const name of entries.filter((n) => /^corrections-.+\.md$/.test(n)).sort()) {
120
+ const raw = await readFileOrNull(path.join(planDir, name));
121
+ if (!raw) continue;
122
+ const status = readCorrectionsStatus(raw);
123
+ if (status === 'open' || status === 'in_progress') {
124
+ out.push(`.aioson/plans/${featureSlug}/${name}`);
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+
94
130
  function deriveNextStepFromPlan(planRaw) {
95
131
  if (!planRaw) return null;
96
132
  const lines = planRaw.split('\n');
@@ -121,12 +157,12 @@ async function buildDevResumeData(projectPath) {
121
157
  const specRaw = await readFileOrNull(path.join(ctxDir, `spec-${featureSlug}.md`));
122
158
  const classification = readClassificationFromFrontmatters([dossierRaw, prdRaw, specRaw]);
123
159
 
124
- const devStateRaw = await readFileOrNull(path.join(ctxDir, 'dev-state.md'));
125
- const devStateFields = extractDevStateFields(devStateRaw);
126
- const useDevState = isCurrentDevStateForFeature(devStateFields, featureSlug);
127
-
128
- const planManifestPath = path.join(targetDir, '.aioson', 'plans', featureSlug, 'manifest.md');
129
- const planRaw = await readFileOrNull(planManifestPath);
160
+ const devStateRaw = await readFileOrNull(path.join(ctxDir, 'dev-state.md'));
161
+ const devStateFields = extractDevStateFields(devStateRaw);
162
+ const useDevState = isCurrentDevStateForFeature(devStateFields, featureSlug);
163
+
164
+ const planManifestPath = path.join(targetDir, '.aioson', 'plans', featureSlug, 'manifest.md');
165
+ const planRaw = await readFileOrNull(planManifestPath);
130
166
  const sheldonPlan = planRaw ? `.aioson/plans/${featureSlug}/manifest.md` : null;
131
167
 
132
168
  const artifactsConsumed = Array.isArray(lastHandoff && lastHandoff.artifact_uris)
@@ -137,22 +173,35 @@ async function buildDevResumeData(projectPath) {
137
173
  ? lastHandoff.decision_rationale
138
174
  : [];
139
175
 
176
+ // QA corrections plans with status open/in_progress take precedence over any
177
+ // persisted next_step: a stale dev-state pointer must not hide mandatory
178
+ // corrections from a fresh @dev session (loop-guardrails incident, 2026-06-09).
179
+ const openCorrections = await listOpenCorrections(targetDir, featureSlug);
180
+ const baseNextStep = useDevState && devStateFields.next_step
181
+ ? devStateFields.next_step
182
+ : deriveNextStepFromPlan(planRaw);
183
+
140
184
  return {
141
- feature_slug: featureSlug,
142
- classification,
143
- current_phase: useDevState && devStateFields.active_phase ? devStateFields.active_phase : 'unknown',
144
- artifacts_consumed: artifactsConsumed,
145
- code_map_paths: extractCodeMapPaths(dossierRaw),
146
- sheldon_plan: sheldonPlan,
147
- next_step: useDevState && devStateFields.next_step ? devStateFields.next_step : deriveNextStepFromPlan(planRaw),
148
- decision_rationale: decisionRationale.length > 0 ? decisionRationale : undefined
149
- };
150
- }
185
+ feature_slug: featureSlug,
186
+ classification,
187
+ current_phase: useDevState && devStateFields.active_phase ? devStateFields.active_phase : 'unknown',
188
+ artifacts_consumed: artifactsConsumed,
189
+ code_map_paths: extractCodeMapPaths(dossierRaw),
190
+ sheldon_plan: sheldonPlan,
191
+ next_step: openCorrections.length > 0
192
+ ? `Apply mandatory corrections from ${openCorrections[0]}, then return to @qa for re-verification`
193
+ : baseNextStep,
194
+ open_corrections: openCorrections.length > 0 ? openCorrections : undefined,
195
+ decision_rationale: decisionRationale.length > 0 ? decisionRationale : undefined
196
+ };
197
+ }
151
198
 
152
199
  module.exports = {
153
- buildDevResumeData,
154
- extractDevStateFields,
155
- isCurrentDevStateForFeature,
156
- extractCodeMapPaths,
157
- deriveNextStepFromPlan
158
- };
200
+ buildDevResumeData,
201
+ extractDevStateFields,
202
+ isCurrentDevStateForFeature,
203
+ extractCodeMapPaths,
204
+ deriveNextStepFromPlan,
205
+ readCorrectionsStatus,
206
+ listOpenCorrections
207
+ };
@@ -225,9 +225,9 @@ async function scanActiveManifest(targetDir, slug) {
225
225
  async function scanArtifacts(targetDir, slug) {
226
226
  const dir = contextDir(targetDir);
227
227
 
228
- async function check(name, filePath) {
229
- const stat = await fileStat(filePath);
230
- if (!stat) return { exists: false };
228
+ async function check(name, filePath) {
229
+ const stat = await fileStat(filePath);
230
+ if (!stat) return { exists: false };
231
231
 
232
232
  const content = await readFileSafe(filePath);
233
233
  const fm = content ? parseFrontmatter(content) : {};
@@ -237,37 +237,37 @@ async function scanArtifacts(targetDir, slug) {
237
237
  path: path.relative(targetDir, filePath),
238
238
  size: stat.size,
239
239
  frontmatter: fm,
240
- content
241
- };
242
- }
243
-
244
- async function checkFirst(name, filePaths) {
245
- for (const filePath of filePaths) {
246
- const result = await check(name, filePath);
247
- if (result.exists) return result;
248
- }
249
- return { exists: false };
250
- }
251
-
252
- const designDocCandidates = slug
253
- ? [path.join(dir, `design-doc-${slug}.md`), path.join(dir, 'design-doc.md')]
254
- : [path.join(dir, 'design-doc.md')];
255
- const readinessCandidates = slug
256
- ? [path.join(dir, `readiness-${slug}.md`), path.join(dir, 'readiness.md')]
257
- : [path.join(dir, 'readiness.md')];
258
-
259
- const results = {
260
- project_context: await check('project.context', path.join(dir, 'project.context.md')),
261
- prd: slug ? await check('prd', path.join(dir, `prd-${slug}.md`)) : { exists: false },
262
- sheldon_enrichment: slug ? await check('sheldon', path.join(dir, `sheldon-enrichment-${slug}.md`)) : { exists: false },
263
- requirements: slug ? await check('requirements', path.join(dir, `requirements-${slug}.md`)) : { exists: false },
264
- spec: slug ? await check('spec', path.join(dir, `spec-${slug}.md`)) : await check('spec', path.join(dir, 'spec.md')),
265
- architecture: await check('architecture', path.join(dir, 'architecture.md')),
266
- design_doc: await checkFirst('design-doc', designDocCandidates),
267
- readiness: await checkFirst('readiness', readinessCandidates),
268
- implementation_plan: slug ? await check('impl-plan', path.join(dir, `implementation-plan-${slug}.md`)) : { exists: false },
269
- conformance: slug ? await check('conformance', path.join(dir, `conformance-${slug}.yaml`)) : { exists: false },
270
- dev_state: await check('dev-state', path.join(dir, 'dev-state.md')),
240
+ content
241
+ };
242
+ }
243
+
244
+ async function checkFirst(name, filePaths) {
245
+ for (const filePath of filePaths) {
246
+ const result = await check(name, filePath);
247
+ if (result.exists) return result;
248
+ }
249
+ return { exists: false };
250
+ }
251
+
252
+ const designDocCandidates = slug
253
+ ? [path.join(dir, `design-doc-${slug}.md`), path.join(dir, 'design-doc.md')]
254
+ : [path.join(dir, 'design-doc.md')];
255
+ const readinessCandidates = slug
256
+ ? [path.join(dir, `readiness-${slug}.md`), path.join(dir, 'readiness.md')]
257
+ : [path.join(dir, 'readiness.md')];
258
+
259
+ const results = {
260
+ project_context: await check('project.context', path.join(dir, 'project.context.md')),
261
+ prd: slug ? await check('prd', path.join(dir, `prd-${slug}.md`)) : { exists: false },
262
+ sheldon_enrichment: slug ? await check('sheldon', path.join(dir, `sheldon-enrichment-${slug}.md`)) : { exists: false },
263
+ requirements: slug ? await check('requirements', path.join(dir, `requirements-${slug}.md`)) : { exists: false },
264
+ spec: slug ? await check('spec', path.join(dir, `spec-${slug}.md`)) : await check('spec', path.join(dir, 'spec.md')),
265
+ architecture: await check('architecture', path.join(dir, 'architecture.md')),
266
+ design_doc: await checkFirst('design-doc', designDocCandidates),
267
+ readiness: await checkFirst('readiness', readinessCandidates),
268
+ implementation_plan: slug ? await check('impl-plan', path.join(dir, `implementation-plan-${slug}.md`)) : { exists: false },
269
+ conformance: slug ? await check('conformance', path.join(dir, `conformance-${slug}.yaml`)) : { exists: false },
270
+ dev_state: await check('dev-state', path.join(dir, 'dev-state.md')),
271
271
  features: await check('features', path.join(dir, 'features.md'))
272
272
  };
273
273
 
@@ -362,19 +362,19 @@ async function readProjectPulse(targetDir) {
362
362
  // ─── Classification reader ────────────────────────────────────────────────────
363
363
 
364
364
  async function detectClassification(targetDir, slug) {
365
- // 1. Try project context
366
- const ctx = await loadProjectContext(targetDir);
367
- if (ctx.data.classification) return ctx.data.classification.toUpperCase();
368
-
369
- // 2. Try spec frontmatter
365
+ // Feature classification takes precedence over the project classification —
366
+ // same contract as resolveClassification (handoff-contract.js) and
367
+ // workflow:next sequencing. A SMALL feature inside a MEDIUM project must be
368
+ // gated as SMALL; the project value is only the fallback.
370
369
  if (slug) {
370
+ // 1. Try spec frontmatter
371
371
  const specContent = await readFileSafe(path.join(contextDir(targetDir), `spec-${slug}.md`));
372
372
  if (specContent) {
373
373
  const fm = parseFrontmatter(specContent);
374
374
  if (fm.classification) return fm.classification.toUpperCase();
375
375
  }
376
376
 
377
- // 3. Try PRD frontmatter
377
+ // 2. Try PRD frontmatter
378
378
  const prdContent = await readFileSafe(path.join(contextDir(targetDir), `prd-${slug}.md`));
379
379
  if (prdContent) {
380
380
  const fm = parseFrontmatter(prdContent);
@@ -382,6 +382,10 @@ async function detectClassification(targetDir, slug) {
382
382
  }
383
383
  }
384
384
 
385
+ // 3. Fall back to project context
386
+ const ctx = await loadProjectContext(targetDir);
387
+ if (ctx.data.classification) return ctx.data.classification.toUpperCase();
388
+
385
389
  return null;
386
390
  }
387
391
 
@@ -464,15 +468,15 @@ async function discoverDesignDocs(targetDir, agent) {
464
468
 
465
469
  // ─── Context package builder ──────────────────────────────────────────────────
466
470
 
467
- function buildContextPackage(agent, slug, classification, artifacts, devState, manifest) {
468
- const pkg = [];
469
- const designDoc = artifacts.design_doc || { exists: false };
470
- const readiness = artifacts.readiness || { exists: false };
471
-
472
- if (artifacts.project_context.exists) pkg.push(artifacts.project_context.path);
471
+ function buildContextPackage(agent, slug, classification, artifacts, devState, manifest) {
472
+ const pkg = [];
473
+ const designDoc = artifacts.design_doc || { exists: false };
474
+ const readiness = artifacts.readiness || { exists: false };
475
+
476
+ if (artifacts.project_context.exists) pkg.push(artifacts.project_context.path);
473
477
 
474
478
  if (slug) {
475
- const downstreamAgents = ['discovery-design-doc', 'pm', 'orchestrator', 'dev', 'deyvin', 'qa'];
479
+ const downstreamAgents = ['discovery-design-doc', 'pm', 'orchestrator', 'dev', 'deyvin', 'qa'];
476
480
  const shouldCarryFullFeatureContext = downstreamAgents.includes(agent);
477
481
 
478
482
  if (shouldCarryFullFeatureContext && artifacts.prd.exists) pkg.push(artifacts.prd.path);
@@ -483,10 +487,10 @@ function buildContextPackage(agent, slug, classification, artifacts, devState, m
483
487
 
484
488
  if (artifacts.spec.exists) pkg.push(artifacts.spec.path);
485
489
 
486
- if (shouldCarryFullFeatureContext && artifacts.architecture.exists) pkg.push(artifacts.architecture.path);
487
- if (shouldCarryFullFeatureContext && designDoc.exists) pkg.push(designDoc.path);
488
- if (shouldCarryFullFeatureContext && readiness.exists) pkg.push(readiness.path);
489
- if (shouldCarryFullFeatureContext && artifacts.conformance.exists) pkg.push(artifacts.conformance.path);
490
+ if (shouldCarryFullFeatureContext && artifacts.architecture.exists) pkg.push(artifacts.architecture.path);
491
+ if (shouldCarryFullFeatureContext && designDoc.exists) pkg.push(designDoc.path);
492
+ if (shouldCarryFullFeatureContext && readiness.exists) pkg.push(readiness.path);
493
+ if (shouldCarryFullFeatureContext && artifacts.conformance.exists) pkg.push(artifacts.conformance.path);
490
494
 
491
495
  // Manifest precedence (AC-SDLC-24, AC-SDLC-25):
492
496
  // If active Sheldon manifest exists and is not done, it is the primary execution artifact.
@@ -608,11 +612,11 @@ function parseFeaturesMap(content) {
608
612
 
609
613
  // ─── Readiness evaluator ─────────────────────────────────────────────────────
610
614
 
611
- function evaluateReadiness(artifacts, phaseGates, classification, agent, devState, slug) {
612
- const blockers = [];
613
- const warnings = [];
614
- const designDoc = artifacts.design_doc || { exists: false };
615
- const readiness = artifacts.readiness || { exists: false };
615
+ function evaluateReadiness(artifacts, phaseGates, classification, agent, devState, slug) {
616
+ const blockers = [];
617
+ const warnings = [];
618
+ const designDoc = artifacts.design_doc || { exists: false };
619
+ const readiness = artifacts.readiness || { exists: false };
616
620
 
617
621
  if (!artifacts.project_context.exists) blockers.push('project.context.md missing');
618
622
 
@@ -625,11 +629,11 @@ function evaluateReadiness(artifacts, phaseGates, classification, agent, devStat
625
629
  }
626
630
  }
627
631
 
628
- if (agent === 'analyst') {
629
- if (slug && !artifacts.prd.exists) {
630
- warnings.push('prd file missing — feature is not framed yet; @analyst may run project discovery/research only, or hand off to @product/@briefing to create prd-{slug}.md before formal feature requirements');
631
- }
632
- }
632
+ if (agent === 'analyst') {
633
+ if (slug && !artifacts.prd.exists) {
634
+ warnings.push('prd file missing — feature is not framed yet; @analyst may run project discovery/research only, or hand off to @product/@briefing to create prd-{slug}.md before formal feature requirements');
635
+ }
636
+ }
633
637
 
634
638
  if (agent === 'architect') {
635
639
  if (!artifacts.requirements.exists) {
@@ -637,25 +641,25 @@ function evaluateReadiness(artifacts, phaseGates, classification, agent, devStat
637
641
  }
638
642
  }
639
643
 
640
- if (agent === 'pm') {
644
+ if (agent === 'pm') {
641
645
  if (!artifacts.architecture.exists) {
642
646
  blockers.push('architecture.md missing — @architect must complete design first (Gate B)');
643
647
  }
644
648
  if (!artifacts.requirements.exists) {
645
649
  warnings.push('requirements file missing — @pm should review it before writing the implementation plan');
646
650
  }
647
- }
648
-
649
- if (agent === 'discovery-design-doc') {
650
- if (!artifacts.architecture.exists) {
651
- blockers.push('architecture.md missing — @architect must complete design first (Gate B)');
652
- }
653
- if (!designDoc.exists) {
654
- warnings.push('design-doc.md missing — @discovery-design-doc must create the project baseline before implementation');
655
- }
656
- }
657
-
658
- if (agent === 'orchestrator') {
651
+ }
652
+
653
+ if (agent === 'discovery-design-doc') {
654
+ if (!artifacts.architecture.exists) {
655
+ blockers.push('architecture.md missing — @architect must complete design first (Gate B)');
656
+ }
657
+ if (!designDoc.exists) {
658
+ warnings.push('design-doc.md missing — @discovery-design-doc must create the project baseline before implementation');
659
+ }
660
+ }
661
+
662
+ if (agent === 'orchestrator') {
659
663
  if (!artifacts.requirements.exists) {
660
664
  blockers.push('requirements file missing — Gate A not satisfied');
661
665
  }
@@ -673,16 +677,16 @@ function evaluateReadiness(artifacts, phaseGates, classification, agent, devStat
673
677
  }
674
678
  }
675
679
 
676
- if (agent === 'dev' || agent === 'deyvin') {
677
- if (!artifacts.spec.exists) blockers.push('spec file missing');
678
- if (classification && classification !== 'MICRO') {
679
- if (!designDoc.exists) {
680
- blockers.push('design-doc.md missing — @discovery-design-doc must run before implementation');
681
- }
682
- if (!readiness.exists) {
683
- blockers.push('readiness.md missing — @discovery-design-doc must run before implementation');
684
- }
685
- }
680
+ if (agent === 'dev' || agent === 'deyvin') {
681
+ if (!artifacts.spec.exists) blockers.push('spec file missing');
682
+ if (classification && classification !== 'MICRO') {
683
+ if (!designDoc.exists) {
684
+ blockers.push('design-doc.md missing — @discovery-design-doc must run before implementation');
685
+ }
686
+ if (!readiness.exists) {
687
+ blockers.push('readiness.md missing — @discovery-design-doc must run before implementation');
688
+ }
689
+ }
686
690
  if (classification === 'MEDIUM') {
687
691
  const implementationPlan = artifacts.implementation_plan || { exists: false, frontmatter: {} };
688
692
  if (!implementationPlan.exists) {
@@ -366,6 +366,10 @@ Action: /scope-check or /architect
366
366
  ```
367
367
  > Recommended: `/clear` before activating — fresh context window.
368
368
 
369
+ ## Autopilot handoff
370
+
371
+ If `auto_handoff: true` in `project.context.md` frontmatter and a feature workflow is active, follow `.aioson/docs/autopilot-handoff.md`: after Gate A is approved and all closing duties above are done, do not stop for manual activation. Determine the next agent from the workflow state (never guess) and auto-invoke `Skill(aioson:agent:<next>)` with `"continue feature {slug} — autopilot handoff from @analyst"`. No user prompt — Ctrl+C interrupts. Emit the manual handoff instead when any stop condition applies: next agent is `@dev`, Gate A not approved, context ≥ `context_warning_threshold`, or routing is ambiguous.
372
+
369
373
  ## Strategic commands (use during session)
370
374
 
371
375
  - Search memory before web research: `aioson memory:search . --query="<topic>" 2>/dev/null || true`
@@ -140,6 +140,10 @@ Action: /pm or /dev
140
140
  ```
141
141
  > Recommended: `/clear` before activating — fresh context window.
142
142
 
143
+ ## Autopilot handoff
144
+
145
+ If `auto_handoff: true` in `project.context.md` frontmatter, a feature workflow is active, and Gate B passed, follow `.aioson/docs/autopilot-handoff.md`: auto-invoke `Skill(aioson:agent:<next>)` for the next workflow stage with `"continue feature {slug} — autopilot handoff from @architect"`. No user prompt — Ctrl+C interrupts. Emit the manual handoff instead when Gate B is blocked, the next agent is `@dev`, or context ≥ `context_warning_threshold`.
146
+
143
147
  ## Rules
144
148
  - Do not redesign entities produced by `@analyst`. Consume the data design as-is.
145
149
  - Keep architecture proportional to classification. Never apply MEDIUM patterns to a MICRO project.
@@ -152,7 +152,7 @@ If flagged, recommend a new chat and offer a handoff with slug, completed phase,
152
152
 
153
153
  Check `.aioson/context/features/{slug}/dossier.md` before per-slug PRD/spec. If present, read it FIRST — it consolidates Why/What + code map and is the canonical entry point for chained context. If absent, continue with standard input (legacy flow).
154
154
 
155
- **Auto-resume (session start):** `aioson dev:resume-data .` returns `{feature_slug, classification, current_phase, artifacts_consumed, code_map_paths, sheldon_plan, next_step}` or `null` (cold start). Skip discovery, start on `next_step`, then emit `aioson runtime:emit . --agent=dev --type=dev_auto_resume --summary="<feature>: phase <N> auto-resumed" 2>/dev/null || true`.
155
+ **Auto-resume (session start):** `aioson dev:resume-data .` returns `{feature_slug, classification, current_phase, artifacts_consumed, code_map_paths, sheldon_plan, next_step, open_corrections?}` or `null` (cold start). When `open_corrections` is non-empty, those QA corrections plans are the top priority regardless of any other pointer. Skip discovery, start on `next_step`, then emit `aioson runtime:emit . --agent=dev --type=dev_auto_resume --summary="<feature>: phase <N> auto-resumed" 2>/dev/null || true`.
156
156
 
157
157
  **Drift detection (prompt-driven):** before modifying/creating a file, check if its path is in `code_map_paths`. If registered AND your change diverges from the upstream plan, or a Sheldon plan step already ran without an Agent Trail entry → DRIFT. On DRIFT: emit `aioson runtime:emit . --agent=dev --type=dev_drift_detected --summary="Drift detected: {what}" 2>/dev/null || true`, give the user 3 options (proceed/revise/abort), record `dossier:add-finding --section="Agent Trail" --content="DRIFT: {what}. Decision. Reason."`.
158
158
 
@@ -285,6 +285,8 @@ Run `aioson` CLI yourself to keep the workflow moving:
285
285
 
286
286
  If `.aioson/runtime/qa-dev-cycle.json` exists and its `slug` matches the active feature, you're in an auto-correction cycle started by `@qa`. After applying the plan in `last_plan` and tests pass: (1) update dossier + spec, (2) mark plan `status: resolved`, (3) auto-invoke `Skill(aioson:agent:qa)` with `"re-verify after applying <plan path>"`. No user prompt — Ctrl+C interrupts. If the file is absent or slug differs, manual handoff as before.
287
287
 
288
+ **Safety net — open corrections without the cycle file:** on every activation with an active feature, also check `.aioson/plans/{active-feature}/corrections-*.md`. If any has frontmatter `status: open` or `in_progress`, those mandatory corrections take priority over the dev-state `next_step` — apply them first, mark the plan `resolved`, then hand off to `@qa` for re-verification. `aioson dev:resume-data` surfaces them as `open_corrections` and already rewrites `next_step` accordingly; trust that over a stale dev-state pointer. This covers QA sessions that created a corrections plan but failed to persist the trail.
289
+
288
290
  ## Optional scope drift checkpoint
289
291
 
290
292
  After a feature slice lands, recommend optional `@scope-check --scope-mode=post-dev` before `@qa` when the implementation changed planned behavior, touched unexpected files, skipped a planned item, or required a trade-off not already captured in the design artifacts. Skip the recommendation for routine implementation that matches the approved plan.
@@ -66,5 +66,9 @@ aioson dossier:add-finding . --slug={slug} --agent=discovery-design-doc --sectio
66
66
 
67
67
  Skip silently when the dossier is absent — projects without dossier still get the appropriate design-doc/readiness pair as the primary handoff.
68
68
 
69
+ ## Autopilot handoff
70
+
71
+ If `auto_handoff: true` in `project.context.md` frontmatter, a feature workflow is active, and readiness is `ready` or `ready_with_warnings`, follow `.aioson/docs/autopilot-handoff.md`: auto-invoke `Skill(aioson:agent:<next>)` for the next workflow stage with `"continue feature {slug} — autopilot handoff from @discovery-design-doc"`. No user prompt — Ctrl+C interrupts. Emit the manual handoff instead when readiness is `blocked`, the next agent is `@dev` (standard handoff + recommend `/clear`), or context ≥ `context_warning_threshold`.
72
+
69
73
  ## Observability
70
74
  At session end, register: `aioson agent:done . --agent=discovery-design-doc --summary="Design doc <slug>: readiness=<level>, next=<agent>" 2>/dev/null || true`