@technomoron/agent-run 0.99.28 → 0.99.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGES +26 -0
  2. package/README.md +202 -26
  3. package/dist/agents/claude.js +93 -0
  4. package/dist/agents/codex.js +77 -0
  5. package/dist/agents/gemini.js +112 -0
  6. package/dist/agents/grok.js +73 -0
  7. package/dist/agents/registry.js +26 -0
  8. package/dist/agents/shared.js +17 -0
  9. package/dist/agents/types.js +4 -0
  10. package/dist/checks.js +4 -0
  11. package/dist/cli.js +45 -12
  12. package/dist/commands.js +72 -18
  13. package/dist/config/render-instructions.js +54 -0
  14. package/dist/config/render-mcp.js +88 -0
  15. package/dist/config/render-settings.js +35 -0
  16. package/dist/config/render-skills.js +51 -0
  17. package/dist/config-tree.js +249 -0
  18. package/dist/constants.js +5 -2
  19. package/dist/defaults.js +65 -8
  20. package/dist/manifest.js +39 -2
  21. package/dist/renderer.js +81 -227
  22. package/dist/runtime/environment.js +17 -0
  23. package/dist/runtime/postflight.js +40 -0
  24. package/dist/runtime/shared-state.js +44 -0
  25. package/dist/runtime/spawn-agent.js +7 -0
  26. package/dist/templates.js +12 -0
  27. package/examples/basic-config/README.md +2 -1
  28. package/examples/basic-config/agent-config/agent-run.defaults.jsonc +8 -2
  29. package/examples/basic-config/agent-config/global/tool-templates/AGENTS.md.njk +2 -0
  30. package/examples/basic-config/agent-config/global/tool-templates/CLAUDE.md.njk +1 -0
  31. package/examples/basic-config/agent-config/global/tool-templates/claude-settings.json.njk +2 -0
  32. package/examples/basic-config/agent-config/global/tool-templates/codex-config.toml.njk +2 -1
  33. package/examples/basic-config/agent-config/global/tool-templates/gemini-settings.json.njk +5 -0
  34. package/examples/basic-config/agent-config/global/tool-templates/grok-config.toml.njk +1 -0
  35. package/examples/basic-config/agent-config/starter/basic-project/agent-run.jsonc +20 -0
  36. package/examples/basic-config/agent-config/starter/basic-project/notes/memory/README.md +8 -0
  37. package/examples/basic-config/agent-config/starter/basic-project/overrides/claude-settings.json.njk +2 -0
  38. package/examples/basic-config/agent-config/starter/basic-project/overrides/codex-config.toml.njk +2 -1
  39. package/ops/systemd/ai-tools-update.service +1 -1
  40. package/package.json +1 -1
  41. package/scripts/install-systemd-jobs.sh +15 -5
  42. package/scripts/update-ai-tools.sh +27 -16
  43. package/dist/tools.js +0 -119
package/dist/renderer.js CHANGED
@@ -4,8 +4,10 @@ exports.renderProfile = renderProfile;
4
4
  exports.syncAgentProfile = syncAgentProfile;
5
5
  exports.checkRenderedProfile = checkRenderedProfile;
6
6
  const fs = require("fs");
7
- const os = require("os");
8
7
  const path = require("path");
8
+ const registry_1 = require("./agents/registry");
9
+ const render_instructions_1 = require("./config/render-instructions");
10
+ const render_skills_1 = require("./config/render-skills");
9
11
  const constants_1 = require("./constants");
10
12
  const config_tree_1 = require("./config-tree");
11
13
  const defaults_1 = require("./defaults");
@@ -24,10 +26,32 @@ function renderProfile(projectRoot, agentDir, checkOnly = false, targetTool = nu
24
26
  (0, config_tree_1.ensureConfigRootGitignore)(configRoot);
25
27
  }
26
28
  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 };
29
+ const canonicalInstructions = (0, render_instructions_1.buildCanonicalInstructions)(env, configRoot, manifest, context, trace);
30
+ context.renderedAgentSections = canonicalInstructions.sections;
31
+ context.skills = (0, render_skills_1.buildCanonicalSkills)(env, configRoot, manifest, context, trace);
32
+ const adapters = (0, registry_1.enabledAgentAdapters)(context).filter((adapter) => targetTool === null || adapter.id === targetTool);
33
+ const agentsMd = adapters.some((adapter) => adapter.id !== 'claude')
34
+ ? (0, render_instructions_1.writeAgentsMd)(env, configRoot, context, trace)
35
+ : '';
36
+ const claudeMd = adapters.some((adapter) => adapter.id === 'claude')
37
+ ? (0, render_instructions_1.writeClaudeMd)(env, configRoot, context, trace)
38
+ : '';
39
+ const runtimes = {};
40
+ for (const adapter of adapters) {
41
+ runtimes[adapter.id] = adapter.generate({
42
+ context,
43
+ configRoot,
44
+ env,
45
+ trace,
46
+ agentsMd,
47
+ claudeMd
48
+ });
49
+ }
50
+ const files = [
51
+ ...Object.values(runtimes).flatMap((runtime) => runtime?.files ?? []),
52
+ ...(0, guards_1.renderGuardShims)(context)
53
+ ];
54
+ return { agentDir, configRoot, profile, context, files, skills: context.skills, runtimes };
31
55
  }
32
56
  function validateManifestProfile(manifestProfile, inferredProfile, agentDir) {
33
57
  if (manifestProfile === inferredProfile) {
@@ -39,58 +63,9 @@ function validateManifestProfile(manifestProfile, inferredProfile, agentDir) {
39
63
  }
40
64
  throw new Error(`${path.join(agentDir, constants_1.MANIFEST_FILE_NAME)} profile must match inferred profile ${inferredProfile}`);
41
65
  }
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
66
  function syncAgentProfile(projectRoot, agentDir, options) {
92
67
  const rendered = renderProfile(projectRoot, agentDir, false, null, undefined, options);
93
- syncRuntimeDirs(rendered.context);
68
+ syncRuntimeDirs(rendered);
94
69
  removeStaleGeneratedEntries(rendered);
95
70
  for (const file of rendered.files) {
96
71
  writeGeneratedFile(file.path, file.content, file.executable ?? false);
@@ -99,6 +74,33 @@ function syncAgentProfile(projectRoot, agentDir, options) {
99
74
  removeLegacyGeneratedClaudeSettings(rendered.context);
100
75
  return rendered;
101
76
  }
77
+ function syncRuntimeDirs(rendered) {
78
+ const context = rendered.context;
79
+ const runtimeDirs = Object.values(rendered.runtimes).flatMap((runtime) => runtime?.requiredDirs ?? []);
80
+ for (const dir of [
81
+ context.paths.reviewDir,
82
+ context.paths.projectMemoryDir,
83
+ context.paths.memoriesDir,
84
+ context.paths.liveDir,
85
+ context.paths.binDir,
86
+ ...runtimeDirs
87
+ ]) {
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ }
90
+ const projectMemoryIndex = path.join(context.paths.projectMemoryDir, 'README.md');
91
+ if (!fs.existsSync(projectMemoryIndex)) {
92
+ fs.writeFileSync(projectMemoryIndex, (0, defaults_1.defaultProjectMemoryIndex)(), 'utf8');
93
+ (0, utils_1.verbose)(`created ${projectMemoryIndex}`);
94
+ }
95
+ migrateLiveReviewFiles(context);
96
+ removeLegacyCodexSkillDirs(context);
97
+ removeLegacyCodeReviewSkillDirs(context);
98
+ for (const runtime of Object.values(rendered.runtimes)) {
99
+ if (runtime) {
100
+ (0, registry_1.getAgentAdapter)(runtime.id).prepare?.(runtime);
101
+ }
102
+ }
103
+ }
102
104
  function writeGeneratedFile(filePath, content, executable) {
103
105
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
104
106
  const unchanged = fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content;
@@ -116,24 +118,24 @@ function writeGeneratedFile(filePath, content, executable) {
116
118
  }
117
119
  function removeStaleGeneratedEntries(rendered) {
118
120
  const expectedFiles = new Set(rendered.files.map((file) => path.resolve(file.path)));
119
- const context = rendered.context;
120
- const generatedCandidates = [
121
- path.join(context.paths.codexHomeDir, 'AGENTS.md'),
122
- path.join(context.paths.codexHomeDir, 'config.toml'),
123
- path.join(context.paths.liveDir, 'CLAUDE.md'),
124
- path.join(context.paths.liveDir, '.claude', 'CLAUDE.md'),
125
- path.join(context.paths.liveDir, '.claude', 'agent-run-settings.json'),
126
- path.join(context.paths.liveDir, '.claude', '.claude-plugin', 'plugin.json'),
127
- ...['git', 'git.cmd', 'npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'gh', 'gh.cmd'].map((name) => path.join(context.paths.binDir, name))
128
- ];
129
- for (const filePath of generatedCandidates) {
121
+ for (const adapter of (0, registry_1.listAgentAdapters)()) {
122
+ const agentLayout = adapter.layout(rendered.context);
123
+ for (const filePath of [agentLayout.instructionFile, ...agentLayout.configFiles]) {
124
+ if (!expectedFiles.has(path.resolve(filePath))) {
125
+ fs.rmSync(filePath, { force: true });
126
+ }
127
+ }
128
+ const expectedSkills = rendered.context.tools[adapter.id]
129
+ ? new Set(rendered.skills.map((skill) => skill.name))
130
+ : new Set();
131
+ removeStaleGeneratedSkills(agentLayout.skillsDir, expectedSkills, agentLayout.preservedSkillNames ?? new Set());
132
+ }
133
+ for (const name of ['git', 'git.cmd', 'npm', 'npm.cmd', 'pnpm', 'pnpm.cmd', 'gh', 'gh.cmd']) {
134
+ const filePath = path.join(rendered.context.paths.binDir, name);
130
135
  if (!expectedFiles.has(path.resolve(filePath))) {
131
136
  fs.rmSync(filePath, { force: true });
132
137
  }
133
138
  }
134
- const skillNames = new Set(rendered.skills.map((skill) => skill.name));
135
- removeStaleGeneratedSkills(context.paths.codexSkillsDir, context.tools.codex ? skillNames : new Set(), new Set(['.system']));
136
- removeStaleGeneratedSkills(context.paths.claudeSkillsDir, context.tools.claude ? skillNames : new Set());
137
139
  }
138
140
  function removeStaleGeneratedSkills(dir, expectedNames, preservedNames = new Set()) {
139
141
  if (!fs.existsSync(dir)) {
@@ -146,10 +148,7 @@ function removeStaleGeneratedSkills(dir, expectedNames, preservedNames = new Set
146
148
  }
147
149
  }
148
150
  function removeLegacyGeneratedCodexFiles(context) {
149
- for (const legacyPath of [
150
- path.join(context.paths.liveDir, 'AGENTS.md'),
151
- path.join(context.paths.liveDir, 'config.toml')
152
- ]) {
151
+ for (const legacyPath of [path.join(context.paths.liveDir, 'AGENTS.md'), path.join(context.paths.liveDir, 'config.toml')]) {
153
152
  if (fs.existsSync(legacyPath)) {
154
153
  fs.rmSync(legacyPath);
155
154
  (0, utils_1.verbose)(`removed legacy generated Codex file ${legacyPath}`);
@@ -181,9 +180,9 @@ function checkRenderedProfile(projectRoot, agentDir) {
181
180
  try {
182
181
  const rendered = renderProfile(projectRoot, agentDir, true);
183
182
  checkRenderedFiles(rendered, findings);
184
- checkRuntimeDirectories(rendered.context, findings);
183
+ checkRuntimeDirectories(rendered, findings);
185
184
  for (const skill of rendered.skills) {
186
- validateRenderedSkill(skill.renderedContent, skill.sourcePath);
185
+ (0, render_skills_1.validateRenderedSkill)(skill.renderedContent, skill.sourcePath);
187
186
  }
188
187
  }
189
188
  catch (error) {
@@ -207,15 +206,16 @@ function checkRenderedFiles(rendered, findings) {
207
206
  }
208
207
  }
209
208
  }
210
- function checkRuntimeDirectories(context, findings) {
209
+ function checkRuntimeDirectories(rendered, findings) {
210
+ const context = rendered.context;
211
+ const runtimeDirs = Object.values(rendered.runtimes).flatMap((runtime) => runtime?.requiredDirs ?? []);
211
212
  for (const dir of [
212
213
  context.paths.reviewDir,
214
+ context.paths.projectMemoryDir,
213
215
  context.paths.memoriesDir,
214
- context.paths.codexHomeDir,
215
216
  context.paths.liveDir,
216
- context.paths.codexSkillsDir,
217
- context.paths.claudeSkillsDir,
218
- context.paths.binDir
217
+ context.paths.binDir,
218
+ ...runtimeDirs
219
219
  ]) {
220
220
  if (!fs.existsSync(dir)) {
221
221
  findings.push({ message: `missing generated runtime directory: ${dir}`, severity: 'ERROR' });
@@ -235,127 +235,6 @@ function checkConfigGitignore(configRoot, findings) {
235
235
  }
236
236
  }
237
237
  }
238
- function renderSkills(env, configRoot, manifest, context, trace) {
239
- return manifest.skills.install.map((name) => {
240
- const sourceTemplate = manifest.skills.overrides[name] ?? `global/skills/${name}/SKILL.md.njk`;
241
- const sourcePath = (0, templates_1.resolveConfigPath)(configRoot, sourceTemplate, context);
242
- if (!fs.existsSync(sourcePath)) {
243
- throw new Error(`missing skill template for ${name}: ${sourcePath}`);
244
- }
245
- const renderedContent = (0, templates_1.renderTemplateFile)(env, configRoot, sourceTemplate, context, trace);
246
- (0, templates_1.assertNoUnexpandedTemplateVars)(`skill ${name}`, renderedContent);
247
- validateRenderedSkill(renderedContent, sourcePath);
248
- return { name, sourcePath, renderedContent, description: extractSkillDescription(renderedContent) };
249
- });
250
- }
251
- function renderNativeSkillFiles(context, targetTool) {
252
- const targetDir = targetTool === 'codex' ? context.paths.codexSkillsDir : context.paths.claudeSkillsDir;
253
- return context.skills.map((skill) => ({
254
- path: path.join(targetDir, skill.name, 'SKILL.md'),
255
- content: skill.renderedContent
256
- }));
257
- }
258
- function renderToolInstructions(templateName, env, configRoot, context, isClaude, trace) {
259
- const templatePath = `global/tool-templates/${templateName}.njk`;
260
- const content = fs.existsSync(path.join(configRoot, templatePath))
261
- ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
262
- : env.renderString((0, defaults_1.defaultToolInstructionsTemplate)(isClaude), context);
263
- (0, templates_1.assertNoUnexpandedTemplateVars)(templateName, content);
264
- return content.replace(/\n*$/, '\n');
265
- }
266
- function renderCodexConfig(env, configRoot, context, trace) {
267
- const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'codex-config.toml.njk', 'global/tool-templates/codex-config.toml.njk');
268
- const content = templatePath
269
- ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
270
- : (0, defaults_1.defaultCodexConfigContent)(context);
271
- (0, templates_1.assertNoUnexpandedTemplateVars)('config.toml', content);
272
- return content.replace(/\n*$/, '\n');
273
- }
274
- function renderClaudeSettings(env, configRoot, context, trace) {
275
- const templatePath = resolveProfileOverrideTemplate(configRoot, context, 'claude-settings.json.njk', 'global/tool-templates/claude-settings.json.njk');
276
- const content = templatePath
277
- ? (0, templates_1.renderTemplateFile)(env, configRoot, templatePath, context, trace)
278
- : (0, defaults_1.defaultClaudeSettingsContent)(context);
279
- (0, templates_1.assertNoUnexpandedTemplateVars)('claude settings.json', content);
280
- try {
281
- JSON.parse(content);
282
- }
283
- catch (error) {
284
- throw new Error(`invalid generated Claude settings JSON: ${(0, utils_1.formatError)(error)}`);
285
- }
286
- return content.replace(/\n*$/, '\n');
287
- }
288
- function claudePluginManifestContent(context) {
289
- return `${JSON.stringify({
290
- name: 'agent-run-profile',
291
- description: `Generated skills for agent-run profile ${context.profile}`,
292
- version: '1.0.0',
293
- author: { name: 'Technomoron' }
294
- }, null, 2)}\n`;
295
- }
296
- function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, globalTemplatePath) {
297
- const overridePath = `${context.profile}/overrides/${overrideFileName}`;
298
- if (fs.existsSync(path.join(configRoot, overridePath))) {
299
- return overridePath;
300
- }
301
- return fs.existsSync(path.join(configRoot, globalTemplatePath)) ? globalTemplatePath : null;
302
- }
303
- function syncRuntimeDirs(context) {
304
- for (const dir of [
305
- context.paths.reviewDir,
306
- context.paths.memoriesDir,
307
- context.paths.codexHomeDir,
308
- context.paths.codexSkillsDir,
309
- context.paths.claudeSkillsDir,
310
- context.paths.binDir,
311
- path.join(context.paths.liveDir, '.claude')
312
- ]) {
313
- fs.mkdirSync(dir, { recursive: true });
314
- }
315
- migrateLiveReviewFiles(context);
316
- removeLegacyCodexSkillDirs(context);
317
- removeLegacyCodeReviewSkillDirs(context);
318
- ensureSharedCodexAuth(context.paths.codexHomeDir);
319
- }
320
- function ensureSharedCodexAuth(codexHomeDir) {
321
- const sharedAuthPath = path.join(os.homedir(), '.codex', 'auth.json');
322
- if (!fs.existsSync(sharedAuthPath)) {
323
- return;
324
- }
325
- const profileAuthPath = path.join(codexHomeDir, 'auth.json');
326
- if (path.resolve(profileAuthPath) === path.resolve(sharedAuthPath) || isSymlinkTo(profileAuthPath, sharedAuthPath)) {
327
- return;
328
- }
329
- if (fs.existsSync(profileAuthPath) || (0, utils_1.isSymlink)(profileAuthPath)) {
330
- const backupPath = (0, utils_1.nextBackupPath)(profileAuthPath);
331
- fs.renameSync(profileAuthPath, backupPath);
332
- (0, utils_1.verbose)(`backed up profile Codex auth ${profileAuthPath} -> ${backupPath}`);
333
- }
334
- fs.mkdirSync(path.dirname(profileAuthPath), { recursive: true });
335
- try {
336
- fs.symlinkSync(sharedAuthPath, profileAuthPath);
337
- (0, utils_1.verbose)(`linked profile Codex auth ${profileAuthPath} -> ${sharedAuthPath}`);
338
- }
339
- catch (error) {
340
- if (!constants_1.IS_WINDOWS) {
341
- throw error;
342
- }
343
- fs.copyFileSync(sharedAuthPath, profileAuthPath);
344
- (0, utils_1.verbose)(`copied shared Codex auth ${sharedAuthPath} -> ${profileAuthPath}`);
345
- }
346
- }
347
- function isSymlinkTo(filePath, targetPath) {
348
- try {
349
- if (!fs.lstatSync(filePath).isSymbolicLink()) {
350
- return false;
351
- }
352
- const linkTarget = fs.readlinkSync(filePath);
353
- return path.resolve(path.dirname(filePath), linkTarget) === path.resolve(targetPath);
354
- }
355
- catch {
356
- return false;
357
- }
358
- }
359
238
  function removeLegacyCodexSkillDirs(context) {
360
239
  const legacyCodexDir = path.join(context.paths.liveDir, '.agents');
361
240
  if (!(0, utils_1.isSamePathOrDescendant)(context.paths.codexSkillsDir, legacyCodexDir)) {
@@ -368,8 +247,7 @@ function removeLegacyCodeReviewSkillDirs(context) {
368
247
  }
369
248
  for (const dir of [
370
249
  path.join(context.paths.liveDir, '.agents', 'skills', 'code-review'),
371
- path.join(context.paths.codexSkillsDir, 'code-review'),
372
- path.join(context.paths.claudeSkillsDir, 'code-review')
250
+ ...(0, registry_1.listAgentAdapters)().map((adapter) => path.join(adapter.layout(context).skillsDir, 'code-review'))
373
251
  ]) {
374
252
  fs.rmSync(dir, { recursive: true, force: true });
375
253
  }
@@ -422,27 +300,3 @@ function moveFile(sourcePath, targetPath) {
422
300
  fs.rmSync(sourcePath);
423
301
  }
424
302
  }
425
- function validateRenderedSkill(content, sourcePath) {
426
- const frontMatter = /^---\n([\s\S]*?)\n---\n/.exec(content.replace(/\r\n/g, '\n'));
427
- if (frontMatter === null) {
428
- throw new Error(`generated skill missing YAML front matter: ${sourcePath}`);
429
- }
430
- const yaml = frontMatter[1] ?? '';
431
- if (!/^name:\s*\S+/m.test(yaml)) {
432
- throw new Error(`generated skill missing name: ${sourcePath}`);
433
- }
434
- if (!/^description:\s*(?:\S|>\s*$)/m.test(yaml)) {
435
- throw new Error(`generated skill missing description: ${sourcePath}`);
436
- }
437
- }
438
- function extractSkillDescription(content) {
439
- const normalized = content.replace(/\r\n/g, '\n');
440
- const simple = /^description:\s*['"]?(.+?)['"]?\s*$/m.exec(normalized);
441
- if (simple?.[1]) {
442
- return simple[1].trim();
443
- }
444
- const folded = /^description:\s*>\s*\n((?:[ \t]+.+\n?)+)/m.exec(normalized);
445
- return folded?.[1]
446
- ? folded[1].split('\n').map((line) => line.trim()).filter(Boolean).join(' ')
447
- : '';
448
- }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAgentEnvironment = buildAgentEnvironment;
4
+ const path = require("path");
5
+ function buildAgentEnvironment(context) {
6
+ const realPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
7
+ return {
8
+ ...process.env,
9
+ AGENT_DIR: context.paths.liveDir,
10
+ AGENT_PROFILE_DIR: context.profileDir,
11
+ AGENT_RUN_PROJECT_ROOT: context.projectRoot,
12
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
13
+ AGENT_GLOBAL_MEMORY_DIR: context.paths.globalMemoryDir,
14
+ AGENT_RUN_REAL_PATH: realPath,
15
+ PATH: `${context.paths.binDir}${path.delimiter}${realPath}`
16
+ };
17
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postflightProjectCheck = postflightProjectCheck;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const project_1 = require("../project");
7
+ function postflightProjectCheck(context, allowLocal, code) {
8
+ if (!context.guardrails.forbidRepoAiFiles) {
9
+ return code;
10
+ }
11
+ if (!allowLocal) {
12
+ removeClaudeLocalSettings(context.projectRoot);
13
+ }
14
+ const localAiFiles = (0, project_1.findLocalAiFiles)(context.projectRoot, context.profileDir);
15
+ if (localAiFiles.length === 0) {
16
+ return code;
17
+ }
18
+ (0, project_1.warnForLocalAiFiles)(null, context.projectRoot, localAiFiles);
19
+ return allowLocal ? code : 1;
20
+ }
21
+ function removeClaudeLocalSettings(projectRoot) {
22
+ const claudeDir = path.join(projectRoot, '.claude');
23
+ const localSettingsPath = path.join(claudeDir, 'settings.local.json');
24
+ if (!fs.existsSync(localSettingsPath)) {
25
+ return;
26
+ }
27
+ fs.rmSync(localSettingsPath);
28
+ try {
29
+ fs.rmdirSync(claudeDir);
30
+ }
31
+ catch (error) {
32
+ if (!isDirectoryNotEmptyError(error)) {
33
+ throw error;
34
+ }
35
+ }
36
+ process.stderr.write(`agent-run: removed Claude's project-local settings: ${localSettingsPath}\n`);
37
+ }
38
+ function isDirectoryNotEmptyError(error) {
39
+ return error instanceof Error && 'code' in error && error.code === 'ENOTEMPTY';
40
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.linkSharedEntry = linkSharedEntry;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const constants_1 = require("../constants");
7
+ const utils_1 = require("../utils");
8
+ function linkSharedEntry(sourcePath, targetPath, label) {
9
+ if (!fs.existsSync(sourcePath) && !(0, utils_1.isSymlink)(sourcePath)) {
10
+ return;
11
+ }
12
+ if (path.resolve(sourcePath) === path.resolve(targetPath) || isSymlinkTo(targetPath, sourcePath)) {
13
+ return;
14
+ }
15
+ if (fs.existsSync(targetPath) || (0, utils_1.isSymlink)(targetPath)) {
16
+ const backupPath = (0, utils_1.nextBackupPath)(targetPath);
17
+ fs.renameSync(targetPath, backupPath);
18
+ (0, utils_1.verbose)(`backed up ${label} ${targetPath} -> ${backupPath}`);
19
+ }
20
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
21
+ try {
22
+ fs.symlinkSync(sourcePath, targetPath, fs.statSync(sourcePath).isDirectory() ? 'junction' : 'file');
23
+ (0, utils_1.verbose)(`linked ${label} ${targetPath} -> ${sourcePath}`);
24
+ }
25
+ catch (error) {
26
+ if (!constants_1.IS_WINDOWS || fs.statSync(sourcePath).isDirectory()) {
27
+ throw error;
28
+ }
29
+ fs.copyFileSync(sourcePath, targetPath);
30
+ (0, utils_1.verbose)(`copied ${label} ${sourcePath} -> ${targetPath}`);
31
+ }
32
+ }
33
+ function isSymlinkTo(filePath, targetPath) {
34
+ try {
35
+ if (!fs.lstatSync(filePath).isSymbolicLink()) {
36
+ return false;
37
+ }
38
+ const linkTarget = fs.readlinkSync(filePath);
39
+ return path.resolve(path.dirname(filePath), linkTarget) === path.resolve(targetPath);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawnAgent = spawnAgent;
4
+ const process_1 = require("../process");
5
+ function spawnAgent(spec) {
6
+ (0, process_1.execCommand)(spec.command, spec.args, spec.env, spec.cwd, spec.onExit);
7
+ }
package/dist/templates.js CHANGED
@@ -31,11 +31,16 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
31
31
  date,
32
32
  checks: manifest.checks,
33
33
  tools: manifest.tools,
34
+ mcpServers: manifest.mcpServers,
34
35
  guardrails: manifest.guardrails
35
36
  };
36
37
  const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
37
38
  const memoriesDir = resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext);
39
+ const projectMemoryDir = resolveRuntimePath(configRoot, manifest.paths.projectMemoryDir, env, baseContext);
38
40
  const codexHomeDir = path.join(memoriesDir, 'codex-home');
41
+ const geminiRuntimeDir = path.join(liveDir, 'gemini');
42
+ const geminiHomeDir = path.join(geminiRuntimeDir, 'home');
43
+ const grokRuntimeDir = path.join(liveDir, 'grok');
39
44
  const paths = {
40
45
  profileDir,
41
46
  liveDir,
@@ -47,10 +52,17 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
47
52
  : path.join(reviewDir, 'REVIEW.md'),
48
53
  memoriesDir,
49
54
  globalMemoryDir: path.join(configRoot, 'notes', 'memory'),
55
+ projectMemoryDir,
56
+ nativeMemoryDir: path.join(codexHomeDir, 'memories'),
50
57
  codexHomeDir,
51
58
  overridesDir: path.join(profileDir, 'overrides'),
52
59
  codexSkillsDir: path.join(codexHomeDir, 'skills'),
53
60
  claudeSkillsDir: path.join(liveDir, '.claude', 'skills'),
61
+ geminiRuntimeDir,
62
+ geminiHomeDir,
63
+ geminiSkillsDir: path.join(geminiHomeDir, '.agents', 'skills'),
64
+ grokRuntimeDir,
65
+ grokSkillsDir: path.join(grokRuntimeDir, 'skills'),
54
66
  binDir: path.join(liveDir, 'bin')
55
67
  };
56
68
  return {
@@ -9,7 +9,8 @@ It contains two pieces:
9
9
  config root.
10
10
  - `agent-config/`: a complete config root with shared manifest defaults, global
11
11
  templates, snippets, skill templates, a personal memory skill, a sparse
12
- profile manifest, profile-local instructions, and profile overrides.
12
+ profile manifest, durable project memory, profile-local instructions, and
13
+ profile overrides.
13
14
 
14
15
  Try it from this repository after building:
15
16
 
@@ -17,7 +17,12 @@
17
17
  },
18
18
  "tools": {
19
19
  "codex": true,
20
- "claude": true
20
+ "claude": true,
21
+ "gemini": true,
22
+ "grok": true
23
+ },
24
+ "mcp": {
25
+ "servers": {}
21
26
  },
22
27
  "checks": [
23
28
  "agent-run check .",
@@ -33,6 +38,7 @@
33
38
  "changesFile": "{{ projectRoot }}/CHANGES",
34
39
  "reviewDir": "{{ profileDir }}/reviews",
35
40
  "reviewFile": "{{ profileDir }}/reviews/REVIEW-{{ date }}.md",
36
- "memoriesDir": "{{ agentDir }}/memories"
41
+ "memoriesDir": "{{ agentDir }}/memories",
42
+ "projectMemoryDir": "{{ profileDir }}/notes/memory"
37
43
  }
38
44
  }
@@ -14,6 +14,8 @@ Do not edit this generated file. Edit root defaults or profile-specific source f
14
14
  - reviewFile: `{{ paths.reviewFile }}`
15
15
  - reviewConsolidatedFile: `{{ paths.reviewConsolidatedFile }}`
16
16
  - memoriesDir: `{{ paths.memoriesDir }}`
17
+ - nativeMemoryDir: `{{ paths.nativeMemoryDir }}`
18
+ - projectMemoryDir: `{{ paths.projectMemoryDir }}`
17
19
  - globalMemoryDir: `{{ paths.globalMemoryDir }}`
18
20
 
19
21
  ## Skills
@@ -6,4 +6,5 @@
6
6
  {% endfor %}
7
7
  Claude reads this file from the private agent directory.
8
8
 
9
+ projectMemoryDir: `{{ paths.projectMemoryDir }}`
9
10
  globalMemoryDir: `{{ paths.globalMemoryDir }}`
@@ -3,6 +3,7 @@
3
3
  "AGENT_DIR": {{ agentDir | dump }},
4
4
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
5
5
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
6
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
6
7
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
7
8
  "STARTER_PROFILE": {{ profile | dump }}
8
9
  },
@@ -15,6 +16,7 @@
15
16
  "AGENT_DIR": {{ agentDir | dump }},
16
17
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
17
18
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
19
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
18
20
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
19
21
  "STARTER_PROFILE": {{ profile | dump }}
20
22
  }
@@ -7,6 +7,7 @@ sandbox_mode = "workspace-write"
7
7
  writable_roots = [
8
8
  {{ projectRoot | dump }},
9
9
  {{ paths.reviewDir | dump }},
10
- {{ paths.memoriesDir | dump }}
10
+ {{ paths.memoriesDir | dump }},
11
+ {{ paths.projectMemoryDir | dump }}
11
12
  ]
12
13
  network_access = false
@@ -0,0 +1,5 @@
1
+ {
2
+ "general": {
3
+ "enableAutoUpdate": true
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ # Generated by agent-run. Do not edit directly.
@@ -17,6 +17,26 @@
17
17
  "triage": "{{ profile }}/overrides/triage.SKILL.md.njk"
18
18
  }
19
19
  },
20
+ "mcp": {
21
+ "servers": {
22
+ "example-stdio": {
23
+ "command": "node",
24
+ "args": ["tools/example-mcp.js"],
25
+ "env": {
26
+ "EXAMPLE_TOKEN": "${EXAMPLE_TOKEN}"
27
+ },
28
+ "enabled": false
29
+ },
30
+ "example-http": {
31
+ "transport": "http",
32
+ "url": "https://example.invalid/mcp",
33
+ "headers": {
34
+ "Authorization": "Bearer ${EXAMPLE_TOKEN}"
35
+ },
36
+ "enabled": false
37
+ }
38
+ }
39
+ },
20
40
  "checks": [
21
41
  "agent-run check .",
22
42
  "pnpm test"
@@ -0,0 +1,8 @@
1
+ # Project memory
2
+
3
+ This directory contains durable notes for this project across agent sessions and machines.
4
+ Keep the notes short, factual, and free of secrets. Link additional topic files from this index.
5
+
6
+ ## Files
7
+
8
+ - Add project memory files here as they are needed.