@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
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const { AGENTS, validateManifest } = require('./schema');
7
+ const { capabilities } = require('./capabilities');
8
+ const { loadModelCatalog } = require('./model-catalog');
9
+ const { resolveModel, validateReasoningEffort } = require('./model-resolver');
10
+
11
+ function manifestPath(projectDir, feature) { return path.join(projectDir, '.aioson', 'context', `agent-execution-${feature}.json`); }
12
+ function defaults(feature, host = 'codex') {
13
+ const agents = {};
14
+ for (const id of AGENTS) agents[id] = { enabled: true, host, mode: 'external', model: 'configured-default', writable_roots: [], fallbacks: [], report: `.aioson/context/reports/${feature}/{run_id}/${id}.json` };
15
+ return { version: 1, feature, host, generated_at: new Date().toISOString(), agents, capacity_policy: { strategy: 'pause', max_attempts: 1, backoff_ms: 0 }, cycle_limits: { dev_qa: 3, tester: 3, pentester: 3 }, reporting: { format: 'json', markdown: true } };
16
+ }
17
+ function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])])); return value; }
18
+ function digest(value) { return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex'); }
19
+ function mergeAdditive(existing, base) {
20
+ if (Array.isArray(existing)) return existing;
21
+ if (!existing || typeof existing !== 'object') return existing === undefined ? base : existing;
22
+ const out = { ...base };
23
+ for (const [key, value] of Object.entries(existing)) out[key] = value && typeof value === 'object' && !Array.isArray(value) ? mergeAdditive(value, base && base[key]) : value;
24
+ return out;
25
+ }
26
+ async function initManifest(projectDir, feature, host, { overwrite = false } = {}) {
27
+ const file = manifestPath(projectDir, feature); const base = defaults(feature, host);
28
+ await fs.mkdir(path.dirname(file), { recursive: true });
29
+ let value = base;
30
+ try { const old = JSON.parse(await fs.readFile(file, 'utf8')); if (!overwrite) value = mergeAdditive(old, base); } catch (error) { if (error.code !== 'ENOENT') throw error; }
31
+ await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
32
+ return { path: file, manifest: value, digest: digest(value) };
33
+ }
34
+ async function loadManifest(projectDir, feature) {
35
+ const file = manifestPath(projectDir, feature);
36
+ try { const value = JSON.parse(await fs.readFile(file, 'utf8')); const validation = validateManifest(value, feature); return { exists: true, path: file, manifest: value, digest: digest(value), ...validation }; }
37
+ catch (error) { if (error.code === 'ENOENT') return { exists: false, legacy: true, path: file, ok: true }; return { exists: true, path: file, ok: false, errors: [{ path: '$', message: error.message }] }; }
38
+ }
39
+ function resolveAgent(manifest, agent, overrides = {}) { const entry = manifest.agents[agent]; return { ...entry, host: overrides.host || entry.host || manifest.host, model: overrides.model || entry.model, source: 'manifest' }; }
40
+ async function resolveExecutionEntry(entry, { catalogLoader = loadModelCatalog } = {}) {
41
+ const cap = capabilities(entry.host);
42
+ if (entry.reasoning_effort && !cap.reasoning_effort) return { ok: false, reason: 'unsupported_reasoning_effort', host: entry.host, model_requested: entry.model, candidates: [] };
43
+ const catalog = cap.model_catalog ? await catalogLoader(entry.host) : { available: false, reason: 'unsupported_model_catalog', models: [] };
44
+ const model = resolveModel(entry.model, catalog);
45
+ if (!model.ok) return { ...entry, ...model, model_requested: entry.model };
46
+ const effort = validateReasoningEffort(model, entry.reasoning_effort);
47
+ if (!effort.ok) return { ...entry, ok: false, reason: effort.reason, supported: effort.supported, candidates: [], model_requested: entry.model, model_resolved: model.resolved };
48
+ return {
49
+ ...entry,
50
+ ok: true,
51
+ model_requested: entry.model,
52
+ model: model.resolved,
53
+ model_resolved: model.resolved,
54
+ model_resolution_strategy: model.strategy,
55
+ catalog_source: model.catalog_source,
56
+ catalog_fetched_at: model.catalog_fetched_at,
57
+ reasoning_effort: effort.reasoning_effort,
58
+ reasoning_effort_verification: effort.verification
59
+ };
60
+ }
61
+ async function resolveAgentExecution(manifest, agent, overrides = {}, options = {}) { return resolveExecutionEntry(resolveAgent(manifest, agent, overrides), options); }
62
+ module.exports = { defaults, digest, initManifest, loadManifest, manifestPath, mergeAdditive, resolveAgent, resolveAgentExecution, resolveExecutionEntry };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { REASONING_EFFORTS } = require('./schema');
7
+
8
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
9
+ const DEFAULT_MAX_MODELS = 1000;
10
+ const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/;
11
+
12
+ function unavailable(reason) {
13
+ return { available: false, reason, source: null, fetched_at: null, client_version: null, models: [] };
14
+ }
15
+
16
+ function codexHome(env, home) {
17
+ const configured = typeof env.CODEX_HOME === 'string' ? env.CODEX_HOME.trim() : '';
18
+ return configured || path.join(home, '.codex');
19
+ }
20
+
21
+ function normalizeEfforts(levels) {
22
+ if (!Array.isArray(levels)) return [];
23
+ const supported = new Set(REASONING_EFFORTS);
24
+ return [...new Set(levels.map(level => typeof level === 'string' ? level : level?.effort)
25
+ .filter(effort => typeof effort === 'string' && supported.has(effort)))];
26
+ }
27
+
28
+ function sanitizeModels(models) {
29
+ if (!Array.isArray(models)) return [];
30
+ const bySlug = new Map();
31
+ for (const model of models) {
32
+ const slug = typeof model?.slug === 'string' ? model.slug.trim() : '';
33
+ if (!SAFE_MODEL_ID.test(slug) || bySlug.has(slug)) continue;
34
+ const display = typeof model.display_name === 'string' && model.display_name.trim()
35
+ ? model.display_name.trim().slice(0, 200)
36
+ : slug;
37
+ bySlug.set(slug, {
38
+ slug,
39
+ display_name: display,
40
+ supported_efforts: normalizeEfforts(model.supported_reasoning_levels)
41
+ });
42
+ }
43
+ return [...bySlug.values()];
44
+ }
45
+
46
+ async function loadModelCatalog(host, options = {}) {
47
+ if (host !== 'codex') return unavailable('unsupported_model_catalog');
48
+ const env = options.env || process.env;
49
+ const home = options.home || os.homedir();
50
+ const file = path.join(codexHome(env, home), 'models_cache.json');
51
+ const maxBytes = Math.max(1, Number(options.maxBytes) || DEFAULT_MAX_BYTES);
52
+ const maxModels = Math.max(1, Number(options.maxModels) || DEFAULT_MAX_MODELS);
53
+ let stat;
54
+ try {
55
+ stat = await fs.stat(file);
56
+ } catch {
57
+ return unavailable('catalog_unavailable');
58
+ }
59
+ if (!stat.isFile()) return unavailable('catalog_unavailable');
60
+ if (stat.size > maxBytes) return unavailable('catalog_too_large');
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(await fs.readFile(file, 'utf8'));
64
+ } catch {
65
+ return unavailable('catalog_invalid');
66
+ }
67
+ if (Array.isArray(parsed?.models) && parsed.models.length > maxModels) return unavailable('catalog_too_many_models');
68
+ const models = sanitizeModels(parsed?.models);
69
+ if (!models.length) return unavailable('catalog_incompatible');
70
+ return {
71
+ available: true,
72
+ reason: null,
73
+ source: 'codex_local_cache',
74
+ fetched_at: typeof parsed.fetched_at === 'string' ? parsed.fetched_at.slice(0, 100) : null,
75
+ client_version: typeof parsed.client_version === 'string' ? parsed.client_version.slice(0, 50) : null,
76
+ models
77
+ };
78
+ }
79
+
80
+ module.exports = { DEFAULT_MAX_BYTES, DEFAULT_MAX_MODELS, loadModelCatalog, sanitizeModels };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const { MAX_MODEL_NAME_LENGTH, REASONING_EFFORTS } = require('./schema');
4
+
5
+ const LITERAL_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/;
6
+ const GENERIC_ALIASES = new Set(['gpt', 'model', 'openai', 'codex']);
7
+
8
+ function tokens(value) {
9
+ return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
10
+ .toLowerCase().match(/[a-z]+|\d+/g) || [];
11
+ }
12
+
13
+ function normalizeModelName(value) { return tokens(value).join('-'); }
14
+ function numericTokens(value) { return tokens(value).filter(token => /^\d+$/.test(token)); }
15
+ function alphaTokens(value) { return tokens(value).filter(token => /^[a-z]+$/.test(token)); }
16
+
17
+ function distance(left, right) {
18
+ const a = String(left); const b = String(right);
19
+ if (a.length > MAX_MODEL_NAME_LENGTH || b.length > MAX_MODEL_NAME_LENGTH) return Number.POSITIVE_INFINITY;
20
+ const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
21
+ for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
22
+ for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
23
+ for (let i = 1; i <= a.length; i++) {
24
+ for (let j = 1; j <= b.length; j++) {
25
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
26
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
27
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
28
+ matrix[i][j] = Math.min(matrix[i][j], matrix[i - 2][j - 2] + cost);
29
+ }
30
+ }
31
+ }
32
+ return matrix[a.length][b.length];
33
+ }
34
+
35
+ function candidates(models) {
36
+ return (Array.isArray(models) ? models : []).filter(model => model && typeof model.slug === 'string')
37
+ .map(model => ({
38
+ slug: model.slug,
39
+ display_name: typeof model.display_name === 'string' ? model.display_name : model.slug,
40
+ supported_efforts: Array.isArray(model.supported_efforts) ? model.supported_efforts : []
41
+ }));
42
+ }
43
+
44
+ function uniqueBySlug(models) { return [...new Map(models.map(model => [model.slug, model])).values()]; }
45
+ function diagnosticSlugs(models) { return uniqueBySlug(models).map(model => model.slug).sort().slice(0, 5); }
46
+
47
+ function success(requested, model, strategy, catalog) {
48
+ return {
49
+ ok: true,
50
+ requested,
51
+ resolved: model.slug,
52
+ strategy,
53
+ catalog_source: catalog?.source || null,
54
+ catalog_fetched_at: catalog?.fetched_at || null,
55
+ supported_efforts: model.supported_efforts || []
56
+ };
57
+ }
58
+
59
+ function failure(requested, reason, models = [], catalog = null) {
60
+ return {
61
+ ok: false,
62
+ requested,
63
+ resolved: null,
64
+ reason,
65
+ candidates: diagnosticSlugs(models),
66
+ catalog_source: catalog?.source || null,
67
+ catalog_fetched_at: catalog?.fetched_at || null
68
+ };
69
+ }
70
+
71
+ function resolveModel(value, catalog) {
72
+ const requested = typeof value === 'string' ? value.trim() : '';
73
+ if (!requested) return failure(requested, 'invalid_model');
74
+ if (requested.length > MAX_MODEL_NAME_LENGTH) return failure(requested.slice(0, MAX_MODEL_NAME_LENGTH), 'invalid_model');
75
+ if (requested === 'configured-default') {
76
+ return success(requested, { slug: requested, supported_efforts: [] }, 'configured_default', catalog);
77
+ }
78
+ if (!catalog?.available) {
79
+ if (!LITERAL_MODEL_ID.test(requested)) return failure(requested, catalog?.reason || 'catalog_unavailable');
80
+ return success(requested, { slug: requested, supported_efforts: [] }, 'unverified_literal', null);
81
+ }
82
+ const models = candidates(catalog.models);
83
+ const exact = models.find(model => model.slug === requested);
84
+ if (exact) return success(requested, exact, 'exact_slug', catalog);
85
+
86
+ const requestedKey = normalizeModelName(requested);
87
+ const normalized = uniqueBySlug(models.filter(model => normalizeModelName(model.slug) === requestedKey
88
+ || normalizeModelName(model.display_name) === requestedKey));
89
+ if (normalized.length === 1) return success(requested, normalized[0], 'normalized_name', catalog);
90
+ if (normalized.length > 1) return failure(requested, 'ambiguous_model', normalized, catalog);
91
+
92
+ const requestedTokens = tokens(requested);
93
+ const isGeneric = requestedTokens.length === 1 && GENERIC_ALIASES.has(requestedTokens[0]);
94
+ const isUsableAlias = requestedKey.length >= 4 && !isGeneric && requestedTokens.some(token => /^[a-z]+$/.test(token));
95
+ if (isUsableAlias) {
96
+ const alias = uniqueBySlug(models.filter(model => {
97
+ const slugTokens = tokens(model.slug); const displayTokens = tokens(model.display_name);
98
+ const suffix = list => list.length >= requestedTokens.length
99
+ && requestedTokens.every((token, index) => token === list[list.length - requestedTokens.length + index]);
100
+ return suffix(slugTokens) || suffix(displayTokens);
101
+ }));
102
+ if (alias.length === 1) return success(requested, alias[0], 'unique_alias', catalog);
103
+ if (alias.length > 1) return failure(requested, 'ambiguous_model', alias, catalog);
104
+ }
105
+
106
+ const requestedNumbers = numericTokens(requested);
107
+ const requestedAlpha = alphaTokens(requested);
108
+ const eligible = models.filter(model => {
109
+ const modelNumbers = numericTokens(model.slug);
110
+ if (requestedNumbers.length && requestedNumbers.join('.') !== modelNumbers.join('.')) return false;
111
+ const modelAlpha = alphaTokens(model.slug);
112
+ return !requestedAlpha.length || requestedAlpha[0] === modelAlpha[0];
113
+ });
114
+ const ranked = eligible.map(model => ({ model, score: distance(requestedKey, normalizeModelName(model.slug)) }))
115
+ .sort((a, b) => a.score - b.score || a.model.slug.localeCompare(b.model.slug));
116
+ const maxDistance = Math.min(3, Math.max(1, Math.floor(requestedKey.length * 0.1)));
117
+ if (!ranked.length || ranked[0].score > maxDistance) return failure(requested, 'model_not_found', [], catalog);
118
+ const best = ranked.filter(item => item.score === ranked[0].score);
119
+ if (best.length !== 1) return failure(requested, 'ambiguous_model', best.map(item => item.model), catalog);
120
+ return success(requested, best[0].model, 'fuzzy_unique', catalog);
121
+ }
122
+
123
+ function validateReasoningEffort(resolution, effort) {
124
+ if (effort === undefined || effort === null || effort === '') return { ok: true, reasoning_effort: null, verification: 'inherited' };
125
+ if (!REASONING_EFFORTS.includes(effort)) return { ok: false, reason: 'invalid_reasoning_effort', supported: REASONING_EFFORTS };
126
+ if (!resolution?.ok) return { ok: false, reason: resolution?.reason || 'model_resolution_failed', supported: [] };
127
+ const supported = Array.isArray(resolution.supported_efforts) ? resolution.supported_efforts : [];
128
+ if (supported.length && !supported.includes(effort)) return { ok: false, reason: 'unsupported_reasoning_effort', supported };
129
+ return { ok: true, reasoning_effort: effort, verification: supported.length ? 'catalog' : 'unverified' };
130
+ }
131
+
132
+ module.exports = { distance, normalizeModelName, resolveModel, validateReasoningEffort };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const VERDICTS = ['PASS', 'FAIL', 'BLOCKED'];
5
+ const IDENTITY_FIELDS = ['feature','run_id','attempt_id','agent','host','model_resolved','manifest_digest'];
6
+ function validateReport(r, expected = null) { const errors=[]; const req=['version','feature','run_id','attempt_id','agent','host','model_requested','model_resolved','manifest_digest','writable_roots','started_at','finished_at','verdict','findings','evidence'];if(expected?.model_resolution_strategy!==undefined)req.push('model_resolution_strategy'); for(const k of req) if(r?.[k]===undefined) errors.push({path:`$.${k}`,message:'is required'}); if(r && !VERDICTS.includes(r.verdict)) errors.push({path:'$.verdict',message:'must be PASS, FAIL, or BLOCKED'}); if(r && !Array.isArray(r.findings)) errors.push({path:'$.findings',message:'must be an array'});if(r&&!Array.isArray(r.writable_roots))errors.push({path:'$.writable_roots',message:'must be an array'}); if(expected) for(const key of IDENTITY_FIELDS) if(r?.[key]!==expected[key]) errors.push({path:`$.${key}`,message:'does not match the registered attempt'});for(const key of ['model_requested','model_resolution_strategy'])if(expected?.[key]!==undefined&&r?.[key]!==expected[key])errors.push({path:`$.${key}`,message:'does not match the registered attempt'});if(expected&&expected.reasoning_effort&&r?.reasoning_effort!==expected.reasoning_effort)errors.push({path:'$.reasoning_effort',message:'does not match the registered attempt'});if(expected&&JSON.stringify(r?.writable_roots)!==JSON.stringify(expected.writable_roots))errors.push({path:'$.writable_roots',message:'does not match the registered attempt'}); if(expected && !['ready','running'].includes(expected.status)) errors.push({path:'$.attempt_id',message:'attempt is not eligible to report'}); return {ok:errors.length===0,errors}; }
7
+ function safeReportPath(projectDir, relative) { const root=path.resolve(projectDir,'.aioson','context','reports'); const target=path.resolve(projectDir,relative); if(target!==root && !target.startsWith(root+path.sep)) throw new Error('report path escapes report root'); return target; }
8
+ async function writeReport(projectDir, relative, report, expected) { const check=validateReport(report,expected); if(!check.ok){const e=new Error('invalid report');e.errors=check.errors;throw e;} const file=safeReportPath(projectDir,relative); await fs.mkdir(path.dirname(file),{recursive:true}); const handle=await fs.open(file,'wx'); try{await handle.writeFile(`${JSON.stringify(report,null,2)}\n`,'utf8')}finally{await handle.close()} return file; }
9
+ module.exports={validateReport,safeReportPath,writeReport};
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const HOSTS = ['claude', 'codex', 'opencode'];
4
+ const MODES = ['fresh-session', 'subagent', 'external', 'current-session'];
5
+ const AGENTS = ['dev', 'qa', 'tester', 'pentester', 'validator'];
6
+ const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
7
+ const MAX_MODEL_NAME_LENGTH = 200;
8
+ const ROOT_KEYS = ['version','feature','host','generated_at','agents','capacity_policy','cycle_limits','reporting'];
9
+ const AGENT_KEYS = ['enabled','host','mode','model','reasoning_effort','writable_roots','fallbacks','report'];
10
+ const SECRET_KEY = /token|secret|password|authorization|api[_-]?key/i;
11
+
12
+ function validateManifest(value, expectedFeature) {
13
+ const errors = [];
14
+ const add = (path, message) => errors.push({ path, message });
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return { ok: false, errors: [{ path: '$', message: 'must be an object' }] };
16
+ const scanSecrets=(node,p='$')=>{if(!node||typeof node!=='object')return;for(const [key,child] of Object.entries(node)){if(SECRET_KEY.test(key))add(`${p}.${key}`,'secret fields are forbidden; use environment configuration');scanSecrets(child,`${p}.${key}`)}};scanSecrets(value);
17
+ for (const key of Object.keys(value)) if (!ROOT_KEYS.includes(key)) add(`$.${key}`, SECRET_KEY.test(key) ? 'secret fields are forbidden; use environment configuration' : 'unknown field');
18
+ if (value.version !== 1) add('$.version', 'must equal 1');
19
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.feature || '')) add('$.feature', 'must be a kebab-case slug');
20
+ if (expectedFeature && value.feature !== expectedFeature) add('$.feature', `must equal ${expectedFeature}`);
21
+ if (!HOSTS.includes(value.host)) add('$.host', `must be one of ${HOSTS.join(', ')}`);
22
+ if (!value.agents || typeof value.agents !== 'object') add('$.agents', 'must be an object');
23
+ else for(const key of Object.keys(value.agents)) if(!AGENTS.includes(key)) add(`$.agents.${key}`,'unknown agent');
24
+ for (const id of AGENTS) {
25
+ const agent = value.agents && value.agents[id];
26
+ if (!agent || typeof agent !== 'object') { add(`$.agents.${id}`, 'is required'); continue; }
27
+ for (const key of Object.keys(agent)) if (!AGENT_KEYS.includes(key)) add(`$.agents.${id}.${key}`, SECRET_KEY.test(key) ? 'secret fields are forbidden; use environment configuration' : 'unknown field');
28
+ if (typeof agent.enabled !== 'boolean') add(`$.agents.${id}.enabled`, 'must be boolean');
29
+ if (!HOSTS.includes(agent.host)) add(`$.agents.${id}.host`, `must be one of ${HOSTS.join(', ')}`);
30
+ if (!MODES.includes(agent.mode)) add(`$.agents.${id}.mode`, `must be one of ${MODES.join(', ')}`);
31
+ if (typeof agent.model !== 'string' || !agent.model.trim()) add(`$.agents.${id}.model`, 'must be a non-empty model id');
32
+ else if (agent.model.length > MAX_MODEL_NAME_LENGTH) add(`$.agents.${id}.model`, `must be at most ${MAX_MODEL_NAME_LENGTH} characters`);
33
+ if (agent.reasoning_effort !== undefined && !REASONING_EFFORTS.includes(agent.reasoning_effort)) add(`$.agents.${id}.reasoning_effort`, `must be one of ${REASONING_EFFORTS.join(', ')}`);
34
+ if(!Array.isArray(agent.writable_roots))add(`$.agents.${id}.writable_roots`,'must be an array');else agent.writable_roots.forEach((root,index)=>{if(typeof root!=='string'||!root.trim())add(`$.agents.${id}.writable_roots[${index}]`,'must be a non-empty path')});
35
+ if (!Array.isArray(agent.fallbacks)) add(`$.agents.${id}.fallbacks`, 'must be an array');
36
+ else agent.fallbacks.forEach((fallback,index)=>{const p=`$.agents.${id}.fallbacks[${index}]`;if(!fallback||typeof fallback!=='object'||Array.isArray(fallback)){add(p,'must be {host, model}');return}for(const key of Object.keys(fallback))if(!['host','model'].includes(key))add(`${p}.${key}`,'unknown field');if(!HOSTS.includes(fallback.host))add(`${p}.host`,`must be one of ${HOSTS.join(', ')}`);if(typeof fallback.model!=='string'||!fallback.model.trim())add(`${p}.model`,'must be a non-empty model id');else if(fallback.model.length>MAX_MODEL_NAME_LENGTH)add(`${p}.model`,`must be at most ${MAX_MODEL_NAME_LENGTH} characters`)});
37
+ if (typeof agent.report !== 'string' || !agent.report.includes('{run_id}')) add(`$.agents.${id}.report`, 'must include {run_id}');
38
+ }
39
+ const limits = value.cycle_limits;
40
+ if (!limits || typeof limits !== 'object') add('$.cycle_limits', 'is required');
41
+ else for (const [key, number] of Object.entries(limits)) if (!Number.isInteger(number) || number < 0) add(`$.cycle_limits.${key}`, 'must be a non-negative integer');
42
+ if (!value.capacity_policy || !['pause', 'retry', 'wait', 'fallback'].includes(value.capacity_policy.strategy)) add('$.capacity_policy.strategy', 'must be pause, retry, wait, or fallback');
43
+ else { for(const key of Object.keys(value.capacity_policy)) if(!['strategy','max_attempts','backoff_ms','allow_cross_host'].includes(key)) add(`$.capacity_policy.${key}`,'unknown field'); if(!Number.isInteger(value.capacity_policy.max_attempts)||value.capacity_policy.max_attempts<1)add('$.capacity_policy.max_attempts','must be a positive integer'); if(!Number.isInteger(value.capacity_policy.backoff_ms)||value.capacity_policy.backoff_ms<0)add('$.capacity_policy.backoff_ms','must be a non-negative integer'); }
44
+ if(!value.reporting||typeof value.reporting!=='object')add('$.reporting','is required');else for(const key of Object.keys(value.reporting))if(!['format','markdown'].includes(key))add(`$.reporting.${key}`,'unknown field');
45
+ return { ok: errors.length === 0, errors };
46
+ }
47
+
48
+ module.exports = { AGENTS, HOSTS, MAX_MODEL_NAME_LENGTH, MODES, REASONING_EFFORTS, validateManifest };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+ const crypto=require('node:crypto');
3
+ const {openRuntimeDb,createExecutionRun,attachExecutionProcess,transitionExecutionRun,appendExecutionEvent,attachExecutionReport,pruneExecutionTelemetry}=require('../runtime-store');
4
+ const DEFAULTS={maxQueueBytes:256*1024,maxEventBytes:16*1024,maxPersistedBytes:1024*1024,maxPendingBytes:64*1024,flushMs:100};
5
+ function redact(value){return String(value||'').replace(/(authorization\s*[:=]\s*(?:bearer\s+)?|api[_-]?key\s*[:=]\s*|token\s*[:=]\s*|password\s*[:=]\s*)[^\s,;]+/gi,'$1[REDACTED]').replace(/\b(sk-[A-Za-z0-9_-]{8,})\b/g,'[REDACTED]');}
6
+ function fingerprint(pid,startedAt){return crypto.createHash('sha256').update(`${pid}:${startedAt}`).digest('hex').slice(0,24);}
7
+ async function createTelemetryBridge(projectDir,correlation,options={}){
8
+ const cfg={...DEFAULTS,...options};const opened=await openRuntimeDb(projectDir);const db=opened.db;pruneExecutionTelemetry(db,{retentionDays:options.retentionDays||14,batch:100});const run=createExecutionRun(db,correlation);let queue=[],queued=0,persisted=0,dropped=0,closed=false,timer=null;const pendingByStream=new Map();
9
+ const flush=()=>{if(!queue.length&&!dropped)return;const items=queue;queue=[];queued=0;for(const item of items){appendExecutionEvent(db,run.telemetry_run_id,item);persisted+=item.bytes;}if(dropped){appendExecutionEvent(db,run.telemetry_run_id,{type:'output_truncated',safe_summary:`${dropped} output bytes dropped`,bytes:Math.min(dropped,cfg.maxEventBytes)});db.prepare('UPDATE agent_execution_runs SET truncated_bytes=truncated_bytes+?,dropped_events=dropped_events+1 WHERE telemetry_run_id=?').run(dropped,run.telemetry_run_id);dropped=0;}};
10
+ const schedule=()=>{if(!timer)timer=setTimeout(()=>{timer=null;try{flush();}catch{}},cfg.flushMs)};
11
+ const enqueue=(stream,text)=>{let safe=redact(text);let bytes=Buffer.byteLength(safe);if(bytes>cfg.maxEventBytes){dropped+=bytes-cfg.maxEventBytes;safe=Buffer.from(safe).subarray(0,cfg.maxEventBytes).toString();bytes=Buffer.byteLength(safe)}if(persisted+queued+bytes>cfg.maxPersistedBytes||queued+bytes>cfg.maxQueueBytes){dropped+=bytes;return}queue.push({type:'output',stream,safe_summary:safe,bytes});queued+=bytes};
12
+ const output=(stream,chunk)=>{if(closed)return;let pending=(pendingByStream.get(stream)||'')+String(chunk||'');if(Buffer.byteLength(pending)>cfg.maxPendingBytes){dropped+=Buffer.byteLength(pending);pendingByStream.set(stream,'');return schedule()}let newline;while((newline=pending.indexOf('\n'))>=0){enqueue(stream,pending.slice(0,newline+1));pending=pending.slice(newline+1)}pendingByStream.set(stream,pending);schedule();};
13
+ return {run,output,onSpawn(pid,startedAt=new Date().toISOString()){return attachExecutionProcess(db,correlation,{pid,fingerprint:fingerprint(pid,startedAt)})},event(type,summary,payload){appendExecutionEvent(db,correlation,{type,safe_summary:redact(summary),payload})},transition(next,reason){flush();return transitionExecutionRun(db,correlation,next,reason)},report(report,path,digest){flush();return attachExecutionReport(db,correlation,{...report,path,digest})},close(){if(closed)return;closed=true;if(timer)clearTimeout(timer);for(const [stream,pending] of pendingByStream)if(pending)enqueue(stream,pending);flush();db.close();}};
14
+ }
15
+ module.exports={createTelemetryBridge,redact,fingerprint};
package/src/agents.js CHANGED
@@ -67,7 +67,7 @@ function buildAgentPrompt(agent, tool, options = {}) {
67
67
  '',
68
68
  `**Language boundary:** Agent instructions are canonical in English. All user-facing communication must be in ${interactionLanguage}.`,
69
69
  '',
70
- `**Scope boundary:** You operate exclusively as ${agent.command}. Do not perform work that belongs to another agent. When your work is complete, output only the handoff — which agent is next and why. Do not continue into that agent\'s territory.${autoHandoff ? ' Exception: autopilot handoff is active for this stage — follow `.aioson/docs/autopilot-handoff.md` and auto-invoke the next agent\'s skill when no stop condition applies. The chain stops before the first `@dev` activation (human clears context and starts implementation) and continues through the post-dev review cycle (`@dev` → `@qa` → `@tester`/`@pentester` when their triggers fire → `@validator`); it NEVER auto-runs `feature:close`/publish — those require explicit human approval.' : ''}`,
70
+ `**Scope boundary:** You operate exclusively as ${agent.command}. Do not perform work that belongs to another agent. When your work is complete, output only the handoff — which agent is next and why. Do not continue into that agent\'s territory.${autoHandoff ? ' Exception: autopilot handoff is active for this stage — follow `.aioson/docs/autopilot-handoff.md` and auto-invoke the next agent\'s skill when no stop condition applies. The chain runs the whole feature: the spec authority (`@sheldon`/`@orchestrator`) seeds the agentic scheme and crosses into `@dev` via the `dev-state.md` cold-start packet once its own gates/decisions are settled, and the post-dev review cycle (`@dev` → `@qa` → `@tester`/`@pentester` when their triggers fire → `@validator`) continues automatically. It stops only for a genuine human decision or a stop condition, and NEVER auto-runs `feature:close`/publish — those require explicit human approval.' : ''}`,
71
71
  ].join('\n');
72
72
 
73
73
  if (safeTool === 'claude') {
@@ -29,7 +29,8 @@ const AGENT_ARTIFACT_KIND = {
29
29
  'design-hybrid-forge': { kind: 'hybrid-skill', needs: 'slug' },
30
30
  copywriter: { kind: 'copy', needs: 'slug' },
31
31
  orache: { kind: 'orache-report', needs: 'file' },
32
- 'site-forge': { kind: 'site', needs: 'dir', opts: { noBuild: true } }
32
+ 'site-forge': { kind: 'site', needs: 'dir', opts: { noBuild: true } },
33
+ 'briefing-refiner': { kind: 'review', needs: 'slug' }
33
34
  };
34
35
 
35
36
  const NEEDS_FLAG = { slug: '--slug=<slug>', file: '--file=<path>', dir: '--dir=<dir>' };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Autopilot activation signal — the single source of truth for "is autopilot
5
+ * on?" outside the agent markdown files.
6
+ *
7
+ * Contract (docs/autopilot-handoff.md "Activation"), highest precedence first:
8
+ * - A scheme for the CURRENT feature with `agentic_policy.enabled: false`
9
+ * (the `--step` disarm) → OFF, even over `auto_handoff: true` — an explicit
10
+ * per-feature choice always wins over the project default.
11
+ * - `auto_handoff: true` in project.context.md frontmatter → ON (project default).
12
+ * - `auto_handoff: false` → OFF, even if a seeded scheme is lying around
13
+ * (step-by-step is the standing choice; a leftover scheme must not override it).
14
+ * - flag absent → ON only when `.aioson/context/workflow-execute.json` exists
15
+ * with `agentic_policy.enabled: true` (the per-feature "Autopilot" choice,
16
+ * which deliberately does NOT persist the frontmatter flag). When a slug is
17
+ * provided, the scheme counts only if it was seeded for THAT feature — a
18
+ * scheme left by a different/closed feature does not.
19
+ */
20
+
21
+ const fs = require('node:fs/promises');
22
+ const path = require('node:path');
23
+ const { validateProjectContextFile } = require('./context');
24
+
25
+ const EXECUTION_STATE_RELATIVE_PATH = '.aioson/context/workflow-execute.json';
26
+
27
+ async function readSeededScheme(targetDir) {
28
+ try {
29
+ const raw = await fs.readFile(path.join(targetDir, EXECUTION_STATE_RELATIVE_PATH), 'utf8');
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ async function resolveAutopilotSignal(targetDir, { slug = null, projectContext = null } = {}) {
37
+ let flag;
38
+ try {
39
+ const context = projectContext || await validateProjectContextFile(targetDir);
40
+ flag = context && context.data && Object.prototype.hasOwnProperty.call(context.data, 'auto_handoff')
41
+ ? context.data.auto_handoff === true
42
+ : null;
43
+ } catch {
44
+ flag = null;
45
+ }
46
+
47
+ const scheme = await readSeededScheme(targetDir);
48
+ const slugMatches = Boolean(
49
+ scheme && (!slug || !scheme.feature || String(scheme.feature) === String(slug))
50
+ );
51
+
52
+ // Explicit per-feature disarm (--step) beats the project-wide flag.
53
+ if (
54
+ scheme && scheme.agentic_policy && scheme.agentic_policy.enabled === false &&
55
+ slugMatches
56
+ ) {
57
+ return { enabled: false, source: 'scheme_disarmed' };
58
+ }
59
+
60
+ if (flag === true) return { enabled: true, source: 'frontmatter' };
61
+ if (flag === false) return { enabled: false, source: 'frontmatter_off' };
62
+
63
+ const schemeEnabled = Boolean(scheme && scheme.agentic_policy && scheme.agentic_policy.enabled === true);
64
+ if (!schemeEnabled) return { enabled: false, source: null };
65
+ if (!slugMatches) return { enabled: false, source: 'scheme_other_feature' };
66
+ return { enabled: true, source: 'seeded_scheme' };
67
+ }
68
+
69
+ module.exports = {
70
+ resolveAutopilotSignal
71
+ };
package/src/cli.js CHANGED
@@ -188,7 +188,8 @@ const { runPreflight } = require('./commands/preflight');
188
188
  const { runClassify } = require('./commands/classify');
189
189
  const { runPrototypeCheck } = require('./commands/prototype-check');
190
190
  const { runVerifyImplementation } = require('./commands/verify-implementation');
191
- const { runVerificationPlan } = require('./commands/verification-plan');
191
+ const { runVerificationPlan } = require('./commands/verification-plan');
192
+ const { runAgentExecution } = require('./commands/agent-execution');
192
193
  const { runSizing } = require('./commands/sizing');
193
194
  const { runDetectTestRunner } = require('./commands/detect-test-runner');
194
195
  const { runPulseUpdate } = require('./commands/pulse-update');
@@ -237,7 +238,7 @@ const { runGenomePublish, runGenomeInstallStore, runGenomeInstall, runGenomeList
237
238
  const { runSkillPublish, runSkillInstallStore, runSkillListRemote } = require('./commands/store-skill');
238
239
  const { runSquadPublish, runSquadInstall, runSquadGrant, runSquadList } = require('./commands/store-squad');
239
240
  const { runSystemPackage, runSystemPublish, runSystemList, runSystemInstall } = require('./commands/store-system');
240
- const { runBriefingApprove, runBriefingUnapprove } = require('./commands/briefing');
241
+ const { runBriefingApprove, runBriefingUnapprove, runBriefingReview, runBriefingApplyFeedback } = require('./commands/briefing');
241
242
  const { runCompressAgents } = require('./commands/compress-agents');
242
243
 
243
244
  const JSON_SUPPORTED_COMMANDS = new Set([
@@ -354,6 +355,10 @@ const JSON_SUPPORTED_COMMANDS = new Set([
354
355
  'audit-code',
355
356
  'verify:artifact',
356
357
  'verify-artifact',
358
+ 'briefing:review',
359
+ 'briefing-review',
360
+ 'briefing:apply-feedback',
361
+ 'briefing-apply-feedback',
357
362
  'review:feature',
358
363
  'review-feature',
359
364
  'config',
@@ -669,7 +674,21 @@ const JSON_SUPPORTED_COMMANDS = new Set([
669
674
  'verify:implementation',
670
675
  'verify-implementation',
671
676
  'verification:plan',
672
- 'verification-plan',
677
+ 'verification-plan',
678
+ 'agent:execution:init',
679
+ 'agent-execution-init',
680
+ 'agent:execution:validate',
681
+ 'agent-execution-validate',
682
+ 'agent:execution:show',
683
+ 'agent-execution-show',
684
+ 'agent:execution:dispatch',
685
+ 'agent-execution-dispatch',
686
+ 'agent:execution:resume',
687
+ 'agent-execution-resume',
688
+ 'agent:execution:status',
689
+ 'agent-execution-status',
690
+ 'agent:execution:events',
691
+ 'agent-execution-events',
673
692
  'sizing',
674
693
  'detect:test-runner',
675
694
  'detect-test-runner',
@@ -1622,8 +1641,11 @@ async function main() {
1622
1641
  result = await runPreflight({ args, options, logger: commandLogger });
1623
1642
  } else if (command === 'classify') {
1624
1643
  result = await runClassify({ args, options, logger: commandLogger });
1625
- } else if (command === 'verification:plan' || command === 'verification-plan') {
1626
- result = await runVerificationPlan({ args, options, logger: commandLogger });
1644
+ } else if (command === 'verification:plan' || command === 'verification-plan') {
1645
+ result = await runVerificationPlan({ args, options, logger: commandLogger });
1646
+ } else if (command.startsWith('agent:execution:') || command.startsWith('agent-execution-')) {
1647
+ const sub = command.replace(/^agent:execution:|^agent-execution-/, '');
1648
+ result = await runAgentExecution({ args, options: { ...options, sub }, logger: commandLogger });
1627
1649
  } else if (command === 'prototype:check' || command === 'prototype-check') {
1628
1650
  result = await runPrototypeCheck({ args, options, logger: commandLogger });
1629
1651
  } else if (command === 'verify:implementation' || command === 'verify-implementation') {
@@ -1770,6 +1792,10 @@ async function main() {
1770
1792
  result = await runBriefingApprove({ args, options, logger: commandLogger });
1771
1793
  } else if (command === 'briefing:unapprove' || command === 'briefing-unapprove') {
1772
1794
  result = await runBriefingUnapprove({ args, options, logger: commandLogger });
1795
+ } else if (command === 'briefing:review' || command === 'briefing-review') {
1796
+ result = await runBriefingReview({ args, options, logger: commandLogger });
1797
+ } else if (command === 'briefing:apply-feedback' || command === 'briefing-apply-feedback') {
1798
+ result = await runBriefingApplyFeedback({ args, options, logger: commandLogger });
1773
1799
  } else {
1774
1800
  const message = t('cli.unknown_command', { command });
1775
1801
  if (jsonMode) {
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ const path=require('node:path');
3
+ const {initManifest,loadManifest,resolveAgentExecution}=require('../agent-execution/manifest');
4
+ const {dispatch,readState}=require('../agent-execution/dispatcher');
5
+ const {openRuntimeDb,getExecutionSnapshot,listExecutionEvents}=require('../runtime-store');
6
+ async function resolveAllAgents(manifest,catalogLoader){
7
+ const entries=await Promise.all(Object.keys(manifest.agents).map(async id=>[id,await resolveAgentExecution(manifest,id,{}, {catalogLoader})]));
8
+ return Object.fromEntries(entries);
9
+ }
10
+ function resolutionErrors(resolutions){
11
+ return Object.entries(resolutions).filter(([,value])=>!value.ok).map(([id,value])=>({path:`$.agents.${id}.model`,message:value.reason,candidates:value.candidates||[],supported:value.supported||[]}));
12
+ }
13
+ async function runAgentExecution({args,options={},logger,catalogLoader}){
14
+ const projectDir=path.resolve(process.cwd(),args[0]||'.'); const sub=options.sub||'show'; const feature=String(options.feature||options.slug||'').trim();
15
+ if(!feature)return {ok:false,reason:'feature_required',message:'Use --feature=<slug>'};
16
+ if(sub==='init'){const created=await initManifest(projectDir,feature,String(options.host||'codex')); const result={ok:true,feature,path:path.relative(projectDir,created.path),digest:created.digest,manifest:created.manifest,availability:'validated_at_dispatch'}; if(!options.json)logger.log(`Agent execution manifest: ${result.path}\nValidate: aioson agent:execution:validate . --feature=${feature}`); return result;}
17
+ const loaded=await loadManifest(projectDir,feature);
18
+ if(sub==='validate'){
19
+ let resolutions={};let errors=loaded.errors||[];
20
+ if(loaded.exists&&loaded.ok){resolutions=await resolveAllAgents(loaded.manifest,catalogLoader);errors=resolutionErrors(resolutions)}
21
+ const result={ok:Boolean(loaded.exists&&loaded.ok&&!errors.length),feature,path:path.relative(projectDir,loaded.path),legacy:!loaded.exists,digest:loaded.digest||null,errors,resolutions,availability:'validated_at_dispatch'};
22
+ if(!options.json)(result.ok?logger.log(`Valid: ${result.path} (models resolved against the current runtime catalog).`):logger.error(JSON.stringify(result.errors,null,2)));return result;
23
+ }
24
+ if(sub==='show'){
25
+ if(!loaded.exists)return {ok:true,legacy:true,feature,path:path.relative(projectDir,loaded.path),message:'Manifest absent; legacy configured-default remains active.'};
26
+ if(!loaded.ok)return {ok:false,feature,errors:loaded.errors};
27
+ const agents=await resolveAllAgents(loaded.manifest,catalogLoader);const errors=resolutionErrors(agents);
28
+ return {ok:!errors.length,feature,path:path.relative(projectDir,loaded.path),digest:loaded.digest,agents,errors,availability:'validated_at_dispatch'};
29
+ }
30
+ if(sub==='dispatch')return dispatch({projectDir,feature,agent:options.agent||'dev',runId:options['run-id'],promptPath:options.prompt,catalogLoader});
31
+ if(sub==='resume'){const state=await readState(projectDir,feature);if(!state)return {ok:false,reason:'state_missing'};return dispatch({projectDir,feature,agent:options.agent||state.attempts.at(-1)?.agent||'dev',runId:state.run_id,promptPath:options.prompt,catalogLoader});}
32
+ if(sub==='status'){const {db}=await openRuntimeDb(projectDir);try{return{ok:true,schema_version:1,feature,runs:getExecutionSnapshot(db,{feature,agent:options.agent,state:options.state,limit:options.limit})}}finally{db.close()}}
33
+ if(sub==='events'){const state=await readState(projectDir,feature);const run=String(options.run||state?.attempts?.at(-1)?.telemetry_run_id||'');if(!run)return{ok:false,reason:'telemetry_run_required'};const {db}=await openRuntimeDb(projectDir);try{const owned=getExecutionSnapshot(db,{feature,limit:200}).some(item=>item.telemetry_run_id===run);if(!owned)return{ok:false,reason:'telemetry_run_not_found'};return{ok:true,schema_version:1,trust:'sanitized_untrusted_output',feature,telemetry_run_id:run,...listExecutionEvents(db,run,{after:options.after,limit:options.limit})}}finally{db.close()}}
34
+ return {ok:false,reason:'invalid_subcommand',valid:['init','validate','show','dispatch','resume','status','events']};
35
+ }
36
+ module.exports={runAgentExecution};
@@ -12,7 +12,8 @@ const {
12
12
  const { normalizeInteractionLanguage } = require('../locales');
13
13
  const { validateProjectContextFile, getInteractionLanguage } = require('../context');
14
14
  const { exists } = require('../utils');
15
- const { loadOrCreateState, runWorkflowNext } = require('./workflow-next');
15
+ const { AUTOPILOT_HANDOFF_STAGES, loadOrCreateState, runWorkflowNext } = require('./workflow-next');
16
+ const { resolveAutopilotSignal } = require('../autopilot-signal');
16
17
  const {
17
18
  bootstrapDirectAgentPrompt,
18
19
  classifyDirectAgentRuntime
@@ -278,12 +279,27 @@ async function runAgentPrompt({ args, options, logger, t }) {
278
279
  manifest,
279
280
  requestedMode: promptAgent.id === 'scope-check' && getScopeCheckModeOption(options) ? null : options.mode || null
280
281
  });
282
+ // Direct handoffs carry the autopilot exception too — without it the
283
+ // injected scope boundary instructs a manual stop and overrides the agent
284
+ // .md's autopilot section even when the flag/scheme says to chain.
285
+ let autoHandoff = false;
286
+ if (AUTOPILOT_HANDOFF_STAGES.has(promptAgent.id)) {
287
+ try {
288
+ const signal = await resolveAutopilotSignal(targetDir, {
289
+ slug: options.feature ? String(options.feature).trim() : null
290
+ });
291
+ autoHandoff = signal.enabled;
292
+ } catch {
293
+ autoHandoff = false;
294
+ }
295
+ }
281
296
  prompt = buildAgentPrompt(promptAgent, tool, {
282
297
  instructionPath,
283
298
  interactionLanguage: locale,
284
299
  autonomyMode: effectiveMode,
285
300
  capabilitySummary: buildAgentCapabilitySummary(manifest, tool),
286
- activationContext
301
+ activationContext,
302
+ autoHandoff
287
303
  });
288
304
  const runtimeClass = classifyDirectAgentRuntime(promptAgent.id);
289
305
  const handoffLabel = runtimeClass.source === 'squad_session'