@jaimevalasek/aioson 1.17.2 → 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 (65) 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 +4 -2
  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 +104 -12
  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 +34 -2
  47. package/template/.aioson/agents/architect.md +33 -1
  48. package/template/.aioson/agents/briefing.md +26 -1
  49. package/template/.aioson/agents/copywriter.md +1 -1
  50. package/template/.aioson/agents/dev.md +2 -2
  51. package/template/.aioson/agents/deyvin.md +12 -12
  52. package/template/.aioson/agents/neo.md +74 -74
  53. package/template/.aioson/agents/orchestrator.md +26 -0
  54. package/template/.aioson/agents/pentester.md +66 -14
  55. package/template/.aioson/agents/pm.md +18 -1
  56. package/template/.aioson/agents/product.md +12 -1
  57. package/template/.aioson/agents/qa.md +3 -3
  58. package/template/.aioson/agents/sheldon.md +24 -4
  59. package/template/.aioson/agents/tester.md +115 -2
  60. package/template/.aioson/docs/briefing/briefing-craft.md +16 -0
  61. package/template/.aioson/docs/deyvin/runtime-handoffs.md +1 -1
  62. package/template/.aioson/docs/handoff-persistence.md +7 -7
  63. package/template/.aioson/docs/pentester/browser-dast-playbook.md +398 -0
  64. package/template/.aioson/rules/agent-structural-contract.md +139 -0
  65. 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'),
@@ -6,6 +6,33 @@ const { exists, ensureDir } = require('../utils');
6
6
  const { readConfig } = require('./config');
7
7
  const { readWorkspace, findProjectRoot } = require('./workspace');
8
8
 
9
+ let _terser = null;
10
+ function getTerser() {
11
+ if (!_terser) _terser = require('terser');
12
+ return _terser;
13
+ }
14
+
15
+ async function createZipBuffer(files) {
16
+ const archiver = require('archiver');
17
+ const { PassThrough } = require('stream');
18
+ return new Promise((resolve, reject) => {
19
+ const chunks = [];
20
+ const stream = new PassThrough();
21
+ stream.on('data', (chunk) => chunks.push(chunk));
22
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
23
+ stream.on('error', reject);
24
+
25
+ const archive = archiver('zip', { zlib: { level: 9 } });
26
+ archive.on('error', reject);
27
+ archive.pipe(stream);
28
+
29
+ for (const [relPath, content] of Object.entries(files)) {
30
+ archive.append(content, { name: relPath });
31
+ }
32
+ archive.finalize();
33
+ });
34
+ }
35
+
9
36
  const DEFAULT_BASE_URL = 'https://aioson.com';
10
37
  const SYSTEM_PACKAGES_DIR = '.aioson/system-packages';
11
38
  const BACKUPS_DIR = '.aioson/.backups';
@@ -25,6 +52,17 @@ const SYSTEM_ALLOWED_EXTS = new Set([
25
52
  '.gitignore',
26
53
  ]);
27
54
 
55
+ const SYSTEM_BUILD_ALLOWED_EXTS = new Set([
56
+ '.js', '.jsx', '.mjs', '.cjs',
57
+ '.json', '.jsonc',
58
+ '.css',
59
+ '.html',
60
+ '.svg', '.ico',
61
+ '.sql',
62
+ '.yaml', '.yml',
63
+ '.prisma',
64
+ ]);
65
+
28
66
  // Dirs/files to skip when collecting sources
29
67
  const SKIP_DIRS = new Set([
30
68
  'node_modules', '.git', 'dist', 'build', '.turbo', '.next',
@@ -33,13 +71,21 @@ const SKIP_DIRS = new Set([
33
71
  '.aioson', '.claude', '.gemini', '.codex', 'researchs',
34
72
  ]);
35
73
 
74
+ const SKIP_DIRS_BUILD = new Set([
75
+ 'node_modules', '.git', '.turbo', '.next',
76
+ '.cache', 'coverage', '.nyc_output',
77
+ 'src', 'dashboard/src',
78
+ '.aioson', '.claude', '.gemini', '.codex', 'researchs',
79
+ ]);
80
+
36
81
  const SKIP_FILES = new Set([
37
82
  'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
38
83
  'bun.lockb',
39
84
  ]);
40
85
 
41
- const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file
42
- const MAX_PACKAGE_BYTES = 20 * 1024 * 1024; // 20 MB total
86
+ const MAX_FILE_BYTES = 512 * 1024; // 512 KB per file (source)
87
+ const MAX_FILE_BYTES_BUILD = 2 * 1024 * 1024; // 2 MB per file (compiled bundles)
88
+ const MAX_PACKAGE_BYTES = 20 * 1024 * 1024; // 20 MB total
43
89
 
44
90
  /**
45
91
  * Parseia lista de emails autorizados a partir de:
@@ -114,15 +160,18 @@ async function storeGet(url, token) {
114
160
  * Collect all eligible source files under `dir`.
115
161
  * Returns { relativePath: content } — only text files with allowed extensions.
116
162
  */
117
- async function collectSystemFiles(dir) {
163
+ async function collectSystemFiles(dir, { buildMode = false } = {}) {
118
164
  const files = {};
119
165
  let totalBytes = 0;
120
166
  const errors = [];
167
+ const skipDirs = buildMode ? SKIP_DIRS_BUILD : SKIP_DIRS;
168
+ const allowedExts = buildMode ? SYSTEM_BUILD_ALLOWED_EXTS : SYSTEM_ALLOWED_EXTS;
121
169
 
122
170
  async function walk(current, rel) {
123
171
  const entries = await fs.readdir(current, { withFileTypes: true });
124
172
  for (const entry of entries) {
125
- if (SKIP_DIRS.has(entry.name)) continue;
173
+ if (skipDirs.has(entry.name)) continue;
174
+ if (rel && skipDirs.has(`${rel}/${entry.name}`)) continue;
126
175
  if (SKIP_FILES.has(entry.name)) continue;
127
176
 
128
177
  const fullPath = path.join(current, entry.name);
@@ -137,12 +186,12 @@ async function collectSystemFiles(dir) {
137
186
  ? `.${entry.name.split('.').pop().toLowerCase()}`
138
187
  : '';
139
188
 
140
- // Allow dotfiles with no extension (like .gitignore) that match skip list check
141
- if (!SYSTEM_ALLOWED_EXTS.has(ext) && ext !== '') continue;
189
+ if (!allowedExts.has(ext) && ext !== '') continue;
142
190
 
143
191
  try {
144
192
  const stat = await fs.stat(fullPath);
145
- if (stat.size > MAX_FILE_BYTES) {
193
+ const maxBytes = buildMode ? MAX_FILE_BYTES_BUILD : MAX_FILE_BYTES;
194
+ if (stat.size > maxBytes) {
146
195
  errors.push(`File too large (skipped): "${relPath}" (${(stat.size / 1024).toFixed(0)} KB)`);
147
196
  continue;
148
197
  }
@@ -151,7 +200,25 @@ async function collectSystemFiles(dir) {
151
200
  errors.push(`Package exceeds ${MAX_PACKAGE_BYTES / 1024 / 1024} MB limit — stop collecting.`);
152
201
  return;
153
202
  }
154
- const content = await fs.readFile(fullPath, 'utf8');
203
+ let content = await fs.readFile(fullPath, 'utf8');
204
+
205
+ if (buildMode && (ext === '.js' || ext === '.mjs' || ext === '.cjs')) {
206
+ try {
207
+ const terser = getTerser();
208
+ const result = await terser.minify(content, {
209
+ compress: { passes: 2, drop_console: false },
210
+ mangle: {
211
+ toplevel: true,
212
+ properties: { regex: /^_/ },
213
+ },
214
+ format: { comments: false },
215
+ });
216
+ if (result.code) content = result.code;
217
+ } catch {
218
+ // terser failed on this file — keep original compiled JS
219
+ }
220
+ }
221
+
155
222
  files[relPath] = content;
156
223
  } catch {
157
224
  // binary or unreadable — skip silently
@@ -231,13 +298,27 @@ async function runSystemPublish({ args, options, logger, t }) {
231
298
  const config = await readConfig();
232
299
  const token = requireToken(config, t);
233
300
  const dir = path.resolve(process.cwd(), args[0] || '.');
301
+ const buildMode = Boolean(options.build);
234
302
 
235
303
  logger.log(t('system.publish_reading_manifest'));
236
304
  const manifest = await readSystemJson(dir, t);
237
305
  logger.log(t('system.package_manifest_ok', { slug: manifest.slug, version: manifest.version, name: manifest.name }));
238
306
 
239
- logger.log(t('system.package_collecting_files'));
240
- const { files, totalBytes, errors } = await collectSystemFiles(dir);
307
+ if (buildMode) {
308
+ const buildCmd = manifest.build_command || 'npm run build';
309
+ logger.log(`Building: ${buildCmd}`);
310
+ const { execSync } = require('child_process');
311
+ try {
312
+ execSync(buildCmd, { cwd: dir, stdio: 'inherit', timeout: 300_000 });
313
+ } catch (e) {
314
+ throw new Error(`Build failed: ${e.message}`);
315
+ }
316
+ logger.log('Build complete. Collecting compiled output (source excluded)...');
317
+ } else {
318
+ logger.log(t('system.package_collecting_files'));
319
+ }
320
+
321
+ const { files, totalBytes, errors } = await collectSystemFiles(dir, { buildMode });
241
322
 
242
323
  if (errors.length > 0) {
243
324
  for (const e of errors) logger.log(` [WARN] ${e}`);
@@ -268,13 +349,24 @@ async function runSystemPublish({ args, options, logger, t }) {
268
349
  return { ok: true, dryRun: true, manifest, fileCount, totalBytes, visibility, authorizedEmails };
269
350
  }
270
351
 
352
+ logger.log('Creating ZIP package...');
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
+ }
358
+ const zipBase64 = zipBuffer.toString('base64');
359
+ const zipKb = (zipBuffer.length / 1024).toFixed(1);
360
+ logger.log(`ZIP: ${zipKb} KB (${fileCount} files)`);
361
+
271
362
  logger.log(t('system.publish_sending'));
272
363
  const baseUrl = resolveBaseUrl(config);
273
364
  const response = await storePost(`${baseUrl}/api/store/systems/publish`, {
274
365
  kind: 'aioson.store.system',
275
366
  slug: manifest.slug,
276
367
  version: manifest.version,
277
- files,
368
+ zipBase64,
369
+ files: buildMode ? undefined : files,
278
370
  manifest,
279
371
  visibility,
280
372
  paid,
@@ -283,7 +375,7 @@ async function runSystemPublish({ args, options, logger, t }) {
283
375
  }, token);
284
376
 
285
377
  logger.log(t('system.publish_done', { slug: manifest.slug, url: `${baseUrl}/store/systems/${manifest.slug}` }));
286
- logger.log(t('system.publish_summary', { files: fileCount, kb: (totalBytes / 1024).toFixed(1) }));
378
+ logger.log(t('system.publish_summary', { files: fileCount, kb: zipKb }));
287
379
  return { ok: true, manifest, fileCount, totalBytes, visibility, paid, response };
288
380
  }
289
381
 
@@ -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.',