@technomoron/agent-run 0.99.3 → 0.99.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGES +17 -1
  2. package/README.md +69 -29
  3. package/dist/agent-run.js +1506 -378
  4. package/package.json +6 -1
package/dist/agent-run.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.renderAgentsMods = renderAgentsMods;
5
- exports.syncGeneratedAgentsFile = syncGeneratedAgentsFile;
6
4
  exports.main = main;
7
5
  exports.parseInvocation = parseInvocation;
8
6
  exports.resolveAgentDir = resolveAgentDir;
@@ -13,120 +11,54 @@ const childProcess = require("child_process");
13
11
  const fs = require("fs");
14
12
  const os = require("os");
15
13
  const path = require("path");
14
+ const jsonc_parser_1 = require("jsonc-parser");
15
+ const nunjucks = require("nunjucks");
16
16
  const IS_WINDOWS = process.platform === 'win32';
17
17
  const ENV_FILE_NAME = '.agent-run.env';
18
18
  const IGNORE_FILE_NAME = '.agent-run-ignore';
19
- const LOCAL_AI_FILE_NAMES = new Set(['AGENTS.md', 'AGENTS-MODS.md', 'CLAUDE.md', 'codex.md']);
19
+ const LOCAL_AI_FILE_NAMES = new Set(['AGENTS.md', 'AGENTS-MODS.md', 'AGENTS.override.md', 'CLAUDE.md', 'codex.md']);
20
20
  const CONFIG_ROOT_OVERRIDE_ENV = 'AGENT_RUN_CONFIG_ROOT_OVERRIDE';
21
+ const CONFIG_DIR_ENV = 'AGENT_CONFIG_DIR';
21
22
  const VERBOSE_ENV = 'AGENT_RUN_VERBOSE';
23
+ const MANIFEST_FILE_NAME = 'agent-run.jsonc';
24
+ const LOCAL_TEMPLATE_FILE_NAME = 'local.md.njk';
25
+ const UNEXPANDED_TEMPLATE_RE = /\{\{[^}]+\}\}|\{%[^%]+%\}/;
22
26
  const agentRunEnvCache = new Map();
23
- function renderAgentsMods(sourceFile, stack = []) {
24
- const resolvedSource = path.resolve(sourceFile);
25
- verbose(`render ${resolvedSource}`);
26
- if (stack.includes(resolvedSource)) {
27
- throw new Error(`Include cycle detected: ${[...stack, resolvedSource].join(' -> ')}`);
28
- }
29
- const lines = fs.readFileSync(resolvedSource, 'utf8').replace(/\r\n/g, '\n').split('\n');
30
- const output = [];
31
- const nextStack = [...stack, resolvedSource];
32
- let sawLeadingInclude = false;
33
- let insertedOverrideNote = false;
34
- let contentStarted = false;
35
- for (const line of lines) {
36
- const trimmed = line.trim();
37
- if (!contentStarted && trimmed === '') {
38
- continue;
39
- }
40
- if (trimmed.startsWith('@')) {
41
- const includePath = trimmed.slice(1).trim();
42
- if (!includePath) {
43
- continue;
44
- }
45
- const resolvedInclude = resolveIncludePath(resolvedSource, includePath);
46
- verbose(`include ${includePath} -> ${resolvedInclude}`);
47
- output.push(renderAgentsMods(resolvedInclude, nextStack));
48
- if (!contentStarted) {
49
- sawLeadingInclude = true;
50
- }
51
- continue;
52
- }
53
- if (sawLeadingInclude && !insertedOverrideNote) {
54
- verbose(`insert override note in ${resolvedSource}`);
55
- output.push('');
56
- output.push('If anything below this point conflicts with anything included above,');
57
- output.push('the later instructions below take precedence.');
58
- output.push('');
59
- insertedOverrideNote = true;
60
- }
61
- output.push(line);
62
- contentStarted = true;
63
- }
64
- return output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
65
- }
66
- function syncGeneratedAgentsFile(agentDir) {
67
- const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
68
- const agentsPath = path.join(agentDir, 'AGENTS.md');
69
- verbose(`sync generated files from ${modsPath}`);
70
- const rendered = expandAgentsTemplateVariables(renderAgentsMods(modsPath), buildAgentsTemplateContext(agentDir));
71
- fs.writeFileSync(agentsPath, rendered, 'utf8');
72
- verbose(`write ${agentsPath}`);
73
- const claudePath = path.join(agentDir, 'CLAUDE.md');
74
- fs.writeFileSync(claudePath, '@AGENTS.md\n', 'utf8');
75
- verbose(`write ${claudePath}`);
76
- }
77
- function buildAgentsTemplateContext(agentDir) {
78
- const resolvedAgentDir = path.resolve(agentDir);
79
- const resolvedConfigRoot = path.resolve(defaultConfigRoot());
80
- const profile = path.relative(resolvedConfigRoot, resolvedAgentDir).replace(/\\/g, '/');
81
- return {
82
- agentDir: resolvedAgentDir,
83
- agentsModsPath: path.join(resolvedAgentDir, 'AGENTS-MODS.md'),
84
- agentsPath: path.join(resolvedAgentDir, 'AGENTS.md'),
85
- claudePath: path.join(resolvedAgentDir, 'CLAUDE.md'),
86
- configRoot: resolvedConfigRoot,
87
- profile
88
- };
89
- }
90
- function expandAgentsTemplateVariables(content, context) {
91
- const replacements = new Map([
92
- ['AGENT_DIR', context.agentDir],
93
- ['AGENTS_MODS_PATH', context.agentsModsPath],
94
- ['AGENTS_PATH', context.agentsPath],
95
- ['CLAUDE_PATH', context.claudePath],
96
- ['CONFIG_ROOT', context.configRoot],
97
- ['PROFILE', context.profile]
98
- ]);
99
- return content.replace(/\{\{([A-Z_]+)\}\}/g, (match, key) => replacements.get(key) ?? match);
100
- }
101
- function resolveIncludePath(sourceFile, includePath) {
102
- if (path.isAbsolute(includePath)) {
103
- verbose(`resolve include absolute ${includePath}`);
104
- return includePath;
105
- }
106
- const sourceRelativePath = path.resolve(path.dirname(sourceFile), includePath);
107
- if (fs.existsSync(sourceRelativePath)) {
108
- verbose(`resolve include relative ${includePath} -> ${sourceRelativePath}`);
109
- return sourceRelativePath;
110
- }
111
- const includeRoot = findIncludeRoot(sourceFile);
112
- const fallbackPath = path.resolve(includeRoot, includePath.replace(/^(\.\.\/)+/, ''));
113
- verbose(`resolve include root fallback ${includePath} -> ${fallbackPath}`);
114
- return fallbackPath;
115
- }
116
- function findIncludeRoot(sourceFile) {
117
- let dir = path.dirname(sourceFile);
118
- for (;;) {
119
- const templatesDir = path.join(dir, 'templates');
120
- if (fs.existsSync(templatesDir)) {
121
- return dir;
122
- }
123
- const parent = path.dirname(dir);
124
- if (parent === dir) {
125
- return path.dirname(sourceFile);
126
- }
127
- dir = parent;
128
- }
129
- }
27
+ const GENERATED_GITIGNORE_ENTRIES = [
28
+ '# Generated agent-run files',
29
+ '**/AGENTS.md',
30
+ '**/CLAUDE.md',
31
+ '**/GEMINI.md',
32
+ '**/config.toml',
33
+ '',
34
+ '# Generated native agent homes',
35
+ '**/.agents/',
36
+ '**/.claude/',
37
+ '**/.codex/',
38
+ '',
39
+ '# Generated runtime files',
40
+ '**/bin/',
41
+ '**/reviews/',
42
+ '**/memories/',
43
+ '',
44
+ '# Generated review reports',
45
+ '**/REVIEW-*.md',
46
+ '',
47
+ '# Optional generated caches',
48
+ '**/.agent-run-cache/'
49
+ ];
50
+ const REQUIRED_GLOBAL_TEMPLATES = [
51
+ 'global/agents/code.md.njk',
52
+ 'global/agents/writing.md.njk',
53
+ 'global/snippets/git-rules.md.njk',
54
+ 'global/snippets/no-ai-files.md.njk',
55
+ 'global/snippets/verification.md.njk',
56
+ 'global/tool-templates/codex-config.toml.njk',
57
+ 'global/tool-templates/claude-settings.json.njk',
58
+ 'global/skills/commit-workflow/SKILL.md.njk',
59
+ 'global/skills/github-release/SKILL.md.njk',
60
+ 'global/skills/code-review/SKILL.md.njk'
61
+ ];
130
62
  function main(invokedTool, argv) {
131
63
  const parsed = parseInvocation(invokedTool, argv);
132
64
  if (parsed.command === 'check') {
@@ -145,6 +77,10 @@ function main(invokedTool, argv) {
145
77
  runUpdate(parsed);
146
78
  return;
147
79
  }
80
+ if (parsed.command === 'migrate-config') {
81
+ runMigrateConfig(parsed);
82
+ return;
83
+ }
148
84
  runTool(parsed);
149
85
  }
150
86
  function parseInvocation(invokedTool, argv) {
@@ -159,12 +95,15 @@ function parseInvocation(invokedTool, argv) {
159
95
  }
160
96
  if (command === null) {
161
97
  if (inputArgs.length === 0) {
162
- fail('usage: agent-run <codex|claude|check|init|edit|update> [options]');
98
+ fail('usage: agent-run <codex|claude|check|init|edit|update> [options] (run with --help for details)');
163
99
  }
164
100
  const firstArg = inputArgs.shift();
101
+ if (isHelpFlag(firstArg)) {
102
+ printHelp('general');
103
+ }
165
104
  command = normalizeCommandName(firstArg ?? '');
166
105
  if (command === null) {
167
- fail(`unknown command: ${firstArg ?? ''}`);
106
+ fail(`unknown command: ${firstArg ?? ''} (run with --help for usage)`);
168
107
  }
169
108
  }
170
109
  if (command === 'check') {
@@ -179,6 +118,9 @@ function parseInvocation(invokedTool, argv) {
179
118
  if (command === 'update') {
180
119
  return parseUpdateCommand(inputArgs);
181
120
  }
121
+ if (command === 'migrate-config') {
122
+ return parseMigrateConfigCommand(inputArgs);
123
+ }
182
124
  return parseRunCommand(command, inputArgs);
183
125
  }
184
126
  function extractGlobalOptions(argv) {
@@ -227,7 +169,9 @@ function extractGlobalOptions(argv) {
227
169
  function parseRunCommand(command, inputArgs) {
228
170
  const wrapperArgs = {
229
171
  none: false,
230
- create: false
172
+ create: false,
173
+ codexSandboxMode: null,
174
+ codexNetwork: false
231
175
  };
232
176
  const args = [];
233
177
  let passthrough = false;
@@ -248,14 +192,47 @@ function parseRunCommand(command, inputArgs) {
248
192
  wrapperArgs.create = true;
249
193
  continue;
250
194
  }
195
+ if (arg === '--danger') {
196
+ if (command !== 'codex') {
197
+ fail('--danger is only supported for agent-run codex');
198
+ }
199
+ if (wrapperArgs.codexSandboxMode === 'sandboxed') {
200
+ fail('cannot combine --danger and --sandboxed');
201
+ }
202
+ wrapperArgs.codexSandboxMode = 'danger';
203
+ continue;
204
+ }
205
+ if (arg === '--sandboxed') {
206
+ if (command !== 'codex') {
207
+ fail('--sandboxed is only supported for agent-run codex');
208
+ }
209
+ if (wrapperArgs.codexSandboxMode === 'danger') {
210
+ fail('cannot combine --danger and --sandboxed');
211
+ }
212
+ wrapperArgs.codexSandboxMode = 'sandboxed';
213
+ continue;
214
+ }
215
+ if (arg === '--network') {
216
+ if (command !== 'codex') {
217
+ fail('--network is only supported for agent-run codex --sandboxed');
218
+ }
219
+ wrapperArgs.codexNetwork = true;
220
+ continue;
221
+ }
251
222
  args.push(arg);
252
223
  }
224
+ if (wrapperArgs.codexNetwork && wrapperArgs.codexSandboxMode !== 'sandboxed') {
225
+ fail('--network is only meaningful with agent-run codex --sandboxed');
226
+ }
253
227
  return { args, command, wrapperArgs };
254
228
  }
255
229
  function parseCheckCommand(inputArgs) {
256
230
  let all = false;
257
231
  let targetPath = process.cwd();
258
232
  for (const arg of inputArgs) {
233
+ if (isHelpFlag(arg)) {
234
+ printHelp('check');
235
+ }
259
236
  if (arg === '--all') {
260
237
  all = true;
261
238
  continue;
@@ -265,50 +242,141 @@ function parseCheckCommand(inputArgs) {
265
242
  }
266
243
  targetPath = arg;
267
244
  }
268
- return {
269
- all,
270
- command: 'check',
271
- targetPath: path.resolve(targetPath)
272
- };
245
+ return { all, command: 'check', targetPath: path.resolve(targetPath) };
273
246
  }
274
247
  function parseInitCommand(inputArgs) {
275
248
  let targetPath = process.cwd();
276
249
  for (const arg of inputArgs) {
250
+ if (isHelpFlag(arg)) {
251
+ printHelp('init');
252
+ }
277
253
  if (arg.startsWith('--')) {
278
254
  fail(`unknown init option: ${arg}`);
279
255
  }
280
256
  targetPath = arg;
281
257
  }
282
- return {
283
- command: 'init',
284
- targetPath: path.resolve(targetPath)
285
- };
258
+ return { command: 'init', targetPath: path.resolve(targetPath) };
286
259
  }
287
260
  function parseEditCommand(inputArgs) {
288
261
  let targetPath = process.cwd();
289
262
  for (const arg of inputArgs) {
263
+ if (isHelpFlag(arg)) {
264
+ printHelp('edit');
265
+ }
290
266
  if (arg.startsWith('--')) {
291
267
  fail(`unknown edit option: ${arg}`);
292
268
  }
293
269
  targetPath = arg;
294
270
  }
295
- return {
296
- command: 'edit',
297
- targetPath: path.resolve(targetPath)
298
- };
271
+ return { command: 'edit', targetPath: path.resolve(targetPath) };
299
272
  }
300
273
  function parseUpdateCommand(inputArgs) {
301
274
  let targetPath = process.cwd();
302
275
  for (const arg of inputArgs) {
276
+ if (isHelpFlag(arg)) {
277
+ printHelp('update');
278
+ }
303
279
  if (arg.startsWith('--')) {
304
280
  fail(`unknown update option: ${arg}`);
305
281
  }
306
282
  targetPath = arg;
307
283
  }
308
- return {
309
- command: 'update',
310
- targetPath: path.resolve(targetPath)
311
- };
284
+ return { command: 'update', targetPath: path.resolve(targetPath) };
285
+ }
286
+ function parseMigrateConfigCommand(inputArgs) {
287
+ let configRoot = defaultConfigRoot();
288
+ for (const arg of inputArgs) {
289
+ if (isHelpFlag(arg)) {
290
+ printHelp('migrate-config');
291
+ }
292
+ if (arg.startsWith('--')) {
293
+ fail(`unknown migrate-config option: ${arg}`);
294
+ }
295
+ configRoot = arg;
296
+ }
297
+ return { command: 'migrate-config', configRoot: path.resolve(configRoot) };
298
+ }
299
+ function isHelpFlag(value) {
300
+ return value === '-h' || value === '--help';
301
+ }
302
+ function printHelp(topic) {
303
+ process.stdout.write(renderHelp(topic));
304
+ process.exit(0);
305
+ }
306
+ function renderHelp(topic) {
307
+ switch (topic) {
308
+ case 'check':
309
+ return [
310
+ 'Usage:',
311
+ ' agent-run check [--all] [path]',
312
+ '',
313
+ 'Check generated profile files, native skills, guard shims, and local AI-file leaks.',
314
+ '',
315
+ 'Options:',
316
+ ' -h, --help Show this help text',
317
+ ' --all Check every repo under path',
318
+ ''
319
+ ].join('\n');
320
+ case 'init':
321
+ return [
322
+ 'Usage:',
323
+ ' agent-run init [path]',
324
+ '',
325
+ 'Create the mapped profile manifest/templates and render generated files.',
326
+ ''
327
+ ].join('\n');
328
+ case 'edit':
329
+ return [
330
+ 'Usage:',
331
+ ' agent-run edit [path]',
332
+ '',
333
+ 'Open the editable local profile template.',
334
+ ''
335
+ ].join('\n');
336
+ case 'update':
337
+ return [
338
+ 'Usage:',
339
+ ' agent-run update [path]',
340
+ '',
341
+ 'Render generated files, native skills, config, and guard shims for the repo at path.',
342
+ ''
343
+ ].join('\n');
344
+ case 'migrate-config':
345
+ return [
346
+ 'Usage:',
347
+ ' agent-run migrate-config [config-root]',
348
+ '',
349
+ 'Convert an existing agent config tree to manifest/local.md.njk/global layout.',
350
+ ''
351
+ ].join('\n');
352
+ case 'general':
353
+ default:
354
+ return [
355
+ 'Usage:',
356
+ ' agent-run <codex|claude|check|init|edit|update> [options]',
357
+ '',
358
+ 'Commands:',
359
+ ' codex [--none] [--create] [--danger|--sandboxed] [--network] [args...]',
360
+ ' Run codex with generated private config',
361
+ ' claude [--none] [--create] [args...] Run claude with generated private config',
362
+ ' check [--all] [path] Validate generated profile output',
363
+ ' init [path] Create profile source files and render output',
364
+ ' edit [path] Open local.md.njk or legacy AGENTS-MODS.md',
365
+ ' update [path] Regenerate profile output',
366
+ ' migrate-config [config-root] Convert existing config tree layout',
367
+ '',
368
+ 'Global options:',
369
+ ' -h, --help Show this help text',
370
+ ' -v, --verbose Print path resolution and wrapper actions',
371
+ ' --config-root DIR Override the agent-config root',
372
+ '',
373
+ 'Codex wrapper options:',
374
+ ' --danger Run Codex with no sandbox: -a never -s danger-full-access (default)',
375
+ ' --sandboxed Run Codex with workspace-write sandbox',
376
+ ' --network Enable network for --sandboxed via config override',
377
+ ''
378
+ ].join('\n');
379
+ }
312
380
  }
313
381
  function normalizeCommandName(value) {
314
382
  if (!value) {
@@ -336,6 +404,9 @@ function normalizeCommandName(value) {
336
404
  if (base === 'update') {
337
405
  return 'update';
338
406
  }
407
+ if (base === 'migrate-config' || base === 'migrate-config.cmd') {
408
+ return 'migrate-config';
409
+ }
339
410
  return null;
340
411
  }
341
412
  function runTool(parsed) {
@@ -358,20 +429,19 @@ function runTool(parsed) {
358
429
  if (profileResult.profile === null) {
359
430
  fail(profileResult.reason);
360
431
  }
432
+ const profile = profileResult.profile;
433
+ const configRoot = defaultConfigRoot(projectRoot);
361
434
  const agentDir = resolveAgentDir(projectRoot);
362
- verbose(`resolved agent path: ${agentDir}`);
363
435
  if (wrapperArgs.create) {
364
- createBlankAgentFiles(agentDir);
365
- process.stderr.write(`agent-run: created blank agent files in ${agentDir}\n`);
366
- }
367
- if (fs.existsSync(path.join(agentDir, 'AGENTS-MODS.md'))) {
368
- syncGeneratedAgentsFile(agentDir);
436
+ runInit({ command: 'init', targetPath: projectRoot });
369
437
  }
438
+ ensureRunnableProfile(configRoot, agentDir, profile);
439
+ syncAgentProfile(projectRoot, agentDir);
370
440
  if (command === 'codex') {
371
- runCodex(realBinary, permissionArgs, agentDir, args);
441
+ runCodex(realBinary, permissionArgs, agentDir, args, projectRoot, wrapperArgs);
372
442
  return;
373
443
  }
374
- runClaude(realBinary, permissionArgs, agentDir, args);
444
+ runClaude(realBinary, permissionArgs, agentDir, args, projectRoot);
375
445
  }
376
446
  function runInit(parsed) {
377
447
  const projectRoot = findProjectRoot(parsed.targetPath);
@@ -380,12 +450,29 @@ function runInit(parsed) {
380
450
  process.stdout.write(`SKIP ignored ${projectRoot}\n`);
381
451
  return;
382
452
  }
453
+ const profile = resolveProfile(projectRoot);
454
+ const configRoot = defaultConfigRoot(projectRoot);
383
455
  const agentDir = resolveAgentDir(projectRoot);
384
- createBlankAgentFiles(agentDir);
385
- if (fs.existsSync(path.join(agentDir, 'AGENTS-MODS.md'))) {
386
- syncGeneratedAgentsFile(agentDir);
387
- }
388
- process.stdout.write(`OK initialized ${agentDir}\n`);
456
+ ensureConfigRootLayout(configRoot);
457
+ ensureConfigRootGitignore(configRoot);
458
+ ensureDefaultGlobalTemplates(configRoot);
459
+ fs.mkdirSync(agentDir, { recursive: true });
460
+ ensureProfileOverridesDir(agentDir);
461
+ convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
462
+ createDefaultManifestFile(agentDir, profile);
463
+ createDefaultLocalFile(agentDir, profile);
464
+ const rendered = syncAgentProfile(projectRoot, agentDir);
465
+ printUpdateSummary(rendered);
466
+ }
467
+ function ensureRunnableProfile(configRoot, agentDir, profile) {
468
+ ensureConfigRootLayout(configRoot);
469
+ ensureConfigRootGitignore(configRoot);
470
+ ensureDefaultGlobalTemplates(configRoot);
471
+ fs.mkdirSync(agentDir, { recursive: true });
472
+ ensureProfileOverridesDir(agentDir);
473
+ convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
474
+ createDefaultManifestFile(agentDir, profile);
475
+ createDefaultLocalFile(agentDir, profile);
389
476
  }
390
477
  function runEdit(parsed) {
391
478
  const projectRoot = findProjectRoot(parsed.targetPath);
@@ -395,13 +482,14 @@ function runEdit(parsed) {
395
482
  process.exit(0);
396
483
  }
397
484
  const agentDir = resolveAgentDir(projectRoot);
398
- createBlankAgentFiles(agentDir);
399
- if (fs.existsSync(path.join(agentDir, 'AGENTS-MODS.md'))) {
400
- syncGeneratedAgentsFile(agentDir);
485
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
486
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
487
+ if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
488
+ runInit({ command: 'init', targetPath: projectRoot });
401
489
  }
402
- const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
403
- process.stdout.write(`Edit: ${modsPath}\n`);
404
- openEditor(modsPath);
490
+ const editPath = fs.existsSync(localPath) ? localPath : legacyPath;
491
+ process.stdout.write(`Edit: ${editPath}\n`);
492
+ openEditor(editPath);
405
493
  }
406
494
  function runUpdate(parsed) {
407
495
  const projectRoot = findProjectRoot(parsed.targetPath);
@@ -410,13 +498,67 @@ function runUpdate(parsed) {
410
498
  process.stdout.write(`SKIP ignored ${projectRoot}\n`);
411
499
  return;
412
500
  }
501
+ const profile = resolveProfile(projectRoot);
502
+ const configRoot = defaultConfigRoot(projectRoot);
413
503
  const agentDir = resolveAgentDir(projectRoot);
414
- const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
415
- if (!fs.existsSync(modsPath)) {
416
- fail(`missing AGENTS-MODS.md: ${modsPath}`);
417
- }
418
- syncGeneratedAgentsFile(agentDir);
419
- process.stdout.write(`OK updated ${path.join(agentDir, 'AGENTS.md')}\n`);
504
+ ensureConfigRootLayout(configRoot);
505
+ ensureConfigRootGitignore(configRoot);
506
+ ensureDefaultGlobalTemplates(configRoot);
507
+ fs.mkdirSync(agentDir, { recursive: true });
508
+ ensureProfileOverridesDir(agentDir);
509
+ convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
510
+ createDefaultManifestFile(agentDir, profile);
511
+ createDefaultLocalFile(agentDir, profile);
512
+ const rendered = syncAgentProfile(projectRoot, agentDir);
513
+ printUpdateSummary(rendered);
514
+ }
515
+ function runMigrateConfig(parsed) {
516
+ const configRoot = parsed.configRoot;
517
+ verbose(`migrate config root=${configRoot}`);
518
+ if (!fs.existsSync(configRoot)) {
519
+ fail(`missing config root: ${configRoot}`);
520
+ }
521
+ ensureConfigRootLayout(configRoot);
522
+ ensureConfigRootGitignore(configRoot);
523
+ migrateOldTemplates(configRoot);
524
+ ensureDefaultGlobalTemplates(configRoot);
525
+ const profileDirs = findLegacyProfileDirs(configRoot);
526
+ let createdManifestCount = 0;
527
+ let createdLocalCount = 0;
528
+ let createdOverridesCount = 0;
529
+ let movedRuntimeCount = 0;
530
+ let movedReviewCount = 0;
531
+ let movedMemoryCount = 0;
532
+ for (const agentDir of profileDirs) {
533
+ const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
534
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
535
+ if (!fs.existsSync(localPath)) {
536
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
537
+ fs.writeFileSync(localPath, convertLegacyTemplateVars(fs.readFileSync(legacyPath, 'utf8')), 'utf8');
538
+ createdLocalCount += 1;
539
+ }
540
+ const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
541
+ if (!fs.existsSync(manifestPath)) {
542
+ createDefaultManifestFile(agentDir, profile);
543
+ createdManifestCount += 1;
544
+ }
545
+ const overridesDir = path.join(agentDir, 'overrides');
546
+ if (!fs.existsSync(overridesDir)) {
547
+ fs.mkdirSync(overridesDir, { recursive: true });
548
+ createdOverridesCount += 1;
549
+ }
550
+ movedRuntimeCount += migrateCodexRuntimeFiles(agentDir);
551
+ movedReviewCount += migrateLooseFiles(agentDir, /^REVIEW(?:-.+)?\.md$/, 'reviews');
552
+ movedMemoryCount += migrateLooseFiles(agentDir, /^memory.*\.md$/i, 'memories');
553
+ }
554
+ process.stdout.write(`OK migrated ${configRoot}\n`);
555
+ process.stdout.write(`Profiles converted: ${profileDirs.length}\n`);
556
+ process.stdout.write(`Created local.md.njk: ${createdLocalCount}\n`);
557
+ process.stdout.write(`Created agent-run.jsonc: ${createdManifestCount}\n`);
558
+ process.stdout.write(`Created overrides dirs: ${createdOverridesCount}\n`);
559
+ process.stdout.write(`Moved Codex runtime entries: ${movedRuntimeCount}\n`);
560
+ process.stdout.write(`Moved review files: ${movedReviewCount}\n`);
561
+ process.stdout.write(`Moved memory files: ${movedMemoryCount}\n`);
420
562
  }
421
563
  function runCheck(parsed) {
422
564
  if (parsed.all) {
@@ -436,6 +578,698 @@ function runCheck(parsed) {
436
578
  printProjectReport(projectRoot, findings);
437
579
  process.exit(hasErrors(findings) ? 1 : 0);
438
580
  }
581
+ function printUpdateSummary(rendered) {
582
+ process.stdout.write(`OK profile ${rendered.profile}\n`);
583
+ process.stdout.write(`Agent dir: ${rendered.agentDir}\n`);
584
+ process.stdout.write(`Generated files: ${rendered.files.length}\n`);
585
+ process.stdout.write(`Installed skills: ${rendered.skills.map((skill) => skill.name).join(', ') || '(none)'}\n`);
586
+ }
587
+ function ensureConfigRootLayout(configRoot) {
588
+ fs.mkdirSync(configRoot, { recursive: true });
589
+ }
590
+ function ensureConfigRootGitignore(configRoot) {
591
+ fs.mkdirSync(configRoot, { recursive: true });
592
+ const gitignorePath = path.join(configRoot, '.gitignore');
593
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8').replace(/\r\n/g, '\n') : '';
594
+ const lines = existing.length > 0 ? existing.replace(/\n+$/, '').split('\n') : [];
595
+ const present = new Set(lines);
596
+ let changed = !fs.existsSync(gitignorePath);
597
+ for (const entry of GENERATED_GITIGNORE_ENTRIES) {
598
+ if (entry === '' || present.has(entry)) {
599
+ continue;
600
+ }
601
+ lines.push(entry);
602
+ present.add(entry);
603
+ changed = true;
604
+ }
605
+ if (!changed) {
606
+ return;
607
+ }
608
+ fs.writeFileSync(gitignorePath, `${lines.join('\n')}\n`, 'utf8');
609
+ }
610
+ function ensureDefaultGlobalTemplates(configRoot) {
611
+ const defaults = new Map([
612
+ ['global/agents/code.md.njk', defaultCodeAgentTemplate()],
613
+ ['global/agents/writing.md.njk', defaultWritingAgentTemplate()],
614
+ ['global/snippets/git-rules.md.njk', defaultGitRulesSnippet()],
615
+ ['global/snippets/no-ai-files.md.njk', defaultNoAiFilesSnippet()],
616
+ ['global/snippets/verification.md.njk', defaultVerificationSnippet()],
617
+ ['global/tool-templates/codex-config.toml.njk', defaultCodexConfigTemplate()],
618
+ ['global/tool-templates/claude-settings.json.njk', defaultClaudeSettingsTemplate()],
619
+ ['global/skills/commit-workflow/SKILL.md.njk', defaultCommitWorkflowSkill()],
620
+ ['global/skills/github-release/SKILL.md.njk', defaultGithubReleaseSkill()],
621
+ ['global/skills/code-review/SKILL.md.njk', defaultCodeReviewSkill()]
622
+ ]);
623
+ for (const [relativePath, content] of defaults) {
624
+ const filePath = path.join(configRoot, relativePath);
625
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
626
+ if (!fs.existsSync(filePath)) {
627
+ fs.writeFileSync(filePath, content, 'utf8');
628
+ verbose(`created ${filePath}`);
629
+ }
630
+ }
631
+ }
632
+ function migrateOldTemplates(configRoot) {
633
+ const oldCodeTemplate = path.join(configRoot, 'templates', 'AGENTS-CODE.md');
634
+ const newCodeTemplate = path.join(configRoot, 'global', 'agents', 'code.md.njk');
635
+ if (fs.existsSync(oldCodeTemplate) && !fs.existsSync(newCodeTemplate)) {
636
+ fs.mkdirSync(path.dirname(newCodeTemplate), { recursive: true });
637
+ fs.writeFileSync(newCodeTemplate, convertLegacyTemplateVars(fs.readFileSync(oldCodeTemplate, 'utf8')), 'utf8');
638
+ }
639
+ }
640
+ function findLegacyProfileDirs(configRoot) {
641
+ const dirs = [];
642
+ walkConfigTree(configRoot, 0, (fullPath, entry) => {
643
+ if (!entry.isDirectory()) {
644
+ return undefined;
645
+ }
646
+ if (shouldPruneConfigEntry(entry)) {
647
+ return 'skip';
648
+ }
649
+ if (fs.existsSync(path.join(fullPath, 'AGENTS-MODS.md'))) {
650
+ dirs.push(fullPath);
651
+ return 'skip';
652
+ }
653
+ return undefined;
654
+ });
655
+ return dirs.sort();
656
+ }
657
+ function walkConfigTree(dir, depth, visitor) {
658
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
659
+ for (const entry of entries) {
660
+ const fullPath = path.join(dir, entry.name);
661
+ const result = visitor(fullPath, entry, depth + 1);
662
+ if (entry.isDirectory() && result !== 'skip') {
663
+ walkConfigTree(fullPath, depth + 1, visitor);
664
+ }
665
+ }
666
+ }
667
+ function shouldPruneConfigEntry(entry) {
668
+ return (entry.isDirectory() &&
669
+ (entry.name === '.git' ||
670
+ entry.name === 'global' ||
671
+ entry.name === 'templates' ||
672
+ entry.name === '.agents' ||
673
+ entry.name === '.claude' ||
674
+ entry.name === '.codex' ||
675
+ entry.name === 'bin' ||
676
+ entry.name === 'reviews' ||
677
+ entry.name === 'memories' ||
678
+ entry.name === 'cache' ||
679
+ entry.name === 'log' ||
680
+ entry.name === 'sessions' ||
681
+ entry.name === 'shell_snapshots' ||
682
+ entry.name === 'skills' ||
683
+ entry.name === 'tmp' ||
684
+ entry.name === '.tmp'));
685
+ }
686
+ function convertLegacyTemplateVars(content) {
687
+ const replacements = new Map([
688
+ ['{{AGENT_DIR}}', '{{ agentDir }}'],
689
+ ['{{AGENTS_MODS_PATH}}', '{{ agentDir }}/AGENTS-MODS.md'],
690
+ ['{{AGENTS_PATH}}', '{{ agentDir }}/AGENTS.md'],
691
+ ['{{CLAUDE_PATH}}', '{{ agentDir }}/CLAUDE.md'],
692
+ ['{{CONFIG_ROOT}}', '{{ configRoot }}'],
693
+ ['{{PROFILE}}', '{{ profile }}']
694
+ ]);
695
+ let converted = content;
696
+ for (const [from, to] of replacements) {
697
+ converted = converted.split(from).join(to);
698
+ }
699
+ return converted
700
+ .replace(/^@[^\n]*templates\/AGENTS-CODE\.md[^\n]*\n?/gm, '')
701
+ .replace(/^\n{2,}/, '\n');
702
+ }
703
+ function migrateCodexRuntimeFiles(agentDir) {
704
+ const runtimeNames = [
705
+ '.personality_migration',
706
+ '.tmp',
707
+ 'auth.json',
708
+ 'cache',
709
+ 'history.jsonl',
710
+ 'installation_id',
711
+ 'log',
712
+ 'logs_2.sqlite',
713
+ 'models_cache.json',
714
+ 'sessions',
715
+ 'shell_snapshots',
716
+ 'skills',
717
+ 'state_5.sqlite',
718
+ 'tmp',
719
+ 'version.json'
720
+ ];
721
+ const codexHome = path.join(agentDir, 'memories', 'codex-home');
722
+ let moved = 0;
723
+ for (const name of runtimeNames) {
724
+ const source = path.join(agentDir, name);
725
+ const target = path.join(codexHome, name);
726
+ if (!fs.existsSync(source) || fs.existsSync(target)) {
727
+ continue;
728
+ }
729
+ fs.mkdirSync(path.dirname(target), { recursive: true });
730
+ fs.renameSync(source, target);
731
+ moved += 1;
732
+ }
733
+ return moved;
734
+ }
735
+ function migrateLooseFiles(agentDir, pattern, targetDirName) {
736
+ const entries = fs.readdirSync(agentDir, { withFileTypes: true });
737
+ let moved = 0;
738
+ for (const entry of entries) {
739
+ if (!entry.isFile() || !pattern.test(entry.name)) {
740
+ continue;
741
+ }
742
+ const source = path.join(agentDir, entry.name);
743
+ const targetDir = path.join(agentDir, targetDirName);
744
+ const target = path.join(targetDir, entry.name);
745
+ if (fs.existsSync(target)) {
746
+ continue;
747
+ }
748
+ fs.mkdirSync(targetDir, { recursive: true });
749
+ fs.renameSync(source, target);
750
+ moved += 1;
751
+ }
752
+ return moved;
753
+ }
754
+ function convertLegacyProfileIfNeeded(_configRoot, agentDir, _profile) {
755
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
756
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
757
+ if (fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
758
+ fs.mkdirSync(agentDir, { recursive: true });
759
+ fs.writeFileSync(localPath, fs.readFileSync(legacyPath, 'utf8'), 'utf8');
760
+ verbose(`converted ${legacyPath} -> ${localPath}`);
761
+ }
762
+ }
763
+ function createDefaultManifestFile(agentDir, profile) {
764
+ const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
765
+ if (fs.existsSync(manifestPath)) {
766
+ return;
767
+ }
768
+ fs.mkdirSync(agentDir, { recursive: true });
769
+ fs.writeFileSync(manifestPath, stringifyDefaultManifest(profile), 'utf8');
770
+ verbose(`created ${manifestPath}`);
771
+ }
772
+ function createDefaultLocalFile(agentDir, profile) {
773
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
774
+ if (fs.existsSync(localPath)) {
775
+ return;
776
+ }
777
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
778
+ const content = fs.existsSync(legacyPath) ? fs.readFileSync(legacyPath, 'utf8') : defaultLocalTemplate(profile);
779
+ fs.mkdirSync(agentDir, { recursive: true });
780
+ fs.writeFileSync(localPath, content, 'utf8');
781
+ verbose(`created ${localPath}`);
782
+ }
783
+ function ensureProfileOverridesDir(agentDir) {
784
+ fs.mkdirSync(path.join(agentDir, 'overrides'), { recursive: true });
785
+ }
786
+ function loadManifest(_configRoot, agentDir, profile) {
787
+ const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
788
+ if (!fs.existsSync(manifestPath)) {
789
+ const manifest = defaultManifest(profile);
790
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
791
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
792
+ if (!fs.existsSync(localPath) && fs.existsSync(legacyPath)) {
793
+ manifest.agent = {
794
+ ...(manifest.agent ?? {}),
795
+ includes: ['{{ profile }}/AGENTS-MODS.md']
796
+ };
797
+ }
798
+ if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
799
+ manifest.agent = {
800
+ ...(manifest.agent ?? {}),
801
+ includes: []
802
+ };
803
+ }
804
+ return manifest;
805
+ }
806
+ const text = fs.readFileSync(manifestPath, 'utf8');
807
+ const errors = [];
808
+ const parsed = (0, jsonc_parser_1.parse)(text, errors, { allowTrailingComma: true });
809
+ if (errors.length > 0) {
810
+ const first = errors[0];
811
+ const detail = first ? `${(0, jsonc_parser_1.printParseErrorCode)(first.error)} at offset ${first.offset}` : 'unknown JSONC parse error';
812
+ throw new Error(`invalid JSONC in ${manifestPath}: ${detail}`);
813
+ }
814
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
815
+ throw new Error(`manifest must be an object: ${manifestPath}`);
816
+ }
817
+ return parsed;
818
+ }
819
+ function defaultManifest(profile) {
820
+ return {
821
+ profile,
822
+ kind: 'code',
823
+ agent: {
824
+ base: 'global/agents/code.md.njk',
825
+ includes: [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`]
826
+ },
827
+ skills: {
828
+ install: ['commit-workflow', 'github-release', 'code-review']
829
+ },
830
+ tools: {
831
+ codex: true,
832
+ claude: true
833
+ },
834
+ checks: ['agent-run check .', 'repo-check check .', 'pnpm run cleanbuild'],
835
+ guardrails: {
836
+ blockGitWrite: true,
837
+ blockPublish: true,
838
+ blockGithubRelease: true,
839
+ forbidRepoAiFiles: true
840
+ },
841
+ paths: {
842
+ changesFile: '{{ projectRoot }}/CHANGES',
843
+ reviewDir: '{{ agentDir }}/reviews',
844
+ reviewFile: '{{ agentDir }}/reviews/REVIEW-{{ date }}.md',
845
+ memoriesDir: '{{ agentDir }}/memories'
846
+ }
847
+ };
848
+ }
849
+ function normalizeManifest(manifest, profile) {
850
+ const baseManifest = defaultManifest(profile);
851
+ const installedSkills = Array.isArray(manifest.skills)
852
+ ? manifest.skills
853
+ : manifest.skills?.install ?? asSkillObject(baseManifest.skills).install;
854
+ const overrides = Array.isArray(manifest.skills) ? {} : manifest.skills?.overrides ?? {};
855
+ return {
856
+ profile: manifest.profile ?? profile,
857
+ kind: manifest.kind ?? baseManifest.kind ?? 'code',
858
+ agent: {
859
+ base: manifest.agent?.base ?? baseManifest.agent?.base ?? 'global/agents/code.md.njk',
860
+ includes: manifest.agent?.includes ?? defaultAgentIncludesForProfile(profile)
861
+ },
862
+ skills: {
863
+ install: installedSkills,
864
+ overrides
865
+ },
866
+ tools: {
867
+ codex: manifest.tools?.codex ?? true,
868
+ claude: manifest.tools?.claude ?? true
869
+ },
870
+ checks: manifest.checks ?? baseManifest.checks ?? [],
871
+ guardrails: {
872
+ blockGitWrite: manifest.guardrails?.blockGitWrite ?? true,
873
+ blockPublish: manifest.guardrails?.blockPublish ?? true,
874
+ blockGithubRelease: manifest.guardrails?.blockGithubRelease ?? true,
875
+ forbidRepoAiFiles: manifest.guardrails?.forbidRepoAiFiles ?? true
876
+ },
877
+ paths: {
878
+ changesFile: manifest.paths?.changesFile ?? baseManifest.paths?.changesFile ?? '{{ projectRoot }}/CHANGES',
879
+ reviewDir: manifest.paths?.reviewDir ?? baseManifest.paths?.reviewDir ?? '{{ agentDir }}/reviews',
880
+ reviewFile: manifest.paths?.reviewFile ?? baseManifest.paths?.reviewFile ?? '{{ agentDir }}/reviews/REVIEW-{{ date }}.md',
881
+ memoriesDir: manifest.paths?.memoriesDir ?? baseManifest.paths?.memoriesDir ?? '{{ agentDir }}/memories'
882
+ }
883
+ };
884
+ }
885
+ function asSkillObject(skills) {
886
+ if (Array.isArray(skills)) {
887
+ return { install: skills, overrides: {} };
888
+ }
889
+ return {
890
+ install: skills?.install ?? [],
891
+ overrides: skills?.overrides ?? {}
892
+ };
893
+ }
894
+ function defaultAgentIncludesForProfile(profile) {
895
+ return [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`, `{{ profile }}/AGENTS-MODS.md`];
896
+ }
897
+ function createNunjucksEnv(configRoot) {
898
+ return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
899
+ autoescape: false,
900
+ trimBlocks: true,
901
+ lstripBlocks: true,
902
+ throwOnUndefined: true
903
+ });
904
+ }
905
+ function buildRenderContext(projectRoot, agentDir, configRoot, manifest, env) {
906
+ const date = localDateString();
907
+ const baseContext = {
908
+ profile: manifest.profile,
909
+ kind: manifest.kind,
910
+ projectRoot,
911
+ agentDir,
912
+ configRoot,
913
+ date,
914
+ checks: manifest.checks,
915
+ guardrails: manifest.guardrails
916
+ };
917
+ const paths = {
918
+ changesFile: resolveRuntimePath(configRoot, manifest.paths.changesFile, env, baseContext),
919
+ reviewDir: resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext),
920
+ reviewFile: resolveRuntimePath(configRoot, manifest.paths.reviewFile, env, baseContext),
921
+ memoriesDir: resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext),
922
+ codexHomeDir: path.join(resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext), 'codex-home'),
923
+ overridesDir: path.join(agentDir, 'overrides'),
924
+ codexSkillsDir: path.join(agentDir, '.agents', 'skills'),
925
+ claudeSkillsDir: path.join(agentDir, '.claude', 'skills'),
926
+ binDir: path.join(agentDir, 'bin')
927
+ };
928
+ return {
929
+ ...baseContext,
930
+ paths,
931
+ skills: [],
932
+ renderedAgentSections: []
933
+ };
934
+ }
935
+ function resolveConfigPath(configRoot, relativePath, context) {
936
+ const env = createNunjucksEnv(configRoot);
937
+ const rendered = renderInlineTemplate(env, relativePath, context);
938
+ const resolved = path.resolve(configRoot, rendered);
939
+ if (!isSamePathOrDescendant(resolved, path.resolve(configRoot))) {
940
+ throw new Error(`config path escapes config root: ${relativePath}`);
941
+ }
942
+ return resolved;
943
+ }
944
+ function resolveRuntimePath(configRoot, pathTemplate, env, context) {
945
+ const rendered = renderInlineTemplate(env, pathTemplate, context);
946
+ return path.isAbsolute(rendered) ? path.resolve(rendered) : path.resolve(configRoot, rendered);
947
+ }
948
+ function renderTemplateFile(env, configRoot, templatePath, context) {
949
+ const resolvedPath = resolveConfigPath(configRoot, templatePath, context);
950
+ if (!fs.existsSync(resolvedPath)) {
951
+ throw new Error(`missing template: ${resolvedPath}`);
952
+ }
953
+ const relativePath = path.relative(configRoot, resolvedPath).replace(/\\/g, '/');
954
+ let content = fs.readFileSync(resolvedPath, 'utf8');
955
+ if (path.basename(resolvedPath) === 'AGENTS-MODS.md') {
956
+ content = renderLegacyAgentsMods(resolvedPath);
957
+ return renderInlineTemplate(env, content, context);
958
+ }
959
+ return env.render(relativePath, context);
960
+ }
961
+ function renderInlineTemplate(env, source, context) {
962
+ return env.renderString(source, context);
963
+ }
964
+ function assertNoUnexpandedTemplateVars(label, content) {
965
+ if (UNEXPANDED_TEMPLATE_RE.test(content)) {
966
+ throw new Error(`unexpanded template syntax remains in ${label}`);
967
+ }
968
+ }
969
+ function writeGeneratedFile(filePath, content, executable = false) {
970
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
971
+ fs.writeFileSync(filePath, content, 'utf8');
972
+ if (executable && !IS_WINDOWS) {
973
+ fs.chmodSync(filePath, 0o755);
974
+ }
975
+ verbose(`write ${filePath}`);
976
+ }
977
+ function renderProfile(projectRoot, agentDir, checkOnly = false) {
978
+ const configRoot = defaultConfigRoot(projectRoot);
979
+ const profile = resolveProfile(projectRoot);
980
+ const env = createNunjucksEnv(configRoot);
981
+ const manifest = normalizeManifest(loadManifest(configRoot, agentDir, profile), profile);
982
+ if (manifest.profile !== profile) {
983
+ const parsed = parseProfile(manifest.profile, `${path.join(agentDir, MANIFEST_FILE_NAME)} profile`);
984
+ if (parsed.profile === null) {
985
+ throw new Error(parsed.reason);
986
+ }
987
+ }
988
+ if (!checkOnly) {
989
+ ensureConfigRootGitignore(configRoot);
990
+ }
991
+ const context = buildRenderContext(projectRoot, agentDir, configRoot, manifest, env);
992
+ const sections = [renderTemplateFile(env, configRoot, manifest.agent.base, context)];
993
+ for (const include of manifest.agent.includes) {
994
+ const includePath = resolveConfigPath(configRoot, include, context);
995
+ if (!fs.existsSync(includePath)) {
996
+ if (include.includes('AGENTS-MODS.md') || include.includes(LOCAL_TEMPLATE_FILE_NAME)) {
997
+ continue;
998
+ }
999
+ throw new Error(`missing template: ${includePath}`);
1000
+ }
1001
+ sections.push(renderTemplateFile(env, configRoot, include, context));
1002
+ }
1003
+ context.renderedAgentSections = sections.map((section, index) => {
1004
+ const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
1005
+ assertNoUnexpandedTemplateVars(label, section);
1006
+ return section.trimEnd();
1007
+ });
1008
+ context.skills = renderSkills(env, configRoot, manifest, context);
1009
+ const files = [];
1010
+ const agentsContent = renderToolInstructions('AGENTS.md', env, configRoot, context, false);
1011
+ const claudeContent = renderToolInstructions('CLAUDE.md', env, configRoot, context, true);
1012
+ files.push({ path: path.join(agentDir, 'AGENTS.md'), content: agentsContent });
1013
+ files.push({ path: path.join(agentDir, 'CLAUDE.md'), content: claudeContent });
1014
+ files.push({ path: path.join(agentDir, '.claude', 'CLAUDE.md'), content: claudeContent });
1015
+ files.push({ path: path.join(agentDir, 'config.toml'), content: renderCodexConfig(env, configRoot, context) });
1016
+ files.push({ path: path.join(agentDir, '.claude', 'settings.json'), content: renderClaudeSettings(env, configRoot, context) });
1017
+ files.push(...renderNativeSkillFiles(context));
1018
+ files.push(...renderGuardShims(context));
1019
+ return {
1020
+ agentDir,
1021
+ configRoot,
1022
+ profile,
1023
+ context,
1024
+ files,
1025
+ skills: context.skills
1026
+ };
1027
+ }
1028
+ function syncAgentProfile(projectRoot, agentDir) {
1029
+ const rendered = renderProfile(projectRoot, agentDir, false);
1030
+ syncRuntimeDirs(rendered.context);
1031
+ for (const file of rendered.files) {
1032
+ writeGeneratedFile(file.path, file.content, file.executable ?? false);
1033
+ }
1034
+ return rendered;
1035
+ }
1036
+ function checkRenderedProfile(projectRoot, agentDir) {
1037
+ const findings = [];
1038
+ const configRoot = defaultConfigRoot(projectRoot);
1039
+ try {
1040
+ const rendered = renderProfile(projectRoot, agentDir, true);
1041
+ for (const file of rendered.files) {
1042
+ if (!fs.existsSync(file.path)) {
1043
+ findings.push({ message: `missing generated file: ${file.path}`, severity: 'ERROR' });
1044
+ continue;
1045
+ }
1046
+ const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
1047
+ if (actual !== file.content) {
1048
+ findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
1049
+ }
1050
+ }
1051
+ for (const dir of [
1052
+ rendered.context.paths.reviewDir,
1053
+ rendered.context.paths.memoriesDir,
1054
+ rendered.context.paths.codexHomeDir,
1055
+ rendered.context.paths.overridesDir,
1056
+ rendered.context.paths.codexSkillsDir,
1057
+ rendered.context.paths.claudeSkillsDir,
1058
+ rendered.context.paths.binDir
1059
+ ]) {
1060
+ if (!fs.existsSync(dir)) {
1061
+ findings.push({ message: `missing generated runtime directory: ${dir}`, severity: 'ERROR' });
1062
+ }
1063
+ }
1064
+ for (const skill of rendered.skills) {
1065
+ validateRenderedSkill(skill.renderedContent, skill.sourcePath);
1066
+ }
1067
+ }
1068
+ catch (error) {
1069
+ findings.push({
1070
+ message: `failed to render profile ${agentDir}: ${formatError(error)}`,
1071
+ severity: 'ERROR'
1072
+ });
1073
+ }
1074
+ const gitignorePath = path.join(configRoot, '.gitignore');
1075
+ if (!fs.existsSync(gitignorePath)) {
1076
+ findings.push({ message: `missing config root .gitignore: ${gitignorePath}`, severity: 'ERROR' });
1077
+ }
1078
+ else {
1079
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
1080
+ for (const entry of GENERATED_GITIGNORE_ENTRIES) {
1081
+ if (entry && entry.startsWith('#') === false && !gitignore.includes(entry)) {
1082
+ findings.push({ message: `config root .gitignore missing entry: ${entry}`, severity: 'ERROR' });
1083
+ }
1084
+ }
1085
+ }
1086
+ return findings;
1087
+ }
1088
+ function renderSkills(env, configRoot, manifest, context) {
1089
+ const skills = [];
1090
+ for (const name of manifest.skills.install) {
1091
+ const sourceTemplate = manifest.skills.overrides[name] ?? `global/skills/${name}/SKILL.md.njk`;
1092
+ const sourcePath = resolveConfigPath(configRoot, sourceTemplate, context);
1093
+ if (!fs.existsSync(sourcePath)) {
1094
+ throw new Error(`missing skill template for ${name}: ${sourcePath}`);
1095
+ }
1096
+ const renderedContent = renderTemplateFile(env, configRoot, sourceTemplate, context);
1097
+ assertNoUnexpandedTemplateVars(`skill ${name}`, renderedContent);
1098
+ validateRenderedSkill(renderedContent, sourcePath);
1099
+ const description = extractSkillDescription(renderedContent);
1100
+ skills.push({ name, sourcePath, renderedContent, description });
1101
+ }
1102
+ return skills;
1103
+ }
1104
+ function renderNativeSkillFiles(context) {
1105
+ const files = [];
1106
+ for (const skill of context.skills) {
1107
+ files.push({
1108
+ path: path.join(context.paths.codexSkillsDir, skill.name, 'SKILL.md'),
1109
+ content: skill.renderedContent
1110
+ });
1111
+ files.push({
1112
+ path: path.join(context.paths.claudeSkillsDir, skill.name, 'SKILL.md'),
1113
+ content: skill.renderedContent
1114
+ });
1115
+ }
1116
+ return files;
1117
+ }
1118
+ function renderToolInstructions(templateName, env, configRoot, context, isClaude) {
1119
+ const templatePath = `global/tool-templates/${templateName}.njk`;
1120
+ const absoluteTemplate = path.join(configRoot, templatePath);
1121
+ const content = fs.existsSync(absoluteTemplate)
1122
+ ? renderTemplateFile(env, configRoot, templatePath, context)
1123
+ : renderInlineTemplate(env, defaultToolInstructionsTemplate(isClaude), context);
1124
+ assertNoUnexpandedTemplateVars(templateName, content);
1125
+ return content.replace(/\n*$/, '\n');
1126
+ }
1127
+ function renderCodexConfig(env, configRoot, context) {
1128
+ const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk');
1129
+ const content = templatePath !== null
1130
+ ? renderTemplateFile(env, configRoot, templatePath, context)
1131
+ : defaultCodexConfigContent(context);
1132
+ assertNoUnexpandedTemplateVars('config.toml', content);
1133
+ return content.replace(/\n*$/, '\n');
1134
+ }
1135
+ function renderClaudeSettings(env, configRoot, context) {
1136
+ const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'claude-settings.json.njk', 'global/tool-templates/claude-settings.json.njk');
1137
+ const content = templatePath !== null
1138
+ ? renderTemplateFile(env, configRoot, templatePath, context)
1139
+ : defaultClaudeSettingsContent(context);
1140
+ assertNoUnexpandedTemplateVars('claude settings.json', content);
1141
+ return content.replace(/\n*$/, '\n');
1142
+ }
1143
+ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
1144
+ const overridePath = `${context.profile}/overrides/${overrideFileName}`;
1145
+ if (fs.existsSync(path.join(configRoot, overridePath))) {
1146
+ return overridePath;
1147
+ }
1148
+ if (fs.existsSync(path.join(configRoot, globalTemplatePath))) {
1149
+ return globalTemplatePath;
1150
+ }
1151
+ return null;
1152
+ }
1153
+ function renderGuardShims(context) {
1154
+ if (IS_WINDOWS) {
1155
+ return ['git', 'npm', 'pnpm', 'gh'].map((name) => ({
1156
+ path: path.join(context.paths.binDir, `${name}.cmd`),
1157
+ content: windowsShim(name)
1158
+ }));
1159
+ }
1160
+ return [
1161
+ { path: path.join(context.paths.binDir, 'git'), content: posixGitShim(), executable: true },
1162
+ { path: path.join(context.paths.binDir, 'npm'), content: posixPublishShim('npm'), executable: true },
1163
+ { path: path.join(context.paths.binDir, 'pnpm'), content: posixPublishShim('pnpm'), executable: true },
1164
+ { path: path.join(context.paths.binDir, 'gh'), content: posixGhShim(), executable: true }
1165
+ ];
1166
+ }
1167
+ function syncRuntimeDirs(context) {
1168
+ for (const dir of [
1169
+ context.paths.reviewDir,
1170
+ context.paths.memoriesDir,
1171
+ context.paths.codexHomeDir,
1172
+ context.paths.overridesDir,
1173
+ context.paths.codexSkillsDir,
1174
+ context.paths.claudeSkillsDir,
1175
+ context.paths.binDir,
1176
+ path.join(context.agentDir, '.claude')
1177
+ ]) {
1178
+ fs.mkdirSync(dir, { recursive: true });
1179
+ }
1180
+ }
1181
+ function validateRenderedSkill(content, sourcePath) {
1182
+ const frontMatter = /^---\n([\s\S]*?)\n---\n/.exec(content.replace(/\r\n/g, '\n'));
1183
+ if (frontMatter === null) {
1184
+ throw new Error(`generated skill missing YAML front matter: ${sourcePath}`);
1185
+ }
1186
+ const yaml = frontMatter[1] ?? '';
1187
+ if (!/^name:\s*\S+/m.test(yaml)) {
1188
+ throw new Error(`generated skill missing name: ${sourcePath}`);
1189
+ }
1190
+ if (!/^description:\s*(?:\S|>\s*$)/m.test(yaml)) {
1191
+ throw new Error(`generated skill missing description: ${sourcePath}`);
1192
+ }
1193
+ }
1194
+ function extractSkillDescription(content) {
1195
+ const normalized = content.replace(/\r\n/g, '\n');
1196
+ const simple = /^description:\s*['"]?(.+?)['"]?\s*$/m.exec(normalized);
1197
+ if (simple?.[1]) {
1198
+ return simple[1].trim();
1199
+ }
1200
+ const folded = /^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m.exec(normalized);
1201
+ if (folded?.[1]) {
1202
+ return folded[1]
1203
+ .split('\n')
1204
+ .map((line) => line.trim())
1205
+ .filter(Boolean)
1206
+ .join(' ');
1207
+ }
1208
+ return '';
1209
+ }
1210
+ function renderLegacyAgentsMods(sourceFile, stack = []) {
1211
+ const resolvedSource = path.resolve(sourceFile);
1212
+ if (stack.includes(resolvedSource)) {
1213
+ throw new Error(`Include cycle detected: ${[...stack, resolvedSource].join(' -> ')}`);
1214
+ }
1215
+ const lines = fs.readFileSync(resolvedSource, 'utf8').replace(/\r\n/g, '\n').split('\n');
1216
+ const output = [];
1217
+ const nextStack = [...stack, resolvedSource];
1218
+ let sawLeadingInclude = false;
1219
+ let insertedOverrideNote = false;
1220
+ let contentStarted = false;
1221
+ for (const line of lines) {
1222
+ const trimmed = line.trim();
1223
+ if (!contentStarted && trimmed === '') {
1224
+ continue;
1225
+ }
1226
+ if (trimmed.startsWith('@')) {
1227
+ const includePath = trimmed.slice(1).trim();
1228
+ if (!includePath) {
1229
+ continue;
1230
+ }
1231
+ output.push(renderLegacyAgentsMods(resolveIncludePath(resolvedSource, includePath), nextStack));
1232
+ if (!contentStarted) {
1233
+ sawLeadingInclude = true;
1234
+ }
1235
+ continue;
1236
+ }
1237
+ if (sawLeadingInclude && !insertedOverrideNote) {
1238
+ output.push('');
1239
+ output.push('If anything below this point conflicts with anything included above,');
1240
+ output.push('the later instructions below take precedence.');
1241
+ output.push('');
1242
+ insertedOverrideNote = true;
1243
+ }
1244
+ output.push(line);
1245
+ contentStarted = true;
1246
+ }
1247
+ return output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
1248
+ }
1249
+ function resolveIncludePath(sourceFile, includePath) {
1250
+ if (path.isAbsolute(includePath)) {
1251
+ return includePath;
1252
+ }
1253
+ const sourceRelativePath = path.resolve(path.dirname(sourceFile), includePath);
1254
+ if (fs.existsSync(sourceRelativePath)) {
1255
+ return sourceRelativePath;
1256
+ }
1257
+ const includeRoot = findIncludeRoot(sourceFile);
1258
+ return path.resolve(includeRoot, includePath.replace(/^(\.\.\/)+/, ''));
1259
+ }
1260
+ function findIncludeRoot(sourceFile) {
1261
+ let dir = path.dirname(sourceFile);
1262
+ for (;;) {
1263
+ if (fs.existsSync(path.join(dir, 'global')) || fs.existsSync(path.join(dir, 'templates'))) {
1264
+ return dir;
1265
+ }
1266
+ const parent = path.dirname(dir);
1267
+ if (parent === dir) {
1268
+ return path.dirname(sourceFile);
1269
+ }
1270
+ dir = parent;
1271
+ }
1272
+ }
439
1273
  function checkProject(projectRoot) {
440
1274
  const findings = [];
441
1275
  verbose(`checking project ${projectRoot}`);
@@ -443,20 +1277,35 @@ function checkProject(projectRoot) {
443
1277
  const excludedDir = profileResult.profile === null ? null : resolveAgentDir(projectRoot);
444
1278
  const localAiFiles = findLocalAiFiles(projectRoot, excludedDir);
445
1279
  for (const file of localAiFiles) {
446
- findings.push({
447
- message: `local AI file in project: ${file}`,
448
- severity: 'ERROR'
449
- });
1280
+ findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
450
1281
  }
451
1282
  if (profileResult.profile === null) {
452
- findings.push({
453
- message: profileResult.reason,
454
- severity: 'ERROR'
455
- });
1283
+ findings.push({ message: profileResult.reason, severity: 'ERROR' });
456
1284
  return findings;
457
1285
  }
458
1286
  const agentDir = resolveAgentDir(projectRoot);
459
- findings.push(...checkAgentDirectory(agentDir));
1287
+ findings.push(...checkAgentDirectory(projectRoot, agentDir));
1288
+ return findings;
1289
+ }
1290
+ function checkAgentDirectory(projectRoot, agentDir) {
1291
+ const findings = [];
1292
+ const configRoot = defaultConfigRoot(projectRoot);
1293
+ if (!fs.existsSync(path.join(agentDir, MANIFEST_FILE_NAME))) {
1294
+ const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
1295
+ const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
1296
+ if (!fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
1297
+ findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
1298
+ }
1299
+ }
1300
+ for (const relativeTemplate of REQUIRED_GLOBAL_TEMPLATES) {
1301
+ if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
1302
+ findings.push({
1303
+ message: `missing required global template: ${path.join(configRoot, relativeTemplate)}`,
1304
+ severity: 'ERROR'
1305
+ });
1306
+ }
1307
+ }
1308
+ findings.push(...checkRenderedProfile(projectRoot, agentDir));
460
1309
  return findings;
461
1310
  }
462
1311
  function checkSourceTree(rootPath) {
@@ -465,11 +1314,7 @@ function checkSourceTree(rootPath) {
465
1314
  findings: checkProject(projectRoot),
466
1315
  label: projectRoot
467
1316
  }));
468
- return {
469
- entries,
470
- hasErrors: entries.some((entry) => hasErrors(entry.findings)),
471
- root: rootPath
472
- };
1317
+ return { entries, hasErrors: entries.some((entry) => hasErrors(entry.findings)), root: rootPath };
473
1318
  }
474
1319
  function findSourceRepos(rootPath) {
475
1320
  const resolvedRoot = path.resolve(rootPath);
@@ -490,62 +1335,7 @@ function findSourceRepos(rootPath) {
490
1335
  return [...repos].sort();
491
1336
  }
492
1337
  function looksLikeRepoRoot(dir) {
493
- if (fs.existsSync(path.join(dir, '.git'))) {
494
- return true;
495
- }
496
- return fs.existsSync(path.join(dir, 'package.json'));
497
- }
498
- function checkAgentDirectory(agentDir) {
499
- const findings = [];
500
- const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
501
- const agentsPath = path.join(agentDir, 'AGENTS.md');
502
- const claudePath = path.join(agentDir, 'CLAUDE.md');
503
- if (!fs.existsSync(modsPath)) {
504
- findings.push({
505
- message: `missing AGENTS-MODS.md: ${modsPath}`,
506
- severity: 'ERROR'
507
- });
508
- }
509
- if (!fs.existsSync(agentsPath)) {
510
- findings.push({
511
- message: `missing AGENTS.md: ${agentsPath}`,
512
- severity: 'ERROR'
513
- });
514
- }
515
- if (!fs.existsSync(claudePath)) {
516
- findings.push({
517
- message: `missing CLAUDE.md: ${claudePath}`,
518
- severity: 'ERROR'
519
- });
520
- }
521
- if (fs.existsSync(claudePath)) {
522
- const claudeText = fs.readFileSync(claudePath, 'utf8').replace(/\r/g, '');
523
- if (claudeText !== '@AGENTS.md\n' && claudeText !== '@AGENTS.md') {
524
- findings.push({
525
- message: `CLAUDE.md must contain only @AGENTS.md: ${claudePath}`,
526
- severity: 'ERROR'
527
- });
528
- }
529
- }
530
- if (fs.existsSync(modsPath) && fs.existsSync(agentsPath)) {
531
- try {
532
- const rendered = renderAgentsMods(modsPath);
533
- const generated = fs.readFileSync(agentsPath, 'utf8').replace(/\r\n/g, '\n');
534
- if (generated !== rendered) {
535
- findings.push({
536
- message: `AGENTS.md is out of date with AGENTS-MODS.md: ${agentsPath}`,
537
- severity: 'ERROR'
538
- });
539
- }
540
- }
541
- catch (error) {
542
- findings.push({
543
- message: `failed to render AGENTS-MODS.md in ${agentDir}: ${formatError(error)}`,
544
- severity: 'ERROR'
545
- });
546
- }
547
- }
548
- return findings;
1338
+ return fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'));
549
1339
  }
550
1340
  function printProjectReport(projectRoot, findings) {
551
1341
  process.stdout.write(`Check: ${projectRoot}\n`);
@@ -592,63 +1382,89 @@ function countWarnings(findings) {
592
1382
  function formatError(error) {
593
1383
  return error instanceof Error ? error.message : String(error);
594
1384
  }
595
- function createBlankAgentFiles(agentDir) {
596
- fs.mkdirSync(agentDir, { recursive: true });
597
- verbose(`ensure agent path exists: ${agentDir}`);
598
- const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
1385
+ function runCodex(realBinary, permissionArgs, agentDir, args, projectRoot, wrapperArgs) {
599
1386
  const agentsPath = path.join(agentDir, 'AGENTS.md');
600
- const claudePath = path.join(agentDir, 'CLAUDE.md');
601
- if (!fs.existsSync(modsPath)) {
602
- fs.writeFileSync(modsPath, '\n', 'utf8');
603
- verbose(`created ${modsPath}`);
604
- }
605
- else {
606
- verbose(`exists ${modsPath}`);
607
- }
608
1387
  if (!fs.existsSync(agentsPath)) {
609
- fs.writeFileSync(agentsPath, '\n', 'utf8');
610
- verbose(`created ${agentsPath}`);
611
- }
612
- else {
613
- verbose(`exists ${agentsPath}`);
614
- }
615
- if (!fs.existsSync(claudePath)) {
616
- fs.writeFileSync(claudePath, '@AGENTS.md\n', 'utf8');
617
- verbose(`created ${claudePath}`);
618
- }
619
- else {
620
- verbose(`exists ${claudePath}`);
621
- }
622
- }
623
- function runCodex(realBinary, permissionArgs, agentDir, args) {
624
- const agentsPath = path.join(agentDir, 'AGENTS.md');
625
- if (fs.existsSync(agentsPath)) {
626
- execTool(realBinary, [...permissionArgs, '--config', `system_prompt_file=${agentsPath}`, ...args]);
627
- return;
628
- }
629
- failMissingConfig('codex', agentDir);
630
- }
631
- function runClaude(realBinary, permissionArgs, agentDir, args) {
1388
+ failMissingConfig('codex', agentDir);
1389
+ }
1390
+ const guardBin = path.join(agentDir, 'bin');
1391
+ const codexHomeDir = path.join(agentDir, 'memories', 'codex-home');
1392
+ fs.mkdirSync(codexHomeDir, { recursive: true });
1393
+ const env = {
1394
+ ...process.env,
1395
+ CODEX_HOME: codexHomeDir,
1396
+ AGENT_DIR: agentDir,
1397
+ AGENT_RUN_PROJECT_ROOT: projectRoot,
1398
+ PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
1399
+ };
1400
+ const codexRuntimeArgs = buildCodexRuntimeArgs(wrapperArgs);
1401
+ execCommand(realBinary, [
1402
+ ...permissionArgs,
1403
+ ...codexRuntimeArgs,
1404
+ '--config',
1405
+ `system_prompt_file=${agentsPath}`,
1406
+ '--config',
1407
+ 'project_doc_max_bytes=65536',
1408
+ '-C',
1409
+ projectRoot,
1410
+ ...(wrapperArgs.codexSandboxMode === 'sandboxed' && wrapperArgs.codexNetwork
1411
+ ? ['--config', 'sandbox_workspace_write.network_access=true']
1412
+ : []),
1413
+ ...args
1414
+ ], shouldUseShell(realBinary), env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, agentDir, code));
1415
+ }
1416
+ function buildCodexRuntimeArgs(wrapperArgs) {
1417
+ const mode = wrapperArgs.codexSandboxMode ?? 'danger';
1418
+ if (mode === 'sandboxed') {
1419
+ return ['-s', 'workspace-write'];
1420
+ }
1421
+ return ['-a', 'never', '-s', 'danger-full-access'];
1422
+ }
1423
+ function runClaude(realBinary, permissionArgs, agentDir, args, projectRoot) {
632
1424
  const claudePath = path.join(agentDir, 'CLAUDE.md');
633
- const agentsPath = path.join(agentDir, 'AGENTS.md');
634
- if (fs.existsSync(claudePath) && fs.existsSync(agentsPath)) {
635
- execTool(realBinary, [...permissionArgs, '--add-dir', agentDir, ...args]);
636
- return;
1425
+ if (!fs.existsSync(claudePath)) {
1426
+ failMissingConfig('claude', agentDir);
1427
+ }
1428
+ const guardBin = path.join(agentDir, 'bin');
1429
+ const claudeConfigDir = path.join(agentDir, '.claude');
1430
+ const env = {
1431
+ ...process.env,
1432
+ CLAUDE_CONFIG_DIR: claudeConfigDir,
1433
+ AGENT_DIR: agentDir,
1434
+ AGENT_RUN_PROJECT_ROOT: projectRoot,
1435
+ PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
1436
+ };
1437
+ execCommand(realBinary, [
1438
+ ...permissionArgs,
1439
+ '--settings',
1440
+ path.join(claudeConfigDir, 'settings.json'),
1441
+ '--add-dir',
1442
+ projectRoot,
1443
+ '--add-dir',
1444
+ agentDir,
1445
+ ...args
1446
+ ], shouldUseShell(realBinary), env, projectRoot, (code) => postflightProjectCheck(projectRoot, agentDir, code));
1447
+ }
1448
+ function postflightProjectCheck(projectRoot, agentDir, code) {
1449
+ const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
1450
+ if (localAiFiles.length === 0) {
1451
+ return code;
1452
+ }
1453
+ process.stderr.write(`agent-run: postflight found local AI files in project root ${projectRoot}\n`);
1454
+ for (const file of localAiFiles) {
1455
+ process.stderr.write(`agent-run: ${file}\n`);
637
1456
  }
638
- failMissingConfig('claude', agentDir);
1457
+ return 1;
639
1458
  }
640
1459
  function failMissingConfig(tool, agentDir) {
641
1460
  process.stderr.write(`agent-run: no ${tool} config found for this project: ${agentDir}\n`);
642
- process.stderr.write(`agent-run: run \`agent-run ${tool} --none\` to bypass agent setup, or \`agent-run ${tool} --create\` to create blank agent files.\n`);
1461
+ process.stderr.write(`agent-run: run \`agent-run ${tool} --none\` to bypass agent setup, or \`agent-run ${tool} --create\` to create profile files.\n`);
643
1462
  process.exit(1);
644
1463
  }
645
1464
  function getPermissionArgs(tool) {
646
1465
  if (process.env.AGENT_WRAPPER_FORCE_PERMISSIVE !== '1') {
647
1466
  return [];
648
1467
  }
649
- if (tool === 'codex') {
650
- return ['-a', 'never', '-s', 'danger-full-access'];
651
- }
652
1468
  if (tool === 'claude' && typeof process.getuid === 'function' && process.getuid() !== 0) {
653
1469
  return ['--permission-mode', 'bypassPermissions'];
654
1470
  }
@@ -663,8 +1479,7 @@ function resolveAgentDir(projectRoot) {
663
1479
  }
664
1480
  function findLocalAiFiles(projectRoot, excludedDir = null) {
665
1481
  const matches = [];
666
- const rootIgnored = isIgnoredDir(projectRoot);
667
- if (rootIgnored) {
1482
+ if (isIgnoredDir(projectRoot)) {
668
1483
  return matches;
669
1484
  }
670
1485
  const resolvedExcludedDir = excludedDir ? path.resolve(excludedDir) : null;
@@ -687,9 +1502,7 @@ function walkSourceTree(dir, depth, visitor) {
687
1502
  if (isIgnoredDir(dir)) {
688
1503
  return;
689
1504
  }
690
- const entries = fs
691
- .readdirSync(dir, { withFileTypes: true })
692
- .sort((a, b) => a.name.localeCompare(b.name));
1505
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
693
1506
  for (const entry of entries) {
694
1507
  const fullPath = path.join(dir, entry.name);
695
1508
  const result = visitor(fullPath, entry, depth + 1);
@@ -702,14 +1515,12 @@ function walk(dir, visitor) {
702
1515
  if (isIgnoredDir(dir)) {
703
1516
  return;
704
1517
  }
705
- const entries = fs
706
- .readdirSync(dir, { withFileTypes: true })
707
- .sort((a, b) => a.name.localeCompare(b.name));
1518
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
708
1519
  for (const entry of entries) {
709
- const fullPath = path.join(dir, entry.name);
710
1520
  if (shouldPruneProjectEntry(entry)) {
711
1521
  continue;
712
1522
  }
1523
+ const fullPath = path.join(dir, entry.name);
713
1524
  const result = visitor(fullPath, entry);
714
1525
  if (entry.isDirectory() && result !== 'skip') {
715
1526
  walk(fullPath, visitor);
@@ -729,7 +1540,7 @@ function shouldPruneProjectEntry(entry) {
729
1540
  entry.name === 'build'));
730
1541
  }
731
1542
  function isLocalAiDirectory(entry) {
732
- return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex');
1543
+ return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex' || entry.name === '.agents');
733
1544
  }
734
1545
  function isLocalAiFile(entry) {
735
1546
  return entry.isFile() && LOCAL_AI_FILE_NAMES.has(entry.name);
@@ -809,6 +1620,11 @@ function resolveProfileResult(projectRoot) {
809
1620
  verbose(`using ${ENV_FILE_NAME} AGENT_RUN_PROFILE=${envProfile}`);
810
1621
  return parseProfile(envProfile, `${ENV_FILE_NAME} AGENT_RUN_PROFILE`);
811
1622
  }
1623
+ const githubProfile = resolveGitHubProfile(projectRoot);
1624
+ if (githubProfile !== null) {
1625
+ verbose(`using GitHub origin profile=${githubProfile}`);
1626
+ return parseProfile(githubProfile, 'GitHub remote origin');
1627
+ }
812
1628
  const packagePath = path.join(projectRoot, 'package.json');
813
1629
  if (fs.existsSync(packagePath)) {
814
1630
  try {
@@ -819,42 +1635,67 @@ function resolveProfileResult(projectRoot) {
819
1635
  }
820
1636
  }
821
1637
  catch {
822
- return {
823
- profile: null,
824
- reason: `cannot parse package.json: ${packagePath}`
825
- };
1638
+ return { profile: null, reason: `cannot parse package.json: ${packagePath}` };
826
1639
  }
827
1640
  }
828
1641
  return {
829
1642
  profile: null,
830
- reason: `cannot resolve agent profile for ${projectRoot}; add ${ENV_FILE_NAME} with AGENT_RUN_PROFILE=<org/project> or set package.json.name`
1643
+ reason: `cannot resolve agent profile for ${projectRoot}; add ${ENV_FILE_NAME} with AGENT_RUN_PROFILE=<org/project>, add a GitHub origin remote, or set package.json.name`
831
1644
  };
832
1645
  }
1646
+ function resolveGitHubProfile(projectRoot) {
1647
+ const remote = readGitOrigin(projectRoot);
1648
+ return remote === null ? null : parseGitHubRemoteProfile(remote);
1649
+ }
1650
+ function readGitOrigin(projectRoot) {
1651
+ const configPath = path.join(projectRoot, '.git', 'config');
1652
+ if (!fs.existsSync(configPath)) {
1653
+ return null;
1654
+ }
1655
+ const lines = fs.readFileSync(configPath, 'utf8').replace(/\r\n/g, '\n').split('\n');
1656
+ let inOrigin = false;
1657
+ for (const line of lines) {
1658
+ const trimmed = line.trim();
1659
+ if (trimmed.startsWith('[')) {
1660
+ inOrigin = trimmed === '[remote "origin"]';
1661
+ continue;
1662
+ }
1663
+ if (inOrigin && trimmed.startsWith('url')) {
1664
+ const equalsIndex = trimmed.indexOf('=');
1665
+ if (equalsIndex >= 0) {
1666
+ return trimmed.slice(equalsIndex + 1).trim();
1667
+ }
1668
+ }
1669
+ }
1670
+ return null;
1671
+ }
1672
+ function parseGitHubRemoteProfile(remote) {
1673
+ const patterns = [
1674
+ /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/,
1675
+ /^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/,
1676
+ /^ssh:\/\/git@github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
1677
+ ];
1678
+ for (const pattern of patterns) {
1679
+ const match = pattern.exec(remote.trim());
1680
+ if (match?.[1] && match[2]) {
1681
+ return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
1682
+ }
1683
+ }
1684
+ return null;
1685
+ }
833
1686
  function parseProfile(profile, source) {
834
1687
  const normalized = profile.trim().replace(/\\/g, '/');
835
1688
  if (!normalized) {
836
- return {
837
- profile: null,
838
- reason: `${source} must be a non-empty path relative to the config root`
839
- };
1689
+ return { profile: null, reason: `${source} must be a non-empty path relative to the config root` };
840
1690
  }
841
1691
  if (normalized.startsWith('/') || normalized.startsWith('\\\\') || /^[A-Za-z]:\//.test(normalized)) {
842
- return {
843
- profile: null,
844
- reason: `${source} must be relative to the config root, not an absolute path`
845
- };
1692
+ return { profile: null, reason: `${source} must be relative to the config root, not an absolute path` };
846
1693
  }
847
1694
  const segments = normalized.split('/').filter((segment) => segment.length > 0);
848
1695
  if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) {
849
- return {
850
- profile: null,
851
- reason: `${source} must be a clean relative path like org/my-project`
852
- };
1696
+ return { profile: null, reason: `${source} must be a clean relative path like org/my-project` };
853
1697
  }
854
- return {
855
- profile: segments.join('/'),
856
- reason: ''
857
- };
1698
+ return { profile: segments.join('/'), reason: '' };
858
1699
  }
859
1700
  function readAgentRunEnv(dir) {
860
1701
  const resolvedDir = path.resolve(dir);
@@ -864,12 +1705,10 @@ function readAgentRunEnv(dir) {
864
1705
  }
865
1706
  const filePath = path.join(resolvedDir, ENV_FILE_NAME);
866
1707
  if (!fs.existsSync(filePath)) {
867
- verbose(`no ${ENV_FILE_NAME} in ${resolvedDir}`);
868
1708
  const emptyEnv = {};
869
1709
  agentRunEnvCache.set(resolvedDir, emptyEnv);
870
1710
  return emptyEnv;
871
1711
  }
872
- verbose(`read ${filePath}`);
873
1712
  const env = {};
874
1713
  const lines = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n').split('\n');
875
1714
  for (const rawLine of lines) {
@@ -883,8 +1722,7 @@ function readAgentRunEnv(dir) {
883
1722
  }
884
1723
  const key = line.slice(0, equalsIndex).trim();
885
1724
  let value = line.slice(equalsIndex + 1).trim();
886
- if (value.length >= 2 &&
887
- ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
1725
+ if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
888
1726
  value = value.slice(1, -1);
889
1727
  }
890
1728
  env[key] = value;
@@ -902,32 +1740,26 @@ function parseBooleanEnv(value) {
902
1740
  function defaultConfigRoot(projectRoot) {
903
1741
  const overrideRoot = process.env[CONFIG_ROOT_OVERRIDE_ENV];
904
1742
  if (overrideRoot) {
905
- const resolved = path.resolve(overrideRoot);
906
- verbose(`using --config-root override: ${resolved}`);
907
- return resolved;
1743
+ return path.resolve(overrideRoot);
1744
+ }
1745
+ if (process.env[CONFIG_DIR_ENV]) {
1746
+ return path.resolve(process.env[CONFIG_DIR_ENV]);
908
1747
  }
909
1748
  if (process.env.AGENT_CONFIG_ROOT) {
910
- const resolved = path.resolve(process.env.AGENT_CONFIG_ROOT);
911
- verbose(`using AGENT_CONFIG_ROOT env: ${resolved}`);
912
- return resolved;
1749
+ return path.resolve(process.env.AGENT_CONFIG_ROOT);
913
1750
  }
914
1751
  if (projectRoot) {
915
1752
  const env = readAgentRunEnv(projectRoot);
1753
+ const localConfigDir = env[CONFIG_DIR_ENV]?.trim();
1754
+ if (localConfigDir) {
1755
+ return path.resolve(projectRoot, localConfigDir);
1756
+ }
916
1757
  const localConfigRoot = env.AGENT_CONFIG_ROOT?.trim();
917
1758
  if (localConfigRoot) {
918
- const resolved = path.resolve(projectRoot, localConfigRoot);
919
- verbose(`using ${ENV_FILE_NAME} AGENT_CONFIG_ROOT: ${resolved}`);
920
- return resolved;
1759
+ return path.resolve(projectRoot, localConfigRoot);
921
1760
  }
922
1761
  }
923
- if (IS_WINDOWS) {
924
- const resolved = path.join(os.homedir(), 'Documents', 'source', 'agent-configs');
925
- verbose(`using default config root: ${resolved}`);
926
- return resolved;
927
- }
928
- const resolved = path.join(os.homedir(), 'source', 'agent-configs');
929
- verbose(`using default config root: ${resolved}`);
930
- return resolved;
1762
+ return path.join(os.homedir(), '.agent-config');
931
1763
  }
932
1764
  function findRealBinary(tool) {
933
1765
  const pathValue = process.env.PATH || '';
@@ -959,10 +1791,7 @@ function findRealBinary(tool) {
959
1791
  for (const dir of pathDirs) {
960
1792
  for (const extension of extensions) {
961
1793
  const candidate = path.join(dir, `${tool}${extension}`);
962
- if (!fs.existsSync(candidate)) {
963
- continue;
964
- }
965
- if (!isExecutable(candidate)) {
1794
+ if (!fs.existsSync(candidate) || !isExecutable(candidate)) {
966
1795
  continue;
967
1796
  }
968
1797
  let resolvedCandidate = candidate;
@@ -972,10 +1801,7 @@ function findRealBinary(tool) {
972
1801
  catch {
973
1802
  resolvedCandidate = candidate;
974
1803
  }
975
- if (resolvedCandidate === currentScript) {
976
- continue;
977
- }
978
- if (wrapperCandidates.has(resolvedCandidate)) {
1804
+ if (resolvedCandidate === currentScript || wrapperCandidates.has(resolvedCandidate)) {
979
1805
  continue;
980
1806
  }
981
1807
  return candidate;
@@ -984,10 +1810,7 @@ function findRealBinary(tool) {
984
1810
  fail(`no ${tool} binary found in PATH`);
985
1811
  }
986
1812
  function getExecutableExtensions(tool) {
987
- if (!IS_WINDOWS) {
988
- return [''];
989
- }
990
- if (path.extname(tool)) {
1813
+ if (!IS_WINDOWS || path.extname(tool)) {
991
1814
  return [''];
992
1815
  }
993
1816
  const pathExt = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
@@ -1012,19 +1835,16 @@ function execTool(command, args) {
1012
1835
  verbose(`exec tool: ${formatCommand(command, args)} shell=${String(shouldUseShell(command))}`);
1013
1836
  execCommand(command, args, shouldUseShell(command));
1014
1837
  }
1015
- function execCommand(command, args, shell, env) {
1016
- verbose(`spawn: ${formatCommand(command, args)} shell=${String(shell)}`);
1017
- const child = childProcess.spawn(command, args, {
1018
- env,
1019
- shell,
1020
- stdio: 'inherit'
1021
- });
1838
+ function execCommand(command, args, shell, env, cwd, onExit) {
1839
+ verbose(`spawn: ${formatCommand(command, args)} shell=${String(shell)} cwd=${cwd ?? process.cwd()}`);
1840
+ const child = childProcess.spawn(command, args, { cwd, env, shell, stdio: 'inherit' });
1022
1841
  child.on('exit', (code, signal) => {
1023
1842
  if (signal) {
1024
1843
  process.kill(process.pid, signal);
1025
1844
  return;
1026
1845
  }
1027
- process.exit(code === null ? 1 : code);
1846
+ const originalCode = code === null ? 1 : code;
1847
+ process.exit(onExit ? onExit(originalCode) : originalCode);
1028
1848
  });
1029
1849
  child.on('error', (error) => {
1030
1850
  fail(error.message);
@@ -1033,39 +1853,32 @@ function execCommand(command, args, shell, env) {
1033
1853
  function openEditor(filePath) {
1034
1854
  const visual = process.env.VISUAL?.trim();
1035
1855
  if (visual) {
1036
- verbose(`open editor via VISUAL=${visual} file=${filePath}`);
1037
1856
  execCommand(visual, [filePath], true);
1038
1857
  return;
1039
1858
  }
1040
1859
  const editor = process.env.EDITOR?.trim();
1041
1860
  if (editor) {
1042
- verbose(`open editor via EDITOR=${editor} file=${filePath}`);
1043
1861
  execCommand(editor, [filePath], true);
1044
1862
  return;
1045
1863
  }
1046
1864
  const vscodeCommand = findVsCodeEditorCommand();
1047
1865
  if (vscodeCommand !== null) {
1048
- verbose(`open editor via VS Code command=${vscodeCommand} file=${filePath}`);
1049
1866
  execCommand(vscodeCommand, ['--reuse-window', filePath], shouldUseShell(vscodeCommand));
1050
1867
  return;
1051
1868
  }
1052
1869
  const fallbackEditor = findFallbackEditor();
1053
1870
  if (fallbackEditor !== null) {
1054
- verbose(`open editor via fallback command=${fallbackEditor} file=${filePath}`);
1055
1871
  execCommand(fallbackEditor, [filePath], shouldUseShell(fallbackEditor));
1056
1872
  return;
1057
1873
  }
1058
1874
  if (IS_WINDOWS) {
1059
- verbose(`open editor via cmd.exe start file=${filePath}`);
1060
1875
  execCommand('cmd.exe', ['/c', 'start', '', filePath], false);
1061
1876
  return;
1062
1877
  }
1063
1878
  if (process.platform === 'darwin') {
1064
- verbose(`open editor via open file=${filePath}`);
1065
1879
  execCommand('open', [filePath], false);
1066
1880
  return;
1067
1881
  }
1068
- verbose(`open editor via xdg-open file=${filePath}`);
1069
1882
  execCommand('xdg-open', [filePath], false);
1070
1883
  }
1071
1884
  function findVsCodeEditorCommand() {
@@ -1082,17 +1895,13 @@ function findVsCodeEditorCommand() {
1082
1895
  }
1083
1896
  function isRunningInVsCodeTerminal() {
1084
1897
  const termProgram = process.env.TERM_PROGRAM?.trim().toLowerCase();
1085
- if (termProgram === 'vscode') {
1086
- return true;
1087
- }
1088
- return (Boolean(process.env.VSCODE_GIT_IPC_HANDLE) ||
1898
+ return (termProgram === 'vscode' ||
1899
+ Boolean(process.env.VSCODE_GIT_IPC_HANDLE) ||
1089
1900
  Boolean(process.env.VSCODE_IPC_HOOK) ||
1090
1901
  Boolean(process.env.VSCODE_IPC_HOOK_CLI));
1091
1902
  }
1092
1903
  function findFallbackEditor() {
1093
- const candidates = IS_WINDOWS
1094
- ? ['notepad.exe']
1095
- : ['joe', 'sensible-editor', 'editor', 'nano', 'nvim', 'vim', 'vi'];
1904
+ const candidates = IS_WINDOWS ? ['notepad.exe'] : ['joe', 'sensible-editor', 'editor', 'nano', 'nvim', 'vim', 'vi'];
1096
1905
  for (const candidate of candidates) {
1097
1906
  const resolved = findExecutable(candidate);
1098
1907
  if (resolved !== null) {
@@ -1104,34 +1913,17 @@ function findFallbackEditor() {
1104
1913
  function findExecutable(command) {
1105
1914
  const pathValue = process.env.PATH || '';
1106
1915
  const pathDirs = pathValue.split(path.delimiter).filter((entry) => entry.length > 0);
1107
- const extensions = getExecutableExtensionsForCommand(command);
1916
+ const extensions = getExecutableExtensions(command);
1108
1917
  for (const dir of pathDirs) {
1109
1918
  for (const extension of extensions) {
1110
1919
  const candidate = path.join(dir, `${command}${extension}`);
1111
- if (!fs.existsSync(candidate)) {
1112
- continue;
1920
+ if (fs.existsSync(candidate) && isExecutable(candidate)) {
1921
+ return candidate;
1113
1922
  }
1114
- if (!isExecutable(candidate)) {
1115
- continue;
1116
- }
1117
- return candidate;
1118
1923
  }
1119
1924
  }
1120
1925
  return null;
1121
1926
  }
1122
- function getExecutableExtensionsForCommand(command) {
1123
- if (!IS_WINDOWS) {
1124
- return [''];
1125
- }
1126
- if (path.extname(command)) {
1127
- return [''];
1128
- }
1129
- const pathExt = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
1130
- .split(';')
1131
- .filter((entry) => entry.length > 0)
1132
- .map((entry) => entry.toLowerCase());
1133
- return [''].concat(pathExt);
1134
- }
1135
1927
  function shouldUseShell(command) {
1136
1928
  if (!IS_WINDOWS) {
1137
1929
  return false;
@@ -1139,14 +1931,350 @@ function shouldUseShell(command) {
1139
1931
  const extension = path.extname(command).toLowerCase();
1140
1932
  return extension === '.cmd' || extension === '.bat';
1141
1933
  }
1934
+ function localDateString() {
1935
+ const now = new Date();
1936
+ const year = String(now.getFullYear()).padStart(4, '0');
1937
+ const month = String(now.getMonth() + 1).padStart(2, '0');
1938
+ const day = String(now.getDate()).padStart(2, '0');
1939
+ return `${year}-${month}-${day}`;
1940
+ }
1941
+ function stringifyDefaultManifest(profile) {
1942
+ const manifest = defaultManifest(profile);
1943
+ return `${JSON.stringify(manifest, null, 2)}\n`;
1944
+ }
1945
+ function defaultLocalTemplate(_profile) {
1946
+ return [
1947
+ '# {{ profile }} local agent instructions',
1948
+ '',
1949
+ 'Project root:',
1950
+ '',
1951
+ '```text',
1952
+ '{{ projectRoot }}',
1953
+ '```',
1954
+ '',
1955
+ 'Agent directory:',
1956
+ '',
1957
+ '```text',
1958
+ '{{ agentDir }}',
1959
+ '```',
1960
+ '',
1961
+ 'Add project-specific rules here.',
1962
+ ''
1963
+ ].join('\n');
1964
+ }
1965
+ function defaultCodeAgentTemplate() {
1966
+ return [
1967
+ '# Code Agent',
1968
+ '',
1969
+ 'Work in the project root shown by agent-run. Keep changes scoped to the user request.',
1970
+ '',
1971
+ '{% include "global/snippets/git-rules.md.njk" %}',
1972
+ '',
1973
+ '{% include "global/snippets/no-ai-files.md.njk" %}',
1974
+ '',
1975
+ '{% include "global/snippets/verification.md.njk" %}',
1976
+ ''
1977
+ ].join('\n');
1978
+ }
1979
+ function defaultWritingAgentTemplate() {
1980
+ return [
1981
+ '# Writing Agent',
1982
+ '',
1983
+ 'Write clearly and preserve the existing voice, structure, and facts in the project.',
1984
+ '',
1985
+ '{% include "global/snippets/no-ai-files.md.njk" %}',
1986
+ ''
1987
+ ].join('\n');
1988
+ }
1989
+ function defaultGitRulesSnippet() {
1990
+ return [
1991
+ '## Git Rules',
1992
+ '',
1993
+ 'Do not commit unless the user explicitly asks. Before committing, show changed files and the exact commit message.',
1994
+ 'Use human commit messages unless the user asks for conventional commits. Do not push unless explicitly asked.',
1995
+ ''
1996
+ ].join('\n');
1997
+ }
1998
+ function defaultNoAiFilesSnippet() {
1999
+ return [
2000
+ '## Agent File Storage',
2001
+ '',
2002
+ 'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or other agent runtime files inside the project repository.',
2003
+ 'Agent-only files belong under {{ agentDir }}.',
2004
+ ''
2005
+ ].join('\n');
2006
+ }
2007
+ function defaultVerificationSnippet() {
2008
+ return [
2009
+ '## Verification',
2010
+ '',
2011
+ 'Run relevant checks before reporting completion. Configured checks:',
2012
+ '',
2013
+ '{% for check in checks %}',
2014
+ '- `{{ check }}`',
2015
+ '{% endfor %}',
2016
+ ''
2017
+ ].join('\n');
2018
+ }
2019
+ function defaultCommitWorkflowSkill() {
2020
+ return [
2021
+ '---',
2022
+ 'name: commit-workflow',
2023
+ 'description: Use for preparing or creating commits with explicit user approval.',
2024
+ '---',
2025
+ '',
2026
+ '# Commit Workflow',
2027
+ '',
2028
+ 'Never commit unless explicitly asked. Before committing, show changed files and the exact commit message.',
2029
+ 'Use human commit messages unless conventional commits are explicitly requested. Do not push.',
2030
+ 'Run configured checks before committing:',
2031
+ '',
2032
+ '{% for check in checks %}',
2033
+ '- `{{ check }}`',
2034
+ '{% endfor %}',
2035
+ ''
2036
+ ].join('\n');
2037
+ }
2038
+ function defaultGithubReleaseSkill() {
2039
+ return [
2040
+ '---',
2041
+ 'name: github-release',
2042
+ 'description: Use for releases, version bumps, tags, publishing, GitHub Releases, and CHANGES updates.',
2043
+ '---',
2044
+ '',
2045
+ '# GitHub Release',
2046
+ '',
2047
+ 'Use {{ paths.changesFile }} for change notes.',
2048
+ 'Require confirmation before writing CHANGES, bumping versions, committing, tagging, pushing, publishing, or creating a GitHub Release.',
2049
+ 'Follow `$commit-workflow` for release commits.',
2050
+ ''
2051
+ ].join('\n');
2052
+ }
2053
+ function defaultCodeReviewSkill() {
2054
+ return [
2055
+ '---',
2056
+ 'name: code-review',
2057
+ 'description: Use for code review, PR review, and diff review.',
2058
+ '---',
2059
+ '',
2060
+ '# Code Review',
2061
+ '',
2062
+ 'Save review notes to {{ paths.reviewFile }}. If the file exists, update it.',
2063
+ 'Also include findings in the final user-facing reply.',
2064
+ 'Do not save review files inside the project repo.',
2065
+ ''
2066
+ ].join('\n');
2067
+ }
2068
+ function defaultToolInstructionsTemplate(isClaude) {
2069
+ return [
2070
+ `# Generated ${isClaude ? 'Claude' : 'agent'} instructions for {{ profile }}`,
2071
+ '',
2072
+ 'Do not edit this file directly. Edit:',
2073
+ '',
2074
+ '```text',
2075
+ '{{ agentDir }}/agent-run.jsonc',
2076
+ '{{ agentDir }}/local.md.njk',
2077
+ '```',
2078
+ '',
2079
+ '{% for section in renderedAgentSections %}',
2080
+ '{{ section }}',
2081
+ '',
2082
+ '{% endfor %}',
2083
+ '## Absolute Paths',
2084
+ '',
2085
+ 'Project root:',
2086
+ '',
2087
+ '```text',
2088
+ '{{ projectRoot }}',
2089
+ '```',
2090
+ '',
2091
+ 'Agent directory:',
2092
+ '',
2093
+ '```text',
2094
+ '{{ agentDir }}',
2095
+ '```',
2096
+ '',
2097
+ 'Review directory:',
2098
+ '',
2099
+ '```text',
2100
+ '{{ paths.reviewDir }}',
2101
+ '```',
2102
+ '',
2103
+ "Today's review file:",
2104
+ '',
2105
+ '```text',
2106
+ '{{ paths.reviewFile }}',
2107
+ '```',
2108
+ '',
2109
+ 'Memories directory:',
2110
+ '',
2111
+ '```text',
2112
+ '{{ paths.memoriesDir }}',
2113
+ '```',
2114
+ '',
2115
+ 'Changes file:',
2116
+ '',
2117
+ '```text',
2118
+ '{{ paths.changesFile }}',
2119
+ '```',
2120
+ '',
2121
+ '## Available Skills',
2122
+ '',
2123
+ '{% for skill in skills %}',
2124
+ '- `${{ skill.name }}`{% if skill.description %}: {{ skill.description }}{% endif %}',
2125
+ '{% endfor %}',
2126
+ '',
2127
+ '## Tool Note',
2128
+ '',
2129
+ isClaude
2130
+ ? 'Claude uses the generated config directory under {{ agentDir }}/.claude and may read {{ agentDir }} via `--add-dir`.'
2131
+ : 'Codex uses {{ agentDir }}/AGENTS.md as `system_prompt_file` and {{ paths.codexHomeDir }} as CODEX_HOME.',
2132
+ '',
2133
+ '## Mandatory Path Rule',
2134
+ '',
2135
+ 'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or AI-related files inside the project repository.',
2136
+ 'Agent-only files must be stored under the agent directory shown above.',
2137
+ ''
2138
+ ].join('\n');
2139
+ }
2140
+ function defaultCodexConfigTemplate() {
2141
+ return [
2142
+ '# Generated by agent-run. Do not edit directly.',
2143
+ '',
2144
+ 'project_doc_max_bytes = 65536',
2145
+ 'sandbox_mode = "workspace-write"',
2146
+ '',
2147
+ '[sandbox_workspace_write]',
2148
+ 'writable_roots = [',
2149
+ ' "{{ projectRoot }}",',
2150
+ ' "{{ agentDir }}"',
2151
+ ']',
2152
+ 'network_access = false',
2153
+ ''
2154
+ ].join('\n');
2155
+ }
2156
+ function defaultClaudeSettingsTemplate() {
2157
+ return [
2158
+ '{',
2159
+ ' "env": {',
2160
+ ' "AGENT_DIR": "{{ agentDir }}",',
2161
+ ' "AGENT_RUN_PROJECT_ROOT": "{{ projectRoot }}"',
2162
+ ' }',
2163
+ '}',
2164
+ ''
2165
+ ].join('\n');
2166
+ }
2167
+ function defaultCodexConfigContent(context) {
2168
+ return [
2169
+ '# Generated by agent-run. Do not edit directly.',
2170
+ '',
2171
+ 'project_doc_max_bytes = 65536',
2172
+ 'sandbox_mode = "workspace-write"',
2173
+ '',
2174
+ '[sandbox_workspace_write]',
2175
+ 'writable_roots = [',
2176
+ ` ${jsonString(context.projectRoot)},`,
2177
+ ` ${jsonString(context.agentDir)}`,
2178
+ ']',
2179
+ 'network_access = false',
2180
+ ''
2181
+ ].join('\n');
2182
+ }
2183
+ function defaultClaudeSettingsContent(context) {
2184
+ return `${JSON.stringify({
2185
+ env: {
2186
+ AGENT_DIR: context.agentDir,
2187
+ AGENT_RUN_PROJECT_ROOT: context.projectRoot
2188
+ }
2189
+ }, null, 2)}\n`;
2190
+ }
2191
+ function jsonString(value) {
2192
+ return JSON.stringify(value);
2193
+ }
2194
+ function posixGitShim() {
2195
+ return [
2196
+ '#!/usr/bin/env bash',
2197
+ 'set -euo pipefail',
2198
+ 'case "${1:-}" in',
2199
+ ' commit|tag|push)',
2200
+ ' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
2201
+ ' echo "agent-run: blocked git $1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
2202
+ ' exit 42',
2203
+ ' fi',
2204
+ ' ;;',
2205
+ 'esac',
2206
+ 'if command -v /usr/bin/git >/dev/null 2>&1; then',
2207
+ ' exec /usr/bin/git "$@"',
2208
+ 'fi',
2209
+ 'exec git "$@"',
2210
+ ''
2211
+ ].join('\n');
2212
+ }
2213
+ function posixPublishShim(tool) {
2214
+ return [
2215
+ '#!/usr/bin/env bash',
2216
+ 'set -euo pipefail',
2217
+ 'if [ "${1:-}" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
2218
+ ` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
2219
+ ' exit 42',
2220
+ 'fi',
2221
+ `if command -v /usr/bin/${tool} >/dev/null 2>&1; then`,
2222
+ ` exec /usr/bin/${tool} "$@"`,
2223
+ 'fi',
2224
+ `exec ${tool} "$@"`,
2225
+ ''
2226
+ ].join('\n');
2227
+ }
2228
+ function posixGhShim() {
2229
+ return [
2230
+ '#!/usr/bin/env bash',
2231
+ 'set -euo pipefail',
2232
+ 'if [ "${1:-}" = "release" ] && [ "${2:-}" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
2233
+ ' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
2234
+ ' exit 42',
2235
+ 'fi',
2236
+ 'if command -v /usr/bin/gh >/dev/null 2>&1; then',
2237
+ ' exec /usr/bin/gh "$@"',
2238
+ 'fi',
2239
+ 'exec gh "$@"',
2240
+ ''
2241
+ ].join('\n');
2242
+ }
2243
+ function windowsShim(name) {
2244
+ const guard = name === 'git'
2245
+ ? [
2246
+ 'if /I "%1"=="commit" goto block_git',
2247
+ 'if /I "%1"=="tag" goto block_git',
2248
+ 'if /I "%1"=="push" goto block_git',
2249
+ 'goto run',
2250
+ ':block_git',
2251
+ 'if "%AGENT_RUN_ALLOW_GIT_WRITE%"=="1" goto run',
2252
+ 'echo agent-run: blocked git %1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation. 1>&2',
2253
+ 'exit /b 42'
2254
+ ]
2255
+ : name === 'gh'
2256
+ ? [
2257
+ 'if /I not "%1"=="release" goto run',
2258
+ 'if /I not "%2"=="create" goto run',
2259
+ 'if "%AGENT_RUN_ALLOW_GITHUB_RELEASE%"=="1" goto run',
2260
+ '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',
2261
+ 'exit /b 42'
2262
+ ]
2263
+ : [
2264
+ 'if /I not "%1"=="publish" goto run',
2265
+ 'if "%AGENT_RUN_ALLOW_PUBLISH%"=="1" goto run',
2266
+ `echo agent-run: blocked ${name} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation. 1>&2`,
2267
+ 'exit /b 42'
2268
+ ];
2269
+ return ['@echo off', ...guard, ':run', `${name}.exe %*`, ''].join('\r\n');
2270
+ }
1142
2271
  function isVerbose() {
1143
2272
  return parseBooleanEnv(process.env[VERBOSE_ENV]);
1144
2273
  }
1145
2274
  function verbose(message) {
1146
- if (!isVerbose()) {
1147
- return;
2275
+ if (isVerbose()) {
2276
+ process.stderr.write(`agent-run: ${message}\n`);
1148
2277
  }
1149
- process.stderr.write(`agent-run: ${message}\n`);
1150
2278
  }
1151
2279
  function formatCommand(command, args) {
1152
2280
  return [command, ...args].map(quoteArg).join(' ');