@technomoron/agent-run 0.99.27 → 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/dist/commands.js CHANGED
@@ -22,7 +22,10 @@ function dispatch(command) {
22
22
  runCheck(command);
23
23
  return;
24
24
  case 'init':
25
- runInit(command);
25
+ runGenerate(command);
26
+ return;
27
+ case 'generate':
28
+ runGenerate(command);
26
29
  return;
27
30
  case 'setup':
28
31
  runSetup(command);
@@ -56,22 +59,21 @@ function runTool(parsed) {
56
59
  (0, utils_1.fail)(profileResult.reason);
57
60
  }
58
61
  const profile = profileResult.profile;
62
+ const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
63
+ const agentDir = path.join(configRoot, profile);
59
64
  if (wrapperArgs.show) {
60
- const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
61
- const agentDir = path.join(configRoot, profile);
62
65
  (0, utils_1.verbose)(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
63
66
  requireProfile(command, agentDir);
64
67
  showToolProfile(command, projectRoot, agentDir);
65
68
  return;
66
69
  }
67
70
  if (wrapperArgs.create) {
68
- runInit({ command: 'init', targetPath: projectRoot });
71
+ runGenerate({ command: 'generate', targetPath: projectRoot });
69
72
  }
70
- const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
71
- const agentDir = path.join(configRoot, profile);
72
73
  (0, utils_1.verbose)(`profile=${profile} configRoot=${configRoot} agentPath=${agentDir}`);
73
74
  prepareConfigRoot(configRoot);
74
75
  requireProfile(command, agentDir);
76
+ migrateProfileOnStart(configRoot, agentDir, false);
75
77
  const preview = (0, renderer_1.renderProfile)(projectRoot, agentDir, true, command);
76
78
  ensureToolEnabled(preview.context, command);
77
79
  if (wrapperArgs.generate) {
@@ -110,7 +112,7 @@ function requireProfile(tool, agentDir) {
110
112
  }
111
113
  const label = tool ?? 'agent';
112
114
  process.stderr.write(`agent-run: no ${label} profile found for this project: ${agentDir}\n`);
113
- process.stderr.write('agent-run: run `agent-run init` or use the tool with `--create`.\n');
115
+ process.stderr.write('agent-run: run `agent-run generate` or use the tool with `--create`.\n');
114
116
  process.exit(1);
115
117
  }
116
118
  function checkLocalAiFiles(command, projectRoot, agentDir, context, allowLocal) {
@@ -146,21 +148,87 @@ function ensureToolEnabled(context, command) {
146
148
  (0, utils_1.fail)(`${command} is disabled for agent-run profile ${context.profile}`);
147
149
  }
148
150
  }
149
- function runInit(parsed) {
150
- const projectRoot = activeProject(parsed.targetPath, 'init');
151
+ function runGenerate(parsed) {
152
+ if (parsed.command === 'init') {
153
+ process.stderr.write('agent-run: `init` is deprecated; use `agent-run generate [path]` instead.\n');
154
+ }
155
+ const projectRoot = activeProject(parsed.targetPath, 'generate');
151
156
  if (projectRoot === null) {
152
157
  return;
153
158
  }
154
159
  const agentDir = initializeProfileSource(projectRoot, true);
160
+ migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
155
161
  printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir));
156
162
  }
157
163
  function runSetup(parsed) {
164
+ const projectRoot = (0, project_1.findProjectRoot)(process.cwd());
165
+ const detectedProfile = (0, project_1.resolveProfile)(projectRoot);
166
+ const profile = parsed.profile === null ? confirmProfile(detectedProfile) : requireValidProfile(parsed.profile);
167
+ const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
158
168
  const sourcePath = (0, config_tree_1.starterConfigRootPath)();
159
169
  if (!fs.existsSync(sourcePath)) {
160
170
  (0, utils_1.fail)(`starter config skeleton not found: ${sourcePath}`);
161
171
  }
162
- (0, config_tree_1.copySkeletonTree)(sourcePath, path.resolve(parsed.targetPath));
163
- process.stdout.write(`OK created starter agent config in ${path.resolve(parsed.targetPath)}\n`);
172
+ (0, config_tree_1.copySkeletonTree)(sourcePath, configRoot, new Set(['starter']));
173
+ (0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
174
+ const agentDir = path.join(configRoot, profile);
175
+ migrateProfileOnStart(configRoot, agentDir, false);
176
+ (0, config_tree_1.copySkeletonTree)(path.join(sourcePath, 'starter', 'basic-project'), agentDir);
177
+ if (profile !== detectedProfile) {
178
+ writeProjectProfileMapping(projectRoot, profile);
179
+ }
180
+ printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir, { configRoot, profile }));
181
+ process.stdout.write(`OK installed default config tree at ${configRoot}\n`);
182
+ }
183
+ function requireValidProfile(value) {
184
+ const parsed = (0, project_1.parseProfile)(value, 'setup profile');
185
+ if (parsed.profile === null) {
186
+ (0, utils_1.fail)(parsed.reason);
187
+ }
188
+ return parsed.profile;
189
+ }
190
+ function confirmProfile(detectedProfile) {
191
+ process.stdout.write(`Detected profile: ${detectedProfile}\n`);
192
+ const confirmation = promptLine('Is this correct? [Y/n] ').trim().toLowerCase();
193
+ if (confirmation === '' || confirmation === 'y' || confirmation === 'yes') {
194
+ return detectedProfile;
195
+ }
196
+ return requireValidProfile(promptLine('Profile (org/repo): '));
197
+ }
198
+ function promptLine(prompt, nonInteractiveMessage = 'setup requires a profile argument when input is not interactive') {
199
+ if (!process.stdin.isTTY) {
200
+ (0, utils_1.fail)(nonInteractiveMessage);
201
+ }
202
+ process.stdout.write(prompt);
203
+ const bytes = [];
204
+ const buffer = Buffer.alloc(1);
205
+ while (fs.readSync(process.stdin.fd, buffer, 0, 1, null) === 1) {
206
+ if (buffer[0] === 10) {
207
+ break;
208
+ }
209
+ if (buffer[0] !== 13) {
210
+ bytes.push(buffer[0] ?? 0);
211
+ }
212
+ }
213
+ return Buffer.from(bytes).toString('utf8');
214
+ }
215
+ function writeProjectProfileMapping(projectRoot, profile) {
216
+ const envPath = path.join(projectRoot, constants_1.ENV_FILE_NAME);
217
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8').replace(/\r\n/g, '\n') : '';
218
+ const lines = existing.length > 0 ? existing.replace(/\n+$/, '').split('\n') : [];
219
+ const profileLine = `AGENT_RUN_PROFILE=${profile}`;
220
+ const index = lines.findIndex((line) => /^\s*AGENT_RUN_PROFILE\s*=/.test(line));
221
+ if (index >= 0) {
222
+ lines[index] = profileLine;
223
+ }
224
+ else {
225
+ lines.push(profileLine);
226
+ }
227
+ const content = `${lines.join('\n')}\n`;
228
+ if (content !== existing) {
229
+ fs.writeFileSync(envPath, content, 'utf8');
230
+ (0, utils_1.verbose)(`write ${envPath}`);
231
+ }
164
232
  }
165
233
  function runEdit(parsed) {
166
234
  const projectRoot = activeProject(parsed.targetPath, 'edit');
@@ -168,6 +236,7 @@ function runEdit(parsed) {
168
236
  return;
169
237
  }
170
238
  const agentDir = initializeProfileSource(projectRoot, false);
239
+ migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
171
240
  const editPath = (0, manifest_1.createDefaultLocalFile)(agentDir);
172
241
  (0, renderer_1.syncAgentProfile)(projectRoot, agentDir);
173
242
  process.stdout.write(`Edit: ${editPath}\n`);
@@ -188,6 +257,7 @@ function runUpdate(parsed) {
188
257
  const agentDir = (0, project_1.resolveAgentDir)(projectRoot);
189
258
  prepareConfigRoot(configRoot);
190
259
  requireProfile(null, agentDir);
260
+ migrateProfileOnStart(configRoot, agentDir, false);
191
261
  printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir));
192
262
  }
193
263
  function runUpdateAll(targetPath) {
@@ -198,6 +268,7 @@ function runUpdateAll(targetPath) {
198
268
  prepareConfigRoot(configRoot);
199
269
  const profileDirs = (0, config_tree_1.findProfileDirs)(configRoot);
200
270
  for (const agentDir of profileDirs) {
271
+ migrateProfileOnStart(configRoot, agentDir, false);
201
272
  const profile = path.relative(configRoot, agentDir).replace(/\\/g, '/');
202
273
  const projectRoot = (0, project_1.projectRootForProfile)(profile, configRoot);
203
274
  (0, renderer_1.syncAgentProfile)(projectRoot, agentDir, { configRoot, profile });
@@ -215,15 +286,16 @@ function runMigrateConfig(parsed) {
215
286
  (0, config_tree_1.migrateOldTemplates)(configRoot);
216
287
  (0, config_tree_1.ensureDefaultGlobalTemplates)(configRoot);
217
288
  (0, manifest_1.ensureRootDefaultsFile)(configRoot);
218
- 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)]);
219
290
  let createdManifestCount = 0;
220
291
  let createdLocalCount = 0;
221
292
  let movedRuntimeCount = 0;
222
293
  let movedReviewCount = 0;
223
294
  let movedMemoryCount = 0;
224
295
  for (const agentDir of profileDirs) {
296
+ const layout = migrateProfileOnStart(configRoot, agentDir, parsed.yes);
225
297
  const localPath = path.join(agentDir, constants_1.LOCAL_TEMPLATE_FILE_NAME);
226
- if (!fs.existsSync(localPath)) {
298
+ if (!fs.existsSync(localPath) && fs.existsSync(path.join(agentDir, 'AGENTS-MODS.md'))) {
227
299
  fs.writeFileSync(localPath, (0, config_tree_1.convertLegacyTemplateVars)(fs.readFileSync(path.join(agentDir, 'AGENTS-MODS.md'), 'utf8')), 'utf8');
228
300
  createdLocalCount += 1;
229
301
  }
@@ -233,7 +305,7 @@ function runMigrateConfig(parsed) {
233
305
  }
234
306
  movedRuntimeCount += (0, config_tree_1.migrateCodexRuntimeFiles)(agentDir);
235
307
  movedReviewCount += (0, config_tree_1.migrateLooseFiles)(agentDir, /^REVIEW(?:-.+)?\.md$/, 'reviews');
236
- movedMemoryCount += (0, config_tree_1.migrateLooseFiles)(agentDir, /^memory.*\.md$/i, 'memories');
308
+ movedMemoryCount += layout.projectMemoryMoves;
237
309
  }
238
310
  process.stdout.write(`OK migrated ${configRoot}\n`);
239
311
  process.stdout.write(`Profiles converted: ${profileDirs.length}\n`);
@@ -243,6 +315,29 @@ function runMigrateConfig(parsed) {
243
315
  process.stdout.write(`Moved review files: ${movedReviewCount}\n`);
244
316
  process.stdout.write(`Moved memory files: ${movedMemoryCount}\n`);
245
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
+ }
246
341
  function runCheck(parsed) {
247
342
  if (parsed.all) {
248
343
  const report = (0, checks_1.checkSourceTree)(parsed.targetPath);
@@ -262,6 +357,7 @@ function prepareConfigRoot(configRoot) {
262
357
  (0, config_tree_1.ensureConfigRootLayout)(configRoot);
263
358
  (0, config_tree_1.ensureConfigRootGitignore)(configRoot);
264
359
  (0, config_tree_1.ensureDefaultGlobalTemplates)(configRoot);
360
+ (0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
265
361
  }
266
362
  function activeProject(targetPath, action) {
267
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,17 +173,246 @@ 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
  }
156
- function copySkeletonTree(sourceDir, targetDir) {
405
+ function copySkeletonTree(sourceDir, targetDir, excludedNames = new Set()) {
157
406
  fs.mkdirSync(targetDir, { recursive: true });
158
407
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
159
408
  for (const entry of entries) {
409
+ if (excludedNames.has(entry.name)) {
410
+ continue;
411
+ }
160
412
  const sourcePath = path.join(sourceDir, entry.name);
161
413
  const targetPath = path.join(targetDir, entry.name === 'gitignore' ? '.gitignore' : entry.name);
162
414
  if (entry.isDirectory()) {
163
- copySkeletonTree(sourcePath, targetPath);
415
+ copySkeletonTree(sourcePath, targetPath, excludedNames);
164
416
  }
165
417
  else if (entry.isFile() && !fs.existsSync(targetPath)) {
166
418
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
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.25';
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
  }