@technomoron/agent-run 0.99.28 → 0.99.29

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 CHANGED
@@ -1,6 +1,19 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ Version 0.99.29 (2026-09-04)
5
+ ----------------------------
6
+ * Add tracked per-profile project memory under `notes/memory`, while keeping
7
+ native Codex memories in ignored machine-local runtime state.
8
+ * Detect older memory layouts before profile updates and agent launches, ask
9
+ before staging tracked moves with `git mv`, and support
10
+ `migrate-config --yes` for non-interactive migrations.
11
+ * Make copied systemd updater files portable, install the helper under
12
+ `/usr/local/libexec`, and remove stale AI tool launchers before global
13
+ package updates.
14
+ * Explain the shared configuration model for native coding agents in the
15
+ README.
16
+
4
17
  Version 0.99.28 (2026-08-30)
5
18
  ----------------------------
6
19
  * Add interactive `agent-run setup [org/repo]`, make `agent-run generate` the
package/README.md CHANGED
@@ -1,9 +1,46 @@
1
1
  # agent-run
2
2
 
3
- Small wrapper for AI coding CLIs like Codex and Claude.
3
+ **Config once. Run with any native coding agent.**
4
4
 
5
- It keeps agent files out of normal repos and stores them in a separate
6
- agent config tree. By default, that tree lives at `~/.agent-run`.
5
+ `agent-run` lets you manage Codex, Claude Code, Gemini CLI, and Grok Build from
6
+ one shared configuration system while continuing to use each vendor's native
7
+ agent.
8
+
9
+ Keep agent configuration in a separate Git repository instead of adding
10
+ agent-specific files to every codebase. Define project instructions, skills,
11
+ MCP servers, policies, and reusable configuration fragments once, then generate
12
+ the native configuration each agent expects.
13
+
14
+ Configuration is template-based using Nunjucks/Jinja-style templates, making it
15
+ easy to build hierarchical setups from global defaults, organization rules,
16
+ project-specific settings, reusable fragments, and local overrides.
17
+
18
+ `agent-run` generates isolated agent runtimes outside the source repository, so
19
+ application repositories remain clean and developers do not have to maintain
20
+ separate `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, skills, and tool configuration
21
+ for every agent.
22
+
23
+ The core idea is simple:
24
+
25
+ **One configuration repository. One set of skills. Multiple native agents.**
26
+
27
+ ```text
28
+ shared config repository
29
+ ↓
30
+ global + project + local templates
31
+ ↓
32
+ canonical instructions / skills / MCP
33
+ ↓
34
+ agent-specific generated runtime
35
+ ↓
36
+ Codex | Claude Code | Gemini CLI | Grok Build
37
+ ```
38
+
39
+ The native agents remain native. `agent-run` does not replace their execution
40
+ engines, authentication, models, or tooling. It provides the portable
41
+ configuration layer around them.
42
+
43
+ **Config once, run anywhere.**
7
44
 
8
45
  ## Model
9
46
 
@@ -23,6 +60,9 @@ Example:
23
60
  agent-run.jsonc
24
61
  local.md.njk # optional
25
62
  overrides/ # optional
63
+ notes/
64
+ memory/
65
+ README.md # durable project memory, normally tracked
26
66
  live/
27
67
  CLAUDE.md
28
68
  .claude/
@@ -31,6 +71,7 @@ Example:
31
71
  codex-home/
32
72
  AGENTS.md
33
73
  config.toml
74
+ memories/ # native Codex memory, local generated state
34
75
  skills/
35
76
  ~/source/org/my-api/
36
77
  ```
@@ -50,6 +91,7 @@ the agent config tree.
50
91
  - `agent-run.jsonc`: sparse profile manifest and profile-discovery marker
51
92
  - `local.md.njk`: optional project-specific instructions
52
93
  - `overrides/`: optional per-profile template overrides
94
+ - `notes/memory/`: durable project memory shared through the config repository
53
95
  - `live/memories/codex-home/AGENTS.md`: generated Codex instructions
54
96
  - `live/memories/codex-home/config.toml`: generated Codex config
55
97
  - `live/memories/codex-home/skills/`: generated Codex skills
@@ -152,6 +194,26 @@ When `~/.codex/auth.json` exists, profile-specific Codex homes link their
152
194
  `auth.json` to that shared login cache so changing profiles does not require a
153
195
  new ChatGPT login.
154
196
 
197
+ ### Project Memory
198
+
199
+ Each profile has a durable `notes/memory/` directory outside the source repo.
200
+ `README.md` is a short index; agents read only the linked files relevant to the
201
+ current task. Agents may update project memory only when the user explicitly
202
+ asks them to remember or update something for that project.
203
+
204
+ Codex's native `$CODEX_HOME/memories/` remains under ignored `live/` state. It
205
+ is generated, machine-local recall data and is not copied into the tracked
206
+ project memory directory. Project memory is plain Markdown so it can be
207
+ reviewed and shared through the Git repository that normally holds the
208
+ `.agent-run` config tree.
209
+
210
+ When a profile still uses `memory/`, loose `memory*.md` files, or Markdown files
211
+ directly under the old `memories/` directory, agent-run detects the old layout
212
+ before updating or launching an agent. If those files are tracked, agent-run
213
+ shows the moves and asks before staging them with `git mv`. In a non-Git config
214
+ tree, or for untracked files, it uses ordinary filesystem moves. Non-interactive
215
+ runs stop with a command that can confirm the migration explicitly.
216
+
155
217
  Claude Code currently has no `--cd` equivalent. `agent-run` keeps Claude's
156
218
  process cwd at the project root, appends the generated `CLAUDE.md` with
157
219
  `--append-system-prompt-file`, passes generated settings with `--settings`, and
@@ -287,14 +349,17 @@ agent-run check --all ~/source
287
349
  Migrate an existing config tree to the manifest/template layout:
288
350
 
289
351
  ```sh
290
- agent-run migrate-config ~/.agent-run
352
+ agent-run migrate-config --yes ~/.agent-run
291
353
  ```
292
354
 
293
355
  This preserves `AGENTS-MODS.md`, creates `local.md.njk` and a sparse
294
356
  `agent-run.jsonc` marker for legacy profiles, creates the root defaults and
295
357
  `global/` templates, moves loose review files into `reviews/`, moves loose
296
- `memory*.md` files into `memories/`, and moves old Codex runtime files into
297
- `live/memories/codex-home` without overwriting existing files.
358
+ project memory files into each profile's `notes/memory/`, and moves old Codex
359
+ runtime files into `live/memories/codex-home` without overwriting existing
360
+ files. Use `--yes` to confirm tracked `git mv` operations in a non-interactive
361
+ run. Omit `--yes` to review and confirm tracked moves interactively. The
362
+ command stages moves but never commits them.
298
363
 
299
364
  ## Systemd Jobs
300
365
 
package/dist/checks.js CHANGED
@@ -8,6 +8,7 @@ exports.hasErrors = hasErrors;
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
10
  const constants_1 = require("./constants");
11
+ const config_tree_1 = require("./config-tree");
11
12
  const manifest_1 = require("./manifest");
12
13
  const project_1 = require("./project");
13
14
  const renderer_1 = require("./renderer");
@@ -47,6 +48,9 @@ function checkAgentDirectory(projectRoot, agentDir) {
47
48
  if (!(0, manifest_1.isProfileConfigured)(agentDir)) {
48
49
  return [{ message: `missing manifest or legacy/local source file in ${agentDir}`, severity: 'ERROR' }];
49
50
  }
51
+ for (const legacyPath of (0, config_tree_1.describeLegacyProfileLayout)(agentDir)) {
52
+ findings.push({ message: `old profile layout needs migration: ${legacyPath}`, severity: 'ERROR' });
53
+ }
50
54
  for (const relativeTemplate of constants_1.REQUIRED_GLOBAL_TEMPLATES) {
51
55
  if (!fs.existsSync(path.join(configRoot, relativeTemplate))) {
52
56
  findings.push({
package/dist/cli.js CHANGED
@@ -254,16 +254,22 @@ function parseUpdateCommand(inputArgs) {
254
254
  }
255
255
  function parseMigrateConfigCommand(inputArgs) {
256
256
  let configRoot = (0, project_1.defaultConfigRoot)();
257
+ let yes = false;
257
258
  for (const arg of inputArgs) {
258
259
  if (isHelpFlag(arg)) {
259
260
  printHelp('migrate-config');
260
261
  }
261
- if (arg.startsWith('--')) {
262
+ if (arg === '--yes') {
263
+ yes = true;
264
+ }
265
+ else if (arg.startsWith('--')) {
262
266
  (0, utils_1.fail)(`unknown migrate-config option: ${arg}`);
263
267
  }
264
- configRoot = arg;
268
+ else {
269
+ configRoot = arg;
270
+ }
265
271
  }
266
- return { command: 'migrate-config', configRoot: path.resolve(configRoot) };
272
+ return { command: 'migrate-config', configRoot: path.resolve(configRoot), yes };
267
273
  }
268
274
  function isHelpFlag(value) {
269
275
  return value === '-h' || value === '--help';
@@ -306,9 +312,12 @@ function renderHelp(topic) {
306
312
  ],
307
313
  'migrate-config': [
308
314
  'Usage:',
309
- ' agent-run migrate-config [config-root]',
315
+ ' agent-run migrate-config [--yes] [config-root]',
310
316
  '',
311
317
  'Convert an existing agent config tree to the current manifest and global-template layout.',
318
+ '',
319
+ 'Options:',
320
+ ' --yes Confirm tracked Git moves without prompting',
312
321
  ''
313
322
  ]
314
323
  };
@@ -331,7 +340,7 @@ function renderHelp(topic) {
331
340
  ' generate [path] Create a sparse profile marker and render output',
332
341
  ' edit [path] Create or open local.md.njk',
333
342
  ' update [--all] [path] Regenerate existing profile output',
334
- ' migrate-config [config-root] Convert existing config tree layout',
343
+ ' migrate-config [--yes] [config-root] Convert existing config tree layout',
335
344
  '',
336
345
  'Global options:',
337
346
  ' -h, --help Show this help text',
package/dist/commands.js CHANGED
@@ -59,9 +59,9 @@ function runTool(parsed) {
59
59
  (0, utils_1.fail)(profileResult.reason);
60
60
  }
61
61
  const profile = profileResult.profile;
62
+ const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
63
+ const agentDir = path.join(configRoot, profile);
62
64
  if (wrapperArgs.show) {
63
- const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
64
- const agentDir = path.join(configRoot, profile);
65
65
  (0, utils_1.verbose)(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
66
66
  requireProfile(command, agentDir);
67
67
  showToolProfile(command, projectRoot, agentDir);
@@ -70,11 +70,10 @@ function runTool(parsed) {
70
70
  if (wrapperArgs.create) {
71
71
  runGenerate({ command: 'generate', targetPath: projectRoot });
72
72
  }
73
- const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
74
- const agentDir = path.join(configRoot, profile);
75
73
  (0, utils_1.verbose)(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
76
74
  prepareConfigRoot(configRoot);
77
75
  requireProfile(command, agentDir);
76
+ migrateProfileOnStart(configRoot, agentDir, false);
78
77
  const preview = (0, renderer_1.renderProfile)(projectRoot, agentDir, true, command);
79
78
  ensureToolEnabled(preview.context, command);
80
79
  if (wrapperArgs.generate) {
@@ -158,6 +157,7 @@ function runGenerate(parsed) {
158
157
  return;
159
158
  }
160
159
  const agentDir = initializeProfileSource(projectRoot, true);
160
+ migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
161
161
  printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir));
162
162
  }
163
163
  function runSetup(parsed) {
@@ -170,7 +170,9 @@ function runSetup(parsed) {
170
170
  (0, utils_1.fail)(`starter config skeleton not found: ${sourcePath}`);
171
171
  }
172
172
  (0, config_tree_1.copySkeletonTree)(sourcePath, configRoot, new Set(['starter']));
173
+ (0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
173
174
  const agentDir = path.join(configRoot, profile);
175
+ migrateProfileOnStart(configRoot, agentDir, false);
174
176
  (0, config_tree_1.copySkeletonTree)(path.join(sourcePath, 'starter', 'basic-project'), agentDir);
175
177
  if (profile !== detectedProfile) {
176
178
  writeProjectProfileMapping(projectRoot, profile);
@@ -193,9 +195,9 @@ function confirmProfile(detectedProfile) {
193
195
  }
194
196
  return requireValidProfile(promptLine('Profile (org/repo): '));
195
197
  }
196
- function promptLine(prompt) {
198
+ function promptLine(prompt, nonInteractiveMessage = 'setup requires a profile argument when input is not interactive') {
197
199
  if (!process.stdin.isTTY) {
198
- (0, utils_1.fail)('setup requires a profile argument when input is not interactive');
200
+ (0, utils_1.fail)(nonInteractiveMessage);
199
201
  }
200
202
  process.stdout.write(prompt);
201
203
  const bytes = [];
@@ -234,6 +236,7 @@ function runEdit(parsed) {
234
236
  return;
235
237
  }
236
238
  const agentDir = initializeProfileSource(projectRoot, false);
239
+ migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
237
240
  const editPath = (0, manifest_1.createDefaultLocalFile)(agentDir);
238
241
  (0, renderer_1.syncAgentProfile)(projectRoot, agentDir);
239
242
  process.stdout.write(`Edit: ${editPath}\n`);
@@ -254,6 +257,7 @@ function runUpdate(parsed) {
254
257
  const agentDir = (0, project_1.resolveAgentDir)(projectRoot);
255
258
  prepareConfigRoot(configRoot);
256
259
  requireProfile(null, agentDir);
260
+ migrateProfileOnStart(configRoot, agentDir, false);
257
261
  printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir));
258
262
  }
259
263
  function runUpdateAll(targetPath) {
@@ -264,6 +268,7 @@ function runUpdateAll(targetPath) {
264
268
  prepareConfigRoot(configRoot);
265
269
  const profileDirs = (0, config_tree_1.findProfileDirs)(configRoot);
266
270
  for (const agentDir of profileDirs) {
271
+ migrateProfileOnStart(configRoot, agentDir, false);
267
272
  const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
268
273
  const projectRoot = (0, project_1.projectRootForProfile)(profile, configRoot);
269
274
  (0, renderer_1.syncAgentProfile)(projectRoot, agentDir, { configRoot, profile });
@@ -281,15 +286,16 @@ function runMigrateConfig(parsed) {
281
286
  (0, config_tree_1.migrateOldTemplates)(configRoot);
282
287
  (0, config_tree_1.ensureDefaultGlobalTemplates)(configRoot);
283
288
  (0, manifest_1.ensureRootDefaultsFile)(configRoot);
284
- const profileDirs = (0, config_tree_1.findLegacyProfileDirs)(configRoot);
289
+ const profileDirs = (0, utils_1.uniqueSorted)([...(0, config_tree_1.findLegacyProfileDirs)(configRoot), ...(0, config_tree_1.findProfileDirs)(configRoot)]);
285
290
  let createdManifestCount = 0;
286
291
  let createdLocalCount = 0;
287
292
  let movedRuntimeCount = 0;
288
293
  let movedReviewCount = 0;
289
294
  let movedMemoryCount = 0;
290
295
  for (const agentDir of profileDirs) {
296
+ const layout = migrateProfileOnStart(configRoot, agentDir, parsed.yes);
291
297
  const localPath = path.join(agentDir, constants_1.LOCAL_TEMPLATE_FILE_NAME);
292
- if (!fs.existsSync(localPath)) {
298
+ if (!fs.existsSync(localPath) && fs.existsSync(path.join(agentDir, 'AGENTS-MODS.md'))) {
293
299
  fs.writeFileSync(localPath, (0, config_tree_1.convertLegacyTemplateVars)(fs.readFileSync(path.join(agentDir, 'AGENTS-MODS.md'), 'utf8')), 'utf8');
294
300
  createdLocalCount += 1;
295
301
  }
@@ -299,7 +305,7 @@ function runMigrateConfig(parsed) {
299
305
  }
300
306
  movedRuntimeCount += (0, config_tree_1.migrateCodexRuntimeFiles)(agentDir);
301
307
  movedReviewCount += (0, config_tree_1.migrateLooseFiles)(agentDir, /^REVIEW(?:-.+)?\.md$/, 'reviews');
302
- movedMemoryCount += (0, config_tree_1.migrateLooseFiles)(agentDir, /^memory.*\.md$/i, 'memories');
308
+ movedMemoryCount += layout.projectMemoryMoves;
303
309
  }
304
310
  process.stdout.write(`OK migrated ${configRoot}\n`);
305
311
  process.stdout.write(`Profiles converted: ${profileDirs.length}\n`);
@@ -309,6 +315,29 @@ function runMigrateConfig(parsed) {
309
315
  process.stdout.write(`Moved review files: ${movedReviewCount}\n`);
310
316
  process.stdout.write(`Moved memory files: ${movedMemoryCount}\n`);
311
317
  }
318
+ function migrateProfileOnStart(configRoot, profileDir, assumeYes) {
319
+ const result = (0, config_tree_1.migrateProfileLayout)(configRoot, profileDir, ({ gitRoot, moves }) => {
320
+ process.stdout.write('agent-run: this profile uses an older project-memory layout.\n');
321
+ process.stdout.write(`Git working tree: ${gitRoot}\n`);
322
+ for (const move of moves) {
323
+ process.stdout.write(` ${path.relative(gitRoot, move.source)} -> ${path.relative(gitRoot, move.target)}\n`);
324
+ }
325
+ if (assumeYes) {
326
+ return true;
327
+ }
328
+ const migrateCommand = (0, utils_1.formatCommand)('agent-run', ['migrate-config', '--yes', configRoot]);
329
+ const message = `tracked project-memory files need migration; rerun interactively or run \`${migrateCommand}\``;
330
+ const answer = promptLine('Stage these moves with git mv? [Y/n] ', message).trim().toLowerCase();
331
+ return answer === '' || answer === 'y' || answer === 'yes';
332
+ });
333
+ if (result.gitMoves > 0) {
334
+ process.stdout.write(`Staged Git moves: ${result.gitMoves}\n`);
335
+ }
336
+ if (result.runtimeMoves > 0) {
337
+ process.stdout.write(`Moved local Codex runtime entries: ${result.runtimeMoves}\n`);
338
+ }
339
+ return result;
340
+ }
312
341
  function runCheck(parsed) {
313
342
  if (parsed.all) {
314
343
  const report = (0, checks_1.checkSourceTree)(parsed.targetPath);
@@ -328,6 +357,7 @@ function prepareConfigRoot(configRoot) {
328
357
  (0, config_tree_1.ensureConfigRootLayout)(configRoot);
329
358
  (0, config_tree_1.ensureConfigRootGitignore)(configRoot);
330
359
  (0, config_tree_1.ensureDefaultGlobalTemplates)(configRoot);
360
+ (0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
331
361
  }
332
362
  function activeProject(targetPath, action) {
333
363
  const projectRoot = (0, project_1.findProjectRoot)(targetPath);
@@ -3,14 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureConfigRootLayout = ensureConfigRootLayout;
4
4
  exports.ensureConfigRootGitignore = ensureConfigRootGitignore;
5
5
  exports.ensureDefaultGlobalTemplates = ensureDefaultGlobalTemplates;
6
+ exports.ensurePortableSystemdFiles = ensurePortableSystemdFiles;
6
7
  exports.migrateOldTemplates = migrateOldTemplates;
7
8
  exports.findLegacyProfileDirs = findLegacyProfileDirs;
8
9
  exports.findProfileDirs = findProfileDirs;
9
10
  exports.convertLegacyTemplateVars = convertLegacyTemplateVars;
10
11
  exports.migrateCodexRuntimeFiles = migrateCodexRuntimeFiles;
11
12
  exports.migrateLooseFiles = migrateLooseFiles;
13
+ exports.describeLegacyProfileLayout = describeLegacyProfileLayout;
14
+ exports.migrateProfileLayout = migrateProfileLayout;
12
15
  exports.starterConfigRootPath = starterConfigRootPath;
13
16
  exports.copySkeletonTree = copySkeletonTree;
17
+ const childProcess = require("child_process");
14
18
  const fs = require("fs");
15
19
  const path = require("path");
16
20
  const constants_1 = require("./constants");
@@ -68,6 +72,25 @@ function ensureDefaultGlobalTemplates(configRoot) {
68
72
  }
69
73
  }
70
74
  }
75
+ function ensurePortableSystemdFiles(configRoot) {
76
+ const packageRoot = path.resolve(__dirname, '..');
77
+ const files = new Map([
78
+ ['install-systemd-jobs.sh', { source: 'scripts/install-systemd-jobs.sh', mode: 0o755 }],
79
+ ['update-ai-tools.sh', { source: 'scripts/update-ai-tools.sh', mode: 0o755 }],
80
+ ['ai-tools-update.service', { source: 'ops/systemd/ai-tools-update.service', mode: 0o644 }],
81
+ ['ai-tools-update.timer', { source: 'ops/systemd/ai-tools-update.timer', mode: 0o644 }]
82
+ ]);
83
+ for (const [targetName, asset] of files) {
84
+ const sourcePath = path.join(packageRoot, asset.source);
85
+ const targetPath = path.join(configRoot, targetName);
86
+ if (!fs.existsSync(sourcePath)) {
87
+ continue;
88
+ }
89
+ fs.copyFileSync(sourcePath, targetPath);
90
+ fs.chmodSync(targetPath, asset.mode);
91
+ (0, utils_1.verbose)(`updated ${targetPath}`);
92
+ }
93
+ }
71
94
  function migrateOldTemplates(configRoot) {
72
95
  const oldCodeTemplate = path.join(configRoot, 'templates', 'AGENTS-CODE.md');
73
96
  const newCodeTemplate = path.join(configRoot, 'global', 'agents', 'code.md.njk');
@@ -150,6 +173,232 @@ function migrateLooseFiles(agentDir, pattern, targetDirName) {
150
173
  }
151
174
  return moved;
152
175
  }
176
+ function describeLegacyProfileLayout(profileDir) {
177
+ const paths = [];
178
+ const legacyMemoryDir = path.join(profileDir, 'memory');
179
+ if (fs.existsSync(legacyMemoryDir)) {
180
+ paths.push(legacyMemoryDir);
181
+ }
182
+ for (const filePath of legacyProjectMemoryFiles(profileDir)) {
183
+ paths.push(filePath);
184
+ }
185
+ const legacyCodexHome = path.join(profileDir, 'memories', 'codex-home');
186
+ if (fs.existsSync(legacyCodexHome) || (0, utils_1.isSymlink)(legacyCodexHome)) {
187
+ paths.push(legacyCodexHome);
188
+ }
189
+ return [...new Set(paths)].sort();
190
+ }
191
+ function migrateProfileLayout(configRoot, profileDir, confirmGitMoves) {
192
+ const projectMoves = planProjectMemoryMoves(profileDir);
193
+ validateMoveTargets(projectMoves);
194
+ const git = findGitWorktree(configRoot);
195
+ const trackedMoves = git === null ? [] : projectMoves.filter((move) => isTrackedByGit(git, move.source));
196
+ if (git !== null && trackedMoves.length > 0 && !confirmGitMoves({ gitRoot: git.root, moves: trackedMoves })) {
197
+ throw new Error('profile layout migration declined; no files were moved');
198
+ }
199
+ let gitMoves = 0;
200
+ for (const move of projectMoves) {
201
+ fs.mkdirSync(path.dirname(move.target), { recursive: true });
202
+ if (git !== null && trackedMoves.includes(move)) {
203
+ runGitMove(git, move);
204
+ gitMoves += 1;
205
+ }
206
+ else {
207
+ fs.renameSync(move.source, move.target);
208
+ }
209
+ removeEmptyParents(path.dirname(move.source), profileDir);
210
+ (0, utils_1.verbose)(`moved project memory ${move.source} -> ${move.target}`);
211
+ }
212
+ removeEmptyParents(path.join(profileDir, 'memory'), profileDir);
213
+ removeEmptyParents(path.join(profileDir, 'memories'), profileDir);
214
+ const runtimeMoves = migrateLegacyCodexHome(profileDir);
215
+ return { gitMoves, projectMemoryMoves: projectMoves.length, runtimeMoves };
216
+ }
217
+ function planProjectMemoryMoves(profileDir) {
218
+ const targetDir = path.join(profileDir, 'notes', 'memory');
219
+ const moves = [];
220
+ const legacyMemoryDir = path.join(profileDir, 'memory');
221
+ for (const source of listTreeFiles(legacyMemoryDir)) {
222
+ moves.push({ source, target: path.join(targetDir, path.relative(legacyMemoryDir, source)) });
223
+ }
224
+ for (const source of legacyProjectMemoryFiles(profileDir)) {
225
+ moves.push({ source, target: path.join(targetDir, path.basename(source)) });
226
+ }
227
+ return moves.sort((left, right) => left.source.localeCompare(right.source));
228
+ }
229
+ function legacyProjectMemoryFiles(profileDir) {
230
+ const files = [];
231
+ for (const entry of safeReadDir(profileDir)) {
232
+ if (entry.isFile() && /^memory.*\.md$/i.test(entry.name)) {
233
+ files.push(path.join(profileDir, entry.name));
234
+ }
235
+ }
236
+ const legacyMemoriesDir = path.join(profileDir, 'memories');
237
+ for (const entry of safeReadDir(legacyMemoriesDir)) {
238
+ if (entry.isFile() && /\.md$/i.test(entry.name)) {
239
+ files.push(path.join(legacyMemoriesDir, entry.name));
240
+ }
241
+ }
242
+ return files.sort();
243
+ }
244
+ function safeReadDir(dir) {
245
+ try {
246
+ return fs.readdirSync(dir, { withFileTypes: true });
247
+ }
248
+ catch (error) {
249
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
250
+ return [];
251
+ }
252
+ throw error;
253
+ }
254
+ }
255
+ function listTreeFiles(root) {
256
+ const files = [];
257
+ for (const entry of safeReadDir(root)) {
258
+ const filePath = path.join(root, entry.name);
259
+ if (entry.isDirectory()) {
260
+ files.push(...listTreeFiles(filePath));
261
+ }
262
+ else if (entry.isFile() || entry.isSymbolicLink()) {
263
+ files.push(filePath);
264
+ }
265
+ }
266
+ return files;
267
+ }
268
+ function validateMoveTargets(moves) {
269
+ const targets = new Set();
270
+ for (const move of moves) {
271
+ const target = path.resolve(move.target);
272
+ if (targets.has(target) || fs.existsSync(target) || (0, utils_1.isSymlink)(target)) {
273
+ throw new Error(`cannot migrate project memory because the destination already exists: ${move.target}`);
274
+ }
275
+ targets.add(target);
276
+ }
277
+ }
278
+ function findGitWorktree(configRoot) {
279
+ const searchPath = process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '';
280
+ const gitCommand = findCommandOnPath('git', searchPath);
281
+ if (gitCommand === null) {
282
+ return null;
283
+ }
284
+ const env = { ...process.env, PATH: searchPath };
285
+ const result = childProcess.spawnSync(gitCommand, ['-C', configRoot, 'rev-parse', '--show-toplevel'], {
286
+ encoding: 'utf8',
287
+ env,
288
+ shell: false
289
+ });
290
+ if (result.status !== 0) {
291
+ return null;
292
+ }
293
+ const root = result.stdout.trim();
294
+ return root ? { command: gitCommand, env, root: path.resolve(root) } : null;
295
+ }
296
+ function findCommandOnPath(command, searchPath) {
297
+ const extensions = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
298
+ for (const dir of searchPath.split(path.delimiter).filter(Boolean)) {
299
+ for (const extension of extensions) {
300
+ const candidate = path.join(dir, `${command}${extension}`);
301
+ try {
302
+ fs.accessSync(candidate, process.platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
303
+ return candidate;
304
+ }
305
+ catch {
306
+ continue;
307
+ }
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+ function isTrackedByGit(git, filePath) {
313
+ const relativePath = path.relative(git.root, filePath).replace(/\\/g, '/');
314
+ const result = childProcess.spawnSync(git.command, ['-C', git.root, '--literal-pathspecs', 'ls-files', '--error-unmatch', '--', relativePath], { encoding: 'utf8', env: git.env, shell: false });
315
+ return result.status === 0;
316
+ }
317
+ function runGitMove(git, move) {
318
+ const source = path.relative(git.root, move.source).replace(/\\/g, '/');
319
+ const target = path.relative(git.root, move.target).replace(/\\/g, '/');
320
+ const result = childProcess.spawnSync(git.command, ['-C', git.root, '--literal-pathspecs', 'mv', '--', source, target], {
321
+ encoding: 'utf8',
322
+ env: git.env,
323
+ shell: false
324
+ });
325
+ if (result.status !== 0) {
326
+ const detail = result.stderr.trim() || result.stdout.trim() || `git exited with status ${result.status}`;
327
+ throw new Error(`cannot migrate tracked project memory ${move.source}: ${detail}`);
328
+ }
329
+ }
330
+ function migrateLegacyCodexHome(profileDir) {
331
+ const source = path.join(profileDir, 'memories', 'codex-home');
332
+ if (!fs.existsSync(source) && !(0, utils_1.isSymlink)(source)) {
333
+ return 0;
334
+ }
335
+ const target = path.join(profileDir, constants_1.LIVE_DIR_NAME, 'memories', 'codex-home');
336
+ if (!fs.existsSync(target) && !(0, utils_1.isSymlink)(target)) {
337
+ fs.mkdirSync(path.dirname(target), { recursive: true });
338
+ fs.renameSync(source, target);
339
+ removeEmptyParents(path.dirname(source), profileDir);
340
+ (0, utils_1.verbose)(`moved Codex runtime ${source} -> ${target}`);
341
+ return 1;
342
+ }
343
+ const sourceIsDirectory = !(0, utils_1.isSymlink)(source) && fs.statSync(source).isDirectory();
344
+ const targetIsDirectory = !(0, utils_1.isSymlink)(target) && fs.statSync(target).isDirectory();
345
+ if (!sourceIsDirectory || !targetIsDirectory) {
346
+ const backupTarget = (0, utils_1.nextBackupPath)(target);
347
+ fs.renameSync(source, backupTarget);
348
+ removeEmptyParents(path.dirname(source), profileDir);
349
+ (0, utils_1.verbose)(`moved Codex runtime ${source} -> ${backupTarget}`);
350
+ return 1;
351
+ }
352
+ const moved = mergeRuntimeTree(source, target);
353
+ removeEmptyParents(path.dirname(source), profileDir);
354
+ return moved;
355
+ }
356
+ function mergeRuntimeTree(sourceDir, targetDir) {
357
+ let moved = 0;
358
+ fs.mkdirSync(targetDir, { recursive: true });
359
+ for (const entry of safeReadDir(sourceDir)) {
360
+ const source = path.join(sourceDir, entry.name);
361
+ let target = path.join(targetDir, entry.name);
362
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
363
+ const targetIsDirectory = fs.existsSync(target) && !(0, utils_1.isSymlink)(target) && fs.statSync(target).isDirectory();
364
+ if ((fs.existsSync(target) || (0, utils_1.isSymlink)(target)) && !targetIsDirectory) {
365
+ target = (0, utils_1.nextBackupPath)(target);
366
+ }
367
+ moved += mergeRuntimeTree(source, target);
368
+ continue;
369
+ }
370
+ const resolvedTarget = fs.existsSync(target) || (0, utils_1.isSymlink)(target) ? (0, utils_1.nextBackupPath)(target) : target;
371
+ fs.mkdirSync(path.dirname(resolvedTarget), { recursive: true });
372
+ fs.renameSync(source, resolvedTarget);
373
+ (0, utils_1.verbose)(`moved Codex runtime ${source} -> ${resolvedTarget}`);
374
+ moved += 1;
375
+ }
376
+ try {
377
+ fs.rmdirSync(sourceDir);
378
+ }
379
+ catch (error) {
380
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
381
+ throw error;
382
+ }
383
+ }
384
+ return moved;
385
+ }
386
+ function removeEmptyParents(startDir, stopDir) {
387
+ let dir = path.resolve(startDir);
388
+ const stop = path.resolve(stopDir);
389
+ while (dir !== stop && (0, utils_1.isSamePathOrDescendant)(dir, stop)) {
390
+ try {
391
+ fs.rmdirSync(dir);
392
+ }
393
+ catch (error) {
394
+ if (error instanceof Error && 'code' in error && ['ENOENT', 'ENOTEMPTY'].includes(String(error.code))) {
395
+ return;
396
+ }
397
+ throw error;
398
+ }
399
+ dir = path.dirname(dir);
400
+ }
401
+ }
153
402
  function starterConfigRootPath() {
154
403
  return path.resolve(__dirname, '..', 'examples', 'basic-config', 'agent-config');
155
404
  }
package/dist/constants.js CHANGED
@@ -11,7 +11,7 @@ exports.LIVE_DIR_NAME = 'live';
11
11
  exports.CONFIG_ROOT_OVERRIDE_ENV = 'AGENT_RUN_CONFIG_ROOT_OVERRIDE';
12
12
  exports.CONFIG_DIR_ENV = 'AGENT_CONFIG_DIR';
13
13
  exports.VERBOSE_ENV = 'AGENT_RUN_VERBOSE';
14
- exports.PACKAGE_VERSION = '0.99.28';
14
+ exports.PACKAGE_VERSION = '0.99.29';
15
15
  exports.UNEXPANDED_TEMPLATE_RE = /\{\{[^}]+\}\}|\{%[^%]+%\}/;
16
16
  exports.LOCAL_AI_FILE_NAMES = new Set([
17
17
  'AGENTS.md',
package/dist/defaults.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultManifest = defaultManifest;
4
4
  exports.stringifyRootDefaults = stringifyRootDefaults;
5
5
  exports.defaultLocalTemplate = defaultLocalTemplate;
6
+ exports.defaultProjectMemoryIndex = defaultProjectMemoryIndex;
6
7
  exports.defaultGlobalTemplates = defaultGlobalTemplates;
7
8
  exports.defaultToolInstructionsTemplate = defaultToolInstructionsTemplate;
8
9
  exports.defaultCodexConfigContent = defaultCodexConfigContent;
@@ -34,7 +35,8 @@ function defaultManifest(profile) {
34
35
  changesFile: '{{ projectRoot }}/CHANGES',
35
36
  reviewDir: '{{ profileDir }}/reviews',
36
37
  reviewFile: '{{ profileDir }}/reviews/REVIEW-{{ date }}.md',
37
- memoriesDir: '{{ agentDir }}/memories'
38
+ memoriesDir: '{{ agentDir }}/memories',
39
+ projectMemoryDir: '{{ profileDir }}/notes/memory'
38
40
  }
39
41
  };
40
42
  }
@@ -71,6 +73,19 @@ function defaultLocalTemplate() {
71
73
  ''
72
74
  ].join('\n');
73
75
  }
76
+ function defaultProjectMemoryIndex() {
77
+ return [
78
+ '# Project memory',
79
+ '',
80
+ 'This directory contains durable notes for this project across agent sessions and machines.',
81
+ 'Keep the notes short, factual, and free of secrets. Link additional topic files from this index.',
82
+ '',
83
+ '## Files',
84
+ '',
85
+ '- Add project memory files here as they are needed.',
86
+ ''
87
+ ].join('\n');
88
+ }
74
89
  function defaultGlobalTemplates() {
75
90
  return new Map([
76
91
  ['global/agents/code.md.njk', defaultCodeAgentTemplate()],
@@ -293,6 +308,18 @@ function defaultToolInstructionsTemplate(isClaude) {
293
308
  '{{ paths.memoriesDir }}',
294
309
  '```',
295
310
  '',
311
+ 'Project memory directory:',
312
+ '',
313
+ '```text',
314
+ '{{ paths.projectMemoryDir }}',
315
+ '```',
316
+ '',
317
+ 'Native Codex memory directory:',
318
+ '',
319
+ '```text',
320
+ '{{ paths.nativeMemoryDir }}',
321
+ '```',
322
+ '',
296
323
  'Global memory directory:',
297
324
  '',
298
325
  '```text',
@@ -337,7 +364,8 @@ function defaultCodexConfigTemplate() {
337
364
  'writable_roots = [',
338
365
  ' {{ projectRoot | dump }},',
339
366
  ' {{ paths.reviewDir | dump }},',
340
- ' {{ paths.memoriesDir | dump }}',
367
+ ' {{ paths.memoriesDir | dump }},',
368
+ ' {{ paths.projectMemoryDir | dump }}',
341
369
  ']',
342
370
  'network_access = false',
343
371
  ''
@@ -349,6 +377,7 @@ function defaultClaudeSettingsTemplate() {
349
377
  ' "AGENT_DIR": {{ agentDir | dump }},',
350
378
  ' "AGENT_PROFILE_DIR": {{ profileDir | dump }},',
351
379
  ' "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},',
380
+ ' "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},',
352
381
  ' "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }}',
353
382
  ' }'
354
383
  ].join('\n');
@@ -377,7 +406,8 @@ function defaultCodexConfigContent(context) {
377
406
  'writable_roots = [',
378
407
  ` ${JSON.stringify(context.projectRoot)},`,
379
408
  ` ${JSON.stringify(context.paths.reviewDir)},`,
380
- ` ${JSON.stringify(context.paths.memoriesDir)}`,
409
+ ` ${JSON.stringify(context.paths.memoriesDir)},`,
410
+ ` ${JSON.stringify(context.paths.projectMemoryDir)}`,
381
411
  ']',
382
412
  'network_access = false',
383
413
  ''
@@ -389,6 +419,7 @@ function defaultClaudeSettingsContent(context) {
389
419
  AGENT_DIR: context.agentDir,
390
420
  AGENT_PROFILE_DIR: context.profileDir,
391
421
  AGENT_RUN_PROJECT_ROOT: context.projectRoot,
422
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
392
423
  AGENT_GLOBAL_MEMORY_DIR: context.paths.globalMemoryDir
393
424
  }
394
425
  };
package/dist/manifest.js CHANGED
@@ -165,7 +165,8 @@ function normalizeManifest(manifest, profile) {
165
165
  reviewDir: normalizeLegacyReviewPath(paths.reviewDir ?? fallbackPaths.reviewDir ?? '{{ profileDir }}/reviews'),
166
166
  reviewFile: normalizeLegacyReviewPath(paths.reviewFile ?? fallbackPaths.reviewFile ?? '{{ profileDir }}/reviews/REVIEW-{{ date }}.md'),
167
167
  reviewConsolidatedFile: paths.reviewConsolidatedFile ?? fallbackPaths.reviewConsolidatedFile,
168
- memoriesDir: paths.memoriesDir ?? fallbackPaths.memoriesDir ?? '{{ agentDir }}/memories'
168
+ memoriesDir: paths.memoriesDir ?? fallbackPaths.memoriesDir ?? '{{ agentDir }}/memories',
169
+ projectMemoryDir: paths.projectMemoryDir ?? fallbackPaths.projectMemoryDir ?? '{{ profileDir }}/notes/memory'
169
170
  }
170
171
  };
171
172
  }
package/dist/renderer.js CHANGED
@@ -51,11 +51,23 @@ function renderAgentSections(env, configRoot, manifest, context, trace) {
51
51
  }
52
52
  sections.push((0, templates_1.renderTemplateFile)(env, configRoot, include, context, trace));
53
53
  }
54
- return sections.map((section, index) => {
54
+ const renderedSections = sections.map((section, index) => {
55
55
  const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
56
56
  (0, templates_1.assertNoUnexpandedTemplateVars)(label, section);
57
57
  return section.trimEnd();
58
58
  });
59
+ renderedSections.push(projectMemoryInstructions(context));
60
+ return renderedSections;
61
+ }
62
+ function projectMemoryInstructions(context) {
63
+ return [
64
+ '## Project Memory',
65
+ '',
66
+ `Project memory is stored in ${context.paths.projectMemoryDir}.`,
67
+ 'Read README.md there before starting work when prior project context may matter, then read only the linked files relevant to the task.',
68
+ 'Do not store secrets, raw chat transcripts, or temporary task state there.',
69
+ 'Update project memory only when the user explicitly asks you to remember or update something for this project.'
70
+ ].join('\n');
59
71
  }
60
72
  function renderProfileFiles(env, configRoot, context, targetTool, trace) {
61
73
  const files = [];
@@ -210,6 +222,7 @@ function checkRenderedFiles(rendered, findings) {
210
222
  function checkRuntimeDirectories(context, findings) {
211
223
  for (const dir of [
212
224
  context.paths.reviewDir,
225
+ context.paths.projectMemoryDir,
213
226
  context.paths.memoriesDir,
214
227
  context.paths.codexHomeDir,
215
228
  context.paths.liveDir,
@@ -303,6 +316,7 @@ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, g
303
316
  function syncRuntimeDirs(context) {
304
317
  for (const dir of [
305
318
  context.paths.reviewDir,
319
+ context.paths.projectMemoryDir,
306
320
  context.paths.memoriesDir,
307
321
  context.paths.codexHomeDir,
308
322
  context.paths.codexSkillsDir,
@@ -312,6 +326,11 @@ function syncRuntimeDirs(context) {
312
326
  ]) {
313
327
  fs.mkdirSync(dir, { recursive: true });
314
328
  }
329
+ const projectMemoryIndex = path.join(context.paths.projectMemoryDir, 'README.md');
330
+ if (!fs.existsSync(projectMemoryIndex)) {
331
+ fs.writeFileSync(projectMemoryIndex, (0, defaults_1.defaultProjectMemoryIndex)(), 'utf8');
332
+ (0, utils_1.verbose)(`created ${projectMemoryIndex}`);
333
+ }
315
334
  migrateLiveReviewFiles(context);
316
335
  removeLegacyCodexSkillDirs(context);
317
336
  removeLegacyCodeReviewSkillDirs(context);
package/dist/templates.js CHANGED
@@ -35,6 +35,7 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
35
35
  };
36
36
  const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
37
37
  const memoriesDir = resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext);
38
+ const projectMemoryDir = resolveRuntimePath(configRoot, manifest.paths.projectMemoryDir, env, baseContext);
38
39
  const codexHomeDir = path.join(memoriesDir, 'codex-home');
39
40
  const paths = {
40
41
  profileDir,
@@ -47,6 +48,8 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
47
48
  : path.join(reviewDir, 'REVIEW.md'),
48
49
  memoriesDir,
49
50
  globalMemoryDir: path.join(configRoot, 'notes', 'memory'),
51
+ projectMemoryDir,
52
+ nativeMemoryDir: path.join(codexHomeDir, 'memories'),
50
53
  codexHomeDir,
51
54
  overridesDir: path.join(profileDir, 'overrides'),
52
55
  codexSkillsDir: path.join(codexHomeDir, 'skills'),
package/dist/tools.js CHANGED
@@ -23,6 +23,7 @@ function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
23
23
  AGENT_DIR: liveDir,
24
24
  AGENT_PROFILE_DIR: context.profileDir,
25
25
  AGENT_RUN_PROJECT_ROOT: projectRoot,
26
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
26
27
  AGENT_RUN_REAL_PATH: realPath,
27
28
  PATH: `${path.join(liveDir, 'bin')}${path.delimiter}${realPath}`
28
29
  };
@@ -30,6 +31,8 @@ function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
30
31
  (0, process_1.execCommand)(realBinary, [
31
32
  ...permissionArgs,
32
33
  ...runtimeArgs,
34
+ '--add-dir',
35
+ context.paths.projectMemoryDir,
33
36
  '-C',
34
37
  projectRoot,
35
38
  ...(wrapperArgs.sandboxMode !== 'danger' && wrapperArgs.codexNetwork
@@ -51,12 +54,15 @@ function runClaude(realBinary, permissionArgs, context, args, wrapperArgs) {
51
54
  AGENT_DIR: liveDir,
52
55
  AGENT_PROFILE_DIR: context.profileDir,
53
56
  AGENT_RUN_PROJECT_ROOT: context.projectRoot,
57
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
54
58
  AGENT_RUN_REAL_PATH: realPath,
55
59
  PATH: `${path.join(liveDir, 'bin')}${path.delimiter}${realPath}`
56
60
  };
57
61
  const runtimeArgs = wrapperArgs.sandboxMode === 'danger' ? getDangerArgs('claude') : permissionArgs;
58
62
  (0, process_1.execCommand)(realBinary, [
59
63
  ...runtimeArgs,
64
+ '--add-dir',
65
+ context.paths.projectMemoryDir,
60
66
  '--append-system-prompt-file',
61
67
  claudePath,
62
68
  '--settings',
@@ -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
 
@@ -33,6 +33,7 @@
33
33
  "changesFile": "{{ projectRoot }}/CHANGES",
34
34
  "reviewDir": "{{ profileDir }}/reviews",
35
35
  "reviewFile": "{{ profileDir }}/reviews/REVIEW-{{ date }}.md",
36
- "memoriesDir": "{{ agentDir }}/memories"
36
+ "memoriesDir": "{{ agentDir }}/memories",
37
+ "projectMemoryDir": "{{ profileDir }}/notes/memory"
37
38
  }
38
39
  }
@@ -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,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.
@@ -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
  "STARTER_OVERRIDE": "true"
@@ -16,6 +17,7 @@
16
17
  "AGENT_DIR": {{ agentDir | dump }},
17
18
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
18
19
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
20
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
19
21
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
20
22
  "STARTER_PROFILE": {{ profile | dump }},
21
23
  "STARTER_OVERRIDE": "true"
@@ -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
@@ -13,4 +13,4 @@ Environment=XDG_CONFIG_HOME=/root/.config
13
13
  Environment=NPM_CONFIG_PREFIX=/usr/local
14
14
  Environment=PNPM_HOME=/usr/local
15
15
  Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
16
- ExecStart=/usr/local/lib/agent-run/update-ai-tools.sh
16
+ ExecStart=/usr/local/libexec/agent-run/update-ai-tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/agent-run",
3
- "version": "0.99.28",
3
+ "version": "0.99.29",
4
4
  "packageManager": "pnpm@10.28.1",
5
5
  "bin": {
6
6
  "agent-run": "dist/agent-run.js"
@@ -19,10 +19,20 @@ USAGE
19
19
  }
20
20
 
21
21
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
22
- INSTALL_LIB_DIR="${INSTALL_LIB_DIR:-/usr/local/lib/agent-run}"
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ INSTALL_LIBEXEC_DIR="${INSTALL_LIBEXEC_DIR:-${INSTALL_LIB_DIR:-/usr/local/libexec/agent-run}}"
23
24
  SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
24
25
  ORIGINAL_ARGS=("$@")
25
26
 
27
+ UPDATE_SCRIPT="$ROOT/scripts/update-ai-tools.sh"
28
+ SERVICE_FILE="$ROOT/ops/systemd/ai-tools-update.service"
29
+ TIMER_FILE="$ROOT/ops/systemd/ai-tools-update.timer"
30
+ if [ ! -f "$UPDATE_SCRIPT" ]; then
31
+ UPDATE_SCRIPT="$SCRIPT_DIR/update-ai-tools.sh"
32
+ SERVICE_FILE="$SCRIPT_DIR/ai-tools-update.service"
33
+ TIMER_FILE="$SCRIPT_DIR/ai-tools-update.timer"
34
+ fi
35
+
26
36
  INSTALL_AI_TOOLS=0
27
37
  DRY_RUN=0
28
38
 
@@ -71,10 +81,10 @@ run() {
71
81
  "$@"
72
82
  }
73
83
 
74
- run install -d -m 0755 "$INSTALL_LIB_DIR" "$SYSTEMD_DIR"
75
- run install -m 0755 "$ROOT/scripts/update-ai-tools.sh" "$INSTALL_LIB_DIR/update-ai-tools.sh"
76
- run install -m 0644 "$ROOT/ops/systemd/ai-tools-update.service" "$SYSTEMD_DIR/ai-tools-update.service"
77
- run install -m 0644 "$ROOT/ops/systemd/ai-tools-update.timer" "$SYSTEMD_DIR/ai-tools-update.timer"
84
+ run install -d -m 0755 "$INSTALL_LIBEXEC_DIR" "$SYSTEMD_DIR"
85
+ run install -m 0755 "$UPDATE_SCRIPT" "$INSTALL_LIBEXEC_DIR/update-ai-tools"
86
+ run install -m 0644 "$SERVICE_FILE" "$SYSTEMD_DIR/ai-tools-update.service"
87
+ run install -m 0644 "$TIMER_FILE" "$SYSTEMD_DIR/ai-tools-update.timer"
78
88
 
79
89
  if [ "$INSTALL_AI_TOOLS" -eq 1 ]; then
80
90
  run systemctl daemon-reload
@@ -6,18 +6,44 @@ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/usr/local}"
6
6
  export PNPM_HOME="${PNPM_HOME:-/usr/local}"
7
7
  export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
8
8
 
9
- NPM_BIN="${AI_TOOLS_NPM_BIN:-/usr/local/bin/npm}"
9
+ NPM_BIN="${AI_TOOLS_NPM_BIN:-/usr/bin/npm}"
10
+ NODE_BIN="${AI_TOOLS_NODE_BIN:-/usr/bin/node}"
10
11
  APT_GET_BIN="${AI_TOOLS_APT_GET_BIN:-/usr/bin/apt-get}"
11
12
 
12
13
  AI_TOOLS_NPM_PACKAGES="${AI_TOOLS_NPM_PACKAGES:-npm@latest pnpm@latest corepack@latest fallow@latest ripgrep@latest pm2@latest tsx@latest typescript@latest @openai/codex@latest @anthropic-ai/claude-code@latest @technomoron/agent-run@latest}"
13
14
  AI_TOOLS_PNPM_PACKAGE="${AI_TOOLS_PNPM_PACKAGE:-pnpm@latest}"
14
15
  AI_TOOLS_APT_PACKAGES="${AI_TOOLS_APT_PACKAGES:-gh}"
16
+ AI_TOOLS_CLEAN_BINS="${AI_TOOLS_CLEAN_BINS:-agent-run claude codex}"
17
+ AI_TOOLS_SYSTEM_BIN_DIRS="${AI_TOOLS_SYSTEM_BIN_DIRS:-/usr/local/bin /usr/bin}"
15
18
 
16
19
  if [ "$(id -u)" -ne 0 ]; then
17
20
  echo "update-ai-tools.sh must run as root" >&2
18
21
  exit 1
19
22
  fi
20
23
 
24
+ clean_tool_bins() {
25
+ local bin_dir="$1"
26
+ local bin_name
27
+ local -a bin_names
28
+
29
+ [ -d "$bin_dir" ] || return 0
30
+ read -r -a bin_names <<<"$AI_TOOLS_CLEAN_BINS"
31
+ for bin_name in "${bin_names[@]}"; do
32
+ case "$bin_name" in
33
+ agent-run|claude|codex)
34
+ rm -f -- "$bin_dir/$bin_name"
35
+ ;;
36
+ *)
37
+ echo "Refusing to clean unexpected AI tool bin: $bin_name" >&2
38
+ exit 1
39
+ ;;
40
+ esac
41
+ done
42
+ }
43
+
44
+ target_bin_dir="${NPM_CONFIG_PREFIX%/}/bin"
45
+ clean_tool_bins "$target_bin_dir"
46
+
21
47
  if [ -n "$AI_TOOLS_NPM_PACKAGES" ]; then
22
48
  read -r -a npm_packages <<<"$AI_TOOLS_NPM_PACKAGES"
23
49
  "$NPM_BIN" install -g --force "${npm_packages[@]}"
@@ -30,9 +56,18 @@ if [ -n "$AI_TOOLS_PNPM_PACKAGE" ]; then
30
56
  "$NPM_BIN" install -g --force "${pnpm_packages[@]}"
31
57
  fi
32
58
 
59
+ # Once the new launchers are installed, remove exact-name duplicates from the
60
+ # other conventional system prefix so PATH ordering cannot select a stale copy.
61
+ read -r -a system_bin_dirs <<<"$AI_TOOLS_SYSTEM_BIN_DIRS"
62
+ for system_bin_dir in "${system_bin_dirs[@]}"; do
63
+ if [ "${system_bin_dir%/}" != "${target_bin_dir%/}" ]; then
64
+ clean_tool_bins "$system_bin_dir"
65
+ fi
66
+ done
67
+
33
68
  CLAUDE_INSTALL="$("$NPM_BIN" root -g)/@anthropic-ai/claude-code/install.cjs"
34
69
  if [ -f "$CLAUDE_INSTALL" ]; then
35
- /usr/local/bin/node "$CLAUDE_INSTALL"
70
+ "$NODE_BIN" "$CLAUDE_INSTALL"
36
71
  fi
37
72
 
38
73
  if [ -n "$AI_TOOLS_APT_PACKAGES" ] && command -v "$APT_GET_BIN" >/dev/null 2>&1; then