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