@technomoron/agent-run 0.99.3 → 0.99.4
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 +12 -1
- package/README.md +69 -29
- package/dist/agent-run.js +1486 -378
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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) {
|
|
@@ -359,19 +430,15 @@ function runTool(parsed) {
|
|
|
359
430
|
fail(profileResult.reason);
|
|
360
431
|
}
|
|
361
432
|
const agentDir = resolveAgentDir(projectRoot);
|
|
362
|
-
verbose(`resolved agent path: ${agentDir}`);
|
|
363
433
|
if (wrapperArgs.create) {
|
|
364
|
-
|
|
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);
|
|
434
|
+
runInit({ command: 'init', targetPath: projectRoot });
|
|
369
435
|
}
|
|
436
|
+
syncAgentProfile(projectRoot, agentDir);
|
|
370
437
|
if (command === 'codex') {
|
|
371
|
-
runCodex(realBinary, permissionArgs, agentDir, args);
|
|
438
|
+
runCodex(realBinary, permissionArgs, agentDir, args, projectRoot, wrapperArgs);
|
|
372
439
|
return;
|
|
373
440
|
}
|
|
374
|
-
runClaude(realBinary, permissionArgs, agentDir, args);
|
|
441
|
+
runClaude(realBinary, permissionArgs, agentDir, args, projectRoot);
|
|
375
442
|
}
|
|
376
443
|
function runInit(parsed) {
|
|
377
444
|
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
@@ -380,12 +447,19 @@ function runInit(parsed) {
|
|
|
380
447
|
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
381
448
|
return;
|
|
382
449
|
}
|
|
450
|
+
const profile = resolveProfile(projectRoot);
|
|
451
|
+
const configRoot = defaultConfigRoot(projectRoot);
|
|
383
452
|
const agentDir = resolveAgentDir(projectRoot);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
453
|
+
ensureConfigRootLayout(configRoot);
|
|
454
|
+
ensureConfigRootGitignore(configRoot);
|
|
455
|
+
ensureDefaultGlobalTemplates(configRoot);
|
|
456
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
457
|
+
ensureProfileOverridesDir(agentDir);
|
|
458
|
+
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
459
|
+
createDefaultManifestFile(agentDir, profile);
|
|
460
|
+
createDefaultLocalFile(agentDir, profile);
|
|
461
|
+
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
462
|
+
printUpdateSummary(rendered);
|
|
389
463
|
}
|
|
390
464
|
function runEdit(parsed) {
|
|
391
465
|
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
@@ -395,13 +469,14 @@ function runEdit(parsed) {
|
|
|
395
469
|
process.exit(0);
|
|
396
470
|
}
|
|
397
471
|
const agentDir = resolveAgentDir(projectRoot);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
472
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
473
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
474
|
+
if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
|
|
475
|
+
runInit({ command: 'init', targetPath: projectRoot });
|
|
401
476
|
}
|
|
402
|
-
const
|
|
403
|
-
process.stdout.write(`Edit: ${
|
|
404
|
-
openEditor(
|
|
477
|
+
const editPath = fs.existsSync(localPath) ? localPath : legacyPath;
|
|
478
|
+
process.stdout.write(`Edit: ${editPath}\n`);
|
|
479
|
+
openEditor(editPath);
|
|
405
480
|
}
|
|
406
481
|
function runUpdate(parsed) {
|
|
407
482
|
const projectRoot = findProjectRoot(parsed.targetPath);
|
|
@@ -410,13 +485,65 @@ function runUpdate(parsed) {
|
|
|
410
485
|
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
411
486
|
return;
|
|
412
487
|
}
|
|
488
|
+
const profile = resolveProfile(projectRoot);
|
|
489
|
+
const configRoot = defaultConfigRoot(projectRoot);
|
|
413
490
|
const agentDir = resolveAgentDir(projectRoot);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
491
|
+
ensureConfigRootLayout(configRoot);
|
|
492
|
+
ensureConfigRootGitignore(configRoot);
|
|
493
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
494
|
+
ensureProfileOverridesDir(agentDir);
|
|
495
|
+
convertLegacyProfileIfNeeded(configRoot, agentDir, profile);
|
|
496
|
+
createDefaultManifestFile(agentDir, profile);
|
|
497
|
+
const rendered = syncAgentProfile(projectRoot, agentDir);
|
|
498
|
+
printUpdateSummary(rendered);
|
|
499
|
+
}
|
|
500
|
+
function runMigrateConfig(parsed) {
|
|
501
|
+
const configRoot = parsed.configRoot;
|
|
502
|
+
verbose(`migrate config root=${configRoot}`);
|
|
503
|
+
if (!fs.existsSync(configRoot)) {
|
|
504
|
+
fail(`missing config root: ${configRoot}`);
|
|
505
|
+
}
|
|
506
|
+
ensureConfigRootLayout(configRoot);
|
|
507
|
+
ensureConfigRootGitignore(configRoot);
|
|
508
|
+
migrateOldTemplates(configRoot);
|
|
509
|
+
ensureDefaultGlobalTemplates(configRoot);
|
|
510
|
+
const profileDirs = findLegacyProfileDirs(configRoot);
|
|
511
|
+
let createdManifestCount = 0;
|
|
512
|
+
let createdLocalCount = 0;
|
|
513
|
+
let createdOverridesCount = 0;
|
|
514
|
+
let movedRuntimeCount = 0;
|
|
515
|
+
let movedReviewCount = 0;
|
|
516
|
+
let movedMemoryCount = 0;
|
|
517
|
+
for (const agentDir of profileDirs) {
|
|
518
|
+
const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
|
|
519
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
520
|
+
if (!fs.existsSync(localPath)) {
|
|
521
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
522
|
+
fs.writeFileSync(localPath, convertLegacyTemplateVars(fs.readFileSync(legacyPath, 'utf8')), 'utf8');
|
|
523
|
+
createdLocalCount += 1;
|
|
524
|
+
}
|
|
525
|
+
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
526
|
+
if (!fs.existsSync(manifestPath)) {
|
|
527
|
+
createDefaultManifestFile(agentDir, profile);
|
|
528
|
+
createdManifestCount += 1;
|
|
529
|
+
}
|
|
530
|
+
const overridesDir = path.join(agentDir, 'overrides');
|
|
531
|
+
if (!fs.existsSync(overridesDir)) {
|
|
532
|
+
fs.mkdirSync(overridesDir, { recursive: true });
|
|
533
|
+
createdOverridesCount += 1;
|
|
534
|
+
}
|
|
535
|
+
movedRuntimeCount += migrateCodexRuntimeFiles(agentDir);
|
|
536
|
+
movedReviewCount += migrateLooseFiles(agentDir, /^REVIEW(?:-.+)?\.md$/, 'reviews');
|
|
537
|
+
movedMemoryCount += migrateLooseFiles(agentDir, /^memory.*\.md$/i, 'memories');
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write(`OK migrated ${configRoot}\n`);
|
|
540
|
+
process.stdout.write(`Profiles converted: ${profileDirs.length}\n`);
|
|
541
|
+
process.stdout.write(`Created local.md.njk: ${createdLocalCount}\n`);
|
|
542
|
+
process.stdout.write(`Created agent-run.jsonc: ${createdManifestCount}\n`);
|
|
543
|
+
process.stdout.write(`Created overrides dirs: ${createdOverridesCount}\n`);
|
|
544
|
+
process.stdout.write(`Moved Codex runtime entries: ${movedRuntimeCount}\n`);
|
|
545
|
+
process.stdout.write(`Moved review files: ${movedReviewCount}\n`);
|
|
546
|
+
process.stdout.write(`Moved memory files: ${movedMemoryCount}\n`);
|
|
420
547
|
}
|
|
421
548
|
function runCheck(parsed) {
|
|
422
549
|
if (parsed.all) {
|
|
@@ -436,6 +563,693 @@ function runCheck(parsed) {
|
|
|
436
563
|
printProjectReport(projectRoot, findings);
|
|
437
564
|
process.exit(hasErrors(findings) ? 1 : 0);
|
|
438
565
|
}
|
|
566
|
+
function printUpdateSummary(rendered) {
|
|
567
|
+
process.stdout.write(`OK profile ${rendered.profile}\n`);
|
|
568
|
+
process.stdout.write(`Agent dir: ${rendered.agentDir}\n`);
|
|
569
|
+
process.stdout.write(`Generated files: ${rendered.files.length}\n`);
|
|
570
|
+
process.stdout.write(`Installed skills: ${rendered.skills.map((skill) => skill.name).join(', ') || '(none)'}\n`);
|
|
571
|
+
}
|
|
572
|
+
function ensureConfigRootLayout(configRoot) {
|
|
573
|
+
fs.mkdirSync(configRoot, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
function ensureConfigRootGitignore(configRoot) {
|
|
576
|
+
fs.mkdirSync(configRoot, { recursive: true });
|
|
577
|
+
const gitignorePath = path.join(configRoot, '.gitignore');
|
|
578
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8').replace(/\r\n/g, '\n') : '';
|
|
579
|
+
const lines = existing.length > 0 ? existing.replace(/\n+$/, '').split('\n') : [];
|
|
580
|
+
const present = new Set(lines);
|
|
581
|
+
for (const entry of GENERATED_GITIGNORE_ENTRIES) {
|
|
582
|
+
if (entry === '' || present.has(entry)) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
lines.push(entry);
|
|
586
|
+
present.add(entry);
|
|
587
|
+
}
|
|
588
|
+
fs.writeFileSync(gitignorePath, `${lines.join('\n')}\n`, 'utf8');
|
|
589
|
+
}
|
|
590
|
+
function ensureDefaultGlobalTemplates(configRoot) {
|
|
591
|
+
const defaults = new Map([
|
|
592
|
+
['global/agents/code.md.njk', defaultCodeAgentTemplate()],
|
|
593
|
+
['global/agents/writing.md.njk', defaultWritingAgentTemplate()],
|
|
594
|
+
['global/snippets/git-rules.md.njk', defaultGitRulesSnippet()],
|
|
595
|
+
['global/snippets/no-ai-files.md.njk', defaultNoAiFilesSnippet()],
|
|
596
|
+
['global/snippets/verification.md.njk', defaultVerificationSnippet()],
|
|
597
|
+
['global/tool-templates/codex-config.toml.njk', defaultCodexConfigTemplate()],
|
|
598
|
+
['global/tool-templates/claude-settings.json.njk', defaultClaudeSettingsTemplate()],
|
|
599
|
+
['global/skills/commit-workflow/SKILL.md.njk', defaultCommitWorkflowSkill()],
|
|
600
|
+
['global/skills/github-release/SKILL.md.njk', defaultGithubReleaseSkill()],
|
|
601
|
+
['global/skills/code-review/SKILL.md.njk', defaultCodeReviewSkill()]
|
|
602
|
+
]);
|
|
603
|
+
for (const [relativePath, content] of defaults) {
|
|
604
|
+
const filePath = path.join(configRoot, relativePath);
|
|
605
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
606
|
+
if (!fs.existsSync(filePath)) {
|
|
607
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
608
|
+
verbose(`created ${filePath}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function migrateOldTemplates(configRoot) {
|
|
613
|
+
const oldCodeTemplate = path.join(configRoot, 'templates', 'AGENTS-CODE.md');
|
|
614
|
+
const newCodeTemplate = path.join(configRoot, 'global', 'agents', 'code.md.njk');
|
|
615
|
+
if (fs.existsSync(oldCodeTemplate) && !fs.existsSync(newCodeTemplate)) {
|
|
616
|
+
fs.mkdirSync(path.dirname(newCodeTemplate), { recursive: true });
|
|
617
|
+
fs.writeFileSync(newCodeTemplate, convertLegacyTemplateVars(fs.readFileSync(oldCodeTemplate, 'utf8')), 'utf8');
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function findLegacyProfileDirs(configRoot) {
|
|
621
|
+
const dirs = [];
|
|
622
|
+
walkConfigTree(configRoot, 0, (fullPath, entry) => {
|
|
623
|
+
if (!entry.isDirectory()) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
if (shouldPruneConfigEntry(entry)) {
|
|
627
|
+
return 'skip';
|
|
628
|
+
}
|
|
629
|
+
if (fs.existsSync(path.join(fullPath, 'AGENTS-MODS.md'))) {
|
|
630
|
+
dirs.push(fullPath);
|
|
631
|
+
return 'skip';
|
|
632
|
+
}
|
|
633
|
+
return undefined;
|
|
634
|
+
});
|
|
635
|
+
return dirs.sort();
|
|
636
|
+
}
|
|
637
|
+
function walkConfigTree(dir, depth, visitor) {
|
|
638
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
639
|
+
for (const entry of entries) {
|
|
640
|
+
const fullPath = path.join(dir, entry.name);
|
|
641
|
+
const result = visitor(fullPath, entry, depth + 1);
|
|
642
|
+
if (entry.isDirectory() && result !== 'skip') {
|
|
643
|
+
walkConfigTree(fullPath, depth + 1, visitor);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function shouldPruneConfigEntry(entry) {
|
|
648
|
+
return (entry.isDirectory() &&
|
|
649
|
+
(entry.name === '.git' ||
|
|
650
|
+
entry.name === 'global' ||
|
|
651
|
+
entry.name === 'templates' ||
|
|
652
|
+
entry.name === '.agents' ||
|
|
653
|
+
entry.name === '.claude' ||
|
|
654
|
+
entry.name === '.codex' ||
|
|
655
|
+
entry.name === 'bin' ||
|
|
656
|
+
entry.name === 'reviews' ||
|
|
657
|
+
entry.name === 'memories' ||
|
|
658
|
+
entry.name === 'cache' ||
|
|
659
|
+
entry.name === 'log' ||
|
|
660
|
+
entry.name === 'sessions' ||
|
|
661
|
+
entry.name === 'shell_snapshots' ||
|
|
662
|
+
entry.name === 'skills' ||
|
|
663
|
+
entry.name === 'tmp' ||
|
|
664
|
+
entry.name === '.tmp'));
|
|
665
|
+
}
|
|
666
|
+
function convertLegacyTemplateVars(content) {
|
|
667
|
+
const replacements = new Map([
|
|
668
|
+
['{{AGENT_DIR}}', '{{ agentDir }}'],
|
|
669
|
+
['{{AGENTS_MODS_PATH}}', '{{ agentDir }}/AGENTS-MODS.md'],
|
|
670
|
+
['{{AGENTS_PATH}}', '{{ agentDir }}/AGENTS.md'],
|
|
671
|
+
['{{CLAUDE_PATH}}', '{{ agentDir }}/CLAUDE.md'],
|
|
672
|
+
['{{CONFIG_ROOT}}', '{{ configRoot }}'],
|
|
673
|
+
['{{PROFILE}}', '{{ profile }}']
|
|
674
|
+
]);
|
|
675
|
+
let converted = content;
|
|
676
|
+
for (const [from, to] of replacements) {
|
|
677
|
+
converted = converted.split(from).join(to);
|
|
678
|
+
}
|
|
679
|
+
return converted
|
|
680
|
+
.replace(/^@[^\n]*templates\/AGENTS-CODE\.md[^\n]*\n?/gm, '')
|
|
681
|
+
.replace(/^\n{2,}/, '\n');
|
|
682
|
+
}
|
|
683
|
+
function migrateCodexRuntimeFiles(agentDir) {
|
|
684
|
+
const runtimeNames = [
|
|
685
|
+
'.personality_migration',
|
|
686
|
+
'.tmp',
|
|
687
|
+
'auth.json',
|
|
688
|
+
'cache',
|
|
689
|
+
'history.jsonl',
|
|
690
|
+
'installation_id',
|
|
691
|
+
'log',
|
|
692
|
+
'logs_2.sqlite',
|
|
693
|
+
'models_cache.json',
|
|
694
|
+
'sessions',
|
|
695
|
+
'shell_snapshots',
|
|
696
|
+
'skills',
|
|
697
|
+
'state_5.sqlite',
|
|
698
|
+
'tmp',
|
|
699
|
+
'version.json'
|
|
700
|
+
];
|
|
701
|
+
const codexHome = path.join(agentDir, 'memories', 'codex-home');
|
|
702
|
+
let moved = 0;
|
|
703
|
+
for (const name of runtimeNames) {
|
|
704
|
+
const source = path.join(agentDir, name);
|
|
705
|
+
const target = path.join(codexHome, name);
|
|
706
|
+
if (!fs.existsSync(source) || fs.existsSync(target)) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
710
|
+
fs.renameSync(source, target);
|
|
711
|
+
moved += 1;
|
|
712
|
+
}
|
|
713
|
+
return moved;
|
|
714
|
+
}
|
|
715
|
+
function migrateLooseFiles(agentDir, pattern, targetDirName) {
|
|
716
|
+
const entries = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
717
|
+
let moved = 0;
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
if (!entry.isFile() || !pattern.test(entry.name)) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const source = path.join(agentDir, entry.name);
|
|
723
|
+
const targetDir = path.join(agentDir, targetDirName);
|
|
724
|
+
const target = path.join(targetDir, entry.name);
|
|
725
|
+
if (fs.existsSync(target)) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
729
|
+
fs.renameSync(source, target);
|
|
730
|
+
moved += 1;
|
|
731
|
+
}
|
|
732
|
+
return moved;
|
|
733
|
+
}
|
|
734
|
+
function convertLegacyProfileIfNeeded(_configRoot, agentDir, _profile) {
|
|
735
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
736
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
737
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
|
|
738
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
739
|
+
fs.writeFileSync(localPath, fs.readFileSync(legacyPath, 'utf8'), 'utf8');
|
|
740
|
+
verbose(`converted ${legacyPath} -> ${localPath}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function createDefaultManifestFile(agentDir, profile) {
|
|
744
|
+
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
745
|
+
if (fs.existsSync(manifestPath)) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
749
|
+
fs.writeFileSync(manifestPath, stringifyDefaultManifest(profile), 'utf8');
|
|
750
|
+
verbose(`created ${manifestPath}`);
|
|
751
|
+
}
|
|
752
|
+
function createDefaultLocalFile(agentDir, profile) {
|
|
753
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
754
|
+
if (fs.existsSync(localPath)) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
758
|
+
const content = fs.existsSync(legacyPath) ? fs.readFileSync(legacyPath, 'utf8') : defaultLocalTemplate(profile);
|
|
759
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
760
|
+
fs.writeFileSync(localPath, content, 'utf8');
|
|
761
|
+
verbose(`created ${localPath}`);
|
|
762
|
+
}
|
|
763
|
+
function ensureProfileOverridesDir(agentDir) {
|
|
764
|
+
fs.mkdirSync(path.join(agentDir, 'overrides'), { recursive: true });
|
|
765
|
+
}
|
|
766
|
+
function loadManifest(_configRoot, agentDir, profile) {
|
|
767
|
+
const manifestPath = path.join(agentDir, MANIFEST_FILE_NAME);
|
|
768
|
+
if (!fs.existsSync(manifestPath)) {
|
|
769
|
+
const manifest = defaultManifest(profile);
|
|
770
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
771
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
772
|
+
if (!fs.existsSync(localPath) && fs.existsSync(legacyPath)) {
|
|
773
|
+
manifest.agent = {
|
|
774
|
+
...(manifest.agent ?? {}),
|
|
775
|
+
includes: ['{{ profile }}/AGENTS-MODS.md']
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (!fs.existsSync(localPath) && !fs.existsSync(legacyPath)) {
|
|
779
|
+
manifest.agent = {
|
|
780
|
+
...(manifest.agent ?? {}),
|
|
781
|
+
includes: []
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return manifest;
|
|
785
|
+
}
|
|
786
|
+
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
787
|
+
const errors = [];
|
|
788
|
+
const parsed = (0, jsonc_parser_1.parse)(text, errors, { allowTrailingComma: true });
|
|
789
|
+
if (errors.length > 0) {
|
|
790
|
+
const first = errors[0];
|
|
791
|
+
const detail = first ? `${(0, jsonc_parser_1.printParseErrorCode)(first.error)} at offset ${first.offset}` : 'unknown JSONC parse error';
|
|
792
|
+
throw new Error(`invalid JSONC in ${manifestPath}: ${detail}`);
|
|
793
|
+
}
|
|
794
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
795
|
+
throw new Error(`manifest must be an object: ${manifestPath}`);
|
|
796
|
+
}
|
|
797
|
+
return parsed;
|
|
798
|
+
}
|
|
799
|
+
function defaultManifest(profile) {
|
|
800
|
+
return {
|
|
801
|
+
profile,
|
|
802
|
+
kind: 'code',
|
|
803
|
+
agent: {
|
|
804
|
+
base: 'global/agents/code.md.njk',
|
|
805
|
+
includes: [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`]
|
|
806
|
+
},
|
|
807
|
+
skills: {
|
|
808
|
+
install: ['commit-workflow', 'github-release', 'code-review']
|
|
809
|
+
},
|
|
810
|
+
tools: {
|
|
811
|
+
codex: true,
|
|
812
|
+
claude: true
|
|
813
|
+
},
|
|
814
|
+
checks: ['agent-run check .', 'repo-check check .', 'pnpm run cleanbuild'],
|
|
815
|
+
guardrails: {
|
|
816
|
+
blockGitWrite: true,
|
|
817
|
+
blockPublish: true,
|
|
818
|
+
blockGithubRelease: true,
|
|
819
|
+
forbidRepoAiFiles: true
|
|
820
|
+
},
|
|
821
|
+
paths: {
|
|
822
|
+
changesFile: '{{ projectRoot }}/CHANGES',
|
|
823
|
+
reviewDir: '{{ agentDir }}/reviews',
|
|
824
|
+
reviewFile: '{{ agentDir }}/reviews/REVIEW-{{ date }}.md',
|
|
825
|
+
memoriesDir: '{{ agentDir }}/memories'
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function normalizeManifest(manifest, profile) {
|
|
830
|
+
const baseManifest = defaultManifest(profile);
|
|
831
|
+
const installedSkills = Array.isArray(manifest.skills)
|
|
832
|
+
? manifest.skills
|
|
833
|
+
: manifest.skills?.install ?? asSkillObject(baseManifest.skills).install;
|
|
834
|
+
const overrides = Array.isArray(manifest.skills) ? {} : manifest.skills?.overrides ?? {};
|
|
835
|
+
return {
|
|
836
|
+
profile: manifest.profile ?? profile,
|
|
837
|
+
kind: manifest.kind ?? baseManifest.kind ?? 'code',
|
|
838
|
+
agent: {
|
|
839
|
+
base: manifest.agent?.base ?? baseManifest.agent?.base ?? 'global/agents/code.md.njk',
|
|
840
|
+
includes: manifest.agent?.includes ?? defaultAgentIncludesForProfile(profile)
|
|
841
|
+
},
|
|
842
|
+
skills: {
|
|
843
|
+
install: installedSkills,
|
|
844
|
+
overrides
|
|
845
|
+
},
|
|
846
|
+
tools: {
|
|
847
|
+
codex: manifest.tools?.codex ?? true,
|
|
848
|
+
claude: manifest.tools?.claude ?? true
|
|
849
|
+
},
|
|
850
|
+
checks: manifest.checks ?? baseManifest.checks ?? [],
|
|
851
|
+
guardrails: {
|
|
852
|
+
blockGitWrite: manifest.guardrails?.blockGitWrite ?? true,
|
|
853
|
+
blockPublish: manifest.guardrails?.blockPublish ?? true,
|
|
854
|
+
blockGithubRelease: manifest.guardrails?.blockGithubRelease ?? true,
|
|
855
|
+
forbidRepoAiFiles: manifest.guardrails?.forbidRepoAiFiles ?? true
|
|
856
|
+
},
|
|
857
|
+
paths: {
|
|
858
|
+
changesFile: manifest.paths?.changesFile ?? baseManifest.paths?.changesFile ?? '{{ projectRoot }}/CHANGES',
|
|
859
|
+
reviewDir: manifest.paths?.reviewDir ?? baseManifest.paths?.reviewDir ?? '{{ agentDir }}/reviews',
|
|
860
|
+
reviewFile: manifest.paths?.reviewFile ?? baseManifest.paths?.reviewFile ?? '{{ agentDir }}/reviews/REVIEW-{{ date }}.md',
|
|
861
|
+
memoriesDir: manifest.paths?.memoriesDir ?? baseManifest.paths?.memoriesDir ?? '{{ agentDir }}/memories'
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function asSkillObject(skills) {
|
|
866
|
+
if (Array.isArray(skills)) {
|
|
867
|
+
return { install: skills, overrides: {} };
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
install: skills?.install ?? [],
|
|
871
|
+
overrides: skills?.overrides ?? {}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function defaultAgentIncludesForProfile(profile) {
|
|
875
|
+
return [`{{ profile }}/${LOCAL_TEMPLATE_FILE_NAME}`, `{{ profile }}/AGENTS-MODS.md`];
|
|
876
|
+
}
|
|
877
|
+
function createNunjucksEnv(configRoot) {
|
|
878
|
+
return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
|
|
879
|
+
autoescape: false,
|
|
880
|
+
trimBlocks: true,
|
|
881
|
+
lstripBlocks: true,
|
|
882
|
+
throwOnUndefined: true
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function buildRenderContext(projectRoot, agentDir, configRoot, manifest, env) {
|
|
886
|
+
const date = localDateString();
|
|
887
|
+
const baseContext = {
|
|
888
|
+
profile: manifest.profile,
|
|
889
|
+
kind: manifest.kind,
|
|
890
|
+
projectRoot,
|
|
891
|
+
agentDir,
|
|
892
|
+
configRoot,
|
|
893
|
+
date,
|
|
894
|
+
checks: manifest.checks,
|
|
895
|
+
guardrails: manifest.guardrails
|
|
896
|
+
};
|
|
897
|
+
const paths = {
|
|
898
|
+
changesFile: resolveRuntimePath(configRoot, manifest.paths.changesFile, env, baseContext),
|
|
899
|
+
reviewDir: resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext),
|
|
900
|
+
reviewFile: resolveRuntimePath(configRoot, manifest.paths.reviewFile, env, baseContext),
|
|
901
|
+
memoriesDir: resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext),
|
|
902
|
+
codexHomeDir: path.join(resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext), 'codex-home'),
|
|
903
|
+
overridesDir: path.join(agentDir, 'overrides'),
|
|
904
|
+
codexSkillsDir: path.join(agentDir, '.agents', 'skills'),
|
|
905
|
+
claudeSkillsDir: path.join(agentDir, '.claude', 'skills'),
|
|
906
|
+
binDir: path.join(agentDir, 'bin')
|
|
907
|
+
};
|
|
908
|
+
return {
|
|
909
|
+
...baseContext,
|
|
910
|
+
paths,
|
|
911
|
+
skills: [],
|
|
912
|
+
renderedAgentSections: []
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function resolveConfigPath(configRoot, relativePath, context) {
|
|
916
|
+
const env = createNunjucksEnv(configRoot);
|
|
917
|
+
const rendered = renderInlineTemplate(env, relativePath, context);
|
|
918
|
+
const resolved = path.resolve(configRoot, rendered);
|
|
919
|
+
if (!isSamePathOrDescendant(resolved, path.resolve(configRoot))) {
|
|
920
|
+
throw new Error(`config path escapes config root: ${relativePath}`);
|
|
921
|
+
}
|
|
922
|
+
return resolved;
|
|
923
|
+
}
|
|
924
|
+
function resolveRuntimePath(configRoot, pathTemplate, env, context) {
|
|
925
|
+
const rendered = renderInlineTemplate(env, pathTemplate, context);
|
|
926
|
+
return path.isAbsolute(rendered) ? path.resolve(rendered) : path.resolve(configRoot, rendered);
|
|
927
|
+
}
|
|
928
|
+
function renderTemplateFile(env, configRoot, templatePath, context) {
|
|
929
|
+
const resolvedPath = resolveConfigPath(configRoot, templatePath, context);
|
|
930
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
931
|
+
throw new Error(`missing template: ${resolvedPath}`);
|
|
932
|
+
}
|
|
933
|
+
const relativePath = path.relative(configRoot, resolvedPath).replace(/\\/g, '/');
|
|
934
|
+
let content = fs.readFileSync(resolvedPath, 'utf8');
|
|
935
|
+
if (path.basename(resolvedPath) === 'AGENTS-MODS.md') {
|
|
936
|
+
content = renderLegacyAgentsMods(resolvedPath);
|
|
937
|
+
return renderInlineTemplate(env, content, context);
|
|
938
|
+
}
|
|
939
|
+
return env.render(relativePath, context);
|
|
940
|
+
}
|
|
941
|
+
function renderInlineTemplate(env, source, context) {
|
|
942
|
+
return env.renderString(source, context);
|
|
943
|
+
}
|
|
944
|
+
function assertNoUnexpandedTemplateVars(label, content) {
|
|
945
|
+
if (UNEXPANDED_TEMPLATE_RE.test(content)) {
|
|
946
|
+
throw new Error(`unexpanded template syntax remains in ${label}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function writeGeneratedFile(filePath, content, executable = false) {
|
|
950
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
951
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
952
|
+
if (executable && !IS_WINDOWS) {
|
|
953
|
+
fs.chmodSync(filePath, 0o755);
|
|
954
|
+
}
|
|
955
|
+
verbose(`write ${filePath}`);
|
|
956
|
+
}
|
|
957
|
+
function renderProfile(projectRoot, agentDir, checkOnly = false) {
|
|
958
|
+
const configRoot = defaultConfigRoot(projectRoot);
|
|
959
|
+
const profile = resolveProfile(projectRoot);
|
|
960
|
+
const env = createNunjucksEnv(configRoot);
|
|
961
|
+
const manifest = normalizeManifest(loadManifest(configRoot, agentDir, profile), profile);
|
|
962
|
+
if (manifest.profile !== profile) {
|
|
963
|
+
const parsed = parseProfile(manifest.profile, `${path.join(agentDir, MANIFEST_FILE_NAME)} profile`);
|
|
964
|
+
if (parsed.profile === null) {
|
|
965
|
+
throw new Error(parsed.reason);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!checkOnly) {
|
|
969
|
+
ensureConfigRootGitignore(configRoot);
|
|
970
|
+
}
|
|
971
|
+
const context = buildRenderContext(projectRoot, agentDir, configRoot, manifest, env);
|
|
972
|
+
const sections = [renderTemplateFile(env, configRoot, manifest.agent.base, context)];
|
|
973
|
+
for (const include of manifest.agent.includes) {
|
|
974
|
+
const includePath = resolveConfigPath(configRoot, include, context);
|
|
975
|
+
if (!fs.existsSync(includePath)) {
|
|
976
|
+
if (include.includes('AGENTS-MODS.md') || include.includes(LOCAL_TEMPLATE_FILE_NAME)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
throw new Error(`missing template: ${includePath}`);
|
|
980
|
+
}
|
|
981
|
+
sections.push(renderTemplateFile(env, configRoot, include, context));
|
|
982
|
+
}
|
|
983
|
+
context.renderedAgentSections = sections.map((section, index) => {
|
|
984
|
+
const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
|
|
985
|
+
assertNoUnexpandedTemplateVars(label, section);
|
|
986
|
+
return section.trimEnd();
|
|
987
|
+
});
|
|
988
|
+
context.skills = renderSkills(env, configRoot, manifest, context);
|
|
989
|
+
const files = [];
|
|
990
|
+
const agentsContent = renderToolInstructions('AGENTS.md', env, configRoot, context, false);
|
|
991
|
+
const claudeContent = renderToolInstructions('CLAUDE.md', env, configRoot, context, true);
|
|
992
|
+
files.push({ path: path.join(agentDir, 'AGENTS.md'), content: agentsContent });
|
|
993
|
+
files.push({ path: path.join(agentDir, 'CLAUDE.md'), content: claudeContent });
|
|
994
|
+
files.push({ path: path.join(agentDir, '.claude', 'CLAUDE.md'), content: claudeContent });
|
|
995
|
+
files.push({ path: path.join(agentDir, 'config.toml'), content: renderCodexConfig(env, configRoot, context) });
|
|
996
|
+
files.push({ path: path.join(agentDir, '.claude', 'settings.json'), content: renderClaudeSettings(env, configRoot, context) });
|
|
997
|
+
files.push(...renderNativeSkillFiles(context));
|
|
998
|
+
files.push(...renderGuardShims(context));
|
|
999
|
+
return {
|
|
1000
|
+
agentDir,
|
|
1001
|
+
configRoot,
|
|
1002
|
+
profile,
|
|
1003
|
+
context,
|
|
1004
|
+
files,
|
|
1005
|
+
skills: context.skills
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function syncAgentProfile(projectRoot, agentDir) {
|
|
1009
|
+
const rendered = renderProfile(projectRoot, agentDir, false);
|
|
1010
|
+
syncRuntimeDirs(rendered.context);
|
|
1011
|
+
for (const file of rendered.files) {
|
|
1012
|
+
writeGeneratedFile(file.path, file.content, file.executable ?? false);
|
|
1013
|
+
}
|
|
1014
|
+
return rendered;
|
|
1015
|
+
}
|
|
1016
|
+
function checkRenderedProfile(projectRoot, agentDir) {
|
|
1017
|
+
const findings = [];
|
|
1018
|
+
const configRoot = defaultConfigRoot(projectRoot);
|
|
1019
|
+
try {
|
|
1020
|
+
const rendered = renderProfile(projectRoot, agentDir, true);
|
|
1021
|
+
for (const file of rendered.files) {
|
|
1022
|
+
if (!fs.existsSync(file.path)) {
|
|
1023
|
+
findings.push({ message: `missing generated file: ${file.path}`, severity: 'ERROR' });
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
|
|
1027
|
+
if (actual !== file.content) {
|
|
1028
|
+
findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
for (const dir of [
|
|
1032
|
+
rendered.context.paths.reviewDir,
|
|
1033
|
+
rendered.context.paths.memoriesDir,
|
|
1034
|
+
rendered.context.paths.codexHomeDir,
|
|
1035
|
+
rendered.context.paths.overridesDir,
|
|
1036
|
+
rendered.context.paths.codexSkillsDir,
|
|
1037
|
+
rendered.context.paths.claudeSkillsDir,
|
|
1038
|
+
rendered.context.paths.binDir
|
|
1039
|
+
]) {
|
|
1040
|
+
if (!fs.existsSync(dir)) {
|
|
1041
|
+
findings.push({ message: `missing generated runtime directory: ${dir}`, severity: 'ERROR' });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
for (const skill of rendered.skills) {
|
|
1045
|
+
validateRenderedSkill(skill.renderedContent, skill.sourcePath);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
findings.push({
|
|
1050
|
+
message: `failed to render profile ${agentDir}: ${formatError(error)}`,
|
|
1051
|
+
severity: 'ERROR'
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const gitignorePath = path.join(configRoot, '.gitignore');
|
|
1055
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1056
|
+
findings.push({ message: `missing config root .gitignore: ${gitignorePath}`, severity: 'ERROR' });
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
1060
|
+
for (const entry of GENERATED_GITIGNORE_ENTRIES) {
|
|
1061
|
+
if (entry && entry.startsWith('#') === false && !gitignore.includes(entry)) {
|
|
1062
|
+
findings.push({ message: `config root .gitignore missing entry: ${entry}`, severity: 'ERROR' });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return findings;
|
|
1067
|
+
}
|
|
1068
|
+
function renderSkills(env, configRoot, manifest, context) {
|
|
1069
|
+
const skills = [];
|
|
1070
|
+
for (const name of manifest.skills.install) {
|
|
1071
|
+
const sourceTemplate = manifest.skills.overrides[name] ?? `global/skills/${name}/SKILL.md.njk`;
|
|
1072
|
+
const sourcePath = resolveConfigPath(configRoot, sourceTemplate, context);
|
|
1073
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1074
|
+
throw new Error(`missing skill template for ${name}: ${sourcePath}`);
|
|
1075
|
+
}
|
|
1076
|
+
const renderedContent = renderTemplateFile(env, configRoot, sourceTemplate, context);
|
|
1077
|
+
assertNoUnexpandedTemplateVars(`skill ${name}`, renderedContent);
|
|
1078
|
+
validateRenderedSkill(renderedContent, sourcePath);
|
|
1079
|
+
const description = extractSkillDescription(renderedContent);
|
|
1080
|
+
skills.push({ name, sourcePath, renderedContent, description });
|
|
1081
|
+
}
|
|
1082
|
+
return skills;
|
|
1083
|
+
}
|
|
1084
|
+
function renderNativeSkillFiles(context) {
|
|
1085
|
+
const files = [];
|
|
1086
|
+
for (const skill of context.skills) {
|
|
1087
|
+
files.push({
|
|
1088
|
+
path: path.join(context.paths.codexSkillsDir, skill.name, 'SKILL.md'),
|
|
1089
|
+
content: skill.renderedContent
|
|
1090
|
+
});
|
|
1091
|
+
files.push({
|
|
1092
|
+
path: path.join(context.paths.claudeSkillsDir, skill.name, 'SKILL.md'),
|
|
1093
|
+
content: skill.renderedContent
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
return files;
|
|
1097
|
+
}
|
|
1098
|
+
function renderToolInstructions(templateName, env, configRoot, context, isClaude) {
|
|
1099
|
+
const templatePath = `global/tool-templates/${templateName}.njk`;
|
|
1100
|
+
const absoluteTemplate = path.join(configRoot, templatePath);
|
|
1101
|
+
const content = fs.existsSync(absoluteTemplate)
|
|
1102
|
+
? renderTemplateFile(env, configRoot, templatePath, context)
|
|
1103
|
+
: renderInlineTemplate(env, defaultToolInstructionsTemplate(isClaude), context);
|
|
1104
|
+
assertNoUnexpandedTemplateVars(templateName, content);
|
|
1105
|
+
return content.replace(/\n*$/, '\n');
|
|
1106
|
+
}
|
|
1107
|
+
function renderCodexConfig(env, configRoot, context) {
|
|
1108
|
+
const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk');
|
|
1109
|
+
const content = templatePath !== null
|
|
1110
|
+
? renderTemplateFile(env, configRoot, templatePath, context)
|
|
1111
|
+
: defaultCodexConfigContent(context);
|
|
1112
|
+
assertNoUnexpandedTemplateVars('config.toml', content);
|
|
1113
|
+
return content.replace(/\n*$/, '\n');
|
|
1114
|
+
}
|
|
1115
|
+
function renderClaudeSettings(env, configRoot, context) {
|
|
1116
|
+
const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'claude-settings.json.njk', 'global/tool-templates/claude-settings.json.njk');
|
|
1117
|
+
const content = templatePath !== null
|
|
1118
|
+
? renderTemplateFile(env, configRoot, templatePath, context)
|
|
1119
|
+
: defaultClaudeSettingsContent(context);
|
|
1120
|
+
assertNoUnexpandedTemplateVars('claude settings.json', content);
|
|
1121
|
+
return content.replace(/\n*$/, '\n');
|
|
1122
|
+
}
|
|
1123
|
+
function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
|
|
1124
|
+
const overridePath = `${context.profile}/overrides/${overrideFileName}`;
|
|
1125
|
+
if (fs.existsSync(path.join(configRoot, overridePath))) {
|
|
1126
|
+
return overridePath;
|
|
1127
|
+
}
|
|
1128
|
+
if (fs.existsSync(path.join(configRoot, globalTemplatePath))) {
|
|
1129
|
+
return globalTemplatePath;
|
|
1130
|
+
}
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
function renderGuardShims(context) {
|
|
1134
|
+
if (IS_WINDOWS) {
|
|
1135
|
+
return ['git', 'npm', 'pnpm', 'gh'].map((name) => ({
|
|
1136
|
+
path: path.join(context.paths.binDir, `${name}.cmd`),
|
|
1137
|
+
content: windowsShim(name)
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
1140
|
+
return [
|
|
1141
|
+
{ path: path.join(context.paths.binDir, 'git'), content: posixGitShim(), executable: true },
|
|
1142
|
+
{ path: path.join(context.paths.binDir, 'npm'), content: posixPublishShim('npm'), executable: true },
|
|
1143
|
+
{ path: path.join(context.paths.binDir, 'pnpm'), content: posixPublishShim('pnpm'), executable: true },
|
|
1144
|
+
{ path: path.join(context.paths.binDir, 'gh'), content: posixGhShim(), executable: true }
|
|
1145
|
+
];
|
|
1146
|
+
}
|
|
1147
|
+
function syncRuntimeDirs(context) {
|
|
1148
|
+
for (const dir of [
|
|
1149
|
+
context.paths.reviewDir,
|
|
1150
|
+
context.paths.memoriesDir,
|
|
1151
|
+
context.paths.codexHomeDir,
|
|
1152
|
+
context.paths.overridesDir,
|
|
1153
|
+
context.paths.codexSkillsDir,
|
|
1154
|
+
context.paths.claudeSkillsDir,
|
|
1155
|
+
context.paths.binDir,
|
|
1156
|
+
path.join(context.agentDir, '.claude')
|
|
1157
|
+
]) {
|
|
1158
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function validateRenderedSkill(content, sourcePath) {
|
|
1162
|
+
const frontMatter = /^---\n([\s\S]*?)\n---\n/.exec(content.replace(/\r\n/g, '\n'));
|
|
1163
|
+
if (frontMatter === null) {
|
|
1164
|
+
throw new Error(`generated skill missing YAML front matter: ${sourcePath}`);
|
|
1165
|
+
}
|
|
1166
|
+
const yaml = frontMatter[1] ?? '';
|
|
1167
|
+
if (!/^name:\s*\S+/m.test(yaml)) {
|
|
1168
|
+
throw new Error(`generated skill missing name: ${sourcePath}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (!/^description:\s*(?:\S|>\s*$)/m.test(yaml)) {
|
|
1171
|
+
throw new Error(`generated skill missing description: ${sourcePath}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function extractSkillDescription(content) {
|
|
1175
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
1176
|
+
const simple = /^description:\s*['"]?(.+?)['"]?\s*$/m.exec(normalized);
|
|
1177
|
+
if (simple?.[1]) {
|
|
1178
|
+
return simple[1].trim();
|
|
1179
|
+
}
|
|
1180
|
+
const folded = /^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m.exec(normalized);
|
|
1181
|
+
if (folded?.[1]) {
|
|
1182
|
+
return folded[1]
|
|
1183
|
+
.split('\n')
|
|
1184
|
+
.map((line) => line.trim())
|
|
1185
|
+
.filter(Boolean)
|
|
1186
|
+
.join(' ');
|
|
1187
|
+
}
|
|
1188
|
+
return '';
|
|
1189
|
+
}
|
|
1190
|
+
function renderLegacyAgentsMods(sourceFile, stack = []) {
|
|
1191
|
+
const resolvedSource = path.resolve(sourceFile);
|
|
1192
|
+
if (stack.includes(resolvedSource)) {
|
|
1193
|
+
throw new Error(`Include cycle detected: ${[...stack, resolvedSource].join(' -> ')}`);
|
|
1194
|
+
}
|
|
1195
|
+
const lines = fs.readFileSync(resolvedSource, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
1196
|
+
const output = [];
|
|
1197
|
+
const nextStack = [...stack, resolvedSource];
|
|
1198
|
+
let sawLeadingInclude = false;
|
|
1199
|
+
let insertedOverrideNote = false;
|
|
1200
|
+
let contentStarted = false;
|
|
1201
|
+
for (const line of lines) {
|
|
1202
|
+
const trimmed = line.trim();
|
|
1203
|
+
if (!contentStarted && trimmed === '') {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (trimmed.startsWith('@')) {
|
|
1207
|
+
const includePath = trimmed.slice(1).trim();
|
|
1208
|
+
if (!includePath) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
output.push(renderLegacyAgentsMods(resolveIncludePath(resolvedSource, includePath), nextStack));
|
|
1212
|
+
if (!contentStarted) {
|
|
1213
|
+
sawLeadingInclude = true;
|
|
1214
|
+
}
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (sawLeadingInclude && !insertedOverrideNote) {
|
|
1218
|
+
output.push('');
|
|
1219
|
+
output.push('If anything below this point conflicts with anything included above,');
|
|
1220
|
+
output.push('the later instructions below take precedence.');
|
|
1221
|
+
output.push('');
|
|
1222
|
+
insertedOverrideNote = true;
|
|
1223
|
+
}
|
|
1224
|
+
output.push(line);
|
|
1225
|
+
contentStarted = true;
|
|
1226
|
+
}
|
|
1227
|
+
return output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
1228
|
+
}
|
|
1229
|
+
function resolveIncludePath(sourceFile, includePath) {
|
|
1230
|
+
if (path.isAbsolute(includePath)) {
|
|
1231
|
+
return includePath;
|
|
1232
|
+
}
|
|
1233
|
+
const sourceRelativePath = path.resolve(path.dirname(sourceFile), includePath);
|
|
1234
|
+
if (fs.existsSync(sourceRelativePath)) {
|
|
1235
|
+
return sourceRelativePath;
|
|
1236
|
+
}
|
|
1237
|
+
const includeRoot = findIncludeRoot(sourceFile);
|
|
1238
|
+
return path.resolve(includeRoot, includePath.replace(/^(\.\.\/)+/, ''));
|
|
1239
|
+
}
|
|
1240
|
+
function findIncludeRoot(sourceFile) {
|
|
1241
|
+
let dir = path.dirname(sourceFile);
|
|
1242
|
+
for (;;) {
|
|
1243
|
+
if (fs.existsSync(path.join(dir, 'global')) || fs.existsSync(path.join(dir, 'templates'))) {
|
|
1244
|
+
return dir;
|
|
1245
|
+
}
|
|
1246
|
+
const parent = path.dirname(dir);
|
|
1247
|
+
if (parent === dir) {
|
|
1248
|
+
return path.dirname(sourceFile);
|
|
1249
|
+
}
|
|
1250
|
+
dir = parent;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
439
1253
|
function checkProject(projectRoot) {
|
|
440
1254
|
const findings = [];
|
|
441
1255
|
verbose(`checking project ${projectRoot}`);
|
|
@@ -443,20 +1257,35 @@ function checkProject(projectRoot) {
|
|
|
443
1257
|
const excludedDir = profileResult.profile === null ? null : resolveAgentDir(projectRoot);
|
|
444
1258
|
const localAiFiles = findLocalAiFiles(projectRoot, excludedDir);
|
|
445
1259
|
for (const file of localAiFiles) {
|
|
446
|
-
findings.push({
|
|
447
|
-
message: `local AI file in project: ${file}`,
|
|
448
|
-
severity: 'ERROR'
|
|
449
|
-
});
|
|
1260
|
+
findings.push({ message: `local AI file in project: ${file}`, severity: 'ERROR' });
|
|
450
1261
|
}
|
|
451
1262
|
if (profileResult.profile === null) {
|
|
452
|
-
findings.push({
|
|
453
|
-
message: profileResult.reason,
|
|
454
|
-
severity: 'ERROR'
|
|
455
|
-
});
|
|
1263
|
+
findings.push({ message: profileResult.reason, severity: 'ERROR' });
|
|
456
1264
|
return findings;
|
|
457
1265
|
}
|
|
458
1266
|
const agentDir = resolveAgentDir(projectRoot);
|
|
459
|
-
findings.push(...checkAgentDirectory(agentDir));
|
|
1267
|
+
findings.push(...checkAgentDirectory(projectRoot, agentDir));
|
|
1268
|
+
return findings;
|
|
1269
|
+
}
|
|
1270
|
+
function checkAgentDirectory(projectRoot, agentDir) {
|
|
1271
|
+
const findings = [];
|
|
1272
|
+
const configRoot = defaultConfigRoot(projectRoot);
|
|
1273
|
+
if (!fs.existsSync(path.join(agentDir, MANIFEST_FILE_NAME))) {
|
|
1274
|
+
const legacyPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
1275
|
+
const localPath = path.join(agentDir, LOCAL_TEMPLATE_FILE_NAME);
|
|
1276
|
+
if (!fs.existsSync(legacyPath) && !fs.existsSync(localPath)) {
|
|
1277
|
+
findings.push({ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
for (const relativeTemplate of REQUIRED_GLOBAL_TEMPLATES) {
|
|
1281
|
+
if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
|
|
1282
|
+
findings.push({
|
|
1283
|
+
message: `missing required global template: ${path.join(configRoot, relativeTemplate)}`,
|
|
1284
|
+
severity: 'ERROR'
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
findings.push(...checkRenderedProfile(projectRoot, agentDir));
|
|
460
1289
|
return findings;
|
|
461
1290
|
}
|
|
462
1291
|
function checkSourceTree(rootPath) {
|
|
@@ -465,11 +1294,7 @@ function checkSourceTree(rootPath) {
|
|
|
465
1294
|
findings: checkProject(projectRoot),
|
|
466
1295
|
label: projectRoot
|
|
467
1296
|
}));
|
|
468
|
-
return {
|
|
469
|
-
entries,
|
|
470
|
-
hasErrors: entries.some((entry) => hasErrors(entry.findings)),
|
|
471
|
-
root: rootPath
|
|
472
|
-
};
|
|
1297
|
+
return { entries, hasErrors: entries.some((entry) => hasErrors(entry.findings)), root: rootPath };
|
|
473
1298
|
}
|
|
474
1299
|
function findSourceRepos(rootPath) {
|
|
475
1300
|
const resolvedRoot = path.resolve(rootPath);
|
|
@@ -490,62 +1315,7 @@ function findSourceRepos(rootPath) {
|
|
|
490
1315
|
return [...repos].sort();
|
|
491
1316
|
}
|
|
492
1317
|
function looksLikeRepoRoot(dir) {
|
|
493
|
-
|
|
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;
|
|
1318
|
+
return fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'));
|
|
549
1319
|
}
|
|
550
1320
|
function printProjectReport(projectRoot, findings) {
|
|
551
1321
|
process.stdout.write(`Check: ${projectRoot}\n`);
|
|
@@ -592,63 +1362,89 @@ function countWarnings(findings) {
|
|
|
592
1362
|
function formatError(error) {
|
|
593
1363
|
return error instanceof Error ? error.message : String(error);
|
|
594
1364
|
}
|
|
595
|
-
function
|
|
596
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
597
|
-
verbose(`ensure agent path exists: ${agentDir}`);
|
|
598
|
-
const modsPath = path.join(agentDir, 'AGENTS-MODS.md');
|
|
1365
|
+
function runCodex(realBinary, permissionArgs, agentDir, args, projectRoot, wrapperArgs) {
|
|
599
1366
|
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
1367
|
if (!fs.existsSync(agentsPath)) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1368
|
+
failMissingConfig('codex', agentDir);
|
|
1369
|
+
}
|
|
1370
|
+
const guardBin = path.join(agentDir, 'bin');
|
|
1371
|
+
const codexHomeDir = path.join(agentDir, 'memories', 'codex-home');
|
|
1372
|
+
fs.mkdirSync(codexHomeDir, { recursive: true });
|
|
1373
|
+
const env = {
|
|
1374
|
+
...process.env,
|
|
1375
|
+
CODEX_HOME: codexHomeDir,
|
|
1376
|
+
AGENT_DIR: agentDir,
|
|
1377
|
+
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
1378
|
+
PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
|
|
1379
|
+
};
|
|
1380
|
+
const codexRuntimeArgs = buildCodexRuntimeArgs(wrapperArgs);
|
|
1381
|
+
execCommand(realBinary, [
|
|
1382
|
+
...permissionArgs,
|
|
1383
|
+
...codexRuntimeArgs,
|
|
1384
|
+
'--config',
|
|
1385
|
+
`system_prompt_file=${agentsPath}`,
|
|
1386
|
+
'--config',
|
|
1387
|
+
'project_doc_max_bytes=65536',
|
|
1388
|
+
'-C',
|
|
1389
|
+
projectRoot,
|
|
1390
|
+
...(wrapperArgs.codexSandboxMode === 'sandboxed' && wrapperArgs.codexNetwork
|
|
1391
|
+
? ['--config', 'sandbox_workspace_write.network_access=true']
|
|
1392
|
+
: []),
|
|
1393
|
+
...args
|
|
1394
|
+
], shouldUseShell(realBinary), env, codexHomeDir, (code) => postflightProjectCheck(projectRoot, agentDir, code));
|
|
1395
|
+
}
|
|
1396
|
+
function buildCodexRuntimeArgs(wrapperArgs) {
|
|
1397
|
+
const mode = wrapperArgs.codexSandboxMode ?? 'danger';
|
|
1398
|
+
if (mode === 'sandboxed') {
|
|
1399
|
+
return ['-s', 'workspace-write'];
|
|
1400
|
+
}
|
|
1401
|
+
return ['-a', 'never', '-s', 'danger-full-access'];
|
|
1402
|
+
}
|
|
1403
|
+
function runClaude(realBinary, permissionArgs, agentDir, args, projectRoot) {
|
|
632
1404
|
const claudePath = path.join(agentDir, 'CLAUDE.md');
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1405
|
+
if (!fs.existsSync(claudePath)) {
|
|
1406
|
+
failMissingConfig('claude', agentDir);
|
|
1407
|
+
}
|
|
1408
|
+
const guardBin = path.join(agentDir, 'bin');
|
|
1409
|
+
const claudeConfigDir = path.join(agentDir, '.claude');
|
|
1410
|
+
const env = {
|
|
1411
|
+
...process.env,
|
|
1412
|
+
CLAUDE_CONFIG_DIR: claudeConfigDir,
|
|
1413
|
+
AGENT_DIR: agentDir,
|
|
1414
|
+
AGENT_RUN_PROJECT_ROOT: projectRoot,
|
|
1415
|
+
PATH: `${guardBin}${path.delimiter}${process.env.PATH ?? ''}`
|
|
1416
|
+
};
|
|
1417
|
+
execCommand(realBinary, [
|
|
1418
|
+
...permissionArgs,
|
|
1419
|
+
'--settings',
|
|
1420
|
+
path.join(claudeConfigDir, 'settings.json'),
|
|
1421
|
+
'--add-dir',
|
|
1422
|
+
projectRoot,
|
|
1423
|
+
'--add-dir',
|
|
1424
|
+
agentDir,
|
|
1425
|
+
...args
|
|
1426
|
+
], shouldUseShell(realBinary), env, projectRoot, (code) => postflightProjectCheck(projectRoot, agentDir, code));
|
|
1427
|
+
}
|
|
1428
|
+
function postflightProjectCheck(projectRoot, agentDir, code) {
|
|
1429
|
+
const localAiFiles = findLocalAiFiles(projectRoot, agentDir);
|
|
1430
|
+
if (localAiFiles.length === 0) {
|
|
1431
|
+
return code;
|
|
1432
|
+
}
|
|
1433
|
+
process.stderr.write(`agent-run: postflight found local AI files in project root ${projectRoot}\n`);
|
|
1434
|
+
for (const file of localAiFiles) {
|
|
1435
|
+
process.stderr.write(`agent-run: ${file}\n`);
|
|
637
1436
|
}
|
|
638
|
-
|
|
1437
|
+
return 1;
|
|
639
1438
|
}
|
|
640
1439
|
function failMissingConfig(tool, agentDir) {
|
|
641
1440
|
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
|
|
1441
|
+
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
1442
|
process.exit(1);
|
|
644
1443
|
}
|
|
645
1444
|
function getPermissionArgs(tool) {
|
|
646
1445
|
if (process.env.AGENT_WRAPPER_FORCE_PERMISSIVE !== '1') {
|
|
647
1446
|
return [];
|
|
648
1447
|
}
|
|
649
|
-
if (tool === 'codex') {
|
|
650
|
-
return ['-a', 'never', '-s', 'danger-full-access'];
|
|
651
|
-
}
|
|
652
1448
|
if (tool === 'claude' && typeof process.getuid === 'function' && process.getuid() !== 0) {
|
|
653
1449
|
return ['--permission-mode', 'bypassPermissions'];
|
|
654
1450
|
}
|
|
@@ -663,8 +1459,7 @@ function resolveAgentDir(projectRoot) {
|
|
|
663
1459
|
}
|
|
664
1460
|
function findLocalAiFiles(projectRoot, excludedDir = null) {
|
|
665
1461
|
const matches = [];
|
|
666
|
-
|
|
667
|
-
if (rootIgnored) {
|
|
1462
|
+
if (isIgnoredDir(projectRoot)) {
|
|
668
1463
|
return matches;
|
|
669
1464
|
}
|
|
670
1465
|
const resolvedExcludedDir = excludedDir ? path.resolve(excludedDir) : null;
|
|
@@ -687,9 +1482,7 @@ function walkSourceTree(dir, depth, visitor) {
|
|
|
687
1482
|
if (isIgnoredDir(dir)) {
|
|
688
1483
|
return;
|
|
689
1484
|
}
|
|
690
|
-
const entries = fs
|
|
691
|
-
.readdirSync(dir, { withFileTypes: true })
|
|
692
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1485
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
693
1486
|
for (const entry of entries) {
|
|
694
1487
|
const fullPath = path.join(dir, entry.name);
|
|
695
1488
|
const result = visitor(fullPath, entry, depth + 1);
|
|
@@ -702,14 +1495,12 @@ function walk(dir, visitor) {
|
|
|
702
1495
|
if (isIgnoredDir(dir)) {
|
|
703
1496
|
return;
|
|
704
1497
|
}
|
|
705
|
-
const entries = fs
|
|
706
|
-
.readdirSync(dir, { withFileTypes: true })
|
|
707
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1498
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
708
1499
|
for (const entry of entries) {
|
|
709
|
-
const fullPath = path.join(dir, entry.name);
|
|
710
1500
|
if (shouldPruneProjectEntry(entry)) {
|
|
711
1501
|
continue;
|
|
712
1502
|
}
|
|
1503
|
+
const fullPath = path.join(dir, entry.name);
|
|
713
1504
|
const result = visitor(fullPath, entry);
|
|
714
1505
|
if (entry.isDirectory() && result !== 'skip') {
|
|
715
1506
|
walk(fullPath, visitor);
|
|
@@ -729,7 +1520,7 @@ function shouldPruneProjectEntry(entry) {
|
|
|
729
1520
|
entry.name === 'build'));
|
|
730
1521
|
}
|
|
731
1522
|
function isLocalAiDirectory(entry) {
|
|
732
|
-
return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex');
|
|
1523
|
+
return entry.isDirectory() && (entry.name === '.claude' || entry.name === '.codex' || entry.name === '.agents');
|
|
733
1524
|
}
|
|
734
1525
|
function isLocalAiFile(entry) {
|
|
735
1526
|
return entry.isFile() && LOCAL_AI_FILE_NAMES.has(entry.name);
|
|
@@ -809,6 +1600,11 @@ function resolveProfileResult(projectRoot) {
|
|
|
809
1600
|
verbose(`using ${ENV_FILE_NAME} AGENT_RUN_PROFILE=${envProfile}`);
|
|
810
1601
|
return parseProfile(envProfile, `${ENV_FILE_NAME} AGENT_RUN_PROFILE`);
|
|
811
1602
|
}
|
|
1603
|
+
const githubProfile = resolveGitHubProfile(projectRoot);
|
|
1604
|
+
if (githubProfile !== null) {
|
|
1605
|
+
verbose(`using GitHub origin profile=${githubProfile}`);
|
|
1606
|
+
return parseProfile(githubProfile, 'GitHub remote origin');
|
|
1607
|
+
}
|
|
812
1608
|
const packagePath = path.join(projectRoot, 'package.json');
|
|
813
1609
|
if (fs.existsSync(packagePath)) {
|
|
814
1610
|
try {
|
|
@@ -819,42 +1615,67 @@ function resolveProfileResult(projectRoot) {
|
|
|
819
1615
|
}
|
|
820
1616
|
}
|
|
821
1617
|
catch {
|
|
822
|
-
return {
|
|
823
|
-
profile: null,
|
|
824
|
-
reason: `cannot parse package.json: ${packagePath}`
|
|
825
|
-
};
|
|
1618
|
+
return { profile: null, reason: `cannot parse package.json: ${packagePath}` };
|
|
826
1619
|
}
|
|
827
1620
|
}
|
|
828
1621
|
return {
|
|
829
1622
|
profile: null,
|
|
830
|
-
reason: `cannot resolve agent profile for ${projectRoot}; add ${ENV_FILE_NAME} with AGENT_RUN_PROFILE=<org/project
|
|
1623
|
+
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
1624
|
};
|
|
832
1625
|
}
|
|
1626
|
+
function resolveGitHubProfile(projectRoot) {
|
|
1627
|
+
const remote = readGitOrigin(projectRoot);
|
|
1628
|
+
return remote === null ? null : parseGitHubRemoteProfile(remote);
|
|
1629
|
+
}
|
|
1630
|
+
function readGitOrigin(projectRoot) {
|
|
1631
|
+
const configPath = path.join(projectRoot, '.git', 'config');
|
|
1632
|
+
if (!fs.existsSync(configPath)) {
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
const lines = fs.readFileSync(configPath, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
1636
|
+
let inOrigin = false;
|
|
1637
|
+
for (const line of lines) {
|
|
1638
|
+
const trimmed = line.trim();
|
|
1639
|
+
if (trimmed.startsWith('[')) {
|
|
1640
|
+
inOrigin = trimmed === '[remote "origin"]';
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (inOrigin && trimmed.startsWith('url')) {
|
|
1644
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
1645
|
+
if (equalsIndex >= 0) {
|
|
1646
|
+
return trimmed.slice(equalsIndex + 1).trim();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
function parseGitHubRemoteProfile(remote) {
|
|
1653
|
+
const patterns = [
|
|
1654
|
+
/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/,
|
|
1655
|
+
/^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
1656
|
+
/^ssh:\/\/git@github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
|
|
1657
|
+
];
|
|
1658
|
+
for (const pattern of patterns) {
|
|
1659
|
+
const match = pattern.exec(remote.trim());
|
|
1660
|
+
if (match?.[1] && match[2]) {
|
|
1661
|
+
return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
833
1666
|
function parseProfile(profile, source) {
|
|
834
1667
|
const normalized = profile.trim().replace(/\\/g, '/');
|
|
835
1668
|
if (!normalized) {
|
|
836
|
-
return {
|
|
837
|
-
profile: null,
|
|
838
|
-
reason: `${source} must be a non-empty path relative to the config root`
|
|
839
|
-
};
|
|
1669
|
+
return { profile: null, reason: `${source} must be a non-empty path relative to the config root` };
|
|
840
1670
|
}
|
|
841
1671
|
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
|
-
};
|
|
1672
|
+
return { profile: null, reason: `${source} must be relative to the config root, not an absolute path` };
|
|
846
1673
|
}
|
|
847
1674
|
const segments = normalized.split('/').filter((segment) => segment.length > 0);
|
|
848
1675
|
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
|
-
};
|
|
1676
|
+
return { profile: null, reason: `${source} must be a clean relative path like org/my-project` };
|
|
853
1677
|
}
|
|
854
|
-
return {
|
|
855
|
-
profile: segments.join('/'),
|
|
856
|
-
reason: ''
|
|
857
|
-
};
|
|
1678
|
+
return { profile: segments.join('/'), reason: '' };
|
|
858
1679
|
}
|
|
859
1680
|
function readAgentRunEnv(dir) {
|
|
860
1681
|
const resolvedDir = path.resolve(dir);
|
|
@@ -864,12 +1685,10 @@ function readAgentRunEnv(dir) {
|
|
|
864
1685
|
}
|
|
865
1686
|
const filePath = path.join(resolvedDir, ENV_FILE_NAME);
|
|
866
1687
|
if (!fs.existsSync(filePath)) {
|
|
867
|
-
verbose(`no ${ENV_FILE_NAME} in ${resolvedDir}`);
|
|
868
1688
|
const emptyEnv = {};
|
|
869
1689
|
agentRunEnvCache.set(resolvedDir, emptyEnv);
|
|
870
1690
|
return emptyEnv;
|
|
871
1691
|
}
|
|
872
|
-
verbose(`read ${filePath}`);
|
|
873
1692
|
const env = {};
|
|
874
1693
|
const lines = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
875
1694
|
for (const rawLine of lines) {
|
|
@@ -883,8 +1702,7 @@ function readAgentRunEnv(dir) {
|
|
|
883
1702
|
}
|
|
884
1703
|
const key = line.slice(0, equalsIndex).trim();
|
|
885
1704
|
let value = line.slice(equalsIndex + 1).trim();
|
|
886
|
-
if (value.length >= 2 &&
|
|
887
|
-
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
1705
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
888
1706
|
value = value.slice(1, -1);
|
|
889
1707
|
}
|
|
890
1708
|
env[key] = value;
|
|
@@ -902,32 +1720,26 @@ function parseBooleanEnv(value) {
|
|
|
902
1720
|
function defaultConfigRoot(projectRoot) {
|
|
903
1721
|
const overrideRoot = process.env[CONFIG_ROOT_OVERRIDE_ENV];
|
|
904
1722
|
if (overrideRoot) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1723
|
+
return path.resolve(overrideRoot);
|
|
1724
|
+
}
|
|
1725
|
+
if (process.env[CONFIG_DIR_ENV]) {
|
|
1726
|
+
return path.resolve(process.env[CONFIG_DIR_ENV]);
|
|
908
1727
|
}
|
|
909
1728
|
if (process.env.AGENT_CONFIG_ROOT) {
|
|
910
|
-
|
|
911
|
-
verbose(`using AGENT_CONFIG_ROOT env: ${resolved}`);
|
|
912
|
-
return resolved;
|
|
1729
|
+
return path.resolve(process.env.AGENT_CONFIG_ROOT);
|
|
913
1730
|
}
|
|
914
1731
|
if (projectRoot) {
|
|
915
1732
|
const env = readAgentRunEnv(projectRoot);
|
|
1733
|
+
const localConfigDir = env[CONFIG_DIR_ENV]?.trim();
|
|
1734
|
+
if (localConfigDir) {
|
|
1735
|
+
return path.resolve(projectRoot, localConfigDir);
|
|
1736
|
+
}
|
|
916
1737
|
const localConfigRoot = env.AGENT_CONFIG_ROOT?.trim();
|
|
917
1738
|
if (localConfigRoot) {
|
|
918
|
-
|
|
919
|
-
verbose(`using ${ENV_FILE_NAME} AGENT_CONFIG_ROOT: ${resolved}`);
|
|
920
|
-
return resolved;
|
|
1739
|
+
return path.resolve(projectRoot, localConfigRoot);
|
|
921
1740
|
}
|
|
922
1741
|
}
|
|
923
|
-
|
|
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;
|
|
1742
|
+
return path.join(os.homedir(), '.agent-config');
|
|
931
1743
|
}
|
|
932
1744
|
function findRealBinary(tool) {
|
|
933
1745
|
const pathValue = process.env.PATH || '';
|
|
@@ -959,10 +1771,7 @@ function findRealBinary(tool) {
|
|
|
959
1771
|
for (const dir of pathDirs) {
|
|
960
1772
|
for (const extension of extensions) {
|
|
961
1773
|
const candidate = path.join(dir, `${tool}${extension}`);
|
|
962
|
-
if (!fs.existsSync(candidate)) {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
if (!isExecutable(candidate)) {
|
|
1774
|
+
if (!fs.existsSync(candidate) || !isExecutable(candidate)) {
|
|
966
1775
|
continue;
|
|
967
1776
|
}
|
|
968
1777
|
let resolvedCandidate = candidate;
|
|
@@ -972,10 +1781,7 @@ function findRealBinary(tool) {
|
|
|
972
1781
|
catch {
|
|
973
1782
|
resolvedCandidate = candidate;
|
|
974
1783
|
}
|
|
975
|
-
if (resolvedCandidate === currentScript) {
|
|
976
|
-
continue;
|
|
977
|
-
}
|
|
978
|
-
if (wrapperCandidates.has(resolvedCandidate)) {
|
|
1784
|
+
if (resolvedCandidate === currentScript || wrapperCandidates.has(resolvedCandidate)) {
|
|
979
1785
|
continue;
|
|
980
1786
|
}
|
|
981
1787
|
return candidate;
|
|
@@ -984,10 +1790,7 @@ function findRealBinary(tool) {
|
|
|
984
1790
|
fail(`no ${tool} binary found in PATH`);
|
|
985
1791
|
}
|
|
986
1792
|
function getExecutableExtensions(tool) {
|
|
987
|
-
if (!IS_WINDOWS) {
|
|
988
|
-
return [''];
|
|
989
|
-
}
|
|
990
|
-
if (path.extname(tool)) {
|
|
1793
|
+
if (!IS_WINDOWS || path.extname(tool)) {
|
|
991
1794
|
return [''];
|
|
992
1795
|
}
|
|
993
1796
|
const pathExt = (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
@@ -1012,19 +1815,16 @@ function execTool(command, args) {
|
|
|
1012
1815
|
verbose(`exec tool: ${formatCommand(command, args)} shell=${String(shouldUseShell(command))}`);
|
|
1013
1816
|
execCommand(command, args, shouldUseShell(command));
|
|
1014
1817
|
}
|
|
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
|
-
});
|
|
1818
|
+
function execCommand(command, args, shell, env, cwd, onExit) {
|
|
1819
|
+
verbose(`spawn: ${formatCommand(command, args)} shell=${String(shell)} cwd=${cwd ?? process.cwd()}`);
|
|
1820
|
+
const child = childProcess.spawn(command, args, { cwd, env, shell, stdio: 'inherit' });
|
|
1022
1821
|
child.on('exit', (code, signal) => {
|
|
1023
1822
|
if (signal) {
|
|
1024
1823
|
process.kill(process.pid, signal);
|
|
1025
1824
|
return;
|
|
1026
1825
|
}
|
|
1027
|
-
|
|
1826
|
+
const originalCode = code === null ? 1 : code;
|
|
1827
|
+
process.exit(onExit ? onExit(originalCode) : originalCode);
|
|
1028
1828
|
});
|
|
1029
1829
|
child.on('error', (error) => {
|
|
1030
1830
|
fail(error.message);
|
|
@@ -1033,39 +1833,32 @@ function execCommand(command, args, shell, env) {
|
|
|
1033
1833
|
function openEditor(filePath) {
|
|
1034
1834
|
const visual = process.env.VISUAL?.trim();
|
|
1035
1835
|
if (visual) {
|
|
1036
|
-
verbose(`open editor via VISUAL=${visual} file=${filePath}`);
|
|
1037
1836
|
execCommand(visual, [filePath], true);
|
|
1038
1837
|
return;
|
|
1039
1838
|
}
|
|
1040
1839
|
const editor = process.env.EDITOR?.trim();
|
|
1041
1840
|
if (editor) {
|
|
1042
|
-
verbose(`open editor via EDITOR=${editor} file=${filePath}`);
|
|
1043
1841
|
execCommand(editor, [filePath], true);
|
|
1044
1842
|
return;
|
|
1045
1843
|
}
|
|
1046
1844
|
const vscodeCommand = findVsCodeEditorCommand();
|
|
1047
1845
|
if (vscodeCommand !== null) {
|
|
1048
|
-
verbose(`open editor via VS Code command=${vscodeCommand} file=${filePath}`);
|
|
1049
1846
|
execCommand(vscodeCommand, ['--reuse-window', filePath], shouldUseShell(vscodeCommand));
|
|
1050
1847
|
return;
|
|
1051
1848
|
}
|
|
1052
1849
|
const fallbackEditor = findFallbackEditor();
|
|
1053
1850
|
if (fallbackEditor !== null) {
|
|
1054
|
-
verbose(`open editor via fallback command=${fallbackEditor} file=${filePath}`);
|
|
1055
1851
|
execCommand(fallbackEditor, [filePath], shouldUseShell(fallbackEditor));
|
|
1056
1852
|
return;
|
|
1057
1853
|
}
|
|
1058
1854
|
if (IS_WINDOWS) {
|
|
1059
|
-
verbose(`open editor via cmd.exe start file=${filePath}`);
|
|
1060
1855
|
execCommand('cmd.exe', ['/c', 'start', '', filePath], false);
|
|
1061
1856
|
return;
|
|
1062
1857
|
}
|
|
1063
1858
|
if (process.platform === 'darwin') {
|
|
1064
|
-
verbose(`open editor via open file=${filePath}`);
|
|
1065
1859
|
execCommand('open', [filePath], false);
|
|
1066
1860
|
return;
|
|
1067
1861
|
}
|
|
1068
|
-
verbose(`open editor via xdg-open file=${filePath}`);
|
|
1069
1862
|
execCommand('xdg-open', [filePath], false);
|
|
1070
1863
|
}
|
|
1071
1864
|
function findVsCodeEditorCommand() {
|
|
@@ -1082,17 +1875,13 @@ function findVsCodeEditorCommand() {
|
|
|
1082
1875
|
}
|
|
1083
1876
|
function isRunningInVsCodeTerminal() {
|
|
1084
1877
|
const termProgram = process.env.TERM_PROGRAM?.trim().toLowerCase();
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
return (Boolean(process.env.VSCODE_GIT_IPC_HANDLE) ||
|
|
1878
|
+
return (termProgram === 'vscode' ||
|
|
1879
|
+
Boolean(process.env.VSCODE_GIT_IPC_HANDLE) ||
|
|
1089
1880
|
Boolean(process.env.VSCODE_IPC_HOOK) ||
|
|
1090
1881
|
Boolean(process.env.VSCODE_IPC_HOOK_CLI));
|
|
1091
1882
|
}
|
|
1092
1883
|
function findFallbackEditor() {
|
|
1093
|
-
const candidates = IS_WINDOWS
|
|
1094
|
-
? ['notepad.exe']
|
|
1095
|
-
: ['joe', 'sensible-editor', 'editor', 'nano', 'nvim', 'vim', 'vi'];
|
|
1884
|
+
const candidates = IS_WINDOWS ? ['notepad.exe'] : ['joe', 'sensible-editor', 'editor', 'nano', 'nvim', 'vim', 'vi'];
|
|
1096
1885
|
for (const candidate of candidates) {
|
|
1097
1886
|
const resolved = findExecutable(candidate);
|
|
1098
1887
|
if (resolved !== null) {
|
|
@@ -1104,34 +1893,17 @@ function findFallbackEditor() {
|
|
|
1104
1893
|
function findExecutable(command) {
|
|
1105
1894
|
const pathValue = process.env.PATH || '';
|
|
1106
1895
|
const pathDirs = pathValue.split(path.delimiter).filter((entry) => entry.length > 0);
|
|
1107
|
-
const extensions =
|
|
1896
|
+
const extensions = getExecutableExtensions(command);
|
|
1108
1897
|
for (const dir of pathDirs) {
|
|
1109
1898
|
for (const extension of extensions) {
|
|
1110
1899
|
const candidate = path.join(dir, `${command}${extension}`);
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
}
|
|
1114
|
-
if (!isExecutable(candidate)) {
|
|
1115
|
-
continue;
|
|
1900
|
+
if (fs.existsSync(candidate) && isExecutable(candidate)) {
|
|
1901
|
+
return candidate;
|
|
1116
1902
|
}
|
|
1117
|
-
return candidate;
|
|
1118
1903
|
}
|
|
1119
1904
|
}
|
|
1120
1905
|
return null;
|
|
1121
1906
|
}
|
|
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
1907
|
function shouldUseShell(command) {
|
|
1136
1908
|
if (!IS_WINDOWS) {
|
|
1137
1909
|
return false;
|
|
@@ -1139,14 +1911,350 @@ function shouldUseShell(command) {
|
|
|
1139
1911
|
const extension = path.extname(command).toLowerCase();
|
|
1140
1912
|
return extension === '.cmd' || extension === '.bat';
|
|
1141
1913
|
}
|
|
1914
|
+
function localDateString() {
|
|
1915
|
+
const now = new Date();
|
|
1916
|
+
const year = String(now.getFullYear()).padStart(4, '0');
|
|
1917
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
1918
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
1919
|
+
return `${year}-${month}-${day}`;
|
|
1920
|
+
}
|
|
1921
|
+
function stringifyDefaultManifest(profile) {
|
|
1922
|
+
const manifest = defaultManifest(profile);
|
|
1923
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1924
|
+
}
|
|
1925
|
+
function defaultLocalTemplate(_profile) {
|
|
1926
|
+
return [
|
|
1927
|
+
'# {{ profile }} local agent instructions',
|
|
1928
|
+
'',
|
|
1929
|
+
'Project root:',
|
|
1930
|
+
'',
|
|
1931
|
+
'```text',
|
|
1932
|
+
'{{ projectRoot }}',
|
|
1933
|
+
'```',
|
|
1934
|
+
'',
|
|
1935
|
+
'Agent directory:',
|
|
1936
|
+
'',
|
|
1937
|
+
'```text',
|
|
1938
|
+
'{{ agentDir }}',
|
|
1939
|
+
'```',
|
|
1940
|
+
'',
|
|
1941
|
+
'Add project-specific rules here.',
|
|
1942
|
+
''
|
|
1943
|
+
].join('\n');
|
|
1944
|
+
}
|
|
1945
|
+
function defaultCodeAgentTemplate() {
|
|
1946
|
+
return [
|
|
1947
|
+
'# Code Agent',
|
|
1948
|
+
'',
|
|
1949
|
+
'Work in the project root shown by agent-run. Keep changes scoped to the user request.',
|
|
1950
|
+
'',
|
|
1951
|
+
'{% include "global/snippets/git-rules.md.njk" %}',
|
|
1952
|
+
'',
|
|
1953
|
+
'{% include "global/snippets/no-ai-files.md.njk" %}',
|
|
1954
|
+
'',
|
|
1955
|
+
'{% include "global/snippets/verification.md.njk" %}',
|
|
1956
|
+
''
|
|
1957
|
+
].join('\n');
|
|
1958
|
+
}
|
|
1959
|
+
function defaultWritingAgentTemplate() {
|
|
1960
|
+
return [
|
|
1961
|
+
'# Writing Agent',
|
|
1962
|
+
'',
|
|
1963
|
+
'Write clearly and preserve the existing voice, structure, and facts in the project.',
|
|
1964
|
+
'',
|
|
1965
|
+
'{% include "global/snippets/no-ai-files.md.njk" %}',
|
|
1966
|
+
''
|
|
1967
|
+
].join('\n');
|
|
1968
|
+
}
|
|
1969
|
+
function defaultGitRulesSnippet() {
|
|
1970
|
+
return [
|
|
1971
|
+
'## Git Rules',
|
|
1972
|
+
'',
|
|
1973
|
+
'Do not commit unless the user explicitly asks. Before committing, show changed files and the exact commit message.',
|
|
1974
|
+
'Use human commit messages unless the user asks for conventional commits. Do not push unless explicitly asked.',
|
|
1975
|
+
''
|
|
1976
|
+
].join('\n');
|
|
1977
|
+
}
|
|
1978
|
+
function defaultNoAiFilesSnippet() {
|
|
1979
|
+
return [
|
|
1980
|
+
'## Agent File Storage',
|
|
1981
|
+
'',
|
|
1982
|
+
'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or other agent runtime files inside the project repository.',
|
|
1983
|
+
'Agent-only files belong under {{ agentDir }}.',
|
|
1984
|
+
''
|
|
1985
|
+
].join('\n');
|
|
1986
|
+
}
|
|
1987
|
+
function defaultVerificationSnippet() {
|
|
1988
|
+
return [
|
|
1989
|
+
'## Verification',
|
|
1990
|
+
'',
|
|
1991
|
+
'Run relevant checks before reporting completion. Configured checks:',
|
|
1992
|
+
'',
|
|
1993
|
+
'{% for check in checks %}',
|
|
1994
|
+
'- `{{ check }}`',
|
|
1995
|
+
'{% endfor %}',
|
|
1996
|
+
''
|
|
1997
|
+
].join('\n');
|
|
1998
|
+
}
|
|
1999
|
+
function defaultCommitWorkflowSkill() {
|
|
2000
|
+
return [
|
|
2001
|
+
'---',
|
|
2002
|
+
'name: commit-workflow',
|
|
2003
|
+
'description: Use for preparing or creating commits with explicit user approval.',
|
|
2004
|
+
'---',
|
|
2005
|
+
'',
|
|
2006
|
+
'# Commit Workflow',
|
|
2007
|
+
'',
|
|
2008
|
+
'Never commit unless explicitly asked. Before committing, show changed files and the exact commit message.',
|
|
2009
|
+
'Use human commit messages unless conventional commits are explicitly requested. Do not push.',
|
|
2010
|
+
'Run configured checks before committing:',
|
|
2011
|
+
'',
|
|
2012
|
+
'{% for check in checks %}',
|
|
2013
|
+
'- `{{ check }}`',
|
|
2014
|
+
'{% endfor %}',
|
|
2015
|
+
''
|
|
2016
|
+
].join('\n');
|
|
2017
|
+
}
|
|
2018
|
+
function defaultGithubReleaseSkill() {
|
|
2019
|
+
return [
|
|
2020
|
+
'---',
|
|
2021
|
+
'name: github-release',
|
|
2022
|
+
'description: Use for releases, version bumps, tags, publishing, GitHub Releases, and CHANGES updates.',
|
|
2023
|
+
'---',
|
|
2024
|
+
'',
|
|
2025
|
+
'# GitHub Release',
|
|
2026
|
+
'',
|
|
2027
|
+
'Use {{ paths.changesFile }} for change notes.',
|
|
2028
|
+
'Require confirmation before writing CHANGES, bumping versions, committing, tagging, pushing, publishing, or creating a GitHub Release.',
|
|
2029
|
+
'Follow `$commit-workflow` for release commits.',
|
|
2030
|
+
''
|
|
2031
|
+
].join('\n');
|
|
2032
|
+
}
|
|
2033
|
+
function defaultCodeReviewSkill() {
|
|
2034
|
+
return [
|
|
2035
|
+
'---',
|
|
2036
|
+
'name: code-review',
|
|
2037
|
+
'description: Use for code review, PR review, and diff review.',
|
|
2038
|
+
'---',
|
|
2039
|
+
'',
|
|
2040
|
+
'# Code Review',
|
|
2041
|
+
'',
|
|
2042
|
+
'Save review notes to {{ paths.reviewFile }}. If the file exists, update it.',
|
|
2043
|
+
'Also include findings in the final user-facing reply.',
|
|
2044
|
+
'Do not save review files inside the project repo.',
|
|
2045
|
+
''
|
|
2046
|
+
].join('\n');
|
|
2047
|
+
}
|
|
2048
|
+
function defaultToolInstructionsTemplate(isClaude) {
|
|
2049
|
+
return [
|
|
2050
|
+
`# Generated ${isClaude ? 'Claude' : 'agent'} instructions for {{ profile }}`,
|
|
2051
|
+
'',
|
|
2052
|
+
'Do not edit this file directly. Edit:',
|
|
2053
|
+
'',
|
|
2054
|
+
'```text',
|
|
2055
|
+
'{{ agentDir }}/agent-run.jsonc',
|
|
2056
|
+
'{{ agentDir }}/local.md.njk',
|
|
2057
|
+
'```',
|
|
2058
|
+
'',
|
|
2059
|
+
'{% for section in renderedAgentSections %}',
|
|
2060
|
+
'{{ section }}',
|
|
2061
|
+
'',
|
|
2062
|
+
'{% endfor %}',
|
|
2063
|
+
'## Absolute Paths',
|
|
2064
|
+
'',
|
|
2065
|
+
'Project root:',
|
|
2066
|
+
'',
|
|
2067
|
+
'```text',
|
|
2068
|
+
'{{ projectRoot }}',
|
|
2069
|
+
'```',
|
|
2070
|
+
'',
|
|
2071
|
+
'Agent directory:',
|
|
2072
|
+
'',
|
|
2073
|
+
'```text',
|
|
2074
|
+
'{{ agentDir }}',
|
|
2075
|
+
'```',
|
|
2076
|
+
'',
|
|
2077
|
+
'Review directory:',
|
|
2078
|
+
'',
|
|
2079
|
+
'```text',
|
|
2080
|
+
'{{ paths.reviewDir }}',
|
|
2081
|
+
'```',
|
|
2082
|
+
'',
|
|
2083
|
+
"Today's review file:",
|
|
2084
|
+
'',
|
|
2085
|
+
'```text',
|
|
2086
|
+
'{{ paths.reviewFile }}',
|
|
2087
|
+
'```',
|
|
2088
|
+
'',
|
|
2089
|
+
'Memories directory:',
|
|
2090
|
+
'',
|
|
2091
|
+
'```text',
|
|
2092
|
+
'{{ paths.memoriesDir }}',
|
|
2093
|
+
'```',
|
|
2094
|
+
'',
|
|
2095
|
+
'Changes file:',
|
|
2096
|
+
'',
|
|
2097
|
+
'```text',
|
|
2098
|
+
'{{ paths.changesFile }}',
|
|
2099
|
+
'```',
|
|
2100
|
+
'',
|
|
2101
|
+
'## Available Skills',
|
|
2102
|
+
'',
|
|
2103
|
+
'{% for skill in skills %}',
|
|
2104
|
+
'- `${{ skill.name }}`{% if skill.description %}: {{ skill.description }}{% endif %}',
|
|
2105
|
+
'{% endfor %}',
|
|
2106
|
+
'',
|
|
2107
|
+
'## Tool Note',
|
|
2108
|
+
'',
|
|
2109
|
+
isClaude
|
|
2110
|
+
? 'Claude uses the generated config directory under {{ agentDir }}/.claude and may read {{ agentDir }} via `--add-dir`.'
|
|
2111
|
+
: 'Codex uses {{ agentDir }}/AGENTS.md as `system_prompt_file` and {{ paths.codexHomeDir }} as CODEX_HOME.',
|
|
2112
|
+
'',
|
|
2113
|
+
'## Mandatory Path Rule',
|
|
2114
|
+
'',
|
|
2115
|
+
'Do not create AGENTS.md, CLAUDE.md, .agents, .claude, .codex, or AI-related files inside the project repository.',
|
|
2116
|
+
'Agent-only files must be stored under the agent directory shown above.',
|
|
2117
|
+
''
|
|
2118
|
+
].join('\n');
|
|
2119
|
+
}
|
|
2120
|
+
function defaultCodexConfigTemplate() {
|
|
2121
|
+
return [
|
|
2122
|
+
'# Generated by agent-run. Do not edit directly.',
|
|
2123
|
+
'',
|
|
2124
|
+
'project_doc_max_bytes = 65536',
|
|
2125
|
+
'sandbox_mode = "workspace-write"',
|
|
2126
|
+
'',
|
|
2127
|
+
'[sandbox_workspace_write]',
|
|
2128
|
+
'writable_roots = [',
|
|
2129
|
+
' "{{ projectRoot }}",',
|
|
2130
|
+
' "{{ agentDir }}"',
|
|
2131
|
+
']',
|
|
2132
|
+
'network_access = false',
|
|
2133
|
+
''
|
|
2134
|
+
].join('\n');
|
|
2135
|
+
}
|
|
2136
|
+
function defaultClaudeSettingsTemplate() {
|
|
2137
|
+
return [
|
|
2138
|
+
'{',
|
|
2139
|
+
' "env": {',
|
|
2140
|
+
' "AGENT_DIR": "{{ agentDir }}",',
|
|
2141
|
+
' "AGENT_RUN_PROJECT_ROOT": "{{ projectRoot }}"',
|
|
2142
|
+
' }',
|
|
2143
|
+
'}',
|
|
2144
|
+
''
|
|
2145
|
+
].join('\n');
|
|
2146
|
+
}
|
|
2147
|
+
function defaultCodexConfigContent(context) {
|
|
2148
|
+
return [
|
|
2149
|
+
'# Generated by agent-run. Do not edit directly.',
|
|
2150
|
+
'',
|
|
2151
|
+
'project_doc_max_bytes = 65536',
|
|
2152
|
+
'sandbox_mode = "workspace-write"',
|
|
2153
|
+
'',
|
|
2154
|
+
'[sandbox_workspace_write]',
|
|
2155
|
+
'writable_roots = [',
|
|
2156
|
+
` ${jsonString(context.projectRoot)},`,
|
|
2157
|
+
` ${jsonString(context.agentDir)}`,
|
|
2158
|
+
']',
|
|
2159
|
+
'network_access = false',
|
|
2160
|
+
''
|
|
2161
|
+
].join('\n');
|
|
2162
|
+
}
|
|
2163
|
+
function defaultClaudeSettingsContent(context) {
|
|
2164
|
+
return `${JSON.stringify({
|
|
2165
|
+
env: {
|
|
2166
|
+
AGENT_DIR: context.agentDir,
|
|
2167
|
+
AGENT_RUN_PROJECT_ROOT: context.projectRoot
|
|
2168
|
+
}
|
|
2169
|
+
}, null, 2)}\n`;
|
|
2170
|
+
}
|
|
2171
|
+
function jsonString(value) {
|
|
2172
|
+
return JSON.stringify(value);
|
|
2173
|
+
}
|
|
2174
|
+
function posixGitShim() {
|
|
2175
|
+
return [
|
|
2176
|
+
'#!/usr/bin/env bash',
|
|
2177
|
+
'set -euo pipefail',
|
|
2178
|
+
'case "${1:-}" in',
|
|
2179
|
+
' commit|tag|push)',
|
|
2180
|
+
' if [ "${AGENT_RUN_ALLOW_GIT_WRITE:-}" != "1" ]; then',
|
|
2181
|
+
' echo "agent-run: blocked git $1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation." >&2',
|
|
2182
|
+
' exit 42',
|
|
2183
|
+
' fi',
|
|
2184
|
+
' ;;',
|
|
2185
|
+
'esac',
|
|
2186
|
+
'if command -v /usr/bin/git >/dev/null 2>&1; then',
|
|
2187
|
+
' exec /usr/bin/git "$@"',
|
|
2188
|
+
'fi',
|
|
2189
|
+
'exec git "$@"',
|
|
2190
|
+
''
|
|
2191
|
+
].join('\n');
|
|
2192
|
+
}
|
|
2193
|
+
function posixPublishShim(tool) {
|
|
2194
|
+
return [
|
|
2195
|
+
'#!/usr/bin/env bash',
|
|
2196
|
+
'set -euo pipefail',
|
|
2197
|
+
'if [ "${1:-}" = "publish" ] && [ "${AGENT_RUN_ALLOW_PUBLISH:-}" != "1" ]; then',
|
|
2198
|
+
` echo "agent-run: blocked ${tool} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation." >&2`,
|
|
2199
|
+
' exit 42',
|
|
2200
|
+
'fi',
|
|
2201
|
+
`if command -v /usr/bin/${tool} >/dev/null 2>&1; then`,
|
|
2202
|
+
` exec /usr/bin/${tool} "$@"`,
|
|
2203
|
+
'fi',
|
|
2204
|
+
`exec ${tool} "$@"`,
|
|
2205
|
+
''
|
|
2206
|
+
].join('\n');
|
|
2207
|
+
}
|
|
2208
|
+
function posixGhShim() {
|
|
2209
|
+
return [
|
|
2210
|
+
'#!/usr/bin/env bash',
|
|
2211
|
+
'set -euo pipefail',
|
|
2212
|
+
'if [ "${1:-}" = "release" ] && [ "${2:-}" = "create" ] && [ "${AGENT_RUN_ALLOW_GITHUB_RELEASE:-}" != "1" ]; then',
|
|
2213
|
+
' echo "agent-run: blocked gh release create. Review and run it manually, or set AGENT_RUN_ALLOW_GITHUB_RELEASE=1 for this invocation." >&2',
|
|
2214
|
+
' exit 42',
|
|
2215
|
+
'fi',
|
|
2216
|
+
'if command -v /usr/bin/gh >/dev/null 2>&1; then',
|
|
2217
|
+
' exec /usr/bin/gh "$@"',
|
|
2218
|
+
'fi',
|
|
2219
|
+
'exec gh "$@"',
|
|
2220
|
+
''
|
|
2221
|
+
].join('\n');
|
|
2222
|
+
}
|
|
2223
|
+
function windowsShim(name) {
|
|
2224
|
+
const guard = name === 'git'
|
|
2225
|
+
? [
|
|
2226
|
+
'if /I "%1"=="commit" goto block_git',
|
|
2227
|
+
'if /I "%1"=="tag" goto block_git',
|
|
2228
|
+
'if /I "%1"=="push" goto block_git',
|
|
2229
|
+
'goto run',
|
|
2230
|
+
':block_git',
|
|
2231
|
+
'if "%AGENT_RUN_ALLOW_GIT_WRITE%"=="1" goto run',
|
|
2232
|
+
'echo agent-run: blocked git %1. Review and run it manually, or set AGENT_RUN_ALLOW_GIT_WRITE=1 for this invocation. 1>&2',
|
|
2233
|
+
'exit /b 42'
|
|
2234
|
+
]
|
|
2235
|
+
: name === 'gh'
|
|
2236
|
+
? [
|
|
2237
|
+
'if /I not "%1"=="release" goto run',
|
|
2238
|
+
'if /I not "%2"=="create" goto run',
|
|
2239
|
+
'if "%AGENT_RUN_ALLOW_GITHUB_RELEASE%"=="1" goto run',
|
|
2240
|
+
'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',
|
|
2241
|
+
'exit /b 42'
|
|
2242
|
+
]
|
|
2243
|
+
: [
|
|
2244
|
+
'if /I not "%1"=="publish" goto run',
|
|
2245
|
+
'if "%AGENT_RUN_ALLOW_PUBLISH%"=="1" goto run',
|
|
2246
|
+
`echo agent-run: blocked ${name} publish. Review and run it manually, or set AGENT_RUN_ALLOW_PUBLISH=1 for this invocation. 1>&2`,
|
|
2247
|
+
'exit /b 42'
|
|
2248
|
+
];
|
|
2249
|
+
return ['@echo off', ...guard, ':run', `${name}.exe %*`, ''].join('\r\n');
|
|
2250
|
+
}
|
|
1142
2251
|
function isVerbose() {
|
|
1143
2252
|
return parseBooleanEnv(process.env[VERBOSE_ENV]);
|
|
1144
2253
|
}
|
|
1145
2254
|
function verbose(message) {
|
|
1146
|
-
if (
|
|
1147
|
-
|
|
2255
|
+
if (isVerbose()) {
|
|
2256
|
+
process.stderr.write(`agent-run: ${message}\n`);
|
|
1148
2257
|
}
|
|
1149
|
-
process.stderr.write(`agent-run: ${message}\n`);
|
|
1150
2258
|
}
|
|
1151
2259
|
function formatCommand(command, args) {
|
|
1152
2260
|
return [command, ...args].map(quoteArg).join(' ');
|