@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 +13 -0
- package/README.md +71 -6
- package/dist/checks.js +4 -0
- package/dist/cli.js +14 -5
- package/dist/commands.js +39 -9
- package/dist/config-tree.js +249 -0
- package/dist/constants.js +1 -1
- package/dist/defaults.js +34 -3
- package/dist/manifest.js +2 -1
- package/dist/renderer.js +20 -1
- package/dist/templates.js +3 -0
- package/dist/tools.js +6 -0
- package/examples/basic-config/README.md +2 -1
- package/examples/basic-config/agent-config/agent-run.defaults.jsonc +2 -1
- package/examples/basic-config/agent-config/global/tool-templates/AGENTS.md.njk +2 -0
- package/examples/basic-config/agent-config/global/tool-templates/CLAUDE.md.njk +1 -0
- package/examples/basic-config/agent-config/global/tool-templates/claude-settings.json.njk +2 -0
- package/examples/basic-config/agent-config/global/tool-templates/codex-config.toml.njk +2 -1
- package/examples/basic-config/agent-config/starter/basic-project/notes/memory/README.md +8 -0
- package/examples/basic-config/agent-config/starter/basic-project/overrides/claude-settings.json.njk +2 -0
- package/examples/basic-config/agent-config/starter/basic-project/overrides/codex-config.toml.njk +2 -1
- package/ops/systemd/ai-tools-update.service +1 -1
- package/package.json +1 -1
- package/scripts/install-systemd-jobs.sh +15 -5
- package/scripts/update-ai-tools.sh +37 -2
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
|
-
|
|
3
|
+
**Config once. Run with any native coding agent.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
297
|
-
`live/memories/codex-home` without overwriting existing
|
|
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
|
|
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
|
-
|
|
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]
|
|
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)(
|
|
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 +=
|
|
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);
|
package/dist/config-tree.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -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
|
}
|
|
@@ -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.
|
package/examples/basic-config/agent-config/starter/basic-project/overrides/claude-settings.json.njk
CHANGED
|
@@ -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"
|
|
@@ -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/
|
|
16
|
+
ExecStart=/usr/local/libexec/agent-run/update-ai-tools
|
package/package.json
CHANGED
|
@@ -19,10 +19,20 @@ USAGE
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
-
|
|
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 "$
|
|
75
|
-
run install -m 0755 "$
|
|
76
|
-
run install -m 0644 "$
|
|
77
|
-
run install -m 0644 "$
|
|
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/
|
|
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
|
-
|
|
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
|