@technomoron/agent-run 0.99.21 → 0.99.23

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.
package/dist/agent-run.js CHANGED
@@ -7,23 +7,35 @@ exports.resolveAgentDir = resolveAgentDir;
7
7
  exports.findProjectRoot = findProjectRoot;
8
8
  exports.resolveProfile = resolveProfile;
9
9
  exports.defaultConfigRoot = defaultConfigRoot;
10
+ exports.defaultConfigRootSearchCandidates = defaultConfigRootSearchCandidates;
11
+ exports.parseEditorCommand = parseEditorCommand;
10
12
  const childProcess = require("child_process");
11
13
  const fs = require("fs");
12
14
  const os = require("os");
13
15
  const path = require("path");
16
+ const crossSpawn = require("cross-spawn");
14
17
  const jsonc_parser_1 = require("jsonc-parser");
15
18
  const nunjucks = require("nunjucks");
16
19
  const IS_WINDOWS = process.platform === 'win32';
17
20
  const ENV_FILE_NAME = '.agent-run.env';
18
21
  const IGNORE_FILE_NAME = '.agent-run-ignore';
19
- const LOCAL_AI_FILE_NAMES = new Set(['AGENTS.md', 'AGENTS-MODS.md', 'AGENTS.override.md', 'CLAUDE.md', 'codex.md']);
22
+ const LOCAL_AI_FILE_NAMES = new Set([
23
+ 'AGENTS.md',
24
+ 'AGENTS-MODS.md',
25
+ 'AGENTS.override.md',
26
+ 'CLAUDE.md',
27
+ 'CLAUDE.local.md',
28
+ '.mcp.json',
29
+ 'codex.md'
30
+ ]);
31
+ const LOCAL_AI_DIRECTORY_NAMES = new Set(['.agents', '.claude', '.codex']);
20
32
  const CONFIG_ROOT_OVERRIDE_ENV = 'AGENT_RUN_CONFIG_ROOT_OVERRIDE';
21
33
  const CONFIG_DIR_ENV = 'AGENT_CONFIG_DIR';
22
34
  const VERBOSE_ENV = 'AGENT_RUN_VERBOSE';
23
35
  const MANIFEST_FILE_NAME = 'agent-run.jsonc';
24
36
  const LOCAL_TEMPLATE_FILE_NAME = 'local.md.njk';
25
37
  const LIVE_DIR_NAME = 'live';
26
- const PACKAGE_VERSION = '0.99.16';
38
+ const PACKAGE_VERSION = '0.99.23';
27
39
  const UNEXPANDED_TEMPLATE_RE = /\{\{[^}]+\}\}|\{%[^%]+%\}/;
28
40
  const agentRunEnvCache = new Map();
29
41
  const GENERATED_GITIGNORE_ENTRIES = [
@@ -31,7 +43,24 @@ const GENERATED_GITIGNORE_ENTRIES = [
31
43
  '**/live/',
32
44
  '',
33
45
  '# Optional generated caches',
34
- '**/.agent-run-cache/'
46
+ '**/.agent-run-cache/',
47
+ '',
48
+ '# Legacy Codex runtime state',
49
+ '**/auth.json',
50
+ '**/history.jsonl',
51
+ '**/sessions/',
52
+ '**/archived_sessions/',
53
+ '**/log/',
54
+ '**/logs/',
55
+ '**/shell_snapshots/',
56
+ '**/*.sqlite*',
57
+ '**/models_cache.json',
58
+ '**/cache/',
59
+ '**/.tmp/',
60
+ '**/installation_id',
61
+ '**/version.json',
62
+ '**/.personality_migration',
63
+ '**/skills/.system/'
35
64
  ];
36
65
  const REQUIRED_GLOBAL_TEMPLATES = [
37
66
  'global/agents/code.md.njk',
@@ -217,6 +246,7 @@ function parseRunCommand(command, inputArgs) {
217
246
  }
218
247
  if (arg === '--') {
219
248
  passthrough = true;
249
+ args.push(arg);
220
250
  continue;
221
251
  }
222
252
  if (arg === '--none') {
@@ -261,15 +291,15 @@ function parseRunCommand(command, inputArgs) {
261
291
  }
262
292
  if (arg === '--network') {
263
293
  if (command !== 'codex') {
264
- fail('--network is only supported for agent-run codex --sandboxed');
294
+ fail('--network is only supported for agent-run codex');
265
295
  }
266
296
  wrapperArgs.codexNetwork = true;
267
297
  continue;
268
298
  }
269
299
  args.push(arg);
270
300
  }
271
- if (wrapperArgs.codexNetwork && wrapperArgs.codexSandboxMode !== 'sandboxed') {
272
- fail('--network is only meaningful with agent-run codex --sandboxed');
301
+ if (wrapperArgs.codexNetwork && wrapperArgs.codexSandboxMode === 'danger') {
302
+ fail('--network cannot be combined with agent-run codex --danger');
273
303
  }
274
304
  return { args, command, wrapperArgs };
275
305
  }
@@ -442,9 +472,9 @@ function renderHelp(topic) {
442
472
  ' --generate Generate files without running the tool',
443
473
  '',
444
474
  'Codex wrapper options:',
445
- ' --danger Run Codex with no sandbox: -a never -s danger-full-access (default)',
446
- ' --sandboxed Run Codex with workspace-write sandbox',
447
- ' --network Enable network for --sandboxed via config override',
475
+ ' --danger Run Codex with no sandbox: -a never -s danger-full-access',
476
+ ' --sandboxed Run Codex with workspace-write sandbox (default)',
477
+ ' --network Enable network for the workspace-write sandbox',
448
478
  ''
449
479
  ].join('\n');
450
480
  }
@@ -524,23 +554,29 @@ function runTool(parsed) {
524
554
  fail(profileResult.reason);
525
555
  }
526
556
  const profile = profileResult.profile;
527
- const configRoot = defaultConfigRoot(projectRoot);
528
- const agentDir = resolveAgentDir(projectRoot);
529
557
  if (wrapperArgs.show) {
558
+ const configRoot = defaultConfigRoot(projectRoot);
559
+ const agentDir = path.join(configRoot, profile);
560
+ verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
530
561
  showToolProfile(command, projectRoot, agentDir);
531
562
  return;
532
563
  }
533
564
  if (wrapperArgs.create) {
534
565
  runInit({ command: 'init', targetPath: projectRoot });
535
566
  }
567
+ const configRoot = defaultConfigRoot(projectRoot);
568
+ const agentDir = path.join(configRoot, profile);
569
+ verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
536
570
  ensureRunnableProfile(configRoot, agentDir, profile);
571
+ const preview = renderProfile(projectRoot, agentDir, true, command);
572
+ ensureToolEnabled(preview.context, command);
537
573
  if (wrapperArgs.generate) {
538
574
  const rendered = syncAgentProfile(projectRoot, agentDir);
539
575
  printUpdateSummary(rendered);
540
576
  return;
541
577
  }
542
578
  const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
543
- if (localAiFiles.length > 0) {
579
+ if (preview.context.guardrails.forbidRepoAiFiles && localAiFiles.length > 0) {
544
580
  if (!wrapperArgs.local) {
545
581
  failForLocalAiFiles(command, projectRoot, localAiFiles);
546
582
  }
@@ -548,16 +584,17 @@ function runTool(parsed) {
548
584
  }
549
585
  const realBinary = findRealBinary(command);
550
586
  const permissionArgs = getPermissionArgs(command);
551
- syncAgentProfile(projectRoot, agentDir);
587
+ const rendered = syncAgentProfile(projectRoot, agentDir);
552
588
  if (command === 'codex') {
553
- runCodex(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs);
589
+ runCodex(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
554
590
  return;
555
591
  }
556
- runClaude(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs);
592
+ runClaude(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
557
593
  }
558
594
  function showToolProfile(command, projectRoot, agentDir) {
559
595
  const trace = { sourceFiles: new Set() };
560
596
  const rendered = renderProfile(projectRoot, agentDir, true, command, trace);
597
+ ensureToolEnabled(rendered.context, command);
561
598
  const generatedFiles = uniqueSorted(rendered.files.map((file) => file.path));
562
599
  const sourceFiles = uniqueSorted([...trace.sourceFiles]);
563
600
  process.stdout.write([
@@ -574,6 +611,11 @@ function showToolProfile(command, projectRoot, agentDir) {
574
611
  ''
575
612
  ].join('\n'));
576
613
  }
614
+ function ensureToolEnabled(context, command) {
615
+ if (!context.tools[command]) {
616
+ fail(`${command} is disabled for agent-run profile ${context.profile}`);
617
+ }
618
+ }
577
619
  function uniqueSorted(values) {
578
620
  return [...new Set(values.map((value) => path.resolve(value)))].sort((a, b) => a.localeCompare(b));
579
621
  }
@@ -588,8 +630,9 @@ function runInit(parsed) {
588
630
  return;
589
631
  }
590
632
  const profile = resolveProfile(projectRoot);
591
- const configRoot = defaultConfigRoot(projectRoot);
592
- const agentDir = resolveAgentDir(projectRoot);
633
+ const configRoot = defaultConfigRoot(projectRoot, { preferProjectRoot: true });
634
+ const agentDir = path.join(configRoot, profile);
635
+ verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
593
636
  ensureConfigRootLayout(configRoot);
594
637
  ensureConfigRootGitignore(configRoot);
595
638
  ensureDefaultGlobalTemplates(configRoot);
@@ -895,31 +938,20 @@ function convertLegacyTemplateVars(content) {
895
938
  .replace(/^\n{2,}/, '\n');
896
939
  }
897
940
  function migrateCodexRuntimeFiles(agentDir) {
898
- const runtimeNames = [
899
- '.personality_migration',
900
- '.tmp',
901
- 'auth.json',
902
- 'cache',
903
- 'history.jsonl',
904
- 'installation_id',
905
- 'log',
906
- 'logs_2.sqlite',
907
- 'models_cache.json',
908
- 'sessions',
909
- 'shell_snapshots',
910
- 'skills',
911
- 'state_5.sqlite',
912
- 'tmp',
913
- 'version.json'
914
- ];
915
- const codexHome = path.join(agentDir, 'memories', 'codex-home');
941
+ const runtimeNames = fs
942
+ .readdirSync(agentDir)
943
+ .filter((name) => /^(?:\.personality_migration|\.tmp|auth\.json|cache|history\.jsonl|installation_id|log|logs|models_cache\.json|sessions|archived_sessions|shell_snapshots|skills|tmp|version\.json)$/.test(name) || /^(?:logs|state)_.*\.sqlite(?:-.+)?$/.test(name));
944
+ const codexHome = path.join(agentDir, LIVE_DIR_NAME, 'memories', 'codex-home');
916
945
  let moved = 0;
917
946
  for (const name of runtimeNames) {
918
947
  const source = path.join(agentDir, name);
919
- const target = path.join(codexHome, name);
920
- if (!fs.existsSync(source) || fs.existsSync(target)) {
948
+ if (!fs.existsSync(source) && !isSymlink(source)) {
921
949
  continue;
922
950
  }
951
+ let target = path.join(codexHome, name);
952
+ if (fs.existsSync(target) || isSymlink(target)) {
953
+ target = nextBackupPath(target);
954
+ }
923
955
  fs.mkdirSync(path.dirname(target), { recursive: true });
924
956
  fs.renameSync(source, target);
925
957
  moved += 1;
@@ -1081,7 +1113,11 @@ function normalizeManifest(manifest, profile) {
1081
1113
  };
1082
1114
  }
1083
1115
  function normalizeSkillName(name) {
1084
- return name === 'code-review' ? 'code-review-organizer' : name;
1116
+ const normalized = name === 'code-review' ? 'code-review-organizer' : name;
1117
+ if (!/^[A-Za-z0-9._-]+$/.test(normalized)) {
1118
+ throw new Error(`invalid skill name: ${name}`);
1119
+ }
1120
+ return normalized;
1085
1121
  }
1086
1122
  function normalizeInstalledSkills(names) {
1087
1123
  return [...new Set(names.map(normalizeSkillName))];
@@ -1108,12 +1144,13 @@ function defaultAgentIncludesForProfile(profile) {
1108
1144
  return [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`, `{{ profile }}/AGENTS-MODS.md`];
1109
1145
  }
1110
1146
  function createNunjucksEnv(configRoot) {
1111
- return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
1147
+ const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
1112
1148
  autoescape: false,
1113
1149
  trimBlocks: true,
1114
1150
  lstripBlocks: true,
1115
1151
  throwOnUndefined: true
1116
1152
  });
1153
+ return env;
1117
1154
  }
1118
1155
  function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env) {
1119
1156
  const date = localDateString();
@@ -1127,6 +1164,7 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
1127
1164
  configRoot,
1128
1165
  date,
1129
1166
  checks: manifest.checks,
1167
+ tools: manifest.tools,
1130
1168
  guardrails: manifest.guardrails
1131
1169
  };
1132
1170
  const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
@@ -1295,20 +1333,23 @@ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = nu
1295
1333
  });
1296
1334
  context.skills = renderSkills(env, configRoot, manifest, context, trace);
1297
1335
  const files = [];
1298
- if (targetTool === null || targetTool === 'codex') {
1336
+ if (context.tools.codex && (targetTool === null || targetTool === 'codex')) {
1299
1337
  const agentsContent = renderToolInstructions('AGENTS.md', env, configRoot, context, false, trace);
1300
- files.push({ path: path.join(context.paths.liveDir, 'AGENTS.md'), content: agentsContent });
1301
- files.push({ path: path.join(context.paths.liveDir, 'config.toml'), content: renderCodexConfig(env, configRoot, context, trace) });
1338
+ files.push({ path: path.join(context.paths.codexHomeDir, 'AGENTS.md'), content: agentsContent });
1339
+ files.push({ path: path.join(context.paths.codexHomeDir, 'config.toml'), content: renderCodexConfig(env, configRoot, context, trace) });
1302
1340
  files.push(...renderNativeSkillFiles(context, 'codex'));
1303
1341
  }
1304
- if (targetTool === null || targetTool === 'claude') {
1342
+ if (context.tools.claude && (targetTool === null || targetTool === 'claude')) {
1305
1343
  const claudeContent = renderToolInstructions('CLAUDE.md', env, configRoot, context, true, trace);
1306
1344
  files.push({ path: path.join(context.paths.liveDir, 'CLAUDE.md'), content: claudeContent });
1307
- files.push({ path: path.join(context.paths.liveDir, '.claude', 'CLAUDE.md'), content: claudeContent });
1308
1345
  files.push({
1309
1346
  path: path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
1310
1347
  content: renderClaudeSettings(env, configRoot, context, trace)
1311
1348
  });
1349
+ files.push({
1350
+ path: path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
1351
+ content: claudePluginManifestContent(context)
1352
+ });
1312
1353
  files.push(...renderNativeSkillFiles(context, 'claude'));
1313
1354
  }
1314
1355
  files.push(...renderGuardShims(context));
@@ -1324,12 +1365,61 @@ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = nu
1324
1365
  function syncAgentProfile(projectRoot, agentDir, options) {
1325
1366
  const rendered = renderProfile(projectRoot, agentDir, false, null, undefined, options);
1326
1367
  syncRuntimeDirs(rendered.context);
1368
+ removeStaleGeneratedEntries(rendered);
1327
1369
  for (const file of rendered.files) {
1328
1370
  writeGeneratedFile(file.path, file.content, file.executable ?? false);
1329
1371
  }
1372
+ removeLegacyGeneratedCodexFiles(rendered.context);
1330
1373
  removeLegacyGeneratedClaudeSettings(rendered.context);
1331
1374
  return rendered;
1332
1375
  }
1376
+ function removeStaleGeneratedEntries(rendered) {
1377
+ const expectedFiles = new Set(rendered.files.map((file) => path.resolve(file.path)));
1378
+ for (const filePath of [
1379
+ path.join(rendered.context.paths.codexHomeDir, 'AGENTS.md'),
1380
+ path.join(rendered.context.paths.codexHomeDir, 'config.toml'),
1381
+ path.join(rendered.context.paths.liveDir, 'CLAUDE.md'),
1382
+ path.join(rendered.context.paths.liveDir, '.claude', 'CLAUDE.md'),
1383
+ path.join(rendered.context.paths.liveDir, '.claude', 'agent-run-settings.json'),
1384
+ path.join(rendered.context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json')
1385
+ ]) {
1386
+ if (!expectedFiles.has(path.resolve(filePath))) {
1387
+ fs.rmSync(filePath, { force: true });
1388
+ }
1389
+ }
1390
+ for (const name of ['git', 'git.cmd', 'npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'gh', 'gh.cmd']) {
1391
+ const filePath = path.join(rendered.context.paths.binDir, name);
1392
+ if (!expectedFiles.has(path.resolve(filePath))) {
1393
+ fs.rmSync(filePath, { force: true });
1394
+ }
1395
+ }
1396
+ const skillNames = new Set(rendered.skills.map((skill) => skill.name));
1397
+ removeStaleGeneratedSkills(rendered.context.paths.codexSkillsDir, rendered.context.tools.codex ? skillNames : new Set(), new Set(['.system']));
1398
+ removeStaleGeneratedSkills(rendered.context.paths.claudeSkillsDir, rendered.context.tools.claude ? skillNames : new Set());
1399
+ }
1400
+ function removeStaleGeneratedSkills(dir, expectedNames, preservedNames = new Set()) {
1401
+ if (!fs.existsSync(dir)) {
1402
+ return;
1403
+ }
1404
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1405
+ if (expectedNames.has(entry.name) || preservedNames.has(entry.name)) {
1406
+ continue;
1407
+ }
1408
+ fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
1409
+ }
1410
+ }
1411
+ function removeLegacyGeneratedCodexFiles(context) {
1412
+ for (const legacyPath of [
1413
+ path.join(context.paths.liveDir, 'AGENTS.md'),
1414
+ path.join(context.paths.liveDir, 'config.toml')
1415
+ ]) {
1416
+ if (!fs.existsSync(legacyPath)) {
1417
+ continue;
1418
+ }
1419
+ fs.rmSync(legacyPath);
1420
+ verbose(`removed legacy generated Codex file ${legacyPath}`);
1421
+ }
1422
+ }
1333
1423
  function removeLegacyGeneratedClaudeSettings(context) {
1334
1424
  for (const legacyPath of [
1335
1425
  path.join(context.paths.liveDir, '.claude', 'settings.json'),
@@ -1459,8 +1549,24 @@ function renderClaudeSettings(env, configRoot, context, trace) {
1459
1549
  ? renderTemplateFile(env, configRoot, templatePath, context, trace)
1460
1550
  : defaultClaudeSettingsContent(context);
1461
1551
  assertNoUnexpandedTemplateVars('claude settings.json', content);
1552
+ try {
1553
+ JSON.parse(content);
1554
+ }
1555
+ catch (error) {
1556
+ throw new Error(`invalid generated Claude settings JSON: ${formatError(error)}`);
1557
+ }
1462
1558
  return content.replace(/\n*$/, '\n');
1463
1559
  }
1560
+ function claudePluginManifestContent(context) {
1561
+ return `${JSON.stringify({
1562
+ name: 'agent-run-profile',
1563
+ description: `Generated skills for agent-run profile ${context.profile}`,
1564
+ version: '1.0.0',
1565
+ author: {
1566
+ name: 'Technomoron'
1567
+ }
1568
+ }, null, 2)}\n`;
1569
+ }
1464
1570
  function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
1465
1571
  const overridePath = `${context.profile}/overrides/${overrideFileName}`;
1466
1572
  if (fs.existsSync(path.join(configRoot, overridePath))) {
@@ -1472,18 +1578,31 @@ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, g
1472
1578
  return null;
1473
1579
  }
1474
1580
  function renderGuardShims(context) {
1581
+ const names = [];
1582
+ if (context.guardrails.blockGitWrite) {
1583
+ names.push('git');
1584
+ }
1585
+ if (context.guardrails.blockPublish) {
1586
+ names.push('npm', 'pnpm');
1587
+ }
1588
+ if (context.guardrails.blockGithubRelease) {
1589
+ names.push('gh');
1590
+ }
1475
1591
  if (IS_WINDOWS) {
1476
- return ['git', 'npm', 'pnpm', 'gh'].map((name) => ({
1592
+ return names.map((name) => ({
1477
1593
  path: path.join(context.paths.binDir, `${name}.cmd`),
1478
1594
  content: windowsShim(name)
1479
1595
  }));
1480
1596
  }
1481
- return [
1482
- { path: path.join(context.paths.binDir, 'git'), content: posixGitShim(), executable: true },
1483
- { path: path.join(context.paths.binDir, 'npm'), content: posixPublishShim('npm'), executable: true },
1484
- { path: path.join(context.paths.binDir, 'pnpm'), content: posixPublishShim('pnpm'), executable: true },
1485
- { path: path.join(context.paths.binDir, 'gh'), content: posixGhShim(), executable: true }
1486
- ];
1597
+ return names.map((name) => ({
1598
+ path: path.join(context.paths.binDir, name),
1599
+ content: name === 'git'
1600
+ ? posixGitShim()
1601
+ : name === 'gh'
1602
+ ? posixGhShim()
1603
+ : posixPublishShim(name),
1604
+ executable: true
1605
+ }));
1487
1606
  }
1488
1607
  function syncRuntimeDirs(context) {
1489
1608
  for (const dir of [
@@ -1566,16 +1685,11 @@ function nextBackupPath(filePath) {
1566
1685
  return candidate;
1567
1686
  }
1568
1687
  function removeLegacyCodexSkillDirs(context) {
1569
- const legacyCodexSkillsDir = path.join(context.paths.liveDir, '.agents', 'skills');
1570
- if (path.resolve(legacyCodexSkillsDir) === path.resolve(context.paths.codexSkillsDir)) {
1688
+ const legacyCodexDir = path.join(context.paths.liveDir, '.agents');
1689
+ if (isSamePathOrDescendant(context.paths.codexSkillsDir, legacyCodexDir)) {
1571
1690
  return;
1572
1691
  }
1573
- for (const skill of context.skills) {
1574
- const legacySkillDir = path.join(legacyCodexSkillsDir, skill.name);
1575
- if (fs.existsSync(legacySkillDir)) {
1576
- fs.rmSync(legacySkillDir, { recursive: true, force: true });
1577
- }
1578
- }
1692
+ fs.rmSync(legacyCodexDir, { recursive: true, force: true });
1579
1693
  }
1580
1694
  function removeLegacyCodeReviewSkillDirs(context) {
1581
1695
  if (!context.skills.some((skill) => skill.name === 'code-review-organizer')) {
@@ -1681,12 +1795,30 @@ function renderLegacyAgentsMods(sourceFile, stack = [], trace) {
1681
1795
  let sawLeadingInclude = false;
1682
1796
  let insertedOverrideNote = false;
1683
1797
  let contentStarted = false;
1798
+ let fence = null;
1684
1799
  for (const line of lines) {
1685
1800
  const trimmed = line.trim();
1801
+ const isIndentedCode = /^(?: {4}|\t)/.test(line);
1802
+ const fenceMatch = isIndentedCode ? null : /^(`{3,}|~{3,})/.exec(trimmed);
1803
+ if (fence !== null) {
1804
+ output.push(line);
1805
+ contentStarted = true;
1806
+ const marker = fenceMatch?.[1];
1807
+ if (marker !== undefined &&
1808
+ marker[0] === fence.character &&
1809
+ marker.length >= fence.length &&
1810
+ trimmed.slice(marker.length).trim() === '') {
1811
+ fence = null;
1812
+ }
1813
+ continue;
1814
+ }
1686
1815
  if (!contentStarted && trimmed === '') {
1687
1816
  continue;
1688
1817
  }
1689
- if (trimmed.startsWith('@')) {
1818
+ if (fenceMatch?.[1]) {
1819
+ fence = { character: fenceMatch[1][0] ?? '`', length: fenceMatch[1].length };
1820
+ }
1821
+ if (fence === null && !isIndentedCode && trimmed.startsWith('@')) {
1690
1822
  const includePath = trimmed.slice(1).trim();
1691
1823
  if (!includePath) {
1692
1824
  continue;
@@ -1739,28 +1871,39 @@ function checkProject(projectRoot) {
1739
1871
  const findings = [];
1740
1872
  verbose(`checking project ${projectRoot}`);
1741
1873
  const profileResult = resolveProfileResult(projectRoot);
1742
- const excludedDir = profileResult.profile === null ? null : resolveAgentDir(projectRoot);
1743
- const localAiFiles = findLocalAiFiles(projectRoot, excludedDir);
1744
- for (const file of localAiFiles) {
1745
- findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
1746
- }
1747
1874
  if (profileResult.profile === null) {
1875
+ for (const file of findLocalAiFiles(projectRoot, null)) {
1876
+ findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
1877
+ }
1748
1878
  findings.push({ message: profileResult.reason, severity: 'ERROR' });
1749
1879
  return findings;
1750
1880
  }
1751
1881
  const agentDir = resolveAgentDir(projectRoot);
1882
+ let forbidRepoAiFiles = true;
1883
+ try {
1884
+ const manifest = normalizeManifest(loadManifest(defaultConfigRoot(projectRoot), agentDir, profileResult.profile), profileResult.profile);
1885
+ forbidRepoAiFiles = manifest.guardrails.forbidRepoAiFiles;
1886
+ }
1887
+ catch {
1888
+ // The profile check below reports malformed or missing source details.
1889
+ }
1890
+ if (forbidRepoAiFiles) {
1891
+ for (const file of findLocalAiFiles(projectRoot, agentDir)) {
1892
+ findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
1893
+ }
1894
+ }
1752
1895
  findings.push(...checkAgentDirectory(projectRoot, agentDir));
1753
1896
  return findings;
1754
1897
  }
1755
1898
  function checkAgentDirectory(projectRoot, agentDir) {
1756
1899
  const findings = [];
1757
1900
  const configRoot = defaultConfigRoot(projectRoot);
1758
- if (!fs.existsSync(path.join(agentDir, MANIFEST_FILE_NAME))) {
1759
- const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
1760
- const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
1761
- if (!fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
1762
- findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
1763
- }
1901
+ const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
1902
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
1903
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
1904
+ if (!fs.existsSync(manifestPath) && !fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
1905
+ findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
1906
+ return findings;
1764
1907
  }
1765
1908
  for (const relativeTemplate of REQUIRED_GLOBAL_TEMPLATES) {
1766
1909
  if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
@@ -1779,20 +1922,26 @@ function checkSourceTree(rootPath) {
1779
1922
  findings: checkProject(projectRoot),
1780
1923
  label: projectRoot
1781
1924
  }));
1782
- return { entries, hasErrors: entries.some((entry) => hasErrors(entry.findings)), root: rootPath };
1925
+ return { entries, hasErrors: entries.length === 0 || entries.some((entry) => hasErrors(entry.findings)), root: rootPath };
1783
1926
  }
1784
1927
  function findSourceRepos(rootPath) {
1785
1928
  const resolvedRoot = path.resolve(rootPath);
1786
1929
  const repos = new Set();
1787
- if (!isIgnoredDir(resolvedRoot) && looksLikeRepoRoot(resolvedRoot)) {
1788
- repos.add(resolvedRoot);
1930
+ if (isIgnoredDir(resolvedRoot)) {
1931
+ return [];
1932
+ }
1933
+ if (looksLikeRepoRoot(resolvedRoot)) {
1934
+ return [resolvedRoot];
1789
1935
  }
1790
- walkSourceTree(resolvedRoot, 0, (fullPath, entry, depth) => {
1791
- if (entry.isDirectory() && entry.name === '.git') {
1792
- repos.add(path.dirname(fullPath));
1936
+ walkSourceTree(resolvedRoot, 0, (fullPath, entry) => {
1937
+ if (!entry.isDirectory()) {
1938
+ return undefined;
1939
+ }
1940
+ if (shouldPruneSourceTreeEntry(entry) || isIgnoredDir(fullPath)) {
1793
1941
  return 'skip';
1794
1942
  }
1795
- if (entry.isDirectory() && depth >= 3) {
1943
+ if (fs.existsSync(path.join(fullPath, '.git'))) {
1944
+ repos.add(fullPath);
1796
1945
  return 'skip';
1797
1946
  }
1798
1947
  return undefined;
@@ -1802,6 +1951,12 @@ function findSourceRepos(rootPath) {
1802
1951
  function looksLikeRepoRoot(dir) {
1803
1952
  return fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'));
1804
1953
  }
1954
+ function shouldPruneSourceTreeEntry(entry) {
1955
+ return (entry.name === 'node_modules' ||
1956
+ entry.name === '.pnpm-store' ||
1957
+ entry.name === 'dist' ||
1958
+ entry.name === 'build');
1959
+ }
1805
1960
  function printProjectReport(projectRoot, findings) {
1806
1961
  process.stdout.write(`Check: ${projectRoot}\n`);
1807
1962
  if (findings.length === 0) {
@@ -1816,7 +1971,7 @@ function printProjectReport(projectRoot, findings) {
1816
1971
  function printBatchReport(rootPath, entries) {
1817
1972
  process.stdout.write(`Check all: ${rootPath}\n`);
1818
1973
  if (entries.length === 0) {
1819
- process.stdout.write('WARN no source repos found\n');
1974
+ process.stdout.write('ERROR no source repos found\n');
1820
1975
  return;
1821
1976
  }
1822
1977
  let totalErrors = 0;
@@ -1847,86 +2002,80 @@ function countWarnings(findings) {
1847
2002
  function formatError(error) {
1848
2003
  return error instanceof Error ? error.message : String(error);
1849
2004
  }
1850
- function runCodex(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs) {
1851
- const liveDir = profileLiveDir(agentDir);
1852
- const agentsPath = path.join(liveDir, 'AGENTS.md');
2005
+ function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
2006
+ const liveDir = context.paths.liveDir;
2007
+ const codexHomeDir = context.paths.codexHomeDir;
2008
+ const projectRoot = context.projectRoot;
2009
+ const agentsPath = path.join(codexHomeDir, 'AGENTS.md');
1853
2010
  if (!fs.existsSync(agentsPath)) {
1854
- failMissingConfig('codex', agentDir);
2011
+ failMissingConfig('codex', context.profileDir);
1855
2012
  }
1856
2013
  const guardBin = path.join(liveDir, 'bin');
1857
- const codexHomeDir = path.join(liveDir, 'memories', 'codex-home');
2014
+ const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
1858
2015
  fs.mkdirSync(codexHomeDir, { recursive: true });
1859
2016
  const env = {
1860
2017
  ...process.env,
1861
2018
  CODEX_HOME: codexHomeDir,
1862
2019
  AGENT_DIR: liveDir,
1863
- AGENT_PROFILE_DIR: agentDir,
2020
+ AGENT_PROFILE_DIR: context.profileDir,
1864
2021
  AGENT_RUN_PROJECT_ROOT: projectRoot,
1865
- PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
2022
+ AGENT_RUN_REAL_PATH: realPath,
2023
+ PATH: `${guardBin}${path.delimiter}${realPath}`
1866
2024
  };
1867
2025
  const codexRuntimeArgs = buildCodexRuntimeArgs(wrapperArgs);
1868
2026
  execCommand(realBinary, [
1869
2027
  ...permissionArgs,
1870
2028
  ...codexRuntimeArgs,
1871
- '--config',
1872
- `system_prompt_file=${agentsPath}`,
1873
- '--config',
1874
- 'project_doc_max_bytes=65536',
1875
- ...globalMemoryArgs(configRoot),
1876
2029
  '-C',
1877
2030
  projectRoot,
1878
- ...(wrapperArgs.codexSandboxMode === 'sandboxed' && wrapperArgs.codexNetwork
2031
+ ...(wrapperArgs.codexSandboxMode !== 'danger' && wrapperArgs.codexNetwork
1879
2032
  ? ['--config', 'sandbox_workspace_write.network_access=true']
1880
2033
  : []),
1881
2034
  ...args
1882
- ], shouldUseShell(realBinary), env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, agentDir, wrapperArgs.local, code));
2035
+ ], env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
1883
2036
  }
1884
2037
  function buildCodexRuntimeArgs(wrapperArgs) {
1885
- const mode = wrapperArgs.codexSandboxMode ?? 'danger';
1886
- if (mode === 'sandboxed') {
1887
- return ['-s', 'workspace-write'];
2038
+ if (wrapperArgs.codexSandboxMode === 'danger') {
2039
+ return ['-a', 'never', '-s', 'danger-full-access'];
1888
2040
  }
1889
- return ['-a', 'never', '-s', 'danger-full-access'];
2041
+ return ['-a', 'on-request', '-s', 'workspace-write'];
1890
2042
  }
1891
- function runClaude(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs) {
1892
- const liveDir = profileLiveDir(agentDir);
2043
+ function runClaude(realBinary, permissionArgs, context, args, wrapperArgs) {
2044
+ const liveDir = context.paths.liveDir;
2045
+ const projectRoot = context.projectRoot;
1893
2046
  const claudePath = path.join(liveDir, 'CLAUDE.md');
1894
2047
  if (!fs.existsSync(claudePath)) {
1895
- failMissingConfig('claude', agentDir);
2048
+ failMissingConfig('claude', context.profileDir);
1896
2049
  }
1897
2050
  const guardBin = path.join(liveDir, 'bin');
1898
2051
  const claudeConfigDir = path.join(liveDir, '.claude');
2052
+ const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
1899
2053
  const env = {
1900
2054
  ...process.env,
1901
- CLAUDE_CONFIG_DIR: claudeConfigDir,
1902
2055
  AGENT_DIR: liveDir,
1903
- AGENT_PROFILE_DIR: agentDir,
2056
+ AGENT_PROFILE_DIR: context.profileDir,
1904
2057
  AGENT_RUN_PROJECT_ROOT: projectRoot,
1905
- PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
2058
+ AGENT_RUN_REAL_PATH: realPath,
2059
+ PATH: `${guardBin}${path.delimiter}${realPath}`
1906
2060
  };
1907
2061
  execCommand(realBinary, [
1908
2062
  ...permissionArgs,
2063
+ '--append-system-prompt-file',
2064
+ claudePath,
1909
2065
  '--settings',
1910
2066
  path.join(claudeConfigDir, 'agent-run-settings.json'),
1911
- '--add-dir',
1912
- projectRoot,
1913
- '--add-dir',
1914
- liveDir,
1915
- ...globalMemoryArgs(configRoot),
2067
+ '--plugin-dir',
2068
+ claudeConfigDir,
1916
2069
  ...args
1917
- ], shouldUseShell(realBinary), env, projectRoot, (code) => postflightProjectCheck(projectRoot, agentDir, wrapperArgs.local, code));
1918
- }
1919
- function profileLiveDir(agentDir) {
1920
- return path.join(agentDir, LIVE_DIR_NAME);
1921
- }
1922
- function globalMemoryArgs(configRoot) {
1923
- const memoryDir = globalMemoryDir(configRoot);
1924
- return fs.existsSync(memoryDir) ? ['--add-dir', memoryDir] : [];
2070
+ ], env, projectRoot, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
1925
2071
  }
1926
2072
  function globalMemoryDir(configRoot) {
1927
2073
  return path.join(configRoot, 'notes', 'memory');
1928
2074
  }
1929
- function postflightProjectCheck(projectRoot, agentDir, allowLocal, code) {
2075
+ function postflightProjectCheck(projectRoot, agentDir, forbidRepoAiFiles, allowLocal, code) {
2076
+ if (!forbidRepoAiFiles) {
2077
+ return code;
2078
+ }
1930
2079
  const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
1931
2080
  if (localAiFiles.length > 0) {
1932
2081
  warnForLocalAiFiles(null, projectRoot, localAiFiles);
@@ -2014,17 +2163,16 @@ function isSamePathOrDescendant(candidatePath, parentPath) {
2014
2163
  function shouldPruneProjectEntry(entry) {
2015
2164
  return (entry.isDirectory() &&
2016
2165
  (entry.name === '.git' ||
2017
- entry.name === '.claude' ||
2018
2166
  entry.name === 'node_modules' ||
2019
2167
  entry.name === '.pnpm-store' ||
2020
2168
  entry.name === 'dist' ||
2021
2169
  entry.name === 'build'));
2022
2170
  }
2023
2171
  function isLocalAiDirectory(entry) {
2024
- return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex' || entry.name === '.agents');
2172
+ return LOCAL_AI_DIRECTORY_NAMES.has(entry.name);
2025
2173
  }
2026
2174
  function isLocalAiFile(entry) {
2027
- return entry.isFile() && LOCAL_AI_FILE_NAMES.has(entry.name);
2175
+ return LOCAL_AI_FILE_NAMES.has(entry.name);
2028
2176
  }
2029
2177
  function isIgnoredDir(dir) {
2030
2178
  if (fs.existsSync(path.join(dir, IGNORE_FILE_NAME))) {
@@ -2057,13 +2205,13 @@ function findProjectRoot(cwd) {
2057
2205
  let gitRoot = '';
2058
2206
  for (;;) {
2059
2207
  const packagePath = path.join(dir, 'package.json');
2208
+ if (isWorkspaceRoot(dir)) {
2209
+ workspaceRoot = dir;
2210
+ }
2060
2211
  if (fs.existsSync(packagePath)) {
2061
2212
  if (!nearestPackageRoot) {
2062
2213
  nearestPackageRoot = dir;
2063
2214
  }
2064
- if (isWorkspaceRoot(dir)) {
2065
- workspaceRoot = dir;
2066
- }
2067
2215
  }
2068
2216
  if (fs.existsSync(path.join(dir, '.git'))) {
2069
2217
  gitRoot = dir;
@@ -2137,26 +2285,16 @@ function resolveGitHubProfile(projectRoot) {
2137
2285
  return remote === null ? null : parseGitHubRemoteProfile(remote);
2138
2286
  }
2139
2287
  function readGitOrigin(projectRoot) {
2140
- const configPath = path.join(projectRoot, '.git', 'config');
2141
- if (!fs.existsSync(configPath)) {
2288
+ const result = childProcess.spawnSync('git', ['-C', projectRoot, 'config', '--get', 'remote.origin.url'], {
2289
+ encoding: 'utf8',
2290
+ shell: false,
2291
+ stdio: ['ignore', 'pipe', 'ignore']
2292
+ });
2293
+ if (result.status !== 0) {
2142
2294
  return null;
2143
2295
  }
2144
- const lines = fs.readFileSync(configPath, 'utf8').replace(/\r\n/g, '\n').split('\n');
2145
- let inOrigin = false;
2146
- for (const line of lines) {
2147
- const trimmed = line.trim();
2148
- if (trimmed.startsWith('[')) {
2149
- inOrigin = trimmed === '[remote "origin"]';
2150
- continue;
2151
- }
2152
- if (inOrigin && trimmed.startsWith('url')) {
2153
- const equalsIndex = trimmed.indexOf('=');
2154
- if (equalsIndex >= 0) {
2155
- return trimmed.slice(equalsIndex + 1).trim();
2156
- }
2157
- }
2158
- }
2159
- return null;
2296
+ const remote = result.stdout.trim();
2297
+ return remote || null;
2160
2298
  }
2161
2299
  function parseGitHubRemoteProfile(remote) {
2162
2300
  const patterns = [
@@ -2181,7 +2319,8 @@ function parseProfile(profile, source) {
2181
2319
  return { profile: null, reason: `${source} must be relative to the config root, not an absolute path` };
2182
2320
  }
2183
2321
  const segments = normalized.split('/').filter((segment) => segment.length > 0);
2184
- if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) {
2322
+ if (segments.length === 0 ||
2323
+ segments.some((segment) => segment === '.' || segment === '..' || !/^[A-Za-z0-9._-]+$/.test(segment))) {
2185
2324
  return { profile: null, reason: `${source} must be a clean relative path like org/my-project` };
2186
2325
  }
2187
2326
  return { profile: segments.join('/'), reason: '' };
@@ -2232,7 +2371,7 @@ function parseBooleanEnv(value) {
2232
2371
  const normalized = value.trim().toLowerCase();
2233
2372
  return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
2234
2373
  }
2235
- function defaultConfigRoot(projectRoot) {
2374
+ function defaultConfigRoot(projectRoot, options = {}) {
2236
2375
  const overrideRoot = process.env[CONFIG_ROOT_OVERRIDE_ENV];
2237
2376
  if (overrideRoot) {
2238
2377
  return path.resolve(overrideRoot);
@@ -2255,9 +2394,15 @@ function defaultConfigRoot(projectRoot) {
2255
2394
  }
2256
2395
  }
2257
2396
  if (projectRoot) {
2258
- return defaultCodeConfigRoot(projectRoot);
2397
+ const projectConfigRoot = defaultCodeConfigRoot(projectRoot);
2398
+ if (options.preferProjectRoot || fs.existsSync(projectConfigRoot)) {
2399
+ return projectConfigRoot;
2400
+ }
2401
+ const discoveredConfigRoot = findExistingConfigRoot(defaultConfigRootSearchCandidates());
2402
+ return discoveredConfigRoot ?? projectConfigRoot;
2259
2403
  }
2260
- return path.join(os.homedir(), '.agent-config');
2404
+ const discoveredConfigRoot = findExistingConfigRoot(defaultConfigRootSearchCandidates());
2405
+ return discoveredConfigRoot ?? path.join(os.homedir(), '.agent-config');
2261
2406
  }
2262
2407
  function defaultCodeConfigRoot(projectRoot) {
2263
2408
  const resolvedProjectRoot = path.resolve(projectRoot);
@@ -2268,6 +2413,39 @@ function defaultCodeConfigRoot(projectRoot) {
2268
2413
  }
2269
2414
  return path.join(codeRoot, 'agent-config');
2270
2415
  }
2416
+ function findExistingConfigRoot(candidates) {
2417
+ for (const candidate of candidates) {
2418
+ if (fs.existsSync(candidate)) {
2419
+ return candidate;
2420
+ }
2421
+ }
2422
+ return null;
2423
+ }
2424
+ function defaultConfigRootSearchCandidates(_projectRoot, options = {}) {
2425
+ const platform = options.platform ?? process.platform;
2426
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
2427
+ const homeDir = options.homeDir ?? os.homedir();
2428
+ const configNames = ['agent-config', 'agent-configs'];
2429
+ const candidates = [];
2430
+ if (platform === 'win32') {
2431
+ for (const codeRoot of [pathApi.join(homeDir, 'Documents', 'code'), pathApi.join(homeDir, 'Desktop', 'code'), 'C:\\code']) {
2432
+ for (const configName of configNames) {
2433
+ candidates.push(pathApi.join(codeRoot, configName));
2434
+ }
2435
+ }
2436
+ return candidates;
2437
+ }
2438
+ for (const configName of configNames) {
2439
+ candidates.push(pathApi.join(homeDir, 'code', configName));
2440
+ }
2441
+ for (const configName of configNames) {
2442
+ candidates.push(pathApi.join(homeDir, configName));
2443
+ }
2444
+ for (const configName of configNames) {
2445
+ candidates.push(pathApi.join(homeDir, `.${configName}`));
2446
+ }
2447
+ return candidates;
2448
+ }
2271
2449
  function findRealBinary(tool) {
2272
2450
  const pathValue = process.env.PATH || '';
2273
2451
  const pathDirs = pathValue.split(path.delimiter).filter((entry) => entry.length > 0);
@@ -2360,12 +2538,12 @@ function isExecutable(filePath) {
2360
2538
  }
2361
2539
  }
2362
2540
  function execTool(command, args) {
2363
- verbose(`exec tool: ${formatCommand(command, args)} shell=${String(shouldUseShell(command))}`);
2364
- execCommand(command, args, shouldUseShell(command));
2541
+ verbose(`exec tool: ${formatCommand(command, args)}`);
2542
+ execCommand(command, args);
2365
2543
  }
2366
- function execCommand(command, args, shell, env, cwd, onExit) {
2367
- verbose(`spawn: ${formatCommand(command, args)} shell=${String(shell)} cwd=${cwd ?? process.cwd()}`);
2368
- const child = childProcess.spawn(command, args, { cwd, env, shell, stdio: 'inherit' });
2544
+ function execCommand(command, args, env, cwd, onExit) {
2545
+ verbose(`spawn: ${formatCommand(command, args)} cwd=${cwd ?? process.cwd()}`);
2546
+ const child = crossSpawn(command, args, { cwd, env, stdio: 'inherit' });
2369
2547
  child.on('exit', (code, signal) => {
2370
2548
  if (signal) {
2371
2549
  process.kill(process.pid, signal);
@@ -2381,33 +2559,106 @@ function execCommand(command, args, shell, env, cwd, onExit) {
2381
2559
  function openEditor(filePath) {
2382
2560
  const visual = process.env.VISUAL?.trim();
2383
2561
  if (visual) {
2384
- execCommand(visual, [filePath], true);
2562
+ runConfiguredEditor(visual, filePath);
2385
2563
  return;
2386
2564
  }
2387
2565
  const editor = process.env.EDITOR?.trim();
2388
2566
  if (editor) {
2389
- execCommand(editor, [filePath], true);
2567
+ runConfiguredEditor(editor, filePath);
2390
2568
  return;
2391
2569
  }
2392
2570
  const vscodeCommand = findVsCodeEditorCommand();
2393
2571
  if (vscodeCommand !== null) {
2394
- execCommand(vscodeCommand, ['--reuse-window', filePath], shouldUseShell(vscodeCommand));
2572
+ execCommand(vscodeCommand, ['--reuse-window', filePath]);
2395
2573
  return;
2396
2574
  }
2397
2575
  const fallbackEditor = findFallbackEditor();
2398
2576
  if (fallbackEditor !== null) {
2399
- execCommand(fallbackEditor, [filePath], shouldUseShell(fallbackEditor));
2577
+ execCommand(fallbackEditor, [filePath]);
2400
2578
  return;
2401
2579
  }
2402
2580
  if (IS_WINDOWS) {
2403
- execCommand('cmd.exe', ['/c', 'start', '', filePath], false);
2581
+ execCommand('explorer.exe', [filePath]);
2404
2582
  return;
2405
2583
  }
2406
2584
  if (process.platform === 'darwin') {
2407
- execCommand('open', [filePath], false);
2585
+ execCommand('open', [filePath]);
2408
2586
  return;
2409
2587
  }
2410
- execCommand('xdg-open', [filePath], false);
2588
+ execCommand('xdg-open', [filePath]);
2589
+ }
2590
+ function runConfiguredEditor(commandLine, filePath) {
2591
+ let editorArgs;
2592
+ try {
2593
+ editorArgs = parseEditorCommand(commandLine);
2594
+ }
2595
+ catch (error) {
2596
+ fail(`invalid editor command: ${formatError(error)}`);
2597
+ }
2598
+ const [command, ...args] = editorArgs;
2599
+ if (!command) {
2600
+ fail('invalid editor command: command is empty');
2601
+ }
2602
+ execCommand(command, [...args, filePath]);
2603
+ }
2604
+ function parseEditorCommand(commandLine) {
2605
+ const args = [];
2606
+ let current = '';
2607
+ let quote = null;
2608
+ let tokenStarted = false;
2609
+ for (let index = 0; index < commandLine.length; index += 1) {
2610
+ const character = commandLine[index] ?? '';
2611
+ if (quote !== null) {
2612
+ if (character === quote) {
2613
+ quote = null;
2614
+ continue;
2615
+ }
2616
+ if (character === '\\' && quote === '"') {
2617
+ const next = commandLine[index + 1];
2618
+ if (next === '"' || next === '\\') {
2619
+ current += next;
2620
+ index += 1;
2621
+ continue;
2622
+ }
2623
+ }
2624
+ current += character;
2625
+ continue;
2626
+ }
2627
+ if (character === "'" || character === '"') {
2628
+ quote = character;
2629
+ tokenStarted = true;
2630
+ continue;
2631
+ }
2632
+ if (/\s/.test(character)) {
2633
+ if (tokenStarted) {
2634
+ args.push(current);
2635
+ current = '';
2636
+ tokenStarted = false;
2637
+ }
2638
+ continue;
2639
+ }
2640
+ if (character === '\\') {
2641
+ const next = commandLine[index + 1];
2642
+ if (next !== undefined && (/\s/.test(next) || next === "'" || next === '"' || next === '\\')) {
2643
+ current += next;
2644
+ index += 1;
2645
+ tokenStarted = true;
2646
+ continue;
2647
+ }
2648
+ }
2649
+ current += character;
2650
+ tokenStarted = true;
2651
+ }
2652
+ if (quote !== null) {
2653
+ throw new Error(`unterminated ${quote} quote`);
2654
+ }
2655
+ if (tokenStarted) {
2656
+ args.push(current);
2657
+ }
2658
+ if (args.length === 0) {
2659
+ throw new Error('command is empty');
2660
+ }
2661
+ return args;
2411
2662
  }
2412
2663
  function findVsCodeEditorCommand() {
2413
2664
  if (!isRunningInVsCodeTerminal()) {
@@ -2452,13 +2703,6 @@ function findExecutable(command) {
2452
2703
  }
2453
2704
  return null;
2454
2705
  }
2455
- function shouldUseShell(command) {
2456
- if (!IS_WINDOWS) {
2457
- return false;
2458
- }
2459
- const extension = path.extname(command).toLowerCase();
2460
- return extension === '.cmd' || extension === '.bat';
2461
- }
2462
2706
  function localDateString() {
2463
2707
  const now = new Date();
2464
2708
  const year = String(now.getFullYear()).padStart(4, '0');
@@ -2608,7 +2852,8 @@ function defaultReleasePackageCheckSkill() {
2608
2852
  '- The current branch has an upstream configured and is in sync with it after fetching.',
2609
2853
  '- The repository does not contain agent files or directories such as `AGENTS.md`, `CLAUDE.md`, `codex.md`, `.claude`, or `.codex`.',
2610
2854
  '- The repository root has a lockfile: `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lockb`, or `bun.lock`.',
2611
- '- GitHub workflows under `.github/workflows` use `actions/setup-node@v6` and `node-version: 24`.',
2855
+ '- Every GitHub Actions `uses:` reference is pinned to a full commit SHA.',
2856
+ '- The pinned `actions/setup-node` release is v6-compatible and workflows use `node-version: 24`.',
2612
2857
  '- GitHub workflows do not reference `node20` or `node22`.',
2613
2858
  '- The git working tree is clean for files under the checked package directory.',
2614
2859
  '- If files beyond `CHANGES` changed in the checked package directory, that package `CHANGES` file is also changed.',
@@ -2724,8 +2969,8 @@ function defaultToolInstructionsTemplate(isClaude) {
2724
2969
  '## Tool Note',
2725
2970
  '',
2726
2971
  isClaude
2727
- ? 'Claude uses the generated config directory under {{ paths.liveDir }}/.claude and may read {{ paths.liveDir }} via `--add-dir`.'
2728
- : 'Codex uses {{ agentDir }}/AGENTS.md as `system_prompt_file` and {{ paths.codexHomeDir }} as CODEX_HOME.',
2972
+ ? 'Claude receives {{ agentDir }}/CLAUDE.md through `--append-system-prompt-file` and loads generated skills as a local plugin.'
2973
+ : 'Codex loads AGENTS.md, config.toml, and skills natively from {{ paths.codexHomeDir }}.',
2729
2974
  '',
2730
2975
  '## Mandatory Path Rule',
2731
2976
  '',
@@ -2740,14 +2985,14 @@ function defaultCodexConfigTemplate() {
2740
2985
  '# Generated by agent-run. Do not edit directly.',
2741
2986
  '',
2742
2987
  'project_doc_max_bytes = 65536',
2988
+ 'approval_policy = "on-request"',
2743
2989
  'sandbox_mode = "workspace-write"',
2744
2990
  '',
2745
2991
  '[sandbox_workspace_write]',
2746
2992
  'writable_roots = [',
2747
- ' "{{ projectRoot }}",',
2748
- ' "{{ agentDir }}",',
2749
- ' "{{ profileDir }}",',
2750
- ' "{{ paths.globalMemoryDir }}"',
2993
+ ' {{ projectRoot | dump }},',
2994
+ ' {{ paths.reviewDir | dump }},',
2995
+ ' {{ paths.memoriesDir | dump }}',
2751
2996
  ']',
2752
2997
  'network_access = false',
2753
2998
  ''
@@ -2756,19 +3001,17 @@ function defaultCodexConfigTemplate() {
2756
3001
  function defaultClaudeSettingsTemplate() {
2757
3002
  const envBlock = [
2758
3003
  ' "env": {',
2759
- ' "AGENT_DIR": "{{ agentDir }}",',
2760
- ' "AGENT_PROFILE_DIR": "{{ profileDir }}",',
2761
- ' "AGENT_RUN_PROJECT_ROOT": "{{ projectRoot }}",',
2762
- ' "AGENT_GLOBAL_MEMORY_DIR": "{{ paths.globalMemoryDir }}"',
3004
+ ' "AGENT_DIR": {{ agentDir | dump }},',
3005
+ ' "AGENT_PROFILE_DIR": {{ profileDir | dump }},',
3006
+ ' "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},',
3007
+ ' "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }}',
2763
3008
  ' }'
2764
3009
  ].join('\n');
2765
3010
  return [
2766
3011
  '{% if permissionsAllow.length %}{',
2767
3012
  envBlock + ',',
2768
3013
  ' "permissions": {',
2769
- ' "allow": [',
2770
- ' "{{ permissionsAllow | join(\'",\\n "\') }}"',
2771
- ' ]',
3014
+ ' "allow": {{ permissionsAllow | dump }}',
2772
3015
  ' }',
2773
3016
  '}',
2774
3017
  '{% else %}{',
@@ -2782,14 +3025,14 @@ function defaultCodexConfigContent(context) {
2782
3025
  '# Generated by agent-run. Do not edit directly.',
2783
3026
  '',
2784
3027
  'project_doc_max_bytes = 65536',
3028
+ 'approval_policy = "on-request"',
2785
3029
  'sandbox_mode = "workspace-write"',
2786
3030
  '',
2787
3031
  '[sandbox_workspace_write]',
2788
3032
  'writable_roots = [',
2789
3033
  ` ${jsonString(context.projectRoot)},`,
2790
- ` ${jsonString(context.agentDir)},`,
2791
- ` ${jsonString(context.profileDir)},`,
2792
- ` ${jsonString(context.paths.globalMemoryDir)}`,
3034
+ ` ${jsonString(context.paths.reviewDir)},`,
3035
+ ` ${jsonString(context.paths.memoriesDir)}`,
2793
3036
  ']',
2794
3037
  'network_access = false',
2795
3038
  ''
@@ -2825,18 +3068,17 @@ function posixGitShim() {
2825
3068
  return [
2826
3069
  '#!/usr/bin/env bash',
2827
3070
  'set -euo pipefail',
2828
- 'case "${1:-}" in',
2829
- ' commit|tag|push)',
2830
- ' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
2831
- ' echo "agent-run: blocked git $1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
2832
- ' exit 42',
2833
- ' fi',
2834
- ' ;;',
2835
- 'esac',
2836
- 'if command -v /usr/bin/git >/dev/null 2>&1; then',
2837
- ' exec /usr/bin/git "$@"',
2838
- 'fi',
2839
- 'exec git "$@"',
3071
+ 'for arg in "$@"; do',
3072
+ ' case "$arg" in',
3073
+ ' commit|tag|push)',
3074
+ ' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
3075
+ ' echo "agent-run: blocked git $arg. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
3076
+ ' exit 42',
3077
+ ' fi',
3078
+ ' ;;',
3079
+ ' esac',
3080
+ 'done',
3081
+ ...posixRunRealCommand('git'),
2840
3082
  ''
2841
3083
  ].join('\n');
2842
3084
  }
@@ -2844,14 +3086,13 @@ function posixPublishShim(tool) {
2844
3086
  return [
2845
3087
  '#!/usr/bin/env bash',
2846
3088
  'set -euo pipefail',
2847
- 'if [ "${1:-}" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
2848
- ` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
2849
- ' exit 42',
2850
- 'fi',
2851
- `if command -v /usr/bin/${tool} >/dev/null 2>&1; then`,
2852
- ` exec /usr/bin/${tool} "$@"`,
2853
- 'fi',
2854
- `exec ${tool} "$@"`,
3089
+ 'for arg in "$@"; do',
3090
+ ' if [ "$arg" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
3091
+ ` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
3092
+ ' exit 42',
3093
+ ' fi',
3094
+ 'done',
3095
+ ...posixRunRealCommand(tool),
2855
3096
  ''
2856
3097
  ].join('\n');
2857
3098
  }
@@ -2859,44 +3100,95 @@ function posixGhShim() {
2859
3100
  return [
2860
3101
  '#!/usr/bin/env bash',
2861
3102
  'set -euo pipefail',
2862
- 'if [ "${1:-}" = "release" ] && [ "${2:-}" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
2863
- ' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
2864
- ' exit 42',
2865
- 'fi',
2866
- 'if command -v /usr/bin/gh >/dev/null 2>&1; then',
2867
- ' exec /usr/bin/gh "$@"',
2868
- 'fi',
2869
- 'exec gh "$@"',
3103
+ 'saw_release=0',
3104
+ 'for arg in "$@"; do',
3105
+ ' if [ "$arg" = "release" ]; then',
3106
+ ' saw_release=1',
3107
+ ' elif [ "$saw_release" = "1" ] && [ "$arg" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
3108
+ ' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
3109
+ ' exit 42',
3110
+ ' fi',
3111
+ 'done',
3112
+ ...posixRunRealCommand('gh'),
2870
3113
  ''
2871
3114
  ].join('\n');
2872
3115
  }
3116
+ function posixRunRealCommand(tool) {
3117
+ return [
3118
+ 'shim_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"',
3119
+ 'real_path="${AGENT_RUN_REAL_PATH:-}"',
3120
+ 'if [ -z "$real_path" ]; then',
3121
+ ' case "${PATH:-}" in',
3122
+ ' "$shim_dir") real_path="" ;;',
3123
+ ' "$shim_dir":*) real_path="${PATH#*:}" ;;',
3124
+ ' *) real_path="${PATH:-}" ;;',
3125
+ ' esac',
3126
+ 'fi',
3127
+ `real_command="$(PATH="$real_path" command -v ${tool} || true)"`,
3128
+ 'if [ -z "$real_command" ]; then',
3129
+ ` echo "agent-run: cannot find the real ${tool} command outside the guard directory." >&2`,
3130
+ ' exit 127',
3131
+ 'fi',
3132
+ `if [ "$real_command" = "$shim_dir/${tool}" ]; then`,
3133
+ ` echo "agent-run: refused recursive ${tool} guard resolution." >&2`,
3134
+ ' exit 127',
3135
+ 'fi',
3136
+ 'PATH="$real_path" exec "$real_command" "$@"'
3137
+ ];
3138
+ }
2873
3139
  function windowsShim(name) {
3140
+ const executable = name === 'npm' || name === 'pnpm' ? `${name}.cmd` : `${name}.exe`;
2874
3141
  const guard = name === 'git'
2875
3142
  ? [
2876
- 'if /I "%1"=="commit" goto block_git',
2877
- 'if /I "%1"=="tag" goto block_git',
2878
- 'if /I "%1"=="push" goto block_git',
2879
- 'goto run',
3143
+ ':scan',
3144
+ 'if "%~1"=="" goto run',
3145
+ 'if /I "%~1"=="commit" goto block_git',
3146
+ 'if /I "%~1"=="tag" goto block_git',
3147
+ 'if /I "%~1"=="push" goto block_git',
3148
+ 'shift',
3149
+ 'goto scan',
2880
3150
  ':block_git',
2881
3151
  'if "%AGENT_RUN_ALLOW_GIT_WRITE%"=="1" goto run',
2882
- 'echo agent-run: blocked git %1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation. 1>&2',
3152
+ 'echo agent-run: blocked git write. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation. 1>&2',
2883
3153
  'exit /b 42'
2884
3154
  ]
2885
3155
  : name === 'gh'
2886
3156
  ? [
2887
- 'if /I not "%1"=="release" goto run',
2888
- 'if /I not "%2"=="create" goto run',
3157
+ 'set "SAW_RELEASE=0"',
3158
+ ':scan',
3159
+ 'if "%~1"=="" goto run',
3160
+ 'if /I "%~1"=="release" set "SAW_RELEASE=1"',
3161
+ 'if "%SAW_RELEASE%"=="1" if /I "%~1"=="create" goto block_release',
3162
+ 'shift',
3163
+ 'goto scan',
3164
+ ':block_release',
2889
3165
  'if "%AGENT_RUN_ALLOW_GITHUB_RELEASE%"=="1" goto run',
2890
3166
  'echo agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation. 1>&2',
2891
3167
  'exit /b 42'
2892
3168
  ]
2893
3169
  : [
2894
- 'if /I not "%1"=="publish" goto run',
3170
+ ':scan',
3171
+ 'if "%~1"=="" goto run',
3172
+ 'if /I "%~1"=="publish" goto block_publish',
3173
+ 'shift',
3174
+ 'goto scan',
3175
+ ':block_publish',
2895
3176
  'if "%AGENT_RUN_ALLOW_PUBLISH%"=="1" goto run',
2896
3177
  `echo agent-run: blocked ${name} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation. 1>&2`,
2897
3178
  'exit /b 42'
2898
3179
  ];
2899
- return ['@echo off', ...guard, ':run', `${name}.exe %*`, ''].join('\n');
3180
+ return [
3181
+ '@echo off',
3182
+ ...guard,
3183
+ ':run',
3184
+ 'if defined AGENT_RUN_REAL_PATH (',
3185
+ ' set "PATH=%AGENT_RUN_REAL_PATH%"',
3186
+ ') else (',
3187
+ ' for /f "tokens=1,* delims=;" %%A in ("%PATH%") do set "PATH=%%B"',
3188
+ ')',
3189
+ `${executable} %*`,
3190
+ ''
3191
+ ].join('\n');
2900
3192
  }
2901
3193
  function isVerbose() {
2902
3194
  return parseBooleanEnv(process.env[VERBOSE_ENV]);