@technomoron/agent-run 0.99.23 → 0.99.25

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.
@@ -0,0 +1,441 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderProfile = renderProfile;
4
+ exports.syncAgentProfile = syncAgentProfile;
5
+ exports.checkRenderedProfile = checkRenderedProfile;
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const constants_1 = require("./constants");
10
+ const config_tree_1 = require("./config-tree");
11
+ const defaults_1 = require("./defaults");
12
+ const guards_1 = require("./guards");
13
+ const manifest_1 = require("./manifest");
14
+ const project_1 = require("./project");
15
+ const templates_1 = require("./templates");
16
+ const utils_1 = require("./utils");
17
+ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = null, trace, options) {
18
+ const configRoot = options?.configRoot ?? (0, project_1.defaultConfigRoot)(projectRoot);
19
+ const profile = options?.profile ?? (0, project_1.resolveProfile)(projectRoot);
20
+ const env = (0, templates_1.createNunjucksEnv)(configRoot);
21
+ const manifest = (0, manifest_1.normalizeManifest)((0, manifest_1.loadManifest)(configRoot, agentDir, profile, trace), profile);
22
+ validateManifestProfile(manifest.profile, profile, agentDir);
23
+ if (!checkOnly) {
24
+ (0, config_tree_1.ensureConfigRootGitignore)(configRoot);
25
+ }
26
+ const context = (0, templates_1.buildRenderContext)(projectRoot, agentDir, configRoot, manifest, env);
27
+ context.renderedAgentSections = renderAgentSections(env, configRoot, manifest, context, trace);
28
+ context.skills = renderSkills(env, configRoot, manifest, context, trace);
29
+ const files = renderProfileFiles(env, configRoot, context, targetTool, trace);
30
+ return { agentDir, configRoot, profile, context, files, skills: context.skills };
31
+ }
32
+ function validateManifestProfile(manifestProfile, inferredProfile, agentDir) {
33
+ if (manifestProfile === inferredProfile) {
34
+ return;
35
+ }
36
+ const parsed = (0, project_1.parseProfile)(manifestProfile, `${path.join(agentDir, constants_1.MANIFEST_FILE_NAME)} profile`);
37
+ if (parsed.profile === null) {
38
+ throw new Error(parsed.reason);
39
+ }
40
+ throw new Error(`${path.join(agentDir, constants_1.MANIFEST_FILE_NAME)} profile must match inferred profile ${inferredProfile}`);
41
+ }
42
+ function renderAgentSections(env, configRoot, manifest, context, trace) {
43
+ const sections = [(0, templates_1.renderTemplateFile)(env, configRoot, manifest.agent.base, context, trace)];
44
+ for (const include of manifest.agent.includes) {
45
+ const includePath = (0, templates_1.resolveConfigPath)(configRoot, include, context);
46
+ if (!fs.existsSync(includePath)) {
47
+ if (include.includes('AGENTS-MODS.md') || include.includes(constants_1.LOCAL_TEMPLATE_FILE_NAME)) {
48
+ continue;
49
+ }
50
+ throw new Error(`missing template: ${includePath}`);
51
+ }
52
+ sections.push((0, templates_1.renderTemplateFile)(env, configRoot, include, context, trace));
53
+ }
54
+ return sections.map((section, index) => {
55
+ const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
56
+ (0, templates_1.assertNoUnexpandedTemplateVars)(label, section);
57
+ return section.trimEnd();
58
+ });
59
+ }
60
+ function renderProfileFiles(env, configRoot, context, targetTool, trace) {
61
+ const files = [];
62
+ if (context.tools.codex && (targetTool === null || targetTool === 'codex')) {
63
+ files.push({
64
+ path: path.join(context.paths.codexHomeDir, 'AGENTS.md'),
65
+ content: renderToolInstructions('AGENTS.md', env, configRoot, context, false, trace)
66
+ });
67
+ files.push({
68
+ path: path.join(context.paths.codexHomeDir, 'config.toml'),
69
+ content: renderCodexConfig(env, configRoot, context, trace)
70
+ });
71
+ files.push(...renderNativeSkillFiles(context, 'codex'));
72
+ }
73
+ if (context.tools.claude && (targetTool === null || targetTool === 'claude')) {
74
+ files.push({
75
+ path: path.join(context.paths.liveDir, 'CLAUDE.md'),
76
+ content: renderToolInstructions('CLAUDE.md', env, configRoot, context, true, trace)
77
+ });
78
+ files.push({
79
+ path: path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
80
+ content: renderClaudeSettings(env, configRoot, context, trace)
81
+ });
82
+ files.push({
83
+ path: path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
84
+ content: claudePluginManifestContent(context)
85
+ });
86
+ files.push(...renderNativeSkillFiles(context, 'claude'));
87
+ }
88
+ files.push(...(0, guards_1.renderGuardShims)(context));
89
+ return files;
90
+ }
91
+ function syncAgentProfile(projectRoot, agentDir, options) {
92
+ const rendered = renderProfile(projectRoot, agentDir, false, null, undefined, options);
93
+ syncRuntimeDirs(rendered.context);
94
+ removeStaleGeneratedEntries(rendered);
95
+ for (const file of rendered.files) {
96
+ writeGeneratedFile(file.path, file.content, file.executable ?? false);
97
+ }
98
+ removeLegacyGeneratedCodexFiles(rendered.context);
99
+ removeLegacyGeneratedClaudeSettings(rendered.context);
100
+ return rendered;
101
+ }
102
+ function writeGeneratedFile(filePath, content, executable) {
103
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
104
+ fs.writeFileSync(filePath, content, 'utf8');
105
+ if (executable && !constants_1.IS_WINDOWS) {
106
+ fs.chmodSync(filePath, 0o755);
107
+ }
108
+ (0, utils_1.verbose)(`write ${filePath}`);
109
+ }
110
+ function removeStaleGeneratedEntries(rendered) {
111
+ const expectedFiles = new Set(rendered.files.map((file) => path.resolve(file.path)));
112
+ const context = rendered.context;
113
+ const generatedCandidates = [
114
+ path.join(context.paths.codexHomeDir, 'AGENTS.md'),
115
+ path.join(context.paths.codexHomeDir, 'config.toml'),
116
+ path.join(context.paths.liveDir, 'CLAUDE.md'),
117
+ path.join(context.paths.liveDir, '.claude', 'CLAUDE.md'),
118
+ path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
119
+ path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
120
+ ...['git', 'git.cmd', 'npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'gh', 'gh.cmd'].map((name) => path.join(context.paths.binDir, name))
121
+ ];
122
+ for (const filePath of generatedCandidates) {
123
+ if (!expectedFiles.has(path.resolve(filePath))) {
124
+ fs.rmSync(filePath, { force: true });
125
+ }
126
+ }
127
+ const skillNames = new Set(rendered.skills.map((skill) => skill.name));
128
+ removeStaleGeneratedSkills(context.paths.codexSkillsDir, context.tools.codex ? skillNames : new Set(), new Set(['.system']));
129
+ removeStaleGeneratedSkills(context.paths.claudeSkillsDir, context.tools.claude ? skillNames : new Set());
130
+ }
131
+ function removeStaleGeneratedSkills(dir, expectedNames, preservedNames = new Set()) {
132
+ if (!fs.existsSync(dir)) {
133
+ return;
134
+ }
135
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
136
+ if (!expectedNames.has(entry.name) && !preservedNames.has(entry.name)) {
137
+ fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
138
+ }
139
+ }
140
+ }
141
+ function removeLegacyGeneratedCodexFiles(context) {
142
+ for (const legacyPath of [
143
+ path.join(context.paths.liveDir, 'AGENTS.md'),
144
+ path.join(context.paths.liveDir, 'config.toml')
145
+ ]) {
146
+ if (fs.existsSync(legacyPath)) {
147
+ fs.rmSync(legacyPath);
148
+ (0, utils_1.verbose)(`removed legacy generated Codex file ${legacyPath}`);
149
+ }
150
+ }
151
+ }
152
+ function removeLegacyGeneratedClaudeSettings(context) {
153
+ for (const legacyPath of [
154
+ path.join(context.paths.liveDir, '.claude', 'settings.json'),
155
+ path.join(context.profileDir, '.claude', 'settings.json')
156
+ ]) {
157
+ if (!fs.existsSync(legacyPath)) {
158
+ continue;
159
+ }
160
+ const actual = fs.readFileSync(legacyPath, 'utf8').replace(/\r\n/g, '\n');
161
+ const generated = [
162
+ (0, defaults_1.legacyDefaultClaudeSettingsContent)(context, context.agentDir),
163
+ (0, defaults_1.legacyDefaultClaudeSettingsContent)(context, context.profileDir)
164
+ ];
165
+ if (generated.includes(actual)) {
166
+ fs.rmSync(legacyPath);
167
+ (0, utils_1.verbose)(`removed legacy generated Claude settings ${legacyPath}`);
168
+ }
169
+ }
170
+ }
171
+ function checkRenderedProfile(projectRoot, agentDir) {
172
+ const findings = [];
173
+ const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
174
+ try {
175
+ const rendered = renderProfile(projectRoot, agentDir, true);
176
+ checkRenderedFiles(rendered, findings);
177
+ checkRuntimeDirectories(rendered.context, findings);
178
+ for (const skill of rendered.skills) {
179
+ validateRenderedSkill(skill.renderedContent, skill.sourcePath);
180
+ }
181
+ }
182
+ catch (error) {
183
+ findings.push({
184
+ message: `failed to render profile ${agentDir}: ${(0, utils_1.formatError)(error)}`,
185
+ severity: 'ERROR'
186
+ });
187
+ }
188
+ checkConfigGitignore(configRoot, findings);
189
+ return findings;
190
+ }
191
+ function checkRenderedFiles(rendered, findings) {
192
+ for (const file of rendered.files) {
193
+ if (!fs.existsSync(file.path)) {
194
+ findings.push({ message: `missing generated file: ${file.path}`, severity: 'ERROR' });
195
+ continue;
196
+ }
197
+ const actual = fs.readFileSync(file.path, 'utf8').replace(/\r\n/g, '\n');
198
+ if (actual !== file.content) {
199
+ findings.push({ message: `generated file is out of date: ${file.path}`, severity: 'ERROR' });
200
+ }
201
+ }
202
+ }
203
+ function checkRuntimeDirectories(context, findings) {
204
+ for (const dir of [
205
+ context.paths.reviewDir,
206
+ context.paths.memoriesDir,
207
+ context.paths.codexHomeDir,
208
+ context.paths.liveDir,
209
+ context.paths.codexSkillsDir,
210
+ context.paths.claudeSkillsDir,
211
+ context.paths.binDir
212
+ ]) {
213
+ if (!fs.existsSync(dir)) {
214
+ findings.push({ message: `missing generated runtime directory: ${dir}`, severity: 'ERROR' });
215
+ }
216
+ }
217
+ }
218
+ function checkConfigGitignore(configRoot, findings) {
219
+ const gitignorePath = path.join(configRoot, '.gitignore');
220
+ if (!fs.existsSync(gitignorePath)) {
221
+ findings.push({ message: `missing config root .gitignore: ${gitignorePath}`, severity: 'ERROR' });
222
+ return;
223
+ }
224
+ const gitignore = fs.readFileSync(gitignorePath, 'utf8');
225
+ for (const entry of constants_1.GENERATED_GITIGNORE_ENTRIES) {
226
+ if (entry && !entry.startsWith('#') && !gitignore.includes(entry)) {
227
+ findings.push({ message: `config root .gitignore missing entry: ${entry}`, severity: 'ERROR' });
228
+ }
229
+ }
230
+ }
231
+ function renderSkills(env, configRoot, manifest, context, trace) {
232
+ return manifest.skills.install.map((name) => {
233
+ const sourceTemplate = manifest.skills.overrides[name] ?? `global/skills/${name}/SKILL.md.njk`;
234
+ const sourcePath = (0, templates_1.resolveConfigPath)(configRoot, sourceTemplate, context);
235
+ if (!fs.existsSync(sourcePath)) {
236
+ throw new Error(`missing skill template for ${name}: ${sourcePath}`);
237
+ }
238
+ const renderedContent = (0, templates_1.renderTemplateFile)(env, configRoot, sourceTemplate, context, trace);
239
+ (0, templates_1.assertNoUnexpandedTemplateVars)(`skill ${name}`, renderedContent);
240
+ validateRenderedSkill(renderedContent, sourcePath);
241
+ return { name, sourcePath, renderedContent, description: extractSkillDescription(renderedContent) };
242
+ });
243
+ }
244
+ function renderNativeSkillFiles(context, targetTool) {
245
+ const targetDir = targetTool === 'codex' ? context.paths.codexSkillsDir : context.paths.claudeSkillsDir;
246
+ return context.skills.map((skill) => ({
247
+ path: path.join(targetDir, skill.name, 'SKILL.md'),
248
+ content: skill.renderedContent
249
+ }));
250
+ }
251
+ function renderToolInstructions(templateName, env, configRoot, context, isClaude, trace) {
252
+ const templatePath = `global/tool-templates/${templateName}.njk`;
253
+ const content = fs.existsSync(path.join(configRoot, templatePath))
254
+ ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
255
+ : env.renderString((0, defaults_1.defaultToolInstructionsTemplate)(isClaude), context);
256
+ (0, templates_1.assertNoUnexpandedTemplateVars)(templateName, content);
257
+ return content.replace(/\n*$/, '\n');
258
+ }
259
+ function renderCodexConfig(env, configRoot, context, trace) {
260
+ const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk');
261
+ const content = templatePath
262
+ ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
263
+ : (0, defaults_1.defaultCodexConfigContent)(context);
264
+ (0, templates_1.assertNoUnexpandedTemplateVars)('config.toml', content);
265
+ return content.replace(/\n*$/, '\n');
266
+ }
267
+ function renderClaudeSettings(env, configRoot, context, trace) {
268
+ const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'claude-settings.json.njk', 'global/tool-templates/claude-settings.json.njk');
269
+ const content = templatePath
270
+ ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
271
+ : (0, defaults_1.defaultClaudeSettingsContent)(context);
272
+ (0, templates_1.assertNoUnexpandedTemplateVars)('claude settings.json', content);
273
+ try {
274
+ JSON.parse(content);
275
+ }
276
+ catch (error) {
277
+ throw new Error(`invalid generated Claude settings JSON: ${(0, utils_1.formatError)(error)}`);
278
+ }
279
+ return content.replace(/\n*$/, '\n');
280
+ }
281
+ function claudePluginManifestContent(context) {
282
+ return `${JSON.stringify({
283
+ name: 'agent-run-profile',
284
+ description: `Generated skills for agent-run profile ${context.profile}`,
285
+ version: '1.0.0',
286
+ author: { name: 'Technomoron' }
287
+ }, null, 2)}\n`;
288
+ }
289
+ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
290
+ const overridePath = `${context.profile}/overrides/${overrideFileName}`;
291
+ if (fs.existsSync(path.join(configRoot, overridePath))) {
292
+ return overridePath;
293
+ }
294
+ return fs.existsSync(path.join(configRoot, globalTemplatePath)) ? globalTemplatePath : null;
295
+ }
296
+ function syncRuntimeDirs(context) {
297
+ for (const dir of [
298
+ context.paths.reviewDir,
299
+ context.paths.memoriesDir,
300
+ context.paths.codexHomeDir,
301
+ context.paths.codexSkillsDir,
302
+ context.paths.claudeSkillsDir,
303
+ context.paths.binDir,
304
+ path.join(context.paths.liveDir, '.claude')
305
+ ]) {
306
+ fs.mkdirSync(dir, { recursive: true });
307
+ }
308
+ migrateLiveReviewFiles(context);
309
+ removeLegacyCodexSkillDirs(context);
310
+ removeLegacyCodeReviewSkillDirs(context);
311
+ ensureSharedCodexAuth(context.paths.codexHomeDir);
312
+ }
313
+ function ensureSharedCodexAuth(codexHomeDir) {
314
+ const sharedAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
315
+ if (!fs.existsSync(sharedAuthPath)) {
316
+ return;
317
+ }
318
+ const profileAuthPath = path.join(codexHomeDir, 'auth.json');
319
+ if (path.resolve(profileAuthPath) === path.resolve(sharedAuthPath) || isSymlinkTo(profileAuthPath, sharedAuthPath)) {
320
+ return;
321
+ }
322
+ if (fs.existsSync(profileAuthPath) || (0, utils_1.isSymlink)(profileAuthPath)) {
323
+ const backupPath = (0, utils_1.nextBackupPath)(profileAuthPath);
324
+ fs.renameSync(profileAuthPath, backupPath);
325
+ (0, utils_1.verbose)(`backed up profile Codex auth ${profileAuthPath} -> ${backupPath}`);
326
+ }
327
+ fs.mkdirSync(path.dirname(profileAuthPath), { recursive: true });
328
+ try {
329
+ fs.symlinkSync(sharedAuthPath, profileAuthPath);
330
+ (0, utils_1.verbose)(`linked profile Codex auth ${profileAuthPath} -> ${sharedAuthPath}`);
331
+ }
332
+ catch (error) {
333
+ if (!constants_1.IS_WINDOWS) {
334
+ throw error;
335
+ }
336
+ fs.copyFileSync(sharedAuthPath, profileAuthPath);
337
+ (0, utils_1.verbose)(`copied shared Codex auth ${sharedAuthPath} -> ${profileAuthPath}`);
338
+ }
339
+ }
340
+ function isSymlinkTo(filePath, targetPath) {
341
+ try {
342
+ if (!fs.lstatSync(filePath).isSymbolicLink()) {
343
+ return false;
344
+ }
345
+ const linkTarget = fs.readlinkSync(filePath);
346
+ return path.resolve(path.dirname(filePath), linkTarget) === path.resolve(targetPath);
347
+ }
348
+ catch {
349
+ return false;
350
+ }
351
+ }
352
+ function removeLegacyCodexSkillDirs(context) {
353
+ const legacyCodexDir = path.join(context.paths.liveDir, '.agents');
354
+ if (!(0, utils_1.isSamePathOrDescendant)(context.paths.codexSkillsDir, legacyCodexDir)) {
355
+ fs.rmSync(legacyCodexDir, { recursive: true, force: true });
356
+ }
357
+ }
358
+ function removeLegacyCodeReviewSkillDirs(context) {
359
+ if (!context.skills.some((skill) => skill.name === 'code-review-organizer')) {
360
+ return;
361
+ }
362
+ for (const dir of [
363
+ path.join(context.paths.liveDir, '.agents', 'skills', 'code-review'),
364
+ path.join(context.paths.codexSkillsDir, 'code-review'),
365
+ path.join(context.paths.claudeSkillsDir, 'code-review')
366
+ ]) {
367
+ fs.rmSync(dir, { recursive: true, force: true });
368
+ }
369
+ }
370
+ function migrateLiveReviewFiles(context) {
371
+ const legacyReviewDir = path.join(context.paths.liveDir, 'reviews');
372
+ if (path.resolve(legacyReviewDir) === path.resolve(context.paths.reviewDir) || !fs.existsSync(legacyReviewDir)) {
373
+ return;
374
+ }
375
+ fs.mkdirSync(context.paths.reviewDir, { recursive: true });
376
+ for (const entry of fs.readdirSync(legacyReviewDir, { withFileTypes: true })) {
377
+ if (!entry.isFile()) {
378
+ continue;
379
+ }
380
+ const sourcePath = path.join(legacyReviewDir, entry.name);
381
+ let targetPath = path.join(context.paths.reviewDir, entry.name);
382
+ if (fs.existsSync(targetPath)) {
383
+ if (filesHaveSameContent(sourcePath, targetPath)) {
384
+ fs.rmSync(sourcePath);
385
+ continue;
386
+ }
387
+ targetPath = nextAvailablePath(context.paths.reviewDir, entry.name);
388
+ }
389
+ moveFile(sourcePath, targetPath);
390
+ }
391
+ }
392
+ function filesHaveSameContent(leftPath, rightPath) {
393
+ const left = fs.readFileSync(leftPath);
394
+ const right = fs.readFileSync(rightPath);
395
+ return left.length === right.length && left.equals(right);
396
+ }
397
+ function nextAvailablePath(dir, filename) {
398
+ const parsed = path.parse(filename);
399
+ for (let sequence = 1;; sequence += 1) {
400
+ const candidate = path.join(dir, `${parsed.name}.${sequence}${parsed.ext}`);
401
+ if (!fs.existsSync(candidate)) {
402
+ return candidate;
403
+ }
404
+ }
405
+ }
406
+ function moveFile(sourcePath, targetPath) {
407
+ try {
408
+ fs.renameSync(sourcePath, targetPath);
409
+ }
410
+ catch (error) {
411
+ if (error.code !== 'EXDEV') {
412
+ throw error;
413
+ }
414
+ fs.copyFileSync(sourcePath, targetPath);
415
+ fs.rmSync(sourcePath);
416
+ }
417
+ }
418
+ function validateRenderedSkill(content, sourcePath) {
419
+ const frontMatter = /^---\n([\s\S]*?)\n---\n/.exec(content.replace(/\r\n/g, '\n'));
420
+ if (frontMatter === null) {
421
+ throw new Error(`generated skill missing YAML front matter: ${sourcePath}`);
422
+ }
423
+ const yaml = frontMatter[1] ?? '';
424
+ if (!/^name:\s*\S+/m.test(yaml)) {
425
+ throw new Error(`generated skill missing name: ${sourcePath}`);
426
+ }
427
+ if (!/^description:\s*(?:\S|>\s*$)/m.test(yaml)) {
428
+ throw new Error(`generated skill missing description: ${sourcePath}`);
429
+ }
430
+ }
431
+ function extractSkillDescription(content) {
432
+ const normalized = content.replace(/\r\n/g, '\n');
433
+ const simple = /^description:\s*['"]?(.+?)['"]?\s*$/m.exec(normalized);
434
+ if (simple?.[1]) {
435
+ return simple[1].trim();
436
+ }
437
+ const folded = /^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m.exec(normalized);
438
+ return folded?.[1]
439
+ ? folded[1].split('\n').map((line) => line.trim()).filter(Boolean).join(' ')
440
+ : '';
441
+ }
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNunjucksEnv = createNunjucksEnv;
4
+ exports.buildRenderContext = buildRenderContext;
5
+ exports.resolveConfigPath = resolveConfigPath;
6
+ exports.renderTemplateFile = renderTemplateFile;
7
+ exports.assertNoUnexpandedTemplateVars = assertNoUnexpandedTemplateVars;
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const nunjucks = require("nunjucks");
11
+ const constants_1 = require("./constants");
12
+ const utils_1 = require("./utils");
13
+ function createNunjucksEnv(configRoot) {
14
+ return new nunjucks.Environment(new nunjucks.FileSystemLoader(configRoot, { noCache: true }), {
15
+ autoescape: false,
16
+ trimBlocks: true,
17
+ lstripBlocks: true,
18
+ throwOnUndefined: true
19
+ });
20
+ }
21
+ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env) {
22
+ const date = (0, utils_1.localDateString)();
23
+ const liveDir = path.join(profileDir, constants_1.LIVE_DIR_NAME);
24
+ const baseContext = {
25
+ profile: manifest.profile,
26
+ kind: manifest.kind,
27
+ projectRoot,
28
+ agentDir: liveDir,
29
+ profileDir,
30
+ configRoot,
31
+ date,
32
+ checks: manifest.checks,
33
+ tools: manifest.tools,
34
+ guardrails: manifest.guardrails
35
+ };
36
+ const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
37
+ const memoriesDir = resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext);
38
+ const codexHomeDir = path.join(memoriesDir, 'codex-home');
39
+ const paths = {
40
+ profileDir,
41
+ liveDir,
42
+ changesFile: resolveRuntimePath(configRoot, manifest.paths.changesFile, env, baseContext),
43
+ reviewDir,
44
+ reviewFile: resolveRuntimePath(configRoot, manifest.paths.reviewFile, env, baseContext),
45
+ reviewConsolidatedFile: manifest.paths.reviewConsolidatedFile
46
+ ? resolveRuntimePath(configRoot, manifest.paths.reviewConsolidatedFile, env, baseContext)
47
+ : path.join(reviewDir, 'REVIEW.md'),
48
+ memoriesDir,
49
+ globalMemoryDir: path.join(configRoot, 'notes', 'memory'),
50
+ codexHomeDir,
51
+ overridesDir: path.join(profileDir, 'overrides'),
52
+ codexSkillsDir: path.join(codexHomeDir, 'skills'),
53
+ claudeSkillsDir: path.join(liveDir, '.claude', 'skills'),
54
+ binDir: path.join(liveDir, 'bin')
55
+ };
56
+ return {
57
+ ...baseContext,
58
+ paths,
59
+ permissionsAllow: buildPermissionsAllow(manifest),
60
+ skills: [],
61
+ renderedAgentSections: []
62
+ };
63
+ }
64
+ function buildPermissionsAllow(manifest) {
65
+ const allow = new Set();
66
+ for (const check of manifest.checks) {
67
+ const normalized = check.trim();
68
+ if (normalized) {
69
+ allow.add(`Bash(${normalized})`);
70
+ }
71
+ }
72
+ return [...allow];
73
+ }
74
+ function resolveConfigPath(configRoot, relativePath, context) {
75
+ const rendered = createNunjucksEnv(configRoot).renderString(relativePath, context);
76
+ const resolved = path.resolve(configRoot, rendered);
77
+ if (!(0, utils_1.isSamePathOrDescendant)(resolved, path.resolve(configRoot))) {
78
+ throw new Error(`config path escapes config root: ${relativePath}`);
79
+ }
80
+ return resolved;
81
+ }
82
+ function resolveRuntimePath(configRoot, pathTemplate, env, context) {
83
+ const rendered = env.renderString(pathTemplate, context);
84
+ return path.isAbsolute(rendered) ? path.resolve(rendered) : path.resolve(configRoot, rendered);
85
+ }
86
+ function renderTemplateFile(env, configRoot, templatePath, context, trace) {
87
+ const resolvedPath = resolveConfigPath(configRoot, templatePath, context);
88
+ if (!fs.existsSync(resolvedPath)) {
89
+ throw new Error(`missing template: ${resolvedPath}`);
90
+ }
91
+ const relativePath = path.relative(configRoot, resolvedPath).replace(/\\/g, '/');
92
+ let content = fs.readFileSync(resolvedPath, 'utf8');
93
+ traceTemplateSource(configRoot, resolvedPath, content, context, trace);
94
+ if (path.basename(resolvedPath) === 'AGENTS-MODS.md') {
95
+ content = renderLegacyAgentsMods(resolvedPath, [], trace);
96
+ return env.renderString(content, context);
97
+ }
98
+ return env.render(relativePath, context);
99
+ }
100
+ function traceTemplateSource(configRoot, sourcePath, content, context, trace, stack = []) {
101
+ if (trace === undefined) {
102
+ return;
103
+ }
104
+ const resolvedSource = path.resolve(sourcePath);
105
+ trace.sourceFiles.add(resolvedSource);
106
+ if (stack.includes(resolvedSource)) {
107
+ return;
108
+ }
109
+ const nextStack = [...stack, resolvedSource];
110
+ const includePattern = /{%\s*(?:include|extends|import)\s+["']([^"']+)["']|{%\s*from\s+["']([^"']+)["']/g;
111
+ for (const match of content.matchAll(includePattern)) {
112
+ const includePath = match[1] ?? match[2];
113
+ if (!includePath) {
114
+ continue;
115
+ }
116
+ const includedPath = resolveConfigPath(configRoot, includePath, context);
117
+ if (fs.existsSync(includedPath)) {
118
+ traceTemplateSource(configRoot, includedPath, fs.readFileSync(includedPath, 'utf8'), context, trace, nextStack);
119
+ }
120
+ }
121
+ }
122
+ function assertNoUnexpandedTemplateVars(label, content) {
123
+ if (constants_1.UNEXPANDED_TEMPLATE_RE.test(content)) {
124
+ throw new Error(`unexpanded template syntax remains in ${label}`);
125
+ }
126
+ }
127
+ function renderLegacyAgentsMods(sourceFile, stack = [], trace) {
128
+ const resolvedSource = path.resolve(sourceFile);
129
+ trace?.sourceFiles.add(resolvedSource);
130
+ if (stack.includes(resolvedSource)) {
131
+ throw new Error(`Include cycle detected: ${[...stack, resolvedSource].join(' -> ')}`);
132
+ }
133
+ const lines = fs.readFileSync(resolvedSource, 'utf8').replace(/\r\n/g, '\n').split('\n');
134
+ const output = [];
135
+ const nextStack = [...stack, resolvedSource];
136
+ let sawLeadingInclude = false;
137
+ let insertedOverrideNote = false;
138
+ let contentStarted = false;
139
+ let fence = null;
140
+ for (const line of lines) {
141
+ const trimmed = line.trim();
142
+ const isIndentedCode = /^(?: {4}|\t)/.test(line);
143
+ const fenceMatch = isIndentedCode ? null : /^(`{3,}|~{3,})/.exec(trimmed);
144
+ if (fence !== null) {
145
+ output.push(line);
146
+ contentStarted = true;
147
+ const marker = fenceMatch?.[1];
148
+ if (marker && marker[0] === fence.character && marker.length >= fence.length && !trimmed.slice(marker.length).trim()) {
149
+ fence = null;
150
+ }
151
+ continue;
152
+ }
153
+ if (!contentStarted && !trimmed) {
154
+ continue;
155
+ }
156
+ if (fenceMatch?.[1]) {
157
+ fence = { character: fenceMatch[1][0] ?? '`', length: fenceMatch[1].length };
158
+ }
159
+ if (fence === null && !isIndentedCode && trimmed.startsWith('@')) {
160
+ const includePath = trimmed.slice(1).trim();
161
+ if (includePath) {
162
+ const resolvedInclude = resolveIncludePath(resolvedSource, includePath);
163
+ trace?.sourceFiles.add(resolvedInclude);
164
+ output.push(renderLegacyAgentsMods(resolvedInclude, nextStack, trace));
165
+ if (!contentStarted) {
166
+ sawLeadingInclude = true;
167
+ }
168
+ }
169
+ continue;
170
+ }
171
+ if (sawLeadingInclude && !insertedOverrideNote) {
172
+ output.push('', 'If anything below this point conflicts with anything included above,');
173
+ output.push('the later instructions below take precedence.', '');
174
+ insertedOverrideNote = true;
175
+ }
176
+ output.push(line);
177
+ contentStarted = true;
178
+ }
179
+ return `${output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
180
+ }
181
+ function resolveIncludePath(sourceFile, includePath) {
182
+ if (path.isAbsolute(includePath)) {
183
+ return includePath;
184
+ }
185
+ const sourceRelativePath = path.resolve(path.dirname(sourceFile), includePath);
186
+ if (fs.existsSync(sourceRelativePath)) {
187
+ return sourceRelativePath;
188
+ }
189
+ return path.resolve(findIncludeRoot(sourceFile), includePath.replace(/^(\.\.\/)+/, ''));
190
+ }
191
+ function findIncludeRoot(sourceFile) {
192
+ let dir = path.dirname(sourceFile);
193
+ for (;;) {
194
+ if (fs.existsSync(path.join(dir, 'global')) || fs.existsSync(path.join(dir, 'templates'))) {
195
+ return dir;
196
+ }
197
+ const parent = path.dirname(dir);
198
+ if (parent === dir) {
199
+ return path.dirname(sourceFile);
200
+ }
201
+ dir = parent;
202
+ }
203
+ }