@technomoron/agent-run 0.99.22 → 0.99.24
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/CHANGES +29 -0
- package/README.md +33 -26
- package/dist/agent-run.js +460 -213
- package/examples/basic-config/agent-config/gitignore +17 -0
- package/examples/basic-config/agent-config/global/skills/release-package-check/SKILL.md.njk +3 -0
- package/examples/basic-config/agent-config/global/tool-templates/claude-settings.json.njk +11 -13
- package/examples/basic-config/agent-config/global/tool-templates/codex-config.toml.njk +5 -6
- package/examples/basic-config/agent-config/starter/basic-project/overrides/claude-settings.json.njk +11 -13
- package/examples/basic-config/agent-config/starter/basic-project/overrides/codex-config.toml.njk +5 -6
- package/ops/systemd/ai-tools-update.service +8 -3
- package/package.json +7 -4
- package/scripts/install-systemd-jobs.sh +0 -2
- package/scripts/update-ai-tools.sh +26 -8
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([
|
|
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.
|
|
38
|
+
const PACKAGE_VERSION = '0.99.24';
|
|
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
|
|
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
|
|
273
|
-
fail('--network
|
|
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
|
|
447
|
-
' --sandboxed Run Codex with workspace-write sandbox',
|
|
448
|
-
' --network Enable network for
|
|
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,
|
|
589
|
+
runCodex(realBinary, permissionArgs, rendered.context, args, wrapperArgs);
|
|
559
590
|
return;
|
|
560
591
|
}
|
|
561
|
-
runClaude(realBinary, permissionArgs,
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1307
|
-
files.push({ path: path.join(context.paths.
|
|
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
|
|
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
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
|
1576
|
-
if (
|
|
1688
|
+
const legacyCodexDir = path.join(context.paths.liveDir, '.agents');
|
|
1689
|
+
if (isSamePathOrDescendant(context.paths.codexSkillsDir, legacyCodexDir)) {
|
|
1577
1690
|
return;
|
|
1578
1691
|
}
|
|
1579
|
-
|
|
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 (
|
|
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
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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 (
|
|
1794
|
-
|
|
1930
|
+
if (isIgnoredDir(resolvedRoot)) {
|
|
1931
|
+
return [];
|
|
1932
|
+
}
|
|
1933
|
+
if (looksLikeRepoRoot(resolvedRoot)) {
|
|
1934
|
+
return [resolvedRoot];
|
|
1795
1935
|
}
|
|
1796
|
-
walkSourceTree(resolvedRoot, 0, (fullPath, entry
|
|
1797
|
-
if (entry.isDirectory()
|
|
1798
|
-
|
|
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 (
|
|
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('
|
|
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,
|
|
1857
|
-
const liveDir =
|
|
1858
|
-
const
|
|
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',
|
|
2011
|
+
failMissingConfig('codex', context.profileDir);
|
|
1861
2012
|
}
|
|
1862
2013
|
const guardBin = path.join(liveDir, 'bin');
|
|
1863
|
-
const
|
|
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:
|
|
2020
|
+
AGENT_PROFILE_DIR: context.profileDir,
|
|
1870
2021
|
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
1871
|
-
|
|
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
|
|
2031
|
+
...(wrapperArgs.codexSandboxMode !== 'danger' && wrapperArgs.codexNetwork
|
|
1885
2032
|
? ['--config', 'sandbox_workspace_write.network_access=true']
|
|
1886
2033
|
: []),
|
|
1887
2034
|
...args
|
|
1888
|
-
],
|
|
2035
|
+
], env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, context.profileDir, context.guardrails.forbidRepoAiFiles, wrapperArgs.local, code));
|
|
1889
2036
|
}
|
|
1890
2037
|
function buildCodexRuntimeArgs(wrapperArgs) {
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
return ['-s', 'workspace-write'];
|
|
2038
|
+
if (wrapperArgs.codexSandboxMode === 'danger') {
|
|
2039
|
+
return ['-a', 'never', '-s', 'danger-full-access'];
|
|
1894
2040
|
}
|
|
1895
|
-
return ['-a', '
|
|
2041
|
+
return ['-a', 'on-request', '-s', 'workspace-write'];
|
|
1896
2042
|
}
|
|
1897
|
-
function runClaude(realBinary, permissionArgs,
|
|
1898
|
-
const liveDir =
|
|
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',
|
|
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:
|
|
2056
|
+
AGENT_PROFILE_DIR: context.profileDir,
|
|
1910
2057
|
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
1911
|
-
|
|
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
|
-
'--
|
|
1918
|
-
|
|
1919
|
-
'--add-dir',
|
|
1920
|
-
liveDir,
|
|
1921
|
-
...globalMemoryArgs(configRoot),
|
|
2067
|
+
'--plugin-dir',
|
|
2068
|
+
claudeConfigDir,
|
|
1922
2069
|
...args
|
|
1923
|
-
],
|
|
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
|
|
2172
|
+
return LOCAL_AI_DIRECTORY_NAMES.has(entry.name);
|
|
2031
2173
|
}
|
|
2032
2174
|
function isLocalAiFile(entry) {
|
|
2033
|
-
return
|
|
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
|
|
2147
|
-
|
|
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
|
|
2151
|
-
|
|
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 ||
|
|
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)}
|
|
2409
|
-
execCommand(command, args
|
|
2541
|
+
verbose(`exec tool: ${formatCommand(command, args)}`);
|
|
2542
|
+
execCommand(command, args);
|
|
2410
2543
|
}
|
|
2411
|
-
function execCommand(command, args,
|
|
2412
|
-
verbose(`spawn: ${formatCommand(command, args)}
|
|
2413
|
-
const child =
|
|
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
|
-
|
|
2562
|
+
runConfiguredEditor(visual, filePath);
|
|
2430
2563
|
return;
|
|
2431
2564
|
}
|
|
2432
2565
|
const editor = process.env.EDITOR?.trim();
|
|
2433
2566
|
if (editor) {
|
|
2434
|
-
|
|
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]
|
|
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]
|
|
2577
|
+
execCommand(fallbackEditor, [filePath]);
|
|
2445
2578
|
return;
|
|
2446
2579
|
}
|
|
2447
2580
|
if (IS_WINDOWS) {
|
|
2448
|
-
execCommand('
|
|
2581
|
+
execCommand('explorer.exe', [filePath]);
|
|
2449
2582
|
return;
|
|
2450
2583
|
}
|
|
2451
2584
|
if (process.platform === 'darwin') {
|
|
2452
|
-
execCommand('open', [filePath]
|
|
2585
|
+
execCommand('open', [filePath]);
|
|
2453
2586
|
return;
|
|
2454
2587
|
}
|
|
2455
|
-
execCommand('xdg-open', [filePath]
|
|
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
|
|
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
|
|
2773
|
-
: 'Codex
|
|
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
|
-
'
|
|
2988
|
+
'approval_policy = "on-request"',
|
|
2989
|
+
'sandbox_mode = "workspace-write"',
|
|
2789
2990
|
'',
|
|
2790
2991
|
'[sandbox_workspace_write]',
|
|
2791
2992
|
'writable_roots = [',
|
|
2792
|
-
'
|
|
2793
|
-
'
|
|
2794
|
-
'
|
|
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":
|
|
2805
|
-
' "AGENT_PROFILE_DIR":
|
|
2806
|
-
' "AGENT_RUN_PROJECT_ROOT":
|
|
2807
|
-
' "AGENT_GLOBAL_MEMORY_DIR":
|
|
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
|
-
'
|
|
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.
|
|
2836
|
-
` ${jsonString(context.
|
|
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
|
-
'
|
|
2874
|
-
'
|
|
2875
|
-
'
|
|
2876
|
-
'
|
|
2877
|
-
'
|
|
2878
|
-
'
|
|
2879
|
-
'
|
|
2880
|
-
'
|
|
2881
|
-
'
|
|
2882
|
-
'
|
|
2883
|
-
'
|
|
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
|
-
'
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
'
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
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
|
-
'
|
|
2908
|
-
'
|
|
2909
|
-
'
|
|
2910
|
-
'
|
|
2911
|
-
'
|
|
2912
|
-
'
|
|
2913
|
-
'
|
|
2914
|
-
'
|
|
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
|
-
'
|
|
2922
|
-
'if
|
|
2923
|
-
'if /I "
|
|
2924
|
-
'goto
|
|
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
|
|
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
|
-
'
|
|
2933
|
-
'
|
|
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
|
-
'
|
|
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 [
|
|
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]);
|