@jaimevalasek/aioson 1.17.3 → 1.18.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 (57) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +85 -51
  3. package/docs/en/3-recipes/full-feature-with-sheldon.md +1 -1
  4. package/docs/en/5-reference/cli-reference.md +4 -4
  5. package/docs/en/5-reference/qa-browser.md +2 -2
  6. package/docs/en/README.md +1 -1
  7. package/docs/en/deyvin-subtask-scout/how-to-use.md +2 -2
  8. package/docs/en/deyvin-subtask-scout/sub-task-scout.md +3 -3
  9. package/docs/en/deyvin-subtask-scout/troubleshooting.md +1 -1
  10. package/docs/pt/3-receitas/publicar-no-aioson-com.md +17 -0
  11. package/docs/pt/5-referencia/comandos-cli.md +2 -2
  12. package/docs/pt/5-referencia/inteligencia-adaptativa.md +3 -3
  13. package/docs/pt/5-referencia/skills.md +1 -1
  14. package/docs/pt/5-referencia/web3.md +3 -3
  15. package/docs/pt/README.md +1 -1
  16. package/docs/pt/_arquivo/README.md +1 -1
  17. package/docs/pt/_arquivo/cenarios.md +31 -31
  18. package/docs/pt/_arquivo/design-hybrid-forge.md +5 -5
  19. package/docs/pt/_arquivo/guia-engineer.md +1 -1
  20. package/docs/pt/_arquivo/profiler-system.md +1 -1
  21. package/docs/pt/_arquivo/site-forge.md +16 -16
  22. package/docs/pt/_arquivo/squad-genome.md +2 -2
  23. package/docs/pt/agentes.md +37 -37
  24. package/docs/pt/deyvin-subtask-scout/como-usar.md +2 -2
  25. package/docs/pt/deyvin-subtask-scout/sub-task-scout.md +1 -1
  26. package/docs/pt/deyvin-subtask-scout/troubleshooting.md +1 -1
  27. package/docs/pt/living-memory/README.md +1 -1
  28. package/docs/pt/living-memory/memoria-viva.md +2 -2
  29. package/docs/pt/living-memory/reflexao-in-harness.md +1 -1
  30. package/docs/pt/living-memory/troubleshooting.md +6 -6
  31. package/package.json +1 -1
  32. package/src/commands/gate-approve.js +56 -1
  33. package/src/commands/live.js +81 -54
  34. package/src/commands/op-capture.js +27 -2
  35. package/src/commands/op-list.js +33 -1
  36. package/src/commands/store-system.js +4 -0
  37. package/src/commands/tool-capabilities.js +14 -10
  38. package/src/commands/workflow-heal.js +47 -1
  39. package/src/i18n/messages/en.js +6 -5
  40. package/src/i18n/messages/pt-BR.js +6 -5
  41. package/src/lib/dev-resume.js +6 -1
  42. package/src/lib/tool-capabilities.js +64 -37
  43. package/src/operator-memory/decision.js +11 -4
  44. package/src/operator-memory/proposal.js +11 -7
  45. package/src/session-handoff.js +52 -1
  46. package/template/.aioson/agents/analyst.md +33 -1
  47. package/template/.aioson/agents/architect.md +33 -1
  48. package/template/.aioson/agents/briefing.md +23 -0
  49. package/template/.aioson/agents/orchestrator.md +26 -0
  50. package/template/.aioson/agents/pentester.md +66 -14
  51. package/template/.aioson/agents/pm.md +18 -1
  52. package/template/.aioson/agents/product.md +11 -0
  53. package/template/.aioson/agents/sheldon.md +21 -1
  54. package/template/.aioson/agents/tester.md +114 -1
  55. package/template/.aioson/docs/pentester/browser-dast-playbook.md +398 -0
  56. package/template/.aioson/rules/agent-structural-contract.md +139 -0
  57. package/template/.aioson/skills/process/decision-presentation/SKILL.md +2 -2
@@ -19,7 +19,7 @@ const {
19
19
  const { ensureDir, exists } = require('../utils');
20
20
  const { SUPPORTED_PROMPT_TOOLS } = require('../prompt-tool');
21
21
  const { isTmuxAvailable, launchTmuxSession, buildSessionName, hasSession, attachSession } = require('../lib/tmux-launcher');
22
- const { resolveResumeArgs } = require('../lib/tool-capabilities');
22
+ const { resolvePermissionModeArgs, resolveResumeArgs } = require('../lib/tool-capabilities');
23
23
 
24
24
  const LIVE_EVENTS_LIMIT = 10;
25
25
  const LIVE_MESSAGE_LIMIT = 500;
@@ -116,14 +116,16 @@ function parseJsonOption(value) {
116
116
  }
117
117
  }
118
118
 
119
- // Combine `--resume` (mapped per-tool via TOOL_CAPS) with user-provided `--tool-args`.
120
- // Resume args go FIRST so that codex `resume --last` (subcommand) lands at argv[1].
121
- function buildLaunchArgs(options, tool) {
122
- const resumeOpt = options.resume !== undefined ? options.resume : options.Resume;
123
- const resumeArgs = resolveResumeArgs(tool, resumeOpt);
124
- const userArgs = parseToolArgs(options['tool-args'] || options.toolArgs);
125
- return [...resumeArgs, ...userArgs];
126
- }
119
+ // Combine `--resume` (mapped per-tool via TOOL_CAPS) with user-provided `--tool-args`.
120
+ // Resume args go FIRST so that codex `resume --last` (subcommand) lands at argv[1].
121
+ function buildLaunchArgs(options, tool) {
122
+ const resumeOpt = options.resume !== undefined ? options.resume : options.Resume;
123
+ const resumeArgs = resolveResumeArgs(tool, resumeOpt);
124
+ const permissionMode = options['permission-mode'] || options.permissionMode;
125
+ const permissionArgs = resolvePermissionModeArgs(tool, permissionMode);
126
+ const userArgs = parseToolArgs(options['tool-args'] || options.toolArgs);
127
+ return [...resumeArgs, ...permissionArgs, ...userArgs];
128
+ }
127
129
 
128
130
  function parseToolArgs(value) {
129
131
  if (value === undefined || value === null || value === '') return [];
@@ -234,7 +236,7 @@ async function resolveExecutablePath(command) {
234
236
  .filter(Boolean);
235
237
 
236
238
  const extensions = process.platform === 'win32'
237
- ? Array.from(new Set(['', ...String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').map((entry) => entry.toLowerCase())]))
239
+ ? Array.from(new Set([...String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').map((entry) => entry.toLowerCase())]))
238
240
  : [''];
239
241
 
240
242
  for (const dir of pathEntries) {
@@ -1235,57 +1237,80 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1235
1237
  };
1236
1238
  }
1237
1239
  } else {
1238
- // Non-tmux reuse logic (original behavior)
1240
+ // Non-tmux reuse logic
1239
1241
  const existingTool = state.tool_session || null;
1240
1242
  if (existingTool && existingTool !== tool) {
1241
- throw new Error(t('live.tool_mismatch', { existing: existingTool, requested: tool }));
1242
- }
1243
-
1244
- const attach = Boolean(options.attach);
1245
- let attachChild = null;
1246
- let attachResult = null;
1247
-
1248
- if (attach && !noLaunch) {
1249
- attachChild = spawn(binaryPath, buildLaunchArgs(options, tool), {
1250
- cwd: targetDir,
1251
- env: process.env,
1252
- stdio: 'inherit'
1243
+ // Auto-close stale session when tool changed (same pattern as tmux recovery above)
1244
+ updateRun(db, {
1245
+ runKey: existing.run.run_key,
1246
+ status: 'completed',
1247
+ summary: `Auto-closed: tool changed from ${existingTool} to ${tool}`,
1248
+ eventType: 'session_closed',
1249
+ phase: 'live',
1250
+ message: `Tool mismatch auto-closed previous ${existingTool} session`
1253
1251
  });
1254
- state.child_pid = attachChild.pid || null;
1255
1252
  if (existing.task?.task_key) {
1256
- const taskMeta = parseTaskMeta(existing.task);
1257
- taskMeta.child_pid = state.child_pid;
1258
- updateTask(db, { taskKey: existing.task.task_key, metaJson: taskMeta });
1253
+ updateTask(db, {
1254
+ taskKey: existing.task.task_key,
1255
+ status: 'completed',
1256
+ goal: `Auto-closed after tool change to ${tool}`
1257
+ });
1258
+ }
1259
+ await clearAgentSession(runtimeDir, agentName);
1260
+ if (!options.json) {
1261
+ logger.log(t('live.tool_mismatch_auto_closed', { existing: existingTool, requested: tool }) ||
1262
+ `Previous session (${existingTool}) auto-closed — starting new with ${tool}`);
1263
+ }
1264
+ // Fall through to create a new session below
1265
+ } else {
1266
+ // Tools match (or no previous tool) — reuse existing session
1267
+ const attach = Boolean(options.attach);
1268
+ let attachChild = null;
1269
+ let attachResult = null;
1270
+
1271
+ if (attach && !noLaunch) {
1272
+ attachChild = spawn(binaryPath, buildLaunchArgs(options, tool), {
1273
+ cwd: targetDir,
1274
+ env: process.env,
1275
+ stdio: 'inherit',
1276
+ shell: process.platform === 'win32'
1277
+ });
1278
+ state.child_pid = attachChild.pid || null;
1279
+ if (existing.task?.task_key) {
1280
+ const taskMeta = parseTaskMeta(existing.task);
1281
+ taskMeta.child_pid = state.child_pid;
1282
+ updateTask(db, { taskKey: existing.task.task_key, metaJson: taskMeta });
1283
+ }
1259
1284
  }
1260
- }
1261
1285
 
1262
- await writeLiveState(runtimeDir, existing.sessionKey, state);
1286
+ await writeLiveState(runtimeDir, existing.sessionKey, state);
1263
1287
 
1264
- if (!options.json) {
1265
- logger.log(t('live.session_already_active', { agent: agentName, session: existing.sessionKey, runKey: existing.run.run_key, dbPath }));
1266
- }
1288
+ if (!options.json) {
1289
+ logger.log(t('live.session_already_active', { agent: agentName, session: existing.sessionKey, runKey: existing.run.run_key, dbPath }));
1290
+ }
1267
1291
 
1268
- if (attachChild) {
1269
- attachResult = await waitForChild(attachChild);
1270
- }
1292
+ if (attachChild) {
1293
+ attachResult = await waitForChild(attachChild);
1294
+ }
1271
1295
 
1272
- return {
1273
- ok: true,
1274
- targetDir,
1275
- dbPath,
1276
- agent: existing.agentName,
1277
- tool: state.tool_session || tool,
1278
- taskKey: existing.task?.task_key || existing.sessionRef?.taskKey || null,
1279
- runKey: existing.run.run_key,
1280
- sessionKey: existing.sessionKey,
1281
- pid: state.child_pid || null,
1282
- processState: detectProcessState(state.child_pid),
1283
- reused: true,
1284
- open: true,
1285
- attached: attach,
1286
- childExitCode: attachResult?.code ?? null,
1287
- childSignal: attachResult?.signal ?? null
1288
- };
1296
+ return {
1297
+ ok: true,
1298
+ targetDir,
1299
+ dbPath,
1300
+ agent: existing.agentName,
1301
+ tool: state.tool_session || tool,
1302
+ taskKey: existing.task?.task_key || existing.sessionRef?.taskKey || null,
1303
+ runKey: existing.run.run_key,
1304
+ sessionKey: existing.sessionKey,
1305
+ pid: state.child_pid || null,
1306
+ processState: detectProcessState(state.child_pid),
1307
+ reused: true,
1308
+ open: true,
1309
+ attached: attach,
1310
+ childExitCode: attachResult?.code ?? null,
1311
+ childSignal: attachResult?.signal ?? null
1312
+ };
1313
+ }
1289
1314
  }
1290
1315
  }
1291
1316
 
@@ -1356,7 +1381,8 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1356
1381
  child = spawn(binaryPath, buildLaunchArgs(options, tool), {
1357
1382
  cwd: targetDir,
1358
1383
  env: process.env,
1359
- stdio: 'inherit'
1384
+ stdio: 'inherit',
1385
+ shell: process.platform === 'win32'
1360
1386
  });
1361
1387
  taskMeta.child_pid = child.pid || null;
1362
1388
  updateTask(db, {
@@ -1368,7 +1394,8 @@ async function runLiveStart({ args, options = {}, logger, t }) {
1368
1394
  child = spawn(binaryPath, buildLaunchArgs(options, tool), {
1369
1395
  cwd: targetDir,
1370
1396
  env: process.env,
1371
- stdio: 'inherit'
1397
+ stdio: 'inherit',
1398
+ shell: process.platform === 'win32'
1372
1399
  });
1373
1400
  taskMeta.child_pid = child.pid || null;
1374
1401
  updateTask(db, {
@@ -27,6 +27,8 @@ const { captureSignal, readProposal, VALID_SIGNAL_TYPES } = require('../operator
27
27
  const { promoteProposal } = require('../operator-memory/decision');
28
28
  const { emitDossierEvent } = require('../lib/dossier-telemetry');
29
29
 
30
+ const CONFIRMATIONS_JSONL = '.aioson/runtime/session-confirmations.jsonl';
31
+
30
32
  const PROMOTION_THRESHOLD = 2;
31
33
 
32
34
  function existsCheckFactory(identity) {
@@ -55,6 +57,8 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
55
57
  const quote = options.quote;
56
58
  const proposal = options.proposal;
57
59
  const sourceAgent = options['source-agent'] || options.sourceAgent || 'unknown';
60
+ const featureSlug = options.feature ? String(options.feature) : null;
61
+ const sessionId = options['session-id'] || options.sessionId || null;
58
62
 
59
63
  if (!signal || !proposal) {
60
64
  const err = `op:capture — required: --signal=<type> --proposal=<paraphrase>. Got signal=${signal}, proposal=${proposal ? 'present' : 'missing'}.`;
@@ -95,7 +99,9 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
95
99
  signal_type: signal,
96
100
  quote,
97
101
  proposal,
98
- source_agent: sourceAgent
102
+ source_agent: sourceAgent,
103
+ feature_slug: featureSlug,
104
+ session_id: sessionId
99
105
  });
100
106
  } catch (err) {
101
107
  const errMsg = `op:capture failed: ${err.message}`;
@@ -104,6 +110,24 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
104
110
  return { ok: false, exitCode: 1, error: errMsg };
105
111
  }
106
112
 
113
+ // M2: append confirmation signals to session accumulator for decision_rationale
114
+ if (signal === 'confirmation') {
115
+ try {
116
+ const accPath = path.join(targetDir, CONFIRMATIONS_JSONL);
117
+ const accDir = path.dirname(accPath);
118
+ fs.mkdirSync(accDir, { recursive: true });
119
+ const entry = JSON.stringify({
120
+ agent: sourceAgent,
121
+ decision: proposal,
122
+ quote: quote || null,
123
+ timestamp: new Date().toISOString()
124
+ });
125
+ fs.appendFileSync(accPath, entry + '\n', 'utf8');
126
+ } catch {
127
+ // best-effort — never block op:capture
128
+ }
129
+ }
130
+
107
131
  const count = result.proposal.detected_count;
108
132
 
109
133
  if (count >= PROMOTION_THRESHOLD) {
@@ -142,5 +166,6 @@ First detection writes to proposals/{slug}.md. Second detection promotes to deci
142
166
 
143
167
  module.exports = {
144
168
  runOpCapture,
145
- PROMOTION_THRESHOLD
169
+ PROMOTION_THRESHOLD,
170
+ CONFIRMATIONS_JSONL
146
171
  };
@@ -21,13 +21,15 @@ const { readDecision } = require('../operator-memory/decision');
21
21
 
22
22
  async function runOpList({ args = [], options = {}, logger }) {
23
23
  if (options.help === true || args.includes('--help') || args.includes('-h')) {
24
- if (logger) logger.log('op:list [--proposals] [--include-archived] [--format=table|json] — list active decisions.');
24
+ if (logger) logger.log('op:list [--proposals] [--include-archived] [--feature=<slug>] [--agent=<name>] [--format=table|json] — list active decisions.');
25
25
  return { ok: true };
26
26
  }
27
27
 
28
28
  const format = options.format || 'table';
29
29
  const showProposals = Boolean(options.proposals);
30
30
  const includeArchived = Boolean(options['include-archived']);
31
+ const filterFeature = options.feature ? String(options.feature) : null;
32
+ const filterAgent = options.agent ? String(options.agent) : null;
31
33
 
32
34
  const resolved = resolveIdentity();
33
35
  ensureStorageTree(resolved.identity);
@@ -65,9 +67,39 @@ async function runOpList({ args = [], options = {}, logger }) {
65
67
  }
66
68
  }
67
69
 
70
+ // M3: apply --feature and --agent filters (BR-AO-07: AND-composable)
71
+ if (filterFeature) {
72
+ items = items.filter((item) => item.feature_slug === filterFeature);
73
+ }
74
+ if (filterAgent) {
75
+ items = items.filter((item) => item.source_agent === filterAgent);
76
+ }
77
+
68
78
  if (format === 'json' || options.json) {
79
+ // BR-AO-09: structured JSON output when --feature is used
80
+ if (filterFeature) {
81
+ const decisions = items.map((item) => ({
82
+ agent: item.source_agent || 'unknown',
83
+ signal: item.signal_type || null,
84
+ quote: Array.isArray(item.quotes) ? (item.quotes[item.quotes.length - 1] || null) : null,
85
+ proposal: item.proposal || item.body || item.title || null,
86
+ timestamp: item.last_reinforced || item.last_detected || null,
87
+ session_id: item.session_id || null
88
+ }));
89
+ const result = {
90
+ ok: true,
91
+ feature: filterFeature,
92
+ decisions,
93
+ total: decisions.length
94
+ };
95
+ if (options.json) return result;
96
+ if (logger) logger.log(JSON.stringify(result, null, 2));
97
+ return result;
98
+ }
99
+
69
100
  const result = {
70
101
  ok: true,
102
+ feature: null,
71
103
  identity: resolved.identity,
72
104
  identity_source: resolved.source,
73
105
  tier: showProposals ? 'proposals' : (includeArchived ? 'active+archive' : 'active'),
@@ -351,6 +351,10 @@ async function runSystemPublish({ args, options, logger, t }) {
351
351
 
352
352
  logger.log('Creating ZIP package...');
353
353
  const zipBuffer = await createZipBuffer(files);
354
+ const MAX_ZIP_BYTES = 2 * 1024 * 1024;
355
+ if (zipBuffer.length > MAX_ZIP_BYTES) {
356
+ throw new Error(`ZIP exceeds 2 MB limit (${(zipBuffer.length / 1024 / 1024).toFixed(2)} MB). Reduce the number of files or bundle size.`);
357
+ }
354
358
  const zipBase64 = zipBuffer.toString('base64');
355
359
  const zipKb = (zipBuffer.length / 1024).toFixed(1);
356
360
  logger.log(`ZIP: ${zipKb} KB (${fileCount} files)`);
@@ -6,9 +6,9 @@ const { TOOL_CAPS, getToolCapabilities, listSupportedTools } = require('../lib/t
6
6
  // so external clients (AIOSON Play, IDE extensions) can drive UI without
7
7
  // hard-coding their own copy of this lookup.
8
8
  //
9
- // Usage:
10
- // aioson tool:capabilities --json
11
- // aioson tool:capabilities --tool=claude --json
9
+ // Usage:
10
+ // aioson tool:capabilities --json
11
+ // aioson tool:capabilities --tool=claude --json
12
12
  async function runToolCapabilities({ args: _args, options = {}, logger, t: _t }) {
13
13
  const tool = options.tool ? String(options.tool).trim() : null;
14
14
 
@@ -22,8 +22,8 @@ async function runToolCapabilities({ args: _args, options = {}, logger, t: _t })
22
22
  payload = { tool: tool.toLowerCase(), capabilities: caps };
23
23
  } else {
24
24
  payload = {
25
- tools: TOOL_CAPS,
26
- schema_version: 1,
25
+ tools: TOOL_CAPS,
26
+ schema_version: 2,
27
27
  };
28
28
  }
29
29
 
@@ -37,8 +37,8 @@ async function runToolCapabilities({ args: _args, options = {}, logger, t: _t })
37
37
  const caps = payload.capabilities;
38
38
  logger.log(`Tool: ${payload.tool}`);
39
39
  logger.log(` binary: ${caps.binary}`);
40
- logger.log(` install_command: ${caps.install_command}`);
41
- logger.log(` supports_resume: ${caps.supports_resume}`);
40
+ logger.log(` install_command: ${caps.install_command}`);
41
+ logger.log(` supports_resume: ${caps.supports_resume}`);
42
42
  if (caps.supports_resume) {
43
43
  logger.log(` resume_last: ${(caps.resume_last || []).join(' ')}`);
44
44
  logger.log(` supports_session_id: ${caps.supports_session_id}`);
@@ -48,9 +48,13 @@ async function runToolCapabilities({ args: _args, options = {}, logger, t: _t })
48
48
  logger.log(` supports_session_picker: ${caps.supports_session_picker}`);
49
49
  if (caps.supports_session_picker) {
50
50
  logger.log(` session_picker: ${(caps.session_picker || []).join(' ')}`);
51
- }
52
- }
53
- } else {
51
+ }
52
+ }
53
+ logger.log(` supports_yolo: ${caps.supports_yolo}`);
54
+ if (caps.supports_yolo) {
55
+ logger.log(` yolo_args: ${(caps.yolo_args || []).join(' ')}`);
56
+ }
57
+ } else {
54
58
  logger.log(`Supported tools: ${listSupportedTools().join(', ')}`);
55
59
  logger.log(`Run with --tool=<name> for details, or --json for the full map.`);
56
60
  }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  const path = require('node:path');
15
+ const fs = require('node:fs/promises');
15
16
  const {
16
17
  loadOrCreateState,
17
18
  activateStage,
@@ -26,6 +27,28 @@ const {
26
27
  incrementRetryCount,
27
28
  buildHealingActivation
28
29
  } = require('../self-healing');
30
+ const { CHECKPOINTS_DIR } = require('./gate-approve');
31
+
32
+ const GATE_ORDER = ['D', 'C', 'B', 'A'];
33
+
34
+ async function readLatestCheckpoint(targetDir, slug) {
35
+ const dir = path.join(targetDir, CHECKPOINTS_DIR);
36
+ try {
37
+ const files = await fs.readdir(dir);
38
+ const matching = files
39
+ .filter((f) => f.endsWith(`-${slug}.json`))
40
+ .sort((a, b) => {
41
+ const gateA = a.replace('gate-', '').charAt(0);
42
+ const gateB = b.replace('gate-', '').charAt(0);
43
+ return GATE_ORDER.indexOf(gateA) - GATE_ORDER.indexOf(gateB);
44
+ });
45
+ if (matching.length === 0) return null;
46
+ const raw = await fs.readFile(path.join(dir, matching[0]), 'utf8');
47
+ return JSON.parse(raw);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
29
52
 
30
53
  async function runWorkflowHeal({ args, options, logger, t }) {
31
54
  const targetDir = path.resolve(process.cwd(), args[0] || '.');
@@ -58,6 +81,10 @@ async function runWorkflowHeal({ args, options, logger, t }) {
58
81
  return { ok: false, reason: 'stage_not_in_sequence' };
59
82
  }
60
83
 
84
+ // M1 checkpoint-at-gate: read latest checkpoint for recovery context (EC-AO-02: graceful fallback)
85
+ const featureSlug = state.featureSlug || null;
86
+ const checkpoint = featureSlug ? await readLatestCheckpoint(targetDir, featureSlug) : null;
87
+
61
88
  // Build healing activation
62
89
  let activation;
63
90
  try {
@@ -74,6 +101,24 @@ async function runWorkflowHeal({ args, options, logger, t }) {
74
101
  return { ok: false, reason: 'activation_failed', error: err.message };
75
102
  }
76
103
 
104
+ if (checkpoint) {
105
+ const sanitize = (s) => String(s || '').replace(/[\r\n]+/g, ' ').slice(0, 200);
106
+ const files = Array.isArray(checkpoint.prerequisites_snapshot)
107
+ ? checkpoint.prerequisites_snapshot.map((s) => sanitize(s && s.file)).join(', ') || 'none'
108
+ : 'none';
109
+ const cpBlock = [
110
+ '',
111
+ '## Checkpoint Recovery Context (auto-injected by AIOSON motor)',
112
+ '',
113
+ `> Last approved gate: **${sanitize(checkpoint.gate)}** (${sanitize(checkpoint.timestamp)})`,
114
+ `> Approved by: ${sanitize(checkpoint.agent)}`,
115
+ `> Artifacts at gate time: ${files}`,
116
+ ''
117
+ ].join('\n');
118
+ activation.prompt = activation.prompt + cpBlock;
119
+ activation.checkpoint = checkpoint;
120
+ }
121
+
77
122
  // Increment retry counter
78
123
  const newCount = await incrementRetryCount(targetDir, stage, activation.prompt.substring(0, 200));
79
124
 
@@ -127,10 +172,11 @@ async function runWorkflowHeal({ args, options, logger, t }) {
127
172
  retryCount: newCount,
128
173
  maxRetries: 3,
129
174
  runtime,
175
+ checkpoint: checkpoint || null,
130
176
  agent: activation.agent,
131
177
  instructionPath: activation.instructionPath,
132
178
  prompt: activation.prompt
133
179
  };
134
180
  }
135
181
 
136
- module.exports = { runWorkflowHeal };
182
+ module.exports = { runWorkflowHeal, readLatestCheckpoint };
@@ -219,7 +219,7 @@ module.exports = {
219
219
  help_runtime_emit:
220
220
  'aioson runtime:emit [path] --agent=<name> [--type=<event>] [--summary=<text>] [--title=<text>] [--refs=<file[,file2]>] [--plan-step=<id>] [--meta=<json>] [--json] [--locale=en]',
221
221
  help_live_start:
222
- 'aioson live:start [path] --tool=codex|claude|gemini|opencode --agent=<name> [--tool-bin=<binary>] [--tool-args=<args>] [--title=<text>] [--goal=<text>] [--plan=<file>] [--session=<key>] [--message=<text>] [--attach] [--no-launch] [--tmux] [--json] [--locale=en]',
222
+ 'aioson live:start [path] --tool=codex|claude|gemini|opencode --agent=<name> [--tool-bin=<binary>] [--permission-mode=default|yolo] [--tool-args=<args>] [--title=<text>] [--goal=<text>] [--plan=<file>] [--session=<key>] [--message=<text>] [--attach] [--no-launch] [--tmux] [--json] [--locale=en]',
223
223
  help_live_status:
224
224
  'aioson live:status [path] [--agent=<name>] [--limit=8] [--watch=2] [--format=compact|tmux-bar] [--json] [--locale=en]',
225
225
  help_live_handoff:
@@ -1090,10 +1090,11 @@ module.exports = {
1090
1090
  plan_not_found: 'Plan file not found: {plan}',
1091
1091
  no_active_session: 'No active live session found for {agent}.',
1092
1092
  session_not_active: 'Live session for {agent} is not active.',
1093
- json_requires_no_launch: '--json requires --no-launch for live:start because foreground launch is interactive.',
1094
- tool_binary_not_found: 'Tool binary not found in PATH: {binary}',
1095
- tool_mismatch: 'Active session uses tool "{existing}" but --tool={requested} was given. Close the session first or use the same tool.',
1096
- micro_task_already_open: 'A live micro-task is already open for {agent}. Emit task_completed before task_started again.',
1093
+ json_requires_no_launch: '--json requires --no-launch for live:start because foreground launch is interactive.',
1094
+ tool_binary_not_found: 'Tool binary not found in PATH: {binary}',
1095
+ tool_mismatch: 'Active session uses tool "{existing}" but --tool={requested} was given. Close the session first or use the same tool.',
1096
+ tool_mismatch_auto_closed: 'Previous live session used "{existing}" and was auto-closed. Starting a new session with "{requested}".',
1097
+ micro_task_already_open: 'A live micro-task is already open for {agent}. Emit task_completed before task_started again.',
1097
1098
  handoff_same_agent: 'live:handoff requires different --agent and --to values.',
1098
1099
  handoff_agent_mismatch: 'No active live session found for {agent}.',
1099
1100
  watch_json_conflict: '--watch cannot be combined with --json.',
@@ -217,7 +217,7 @@ module.exports = {
217
217
  help_runtime_emit:
218
218
  'aioson runtime:emit [path] --agent=<nome> [--type=<evento>] [--summary=<texto>] [--title=<texto>] [--refs=<arquivo[,arquivo2]>] [--plan-step=<id>] [--meta=<json>] [--json] [--locale=pt-BR]',
219
219
  help_live_start:
220
- 'aioson live:start [path] --tool=codex|claude|gemini|opencode --agent=<nome> [--tool-bin=<binario>] [--tool-args=<args>] [--title=<texto>] [--goal=<texto>] [--plan=<arquivo>] [--session=<chave>] [--message=<texto>] [--attach] [--no-launch] [--tmux] [--json] [--locale=pt-BR]',
220
+ 'aioson live:start [path] --tool=codex|claude|gemini|opencode --agent=<nome> [--tool-bin=<binario>] [--permission-mode=default|yolo] [--tool-args=<args>] [--title=<texto>] [--goal=<texto>] [--plan=<arquivo>] [--session=<chave>] [--message=<texto>] [--attach] [--no-launch] [--tmux] [--json] [--locale=pt-BR]',
221
221
  help_live_status:
222
222
  'aioson live:status [path] [--agent=<nome>] [--limit=8] [--watch=2] [--format=compact|tmux-bar] [--json] [--locale=pt-BR]',
223
223
  help_live_handoff:
@@ -1116,10 +1116,11 @@ module.exports = {
1116
1116
  plan_not_found: 'Arquivo de plano nao encontrado: {plan}',
1117
1117
  no_active_session: 'Nenhuma sessao live ativa encontrada para {agent}.',
1118
1118
  session_not_active: 'A sessao live de {agent} nao esta ativa.',
1119
- json_requires_no_launch: '--json requer --no-launch para live:start porque o lancamento em primeiro plano e interativo.',
1120
- tool_binary_not_found: 'Binario da ferramenta nao encontrado no PATH: {binary}',
1121
- tool_mismatch: 'A sessao ativa usa a ferramenta "{existing}" mas --tool={requested} foi informado. Encerre a sessao primeiro ou use a mesma ferramenta.',
1122
- micro_task_already_open: 'Uma micro-tarefa live ja esta aberta para {agent}. Emita task_completed antes de task_started novamente.',
1119
+ json_requires_no_launch: '--json requer --no-launch para live:start porque o lancamento em primeiro plano e interativo.',
1120
+ tool_binary_not_found: 'Binario da ferramenta nao encontrado no PATH: {binary}',
1121
+ tool_mismatch: 'A sessao ativa usa a ferramenta "{existing}" mas --tool={requested} foi informado. Encerre a sessao primeiro ou use a mesma ferramenta.',
1122
+ tool_mismatch_auto_closed: 'A sessao live anterior usava "{existing}" e foi fechada automaticamente. Iniciando nova sessao com "{requested}".',
1123
+ micro_task_already_open: 'Uma micro-tarefa live ja esta aberta para {agent}. Emita task_completed antes de task_started novamente.',
1123
1124
  handoff_same_agent: 'live:handoff requer valores diferentes para --agent e --to.',
1124
1125
  handoff_agent_mismatch: 'Nenhuma sessao live ativa encontrada para {agent}.',
1125
1126
  watch_json_conflict: '--watch nao pode ser combinado com --json.',
@@ -121,6 +121,10 @@ async function buildDevResumeData(projectPath) {
121
121
  ? lastHandoff.artifact_uris
122
122
  : [];
123
123
 
124
+ const decisionRationale = Array.isArray(lastHandoff && lastHandoff.decision_rationale)
125
+ ? lastHandoff.decision_rationale
126
+ : [];
127
+
124
128
  return {
125
129
  feature_slug: featureSlug,
126
130
  classification,
@@ -128,7 +132,8 @@ async function buildDevResumeData(projectPath) {
128
132
  artifacts_consumed: artifactsConsumed,
129
133
  code_map_paths: extractCodeMapPaths(dossierRaw),
130
134
  sheldon_plan: sheldonPlan,
131
- next_step: devStateNext || deriveNextStepFromPlan(planRaw)
135
+ next_step: devStateNext || deriveNextStepFromPlan(planRaw),
136
+ decision_rationale: decisionRationale.length > 0 ? decisionRationale : undefined
132
137
  };
133
138
  }
134
139
 
@@ -5,10 +5,11 @@
5
5
  // "continue last conversation" is achieved by passing the right resume flag
6
6
  // at spawn time — AIOSON never has to track an internal session ID.
7
7
  //
8
- // Used by:
9
- // - `aioson live:start --resume[=last|<id>]` to map to the correct argv
10
- // - `aioson tool:capabilities` to expose this map as JSON to UI clients
11
- // (e.g. AIOSON Play) so they don't duplicate the lookup.
8
+ // Used by:
9
+ // - `aioson live:start --resume[=last|<id>]` to map to the correct argv
10
+ // - `aioson live:start --permission-mode=yolo` to map to the correct argv
11
+ // - `aioson tool:capabilities` to expose this map as JSON to UI clients
12
+ // (e.g. AIOSON Play) so they don't duplicate the lookup.
12
13
  //
13
14
  // Keep entries minimal and source-of-truth here. Adding a new CLI = one entry.
14
15
  const TOOL_CAPS = {
@@ -17,42 +18,50 @@ const TOOL_CAPS = {
17
18
  binary: 'claude',
18
19
  supports_resume: true,
19
20
  resume_last: ['--continue'],
20
- supports_session_id: true,
21
- resume_session_id: ['--resume', '<id>'],
22
- supports_session_picker: true,
23
- session_picker: ['--resume'],
24
- },
25
- codex: {
21
+ supports_session_id: true,
22
+ resume_session_id: ['--resume', '<id>'],
23
+ supports_session_picker: true,
24
+ session_picker: ['--resume'],
25
+ supports_yolo: true,
26
+ yolo_args: ['--dangerously-skip-permissions'],
27
+ },
28
+ codex: {
26
29
  install_command: 'npm install -g @openai/codex',
27
30
  binary: 'codex',
28
31
  supports_resume: true,
29
32
  resume_last: ['resume', '--last'],
30
- supports_session_id: true,
31
- resume_session_id: ['resume', '<id>'],
32
- supports_session_picker: true,
33
- session_picker: ['resume'],
34
- },
35
- opencode: {
33
+ supports_session_id: true,
34
+ resume_session_id: ['resume', '<id>'],
35
+ supports_session_picker: true,
36
+ session_picker: ['resume'],
37
+ supports_yolo: true,
38
+ yolo_args: ['--dangerously-bypass-approvals-and-sandbox'],
39
+ },
40
+ opencode: {
36
41
  install_command: 'npm install -g opencode-ai',
37
42
  binary: 'opencode',
38
43
  supports_resume: true,
39
44
  resume_last: ['--continue'],
40
- supports_session_id: true,
41
- resume_session_id: ['--session', '<id>'],
42
- supports_session_picker: false,
43
- session_picker: null,
44
- },
45
- gemini: {
45
+ supports_session_id: true,
46
+ resume_session_id: ['--session', '<id>'],
47
+ supports_session_picker: false,
48
+ session_picker: null,
49
+ supports_yolo: false,
50
+ yolo_args: null,
51
+ },
52
+ gemini: {
46
53
  install_command: 'npm install -g @google/gemini-cli',
47
54
  binary: 'gemini',
48
55
  supports_resume: false,
49
56
  resume_last: null,
50
- supports_session_id: false,
51
- resume_session_id: null,
52
- supports_session_picker: false,
53
- session_picker: null,
54
- },
55
- };
57
+ supports_session_id: false,
58
+ resume_session_id: null,
59
+ supports_session_picker: false,
60
+ session_picker: null,
61
+ supports_yolo: false,
62
+ yolo_args: null,
63
+ },
64
+ };
56
65
 
57
66
  function getToolCapabilities(tool) {
58
67
  const key = String(tool || '').trim().toLowerCase();
@@ -71,7 +80,7 @@ function listSupportedTools() {
71
80
  // - '' / undefined / null / false → no resume
72
81
  // - any other string → treat as session id
73
82
  // Returns [] when the tool doesn't support resume or resumeOpt is falsy.
74
- function resolveResumeArgs(tool, resumeOpt) {
83
+ function resolveResumeArgs(tool, resumeOpt) {
75
84
  if (resumeOpt === undefined || resumeOpt === null || resumeOpt === '' || resumeOpt === false) {
76
85
  return [];
77
86
  }
@@ -92,11 +101,29 @@ function resolveResumeArgs(tool, resumeOpt) {
92
101
  }
93
102
 
94
103
  return Array.isArray(caps.resume_last) ? [...caps.resume_last] : [];
95
- }
96
-
97
- module.exports = {
98
- TOOL_CAPS,
99
- getToolCapabilities,
100
- listSupportedTools,
101
- resolveResumeArgs,
102
- };
104
+ }
105
+
106
+ function resolvePermissionModeArgs(tool, permissionMode) {
107
+ const mode = String(permissionMode || '').trim().toLowerCase();
108
+ if (!mode || mode === 'default') return [];
109
+ if (mode !== 'yolo') {
110
+ throw new Error(`permission_mode_unknown:${permissionMode}`);
111
+ }
112
+
113
+ const caps = getToolCapabilities(tool);
114
+ if (!caps) {
115
+ throw new Error(`tool_unknown:${tool}`);
116
+ }
117
+ if (!caps.supports_yolo || !Array.isArray(caps.yolo_args)) {
118
+ throw new Error(`permission_mode_unsupported:${tool}:yolo`);
119
+ }
120
+ return [...caps.yolo_args];
121
+ }
122
+
123
+ module.exports = {
124
+ TOOL_CAPS,
125
+ getToolCapabilities,
126
+ listSupportedTools,
127
+ resolveResumeArgs,
128
+ resolvePermissionModeArgs,
129
+ };