@technomoron/agent-run 0.99.22 → 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
@@ -8,23 +8,34 @@ exports.findProjectRoot = findProjectRoot;
8
8
  exports.resolveProfile = resolveProfile;
9
9
  exports.defaultConfigRoot = defaultConfigRoot;
10
10
  exports.defaultConfigRootSearchCandidates = defaultConfigRootSearchCandidates;
11
+ exports.parseEditorCommand = parseEditorCommand;
11
12
  const childProcess = require("child_process");
12
13
  const fs = require("fs");
13
14
  const os = require("os");
14
15
  const path = require("path");
16
+ const crossSpawn = require("cross-spawn");
15
17
  const jsonc_parser_1 = require("jsonc-parser");
16
18
  const nunjucks = require("nunjucks");
17
19
  const IS_WINDOWS = process.platform === 'win32';
18
20
  const ENV_FILE_NAME = '.agent-run.env';
19
21
  const IGNORE_FILE_NAME = '.agent-run-ignore';
20
- 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']);
21
32
  const CONFIG_ROOT_OVERRIDE_ENV = 'AGENT_RUN_CONFIG_ROOT_OVERRIDE';
22
33
  const CONFIG_DIR_ENV = 'AGENT_CONFIG_DIR';
23
34
  const VERBOSE_ENV = 'AGENT_RUN_VERBOSE';
24
35
  const MANIFEST_FILE_NAME = 'agent-run.jsonc';
25
36
  const LOCAL_TEMPLATE_FILE_NAME = 'local.md.njk';
26
37
  const LIVE_DIR_NAME = 'live';
27
- const PACKAGE_VERSION = '0.99.22';
38
+ const PACKAGE_VERSION = '0.99.23';
28
39
  const UNEXPANDED_TEMPLATE_RE = /\{\{[^}]+\}\}|\{%[^%]+%\}/;
29
40
  const agentRunEnvCache = new Map();
30
41
  const GENERATED_GITIGNORE_ENTRIES = [
@@ -32,7 +43,24 @@ const GENERATED_GITIGNORE_ENTRIES = [
32
43
  '**/live/',
33
44
  '',
34
45
  '# Optional generated caches',
35
- '**/.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/'
36
64
  ];
37
65
  const REQUIRED_GLOBAL_TEMPLATES = [
38
66
  'global/agents/code.md.njk',
@@ -218,6 +246,7 @@ function parseRunCommand(command, inputArgs) {
218
246
  }
219
247
  if (arg === '--') {
220
248
  passthrough = true;
249
+ args.push(arg);
221
250
  continue;
222
251
  }
223
252
  if (arg === '--none') {
@@ -262,15 +291,15 @@ function parseRunCommand(command, inputArgs) {
262
291
  }
263
292
  if (arg === '--network') {
264
293
  if (command !== 'codex') {
265
- fail('--network is only supported for agent-run codex --sandboxed');
294
+ fail('--network is only supported for agent-run codex');
266
295
  }
267
296
  wrapperArgs.codexNetwork = true;
268
297
  continue;
269
298
  }
270
299
  args.push(arg);
271
300
  }
272
- if (wrapperArgs.codexNetwork && wrapperArgs.codexSandboxMode !== 'sandboxed') {
273
- 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');
274
303
  }
275
304
  return { args, command, wrapperArgs };
276
305
  }
@@ -443,9 +472,9 @@ function renderHelp(topic) {
443
472
  ' --generate Generate files without running the tool',
444
473
  '',
445
474
  'Codex wrapper options:',
446
- ' --danger Run Codex with no sandbox: -a never -s danger-full-access (default)',
447
- ' --sandboxed Run Codex with workspace-write sandbox',
448
- ' --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',
449
478
  ''
450
479
  ].join('\n');
451
480
  }
@@ -539,13 +568,15 @@ function runTool(parsed) {
539
568
  const agentDir = path.join(configRoot, profile);
540
569
  verbose(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
541
570
  ensureRunnableProfile(configRoot, agentDir, profile);
571
+ const preview = renderProfile(projectRoot, agentDir, true, command);
572
+ ensureToolEnabled(preview.context, command);
542
573
  if (wrapperArgs.generate) {
543
574
  const rendered = syncAgentProfile(projectRoot, agentDir);
544
575
  printUpdateSummary(rendered);
545
576
  return;
546
577
  }
547
578
  const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
548
- if (localAiFiles.length > 0) {
579
+ if (preview.context.guardrails.forbidRepoAiFiles && localAiFiles.length > 0) {
549
580
  if (!wrapperArgs.local) {
550
581
  failForLocalAiFiles(command, projectRoot, localAiFiles);
551
582
  }
@@ -553,16 +584,17 @@ function runTool(parsed) {
553
584
  }
554
585
  const realBinary = findRealBinary(command);
555
586
  const permissionArgs = getPermissionArgs(command);
556
- syncAgentProfile(projectRoot, agentDir);
587
+ const rendered = syncAgentProfile(projectRoot, agentDir);
557
588
  if (command === 'codex') {
558
- runCodex(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs);
589
+ runCodex(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
559
590
  return;
560
591
  }
561
- runClaude(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs);
592
+ runClaude(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
562
593
  }
563
594
  function showToolProfile(command, projectRoot, agentDir) {
564
595
  const trace = { sourceFiles: new Set() };
565
596
  const rendered = renderProfile(projectRoot, agentDir, true, command, trace);
597
+ ensureToolEnabled(rendered.context, command);
566
598
  const generatedFiles = uniqueSorted(rendered.files.map((file) => file.path));
567
599
  const sourceFiles = uniqueSorted([...trace.sourceFiles]);
568
600
  process.stdout.write([
@@ -579,6 +611,11 @@ function showToolProfile(command, projectRoot, agentDir) {
579
611
  ''
580
612
  ].join('\n'));
581
613
  }
614
+ function ensureToolEnabled(context, command) {
615
+ if (!context.tools[command]) {
616
+ fail(`${command} is disabled for agent-run profile ${context.profile}`);
617
+ }
618
+ }
582
619
  function uniqueSorted(values) {
583
620
  return [...new Set(values.map((value) => path.resolve(value)))].sort((a, b) => a.localeCompare(b));
584
621
  }
@@ -901,31 +938,20 @@ function convertLegacyTemplateVars(content) {
901
938
  .replace(/^\n{2,}/, '\n');
902
939
  }
903
940
  function migrateCodexRuntimeFiles(agentDir) {
904
- const runtimeNames = [
905
- '.personality_migration',
906
- '.tmp',
907
- 'auth.json',
908
- 'cache',
909
- 'history.jsonl',
910
- 'installation_id',
911
- 'log',
912
- 'logs_2.sqlite',
913
- 'models_cache.json',
914
- 'sessions',
915
- 'shell_snapshots',
916
- 'skills',
917
- 'state_5.sqlite',
918
- 'tmp',
919
- 'version.json'
920
- ];
921
- 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');
922
945
  let moved = 0;
923
946
  for (const name of runtimeNames) {
924
947
  const source = path.join(agentDir, name);
925
- const target = path.join(codexHome, name);
926
- if (!fs.existsSync(source) || fs.existsSync(target)) {
948
+ if (!fs.existsSync(source) && !isSymlink(source)) {
927
949
  continue;
928
950
  }
951
+ let target = path.join(codexHome, name);
952
+ if (fs.existsSync(target) || isSymlink(target)) {
953
+ target = nextBackupPath(target);
954
+ }
929
955
  fs.mkdirSync(path.dirname(target), { recursive: true });
930
956
  fs.renameSync(source, target);
931
957
  moved += 1;
@@ -1087,7 +1113,11 @@ function normalizeManifest(manifest, profile) {
1087
1113
  };
1088
1114
  }
1089
1115
  function normalizeSkillName(name) {
1090
- 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;
1091
1121
  }
1092
1122
  function normalizeInstalledSkills(names) {
1093
1123
  return [...new Set(names.map(normalizeSkillName))];
@@ -1114,12 +1144,13 @@ function defaultAgentIncludesForProfile(profile) {
1114
1144
  return [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`, `{{ profile }}/AGENTS-MODS.md`];
1115
1145
  }
1116
1146
  function createNunjucksEnv(configRoot) {
1117
- return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
1147
+ const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
1118
1148
  autoescape: false,
1119
1149
  trimBlocks: true,
1120
1150
  lstripBlocks: true,
1121
1151
  throwOnUndefined: true
1122
1152
  });
1153
+ return env;
1123
1154
  }
1124
1155
  function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env) {
1125
1156
  const date = localDateString();
@@ -1133,6 +1164,7 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
1133
1164
  configRoot,
1134
1165
  date,
1135
1166
  checks: manifest.checks,
1167
+ tools: manifest.tools,
1136
1168
  guardrails: manifest.guardrails
1137
1169
  };
1138
1170
  const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
@@ -1301,20 +1333,23 @@ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = nu
1301
1333
  });
1302
1334
  context.skills = renderSkills(env, configRoot, manifest, context, trace);
1303
1335
  const files = [];
1304
- if (targetTool === null || targetTool === 'codex') {
1336
+ if (context.tools.codex && (targetTool === null || targetTool === 'codex')) {
1305
1337
  const agentsContent = renderToolInstructions('AGENTS.md', env, configRoot, context, false, trace);
1306
- files.push({ path: path.join(context.paths.liveDir, 'AGENTS.md'), content: agentsContent });
1307
- 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) });
1308
1340
  files.push(...renderNativeSkillFiles(context, 'codex'));
1309
1341
  }
1310
- if (targetTool === null || targetTool === 'claude') {
1342
+ if (context.tools.claude && (targetTool === null || targetTool === 'claude')) {
1311
1343
  const claudeContent = renderToolInstructions('CLAUDE.md', env, configRoot, context, true, trace);
1312
1344
  files.push({ path: path.join(context.paths.liveDir, 'CLAUDE.md'), content: claudeContent });
1313
- files.push({ path: path.join(context.paths.liveDir, '.claude', 'CLAUDE.md'), content: claudeContent });
1314
1345
  files.push({
1315
1346
  path: path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
1316
1347
  content: renderClaudeSettings(env, configRoot, context, trace)
1317
1348
  });
1349
+ files.push({
1350
+ path: path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
1351
+ content: claudePluginManifestContent(context)
1352
+ });
1318
1353
  files.push(...renderNativeSkillFiles(context, 'claude'));
1319
1354
  }
1320
1355
  files.push(...renderGuardShims(context));
@@ -1330,12 +1365,61 @@ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = nu
1330
1365
  function syncAgentProfile(projectRoot, agentDir, options) {
1331
1366
  const rendered = renderProfile(projectRoot, agentDir, false, null, undefined, options);
1332
1367
  syncRuntimeDirs(rendered.context);
1368
+ removeStaleGeneratedEntries(rendered);
1333
1369
  for (const file of rendered.files) {
1334
1370
  writeGeneratedFile(file.path, file.content, file.executable ?? false);
1335
1371
  }
1372
+ removeLegacyGeneratedCodexFiles(rendered.context);
1336
1373
  removeLegacyGeneratedClaudeSettings(rendered.context);
1337
1374
  return rendered;
1338
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
+ }
1339
1423
  function removeLegacyGeneratedClaudeSettings(context) {
1340
1424
  for (const legacyPath of [
1341
1425
  path.join(context.paths.liveDir, '.claude', 'settings.json'),
@@ -1465,8 +1549,24 @@ function renderClaudeSettings(env, configRoot, context, trace) {
1465
1549
  ? renderTemplateFile(env, configRoot, templatePath, context, trace)
1466
1550
  : defaultClaudeSettingsContent(context);
1467
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
+ }
1468
1558
  return content.replace(/\n*$/, '\n');
1469
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
+ }
1470
1570
  function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
1471
1571
  const overridePath = `${context.profile}/overrides/${overrideFileName}`;
1472
1572
  if (fs.existsSync(path.join(configRoot, overridePath))) {
@@ -1478,18 +1578,31 @@ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, g
1478
1578
  return null;
1479
1579
  }
1480
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
+ }
1481
1591
  if (IS_WINDOWS) {
1482
- return ['git', 'npm', 'pnpm', 'gh'].map((name) => ({
1592
+ return names.map((name) => ({
1483
1593
  path: path.join(context.paths.binDir, `${name}.cmd`),
1484
1594
  content: windowsShim(name)
1485
1595
  }));
1486
1596
  }
1487
- return [
1488
- { path: path.join(context.paths.binDir, 'git'), content: posixGitShim(), executable: true },
1489
- { path: path.join(context.paths.binDir, 'npm'), content: posixPublishShim('npm'), executable: true },
1490
- { path: path.join(context.paths.binDir, 'pnpm'), content: posixPublishShim('pnpm'), executable: true },
1491
- { path: path.join(context.paths.binDir, 'gh'), content: posixGhShim(), executable: true }
1492
- ];
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
+ }));
1493
1606
  }
1494
1607
  function syncRuntimeDirs(context) {
1495
1608
  for (const dir of [
@@ -1572,16 +1685,11 @@ function nextBackupPath(filePath) {
1572
1685
  return candidate;
1573
1686
  }
1574
1687
  function removeLegacyCodexSkillDirs(context) {
1575
- const legacyCodexSkillsDir = path.join(context.paths.liveDir, '.agents', 'skills');
1576
- 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)) {
1577
1690
  return;
1578
1691
  }
1579
- for (const skill of context.skills) {
1580
- const legacySkillDir = path.join(legacyCodexSkillsDir, skill.name);
1581
- if (fs.existsSync(legacySkillDir)) {
1582
- fs.rmSync(legacySkillDir, { recursive: true, force: true });
1583
- }
1584
- }
1692
+ fs.rmSync(legacyCodexDir, { recursive: true, force: true });
1585
1693
  }
1586
1694
  function removeLegacyCodeReviewSkillDirs(context) {
1587
1695
  if (!context.skills.some((skill) => skill.name === 'code-review-organizer')) {
@@ -1687,12 +1795,30 @@ function renderLegacyAgentsMods(sourceFile, stack = [], trace) {
1687
1795
  let sawLeadingInclude = false;
1688
1796
  let insertedOverrideNote = false;
1689
1797
  let contentStarted = false;
1798
+ let fence = null;
1690
1799
  for (const line of lines) {
1691
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
+ }
1692
1815
  if (!contentStarted && trimmed === '') {
1693
1816
  continue;
1694
1817
  }
1695
- 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('@')) {
1696
1822
  const includePath = trimmed.slice(1).trim();
1697
1823
  if (!includePath) {
1698
1824
  continue;
@@ -1745,28 +1871,39 @@ function checkProject(projectRoot) {
1745
1871
  const findings = [];
1746
1872
  verbose(`checking project ${projectRoot}`);
1747
1873
  const profileResult = resolveProfileResult(projectRoot);
1748
- const excludedDir = profileResult.profile === null ? null : resolveAgentDir(projectRoot);
1749
- const localAiFiles = findLocalAiFiles(projectRoot, excludedDir);
1750
- for (const file of localAiFiles) {
1751
- findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
1752
- }
1753
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
+ }
1754
1878
  findings.push({ message: profileResult.reason, severity: 'ERROR' });
1755
1879
  return findings;
1756
1880
  }
1757
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
+ }
1758
1895
  findings.push(...checkAgentDirectory(projectRoot, agentDir));
1759
1896
  return findings;
1760
1897
  }
1761
1898
  function checkAgentDirectory(projectRoot, agentDir) {
1762
1899
  const findings = [];
1763
1900
  const configRoot = defaultConfigRoot(projectRoot);
1764
- if (!fs.existsSync(path.join(agentDir, MANIFEST_FILE_NAME))) {
1765
- const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
1766
- const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
1767
- if (!fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
1768
- findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
1769
- }
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;
1770
1907
  }
1771
1908
  for (const relativeTemplate of REQUIRED_GLOBAL_TEMPLATES) {
1772
1909
  if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
@@ -1785,20 +1922,26 @@ function checkSourceTree(rootPath) {
1785
1922
  findings: checkProject(projectRoot),
1786
1923
  label: projectRoot
1787
1924
  }));
1788
- 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 };
1789
1926
  }
1790
1927
  function findSourceRepos(rootPath) {
1791
1928
  const resolvedRoot = path.resolve(rootPath);
1792
1929
  const repos = new Set();
1793
- if (!isIgnoredDir(resolvedRoot) && looksLikeRepoRoot(resolvedRoot)) {
1794
- repos.add(resolvedRoot);
1930
+ if (isIgnoredDir(resolvedRoot)) {
1931
+ return [];
1932
+ }
1933
+ if (looksLikeRepoRoot(resolvedRoot)) {
1934
+ return [resolvedRoot];
1795
1935
  }
1796
- walkSourceTree(resolvedRoot, 0, (fullPath, entry, depth) => {
1797
- if (entry.isDirectory() && entry.name === '.git') {
1798
- 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)) {
1799
1941
  return 'skip';
1800
1942
  }
1801
- if (entry.isDirectory() && depth >= 3) {
1943
+ if (fs.existsSync(path.join(fullPath, '.git'))) {
1944
+ repos.add(fullPath);
1802
1945
  return 'skip';
1803
1946
  }
1804
1947
  return undefined;
@@ -1808,6 +1951,12 @@ function findSourceRepos(rootPath) {
1808
1951
  function looksLikeRepoRoot(dir) {
1809
1952
  return fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'));
1810
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
+ }
1811
1960
  function printProjectReport(projectRoot, findings) {
1812
1961
  process.stdout.write(`Check: ${projectRoot}\n`);
1813
1962
  if (findings.length === 0) {
@@ -1822,7 +1971,7 @@ function printProjectReport(projectRoot, findings) {
1822
1971
  function printBatchReport(rootPath, entries) {
1823
1972
  process.stdout.write(`Check all: ${rootPath}\n`);
1824
1973
  if (entries.length === 0) {
1825
- process.stdout.write('WARN no source repos found\n');
1974
+ process.stdout.write('ERROR no source repos found\n');
1826
1975
  return;
1827
1976
  }
1828
1977
  let totalErrors = 0;
@@ -1853,86 +2002,80 @@ function countWarnings(findings) {
1853
2002
  function formatError(error) {
1854
2003
  return error instanceof Error ? error.message : String(error);
1855
2004
  }
1856
- function runCodex(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs) {
1857
- const liveDir = profileLiveDir(agentDir);
1858
- 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');
1859
2010
  if (!fs.existsSync(agentsPath)) {
1860
- failMissingConfig('codex', agentDir);
2011
+ failMissingConfig('codex', context.profileDir);
1861
2012
  }
1862
2013
  const guardBin = path.join(liveDir, 'bin');
1863
- const codexHomeDir = path.join(liveDir, 'memories', 'codex-home');
2014
+ const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
1864
2015
  fs.mkdirSync(codexHomeDir, { recursive: true });
1865
2016
  const env = {
1866
2017
  ...process.env,
1867
2018
  CODEX_HOME: codexHomeDir,
1868
2019
  AGENT_DIR: liveDir,
1869
- AGENT_PROFILE_DIR: agentDir,
2020
+ AGENT_PROFILE_DIR: context.profileDir,
1870
2021
  AGENT_RUN_PROJECT_ROOT: projectRoot,
1871
- PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
2022
+ AGENT_RUN_REAL_PATH: realPath,
2023
+ PATH: `${guardBin}${path.delimiter}${realPath}`
1872
2024
  };
1873
2025
  const codexRuntimeArgs = buildCodexRuntimeArgs(wrapperArgs);
1874
2026
  execCommand(realBinary, [
1875
2027
  ...permissionArgs,
1876
2028
  ...codexRuntimeArgs,
1877
- '--config',
1878
- `system_prompt_file=${agentsPath}`,
1879
- '--config',
1880
- 'project_doc_max_bytes=65536',
1881
- ...globalMemoryArgs(configRoot),
1882
2029
  '-C',
1883
2030
  projectRoot,
1884
- ...(wrapperArgs.codexSandboxMode === 'sandboxed' && wrapperArgs.codexNetwork
2031
+ ...(wrapperArgs.codexSandboxMode !== 'danger' && wrapperArgs.codexNetwork
1885
2032
  ? ['--config', 'sandbox_workspace_write.network_access=true']
1886
2033
  : []),
1887
2034
  ...args
1888
- ], 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));
1889
2036
  }
1890
2037
  function buildCodexRuntimeArgs(wrapperArgs) {
1891
- const mode = wrapperArgs.codexSandboxMode ?? 'danger';
1892
- if (mode === 'sandboxed') {
1893
- return ['-s', 'workspace-write'];
2038
+ if (wrapperArgs.codexSandboxMode === 'danger') {
2039
+ return ['-a', 'never', '-s', 'danger-full-access'];
1894
2040
  }
1895
- return ['-a', 'never', '-s', 'danger-full-access'];
2041
+ return ['-a', 'on-request', '-s', 'workspace-write'];
1896
2042
  }
1897
- function runClaude(realBinary, permissionArgs, agentDir, configRoot, args, projectRoot, wrapperArgs) {
1898
- const liveDir = profileLiveDir(agentDir);
2043
+ function runClaude(realBinary, permissionArgs, context, args, wrapperArgs) {
2044
+ const liveDir = context.paths.liveDir;
2045
+ const projectRoot = context.projectRoot;
1899
2046
  const claudePath = path.join(liveDir, 'CLAUDE.md');
1900
2047
  if (!fs.existsSync(claudePath)) {
1901
- failMissingConfig('claude', agentDir);
2048
+ failMissingConfig('claude', context.profileDir);
1902
2049
  }
1903
2050
  const guardBin = path.join(liveDir, 'bin');
1904
2051
  const claudeConfigDir = path.join(liveDir, '.claude');
2052
+ const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
1905
2053
  const env = {
1906
2054
  ...process.env,
1907
- CLAUDE_CONFIG_DIR: claudeConfigDir,
1908
2055
  AGENT_DIR: liveDir,
1909
- AGENT_PROFILE_DIR: agentDir,
2056
+ AGENT_PROFILE_DIR: context.profileDir,
1910
2057
  AGENT_RUN_PROJECT_ROOT: projectRoot,
1911
- PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
2058
+ AGENT_RUN_REAL_PATH: realPath,
2059
+ PATH: `${guardBin}${path.delimiter}${realPath}`
1912
2060
  };
1913
2061
  execCommand(realBinary, [
1914
2062
  ...permissionArgs,
2063
+ '--append-system-prompt-file',
2064
+ claudePath,
1915
2065
  '--settings',
1916
2066
  path.join(claudeConfigDir, 'agent-run-settings.json'),
1917
- '--add-dir',
1918
- projectRoot,
1919
- '--add-dir',
1920
- liveDir,
1921
- ...globalMemoryArgs(configRoot),
2067
+ '--plugin-dir',
2068
+ claudeConfigDir,
1922
2069
  ...args
1923
- ], shouldUseShell(realBinary), env, projectRoot, (code) => postflightProjectCheck(projectRoot, agentDir, wrapperArgs.local, code));
1924
- }
1925
- function profileLiveDir(agentDir) {
1926
- return path.join(agentDir, LIVE_DIR_NAME);
1927
- }
1928
- function globalMemoryArgs(configRoot) {
1929
- const memoryDir = globalMemoryDir(configRoot);
1930
- return fs.existsSync(memoryDir) ? ['--add-dir', memoryDir] : [];
2070
+ ], env, projectRoot, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
1931
2071
  }
1932
2072
  function globalMemoryDir(configRoot) {
1933
2073
  return path.join(configRoot, 'notes', 'memory');
1934
2074
  }
1935
- function postflightProjectCheck(projectRoot, agentDir, allowLocal, code) {
2075
+ function postflightProjectCheck(projectRoot, agentDir, forbidRepoAiFiles, allowLocal, code) {
2076
+ if (!forbidRepoAiFiles) {
2077
+ return code;
2078
+ }
1936
2079
  const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
1937
2080
  if (localAiFiles.length > 0) {
1938
2081
  warnForLocalAiFiles(null, projectRoot, localAiFiles);
@@ -2020,17 +2163,16 @@ function isSamePathOrDescendant(candidatePath, parentPath) {
2020
2163
  function shouldPruneProjectEntry(entry) {
2021
2164
  return (entry.isDirectory() &&
2022
2165
  (entry.name === '.git' ||
2023
- entry.name === '.claude' ||
2024
2166
  entry.name === 'node_modules' ||
2025
2167
  entry.name === '.pnpm-store' ||
2026
2168
  entry.name === 'dist' ||
2027
2169
  entry.name === 'build'));
2028
2170
  }
2029
2171
  function isLocalAiDirectory(entry) {
2030
- return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex' || entry.name === '.agents');
2172
+ return LOCAL_AI_DIRECTORY_NAMES.has(entry.name);
2031
2173
  }
2032
2174
  function isLocalAiFile(entry) {
2033
- return entry.isFile() && LOCAL_AI_FILE_NAMES.has(entry.name);
2175
+ return LOCAL_AI_FILE_NAMES.has(entry.name);
2034
2176
  }
2035
2177
  function isIgnoredDir(dir) {
2036
2178
  if (fs.existsSync(path.join(dir, IGNORE_FILE_NAME))) {
@@ -2063,13 +2205,13 @@ function findProjectRoot(cwd) {
2063
2205
  let gitRoot = '';
2064
2206
  for (;;) {
2065
2207
  const packagePath = path.join(dir, 'package.json');
2208
+ if (isWorkspaceRoot(dir)) {
2209
+ workspaceRoot = dir;
2210
+ }
2066
2211
  if (fs.existsSync(packagePath)) {
2067
2212
  if (!nearestPackageRoot) {
2068
2213
  nearestPackageRoot = dir;
2069
2214
  }
2070
- if (isWorkspaceRoot(dir)) {
2071
- workspaceRoot = dir;
2072
- }
2073
2215
  }
2074
2216
  if (fs.existsSync(path.join(dir, '.git'))) {
2075
2217
  gitRoot = dir;
@@ -2143,26 +2285,16 @@ function resolveGitHubProfile(projectRoot) {
2143
2285
  return remote === null ? null : parseGitHubRemoteProfile(remote);
2144
2286
  }
2145
2287
  function readGitOrigin(projectRoot) {
2146
- const configPath = path.join(projectRoot, '.git', 'config');
2147
- 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) {
2148
2294
  return null;
2149
2295
  }
2150
- const lines = fs.readFileSync(configPath, 'utf8').replace(/\r\n/g, '\n').split('\n');
2151
- let inOrigin = false;
2152
- for (const line of lines) {
2153
- const trimmed = line.trim();
2154
- if (trimmed.startsWith('[')) {
2155
- inOrigin = trimmed === '[remote "origin"]';
2156
- continue;
2157
- }
2158
- if (inOrigin && trimmed.startsWith('url')) {
2159
- const equalsIndex = trimmed.indexOf('=');
2160
- if (equalsIndex >= 0) {
2161
- return trimmed.slice(equalsIndex + 1).trim();
2162
- }
2163
- }
2164
- }
2165
- return null;
2296
+ const remote = result.stdout.trim();
2297
+ return remote || null;
2166
2298
  }
2167
2299
  function parseGitHubRemoteProfile(remote) {
2168
2300
  const patterns = [
@@ -2187,7 +2319,8 @@ function parseProfile(profile, source) {
2187
2319
  return { profile: null, reason: `${source} must be relative to the config root, not an absolute path` };
2188
2320
  }
2189
2321
  const segments = normalized.split('/').filter((segment) => segment.length > 0);
2190
- 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))) {
2191
2324
  return { profile: null, reason: `${source} must be a clean relative path like org/my-project` };
2192
2325
  }
2193
2326
  return { profile: segments.join('/'), reason: '' };
@@ -2405,12 +2538,12 @@ function isExecutable(filePath) {
2405
2538
  }
2406
2539
  }
2407
2540
  function execTool(command, args) {
2408
- verbose(`exec tool: ${formatCommand(command, args)} shell=${String(shouldUseShell(command))}`);
2409
- execCommand(command, args, shouldUseShell(command));
2541
+ verbose(`exec tool: ${formatCommand(command, args)}`);
2542
+ execCommand(command, args);
2410
2543
  }
2411
- function execCommand(command, args, shell, env, cwd, onExit) {
2412
- verbose(`spawn: ${formatCommand(command, args)} shell=${String(shell)} cwd=${cwd ?? process.cwd()}`);
2413
- 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' });
2414
2547
  child.on('exit', (code, signal) => {
2415
2548
  if (signal) {
2416
2549
  process.kill(process.pid, signal);
@@ -2426,33 +2559,106 @@ function execCommand(command, args, shell, env, cwd, onExit) {
2426
2559
  function openEditor(filePath) {
2427
2560
  const visual = process.env.VISUAL?.trim();
2428
2561
  if (visual) {
2429
- execCommand(visual, [filePath], true);
2562
+ runConfiguredEditor(visual, filePath);
2430
2563
  return;
2431
2564
  }
2432
2565
  const editor = process.env.EDITOR?.trim();
2433
2566
  if (editor) {
2434
- execCommand(editor, [filePath], true);
2567
+ runConfiguredEditor(editor, filePath);
2435
2568
  return;
2436
2569
  }
2437
2570
  const vscodeCommand = findVsCodeEditorCommand();
2438
2571
  if (vscodeCommand !== null) {
2439
- execCommand(vscodeCommand, ['--reuse-window', filePath], shouldUseShell(vscodeCommand));
2572
+ execCommand(vscodeCommand, ['--reuse-window', filePath]);
2440
2573
  return;
2441
2574
  }
2442
2575
  const fallbackEditor = findFallbackEditor();
2443
2576
  if (fallbackEditor !== null) {
2444
- execCommand(fallbackEditor, [filePath], shouldUseShell(fallbackEditor));
2577
+ execCommand(fallbackEditor, [filePath]);
2445
2578
  return;
2446
2579
  }
2447
2580
  if (IS_WINDOWS) {
2448
- execCommand('cmd.exe', ['/c', 'start', '', filePath], false);
2581
+ execCommand('explorer.exe', [filePath]);
2449
2582
  return;
2450
2583
  }
2451
2584
  if (process.platform === 'darwin') {
2452
- execCommand('open', [filePath], false);
2585
+ execCommand('open', [filePath]);
2453
2586
  return;
2454
2587
  }
2455
- 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;
2456
2662
  }
2457
2663
  function findVsCodeEditorCommand() {
2458
2664
  if (!isRunningInVsCodeTerminal()) {
@@ -2497,13 +2703,6 @@ function findExecutable(command) {
2497
2703
  }
2498
2704
  return null;
2499
2705
  }
2500
- function shouldUseShell(command) {
2501
- if (!IS_WINDOWS) {
2502
- return false;
2503
- }
2504
- const extension = path.extname(command).toLowerCase();
2505
- return extension === '.cmd' || extension === '.bat';
2506
- }
2507
2706
  function localDateString() {
2508
2707
  const now = new Date();
2509
2708
  const year = String(now.getFullYear()).padStart(4, '0');
@@ -2653,7 +2852,8 @@ function defaultReleasePackageCheckSkill() {
2653
2852
  '- The current branch has an upstream configured and is in sync with it after fetching.',
2654
2853
  '- The repository does not contain agent files or directories such as `AGENTS.md`, `CLAUDE.md`, `codex.md`, `.claude`, or `.codex`.',
2655
2854
  '- The repository root has a lockfile: `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lockb`, or `bun.lock`.',
2656
- '- 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`.',
2657
2857
  '- GitHub workflows do not reference `node20` or `node22`.',
2658
2858
  '- The git working tree is clean for files under the checked package directory.',
2659
2859
  '- If files beyond `CHANGES` changed in the checked package directory, that package `CHANGES` file is also changed.',
@@ -2769,8 +2969,8 @@ function defaultToolInstructionsTemplate(isClaude) {
2769
2969
  '## Tool Note',
2770
2970
  '',
2771
2971
  isClaude
2772
- ? 'Claude uses the generated config directory under {{ paths.liveDir }}/.claude and may read {{ paths.liveDir }} via `--add-dir`.'
2773
- : '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 }}.',
2774
2974
  '',
2775
2975
  '## Mandatory Path Rule',
2776
2976
  '',
@@ -2785,14 +2985,14 @@ function defaultCodexConfigTemplate() {
2785
2985
  '# Generated by agent-run. Do not edit directly.',
2786
2986
  '',
2787
2987
  'project_doc_max_bytes = 65536',
2788
- 'sandbox_mode = "danger-full-access"',
2988
+ 'approval_policy = "on-request"',
2989
+ 'sandbox_mode = "workspace-write"',
2789
2990
  '',
2790
2991
  '[sandbox_workspace_write]',
2791
2992
  'writable_roots = [',
2792
- ' "{{ projectRoot }}",',
2793
- ' "{{ agentDir }}",',
2794
- ' "{{ profileDir }}",',
2795
- ' "{{ paths.globalMemoryDir }}"',
2993
+ ' {{ projectRoot | dump }},',
2994
+ ' {{ paths.reviewDir | dump }},',
2995
+ ' {{ paths.memoriesDir | dump }}',
2796
2996
  ']',
2797
2997
  'network_access = false',
2798
2998
  ''
@@ -2801,19 +3001,17 @@ function defaultCodexConfigTemplate() {
2801
3001
  function defaultClaudeSettingsTemplate() {
2802
3002
  const envBlock = [
2803
3003
  ' "env": {',
2804
- ' "AGENT_DIR": "{{ agentDir }}",',
2805
- ' "AGENT_PROFILE_DIR": "{{ profileDir }}",',
2806
- ' "AGENT_RUN_PROJECT_ROOT": "{{ projectRoot }}",',
2807
- ' "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 }}',
2808
3008
  ' }'
2809
3009
  ].join('\n');
2810
3010
  return [
2811
3011
  '{% if permissionsAllow.length %}{',
2812
3012
  envBlock + ',',
2813
3013
  ' "permissions": {',
2814
- ' "allow": [',
2815
- ' "{{ permissionsAllow | join(\'",\\n "\') }}"',
2816
- ' ]',
3014
+ ' "allow": {{ permissionsAllow | dump }}',
2817
3015
  ' }',
2818
3016
  '}',
2819
3017
  '{% else %}{',
@@ -2827,14 +3025,14 @@ function defaultCodexConfigContent(context) {
2827
3025
  '# Generated by agent-run. Do not edit directly.',
2828
3026
  '',
2829
3027
  'project_doc_max_bytes = 65536',
2830
- 'sandbox_mode = "danger-full-access"',
3028
+ 'approval_policy = "on-request"',
3029
+ 'sandbox_mode = "workspace-write"',
2831
3030
  '',
2832
3031
  '[sandbox_workspace_write]',
2833
3032
  'writable_roots = [',
2834
3033
  ` ${jsonString(context.projectRoot)},`,
2835
- ` ${jsonString(context.agentDir)},`,
2836
- ` ${jsonString(context.profileDir)},`,
2837
- ` ${jsonString(context.paths.globalMemoryDir)}`,
3034
+ ` ${jsonString(context.paths.reviewDir)},`,
3035
+ ` ${jsonString(context.paths.memoriesDir)}`,
2838
3036
  ']',
2839
3037
  'network_access = false',
2840
3038
  ''
@@ -2870,18 +3068,17 @@ function posixGitShim() {
2870
3068
  return [
2871
3069
  '#!/usr/bin/env bash',
2872
3070
  'set -euo pipefail',
2873
- 'case "${1:-}" in',
2874
- ' commit|tag|push)',
2875
- ' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
2876
- ' echo "agent-run: blocked git $1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
2877
- ' exit 42',
2878
- ' fi',
2879
- ' ;;',
2880
- 'esac',
2881
- 'if command -v /usr/bin/git >/dev/null 2>&1; then',
2882
- ' exec /usr/bin/git "$@"',
2883
- 'fi',
2884
- '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'),
2885
3082
  ''
2886
3083
  ].join('\n');
2887
3084
  }
@@ -2889,14 +3086,13 @@ function posixPublishShim(tool) {
2889
3086
  return [
2890
3087
  '#!/usr/bin/env bash',
2891
3088
  'set -euo pipefail',
2892
- 'if [ "${1:-}" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
2893
- ` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
2894
- ' exit 42',
2895
- 'fi',
2896
- `if command -v /usr/bin/${tool} >/dev/null 2>&1; then`,
2897
- ` exec /usr/bin/${tool} "$@"`,
2898
- 'fi',
2899
- `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),
2900
3096
  ''
2901
3097
  ].join('\n');
2902
3098
  }
@@ -2904,44 +3100,95 @@ function posixGhShim() {
2904
3100
  return [
2905
3101
  '#!/usr/bin/env bash',
2906
3102
  'set -euo pipefail',
2907
- 'if [ "${1:-}" = "release" ] && [ "${2:-}" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
2908
- ' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
2909
- ' exit 42',
2910
- 'fi',
2911
- 'if command -v /usr/bin/gh >/dev/null 2>&1; then',
2912
- ' exec /usr/bin/gh "$@"',
2913
- 'fi',
2914
- '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'),
2915
3113
  ''
2916
3114
  ].join('\n');
2917
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
+ }
2918
3139
  function windowsShim(name) {
3140
+ const executable = name === 'npm' || name === 'pnpm' ? `${name}.cmd` : `${name}.exe`;
2919
3141
  const guard = name === 'git'
2920
3142
  ? [
2921
- 'if /I "%1"=="commit" goto block_git',
2922
- 'if /I "%1"=="tag" goto block_git',
2923
- 'if /I "%1"=="push" goto block_git',
2924
- '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',
2925
3150
  ':block_git',
2926
3151
  'if "%AGENT_RUN_ALLOW_GIT_WRITE%"=="1" goto run',
2927
- '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',
2928
3153
  'exit /b 42'
2929
3154
  ]
2930
3155
  : name === 'gh'
2931
3156
  ? [
2932
- 'if /I not "%1"=="release" goto run',
2933
- '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',
2934
3165
  'if "%AGENT_RUN_ALLOW_GITHUB_RELEASE%"=="1" goto run',
2935
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',
2936
3167
  'exit /b 42'
2937
3168
  ]
2938
3169
  : [
2939
- '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',
2940
3176
  'if "%AGENT_RUN_ALLOW_PUBLISH%"=="1" goto run',
2941
3177
  `echo agent-run: blocked ${name} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation. 1>&2`,
2942
3178
  'exit /b 42'
2943
3179
  ];
2944
- 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');
2945
3192
  }
2946
3193
  function isVerbose() {
2947
3194
  return parseBooleanEnv(process.env[VERBOSE_ENV]);