@technomoron/agent-run 0.99.32 → 0.99.34
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 +41 -0
- package/README.md +83 -11
- package/dist/agent-run.js +1 -2
- package/dist/agents/claude.js +1 -5
- package/dist/brain/cli.js +31 -22
- package/dist/brain/config.js +1 -1
- package/dist/brain/git.js +8 -2
- package/dist/brain/projects.js +1 -1
- package/dist/brain/server.js +15 -6
- package/dist/brain/skills.js +2 -1
- package/dist/brain/store.js +197 -48
- package/dist/brain/todos.js +36 -7
- package/dist/commands.js +7 -9
- package/dist/config/render-instructions.js +1 -1
- package/dist/config/render-mcp.js +2 -3
- package/dist/config/render-skills.js +3 -2
- package/dist/config-tree.js +0 -22
- package/dist/guards.js +114 -8
- package/dist/process.js +0 -5
- package/dist/project.js +2 -23
- package/dist/renderer.js +7 -2
- package/dist/templates.js +35 -7
- package/examples/basic-config/agent-config/starter/basic-project/agent-run.jsonc +1 -1
- package/package.json +2 -3
- package/scripts/ensure-agent-brain.sh +1 -1
- package/scripts/install-systemd-jobs.sh +1 -8
- /package/examples/basic-config/agent-config/starter/basic-project/{overrides/triage.SKILL.md.njk → skills/triage/SKILL.md.njk} +0 -0
package/dist/brain/store.js
CHANGED
|
@@ -50,6 +50,12 @@ exports.knowledgeChanges = zod_1.z.object({
|
|
|
50
50
|
tags: zod_1.z.array(zod_1.z.string().max(100)).max(50).optional(),
|
|
51
51
|
applies_to: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional()
|
|
52
52
|
}).strict();
|
|
53
|
+
const reviewClosureSchema = zod_1.z.object({
|
|
54
|
+
id: zod_1.z.string(), finding: zod_1.z.string().regex(/^[CHML][1-9][0-9]*$/), severity: exports.severitySchema,
|
|
55
|
+
scope: exports.scopeSchema, title: zod_1.z.string(), state: zod_1.z.enum(['fixed', 'wontfix']),
|
|
56
|
+
closed: zod_1.z.string(), reason: zod_1.z.string(), source: zod_1.z.string(), revision: zod_1.z.string()
|
|
57
|
+
}).strict();
|
|
58
|
+
const reviewCountersSchema = zod_1.z.object({ C: zod_1.z.number().int().nonnegative(), H: zod_1.z.number().int().nonnegative(), M: zod_1.z.number().int().nonnegative(), L: zod_1.z.number().int().nonnegative() }).strict();
|
|
53
59
|
exports.knowledgeDirectories = {
|
|
54
60
|
rule: 'rules', preference: 'preferences', convention: 'conventions', constraint: 'constraints',
|
|
55
61
|
decision: 'decisions', spec: 'specs', review: 'reviews', memory: 'memory', observation: 'observations'
|
|
@@ -57,14 +63,35 @@ exports.knowledgeDirectories = {
|
|
|
57
63
|
function readMarkdown(text) {
|
|
58
64
|
const normalized = text.replace(/\r\n/g, '\n');
|
|
59
65
|
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(normalized);
|
|
66
|
+
if (normalized.startsWith('---\n') && !match)
|
|
67
|
+
throw new Error('Unterminated YAML front matter');
|
|
60
68
|
return match ? { metadata: (0, yaml_1.parse)(match[1], { maxAliasCount: 50 }), content: normalized.slice(match[0].length).trim() }
|
|
61
69
|
: { metadata: null, content: normalized.trim() };
|
|
62
70
|
}
|
|
63
71
|
function hash(text) { return (0, node_crypto_1.createHash)('sha256').update(text).digest('hex'); }
|
|
64
72
|
class BrainStore {
|
|
73
|
+
brokenFile(file, error, id) {
|
|
74
|
+
const message = error instanceof zod_1.z.ZodError
|
|
75
|
+
? error.issues.map((issue) => `${issue.path.join('.') || 'metadata'}: ${issue.message}`).join('; ')
|
|
76
|
+
: error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
77
|
+
return { source: path.relative(this.configRoot, file), status: 'broken', error: message, ...(id ? { id } : {}) };
|
|
78
|
+
}
|
|
79
|
+
withDiagnostics(value) {
|
|
80
|
+
const brokenFiles = [...this.knowledgeErrors, ...this.todoErrors];
|
|
81
|
+
if (!brokenFiles.length)
|
|
82
|
+
return value;
|
|
83
|
+
return Array.isArray(value) ? { items: value, brokenFiles } : { ...value, brokenFiles };
|
|
84
|
+
}
|
|
85
|
+
assertReadable(id, errors) {
|
|
86
|
+
const matches = errors.filter((item) => item.id === id || item.source === id || path.basename(item.source, '.md') === id);
|
|
87
|
+
if (matches.length)
|
|
88
|
+
throw new Error(`Cannot read or update broken file: ${matches.map((item) => `${item.source}: ${item.error}`).join('; ')}. Correct the file on disk first.`);
|
|
89
|
+
}
|
|
65
90
|
constructor(configRoot, cwd) {
|
|
66
91
|
this.configRoot = configRoot;
|
|
67
92
|
this.cwd = cwd;
|
|
93
|
+
this.knowledgeErrors = [];
|
|
94
|
+
this.todoErrors = [];
|
|
68
95
|
this.configRoot = path.resolve(configRoot);
|
|
69
96
|
this.projectRoot = (0, config_1.registeredBrainProject)(this.configRoot, cwd)?.root ?? (0, project_1.findProjectRoot)(cwd);
|
|
70
97
|
const resolved = (0, project_1.resolveProfileResult)(this.projectRoot, this.configRoot).profile;
|
|
@@ -107,14 +134,31 @@ class BrainStore {
|
|
|
107
134
|
throw new Error(`Scope ${scope} is not available in this environment`);
|
|
108
135
|
return this.safePath(directory);
|
|
109
136
|
}
|
|
110
|
-
files(directory) {
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
files(directory, options = {}) {
|
|
138
|
+
const { onError } = options;
|
|
139
|
+
try {
|
|
140
|
+
this.safePath(directory);
|
|
141
|
+
if (!fs.existsSync(directory))
|
|
142
|
+
return [];
|
|
143
|
+
return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
|
|
144
|
+
try {
|
|
145
|
+
const file = this.safePath(directory, entry.name);
|
|
146
|
+
return entry.isDirectory() ? this.files(file, options) : entry.isFile() && (options.includeAllFiles || entry.name.endsWith('.md')) ? [file] : [];
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!onError)
|
|
150
|
+
throw error;
|
|
151
|
+
onError(path.join(directory, entry.name), error);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!onError)
|
|
158
|
+
throw error;
|
|
159
|
+
onError(directory, error);
|
|
113
160
|
return [];
|
|
114
|
-
|
|
115
|
-
const file = this.safePath(directory, entry.name);
|
|
116
|
-
return entry.isDirectory() ? this.files(file) : entry.isFile() && entry.name.endsWith('.md') ? [file] : [];
|
|
117
|
-
});
|
|
161
|
+
}
|
|
118
162
|
}
|
|
119
163
|
read(file) {
|
|
120
164
|
const target = this.safePath(file);
|
|
@@ -155,72 +199,113 @@ class BrainStore {
|
|
|
155
199
|
}
|
|
156
200
|
}
|
|
157
201
|
allKnowledge() {
|
|
202
|
+
this.knowledgeErrors.length = 0;
|
|
158
203
|
const result = [];
|
|
159
204
|
for (const { scope, directory } of this.scopes) {
|
|
160
205
|
const directories = [...Object.entries(exports.knowledgeDirectories), ['memory', path.join('notes', 'memory')]];
|
|
161
206
|
if (scope === 'global')
|
|
162
207
|
directories.push(['memory', path.relative(directory, path.join(this.configRoot, 'notes', 'memory'))]);
|
|
163
208
|
for (const [type, folder] of directories) {
|
|
164
|
-
for (const file of this.files(path.join(directory, folder))) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
209
|
+
for (const file of this.files(path.join(directory, folder), { onError: (file, error) => this.knowledgeErrors.push(this.brokenFile(file, error)) })) {
|
|
210
|
+
let id;
|
|
211
|
+
try {
|
|
212
|
+
const text = this.read(file);
|
|
213
|
+
const document = readMarkdown(text);
|
|
214
|
+
if (document.metadata && typeof document.metadata === 'object' && 'id' in document.metadata && typeof document.metadata.id === 'string')
|
|
215
|
+
id = document.metadata.id;
|
|
216
|
+
const source = path.relative(this.configRoot, file);
|
|
217
|
+
const legacy = document.metadata !== null && typeof document.metadata === 'object' && !Array.isArray(document.metadata)
|
|
218
|
+
&& !('id' in document.metadata) && !('type' in document.metadata);
|
|
219
|
+
const legacyStatus = legacy ? document.metadata.status : undefined;
|
|
220
|
+
const metadata = document.metadata === null || legacy ? {
|
|
221
|
+
id: `document-${hash(source).slice(0, 24)}`, scope, type,
|
|
222
|
+
title: /^#\s+(.+)$/m.exec(document.content)?.[1] ?? path.basename(file, '.md'),
|
|
223
|
+
status: type === 'review' && ['addressed', 'resolved', 'done', 'closed'].includes(String(legacyStatus)) ? 'deprecated' : 'active',
|
|
224
|
+
created: '', updated: ''
|
|
225
|
+
} : { title: /^#\s+(.+)$/m.exec(document.content)?.[1] ?? path.basename(file, '.md'), ...document.metadata };
|
|
226
|
+
const parsed = exports.knowledgeMetadata.safeParse(metadata);
|
|
227
|
+
if (!parsed.success)
|
|
228
|
+
throw parsed.error;
|
|
229
|
+
if (parsed.data.scope !== scope)
|
|
230
|
+
throw new Error(`Knowledge scope does not match its directory: ${source}`);
|
|
231
|
+
result.push({ ...parsed.data, content: legacy ? text.trim() : document.content, source, revision: hash(text) });
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
this.knowledgeErrors.push(this.brokenFile(file, error, id));
|
|
235
|
+
}
|
|
183
236
|
}
|
|
184
237
|
}
|
|
185
238
|
}
|
|
186
|
-
const
|
|
239
|
+
const counts = new Map();
|
|
240
|
+
for (const item of [...result, ...this.knowledgeErrors]) {
|
|
241
|
+
if (item.id)
|
|
242
|
+
counts.set(item.id, (counts.get(item.id) ?? 0) + 1);
|
|
243
|
+
}
|
|
187
244
|
for (const item of result) {
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
ids.add(item.id);
|
|
245
|
+
if (counts.get(item.id) > 1)
|
|
246
|
+
this.knowledgeErrors.push(this.brokenFile(path.join(this.configRoot, item.source), new Error(`Duplicate knowledge id: ${item.id}`), item.id));
|
|
191
247
|
}
|
|
192
|
-
return result;
|
|
248
|
+
return result.filter((item) => counts.get(item.id) === 1);
|
|
193
249
|
}
|
|
194
250
|
search(query, options = {}) {
|
|
195
|
-
|
|
251
|
+
return this.searchItems(this.allKnowledge(), query, options);
|
|
252
|
+
}
|
|
253
|
+
searchItems(items, query, options = {}) {
|
|
196
254
|
const environment = this.profile ?? 'default';
|
|
255
|
+
this.refreshIndex(items, environment);
|
|
256
|
+
const terms = query.match(/[\p{L}\p{N}_-]+/gu)?.slice(0, 50) ?? [];
|
|
257
|
+
const ranked = terms.length ? this.db.prepare('SELECT source FROM knowledge WHERE knowledge MATCH ? AND environment = ? ORDER BY bm25(knowledge, 0, 0, 5, 1, 3), source')
|
|
258
|
+
.all(terms.map((term) => `"${term.replace(/"/g, '""')}"`).join(' OR '), environment).map((row) => String(row.source)) : items.map((item) => item.source);
|
|
259
|
+
const bySource = new Map(items.map((item) => [item.source, item]));
|
|
260
|
+
return ranked.map((source) => bySource.get(source)).filter((item) => item !== undefined).filter((item) => (options.includeDeprecated || item.status === 'active') && (!options.scopes || options.scopes.includes(item.scope)) &&
|
|
261
|
+
(!options.types || options.types.includes(item.type))).slice(0, options.limit ?? 50);
|
|
262
|
+
}
|
|
263
|
+
refreshIndex(items, environment) {
|
|
264
|
+
const desired = new Map(items.map((item) => [item.source, {
|
|
265
|
+
source: item.source, title: item.title, content: item.content, tags: [...item.tags, item.finding ?? ''].join(' ').trim()
|
|
266
|
+
}]));
|
|
267
|
+
const changes = () => {
|
|
268
|
+
const current = this.db.prepare('SELECT rowid, source, title, content, tags FROM knowledge WHERE environment = ?').all(environment);
|
|
269
|
+
const unchanged = new Set();
|
|
270
|
+
const removed = current.filter((row) => {
|
|
271
|
+
const item = desired.get(String(row.source));
|
|
272
|
+
if (item && !unchanged.has(item.source) && row.title === item.title && row.content === item.content && row.tags === item.tags) {
|
|
273
|
+
unchanged.add(item.source);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
return { removed, added: [...desired.values()].filter((item) => !unchanged.has(item.source)) };
|
|
279
|
+
};
|
|
280
|
+
const pending = changes();
|
|
281
|
+
if (!pending.removed.length && !pending.added.length)
|
|
282
|
+
return;
|
|
197
283
|
this.db.exec('BEGIN IMMEDIATE');
|
|
198
284
|
try {
|
|
199
|
-
|
|
285
|
+
// Another connection may have refreshed the index while we waited for the lock.
|
|
286
|
+
const { removed, added } = changes();
|
|
287
|
+
const remove = this.db.prepare('DELETE FROM knowledge WHERE rowid = ?');
|
|
288
|
+
for (const row of removed)
|
|
289
|
+
remove.run(row.rowid);
|
|
200
290
|
const insert = this.db.prepare('INSERT INTO knowledge(source, environment, title, content, tags) VALUES (?, ?, ?, ?, ?)');
|
|
201
|
-
for (const item of
|
|
202
|
-
insert.run(item.source, environment, item.title, item.content,
|
|
291
|
+
for (const item of added)
|
|
292
|
+
insert.run(item.source, environment, item.title, item.content, item.tags);
|
|
203
293
|
this.db.exec('COMMIT');
|
|
204
294
|
}
|
|
205
295
|
catch (error) {
|
|
206
296
|
this.db.exec('ROLLBACK');
|
|
207
297
|
throw error;
|
|
208
298
|
}
|
|
209
|
-
const terms = query.match(/[\p{L}\p{N}_-]+/gu)?.slice(0, 50) ?? [];
|
|
210
|
-
const ranked = terms.length ? this.db.prepare('SELECT source FROM knowledge WHERE knowledge MATCH ? AND environment = ? ORDER BY bm25(knowledge, 0, 0, 5, 1, 3), source')
|
|
211
|
-
.all(terms.map((term) => `"${term.replace(/"/g, '""')}"`).join(' OR '), environment).map((row) => String(row.source)) : items.map((item) => item.source);
|
|
212
|
-
const bySource = new Map(items.map((item) => [item.source, item]));
|
|
213
|
-
return ranked.map((source) => bySource.get(source)).filter((item) => (options.includeDeprecated || item.status === 'active') && (!options.scopes || options.scopes.includes(item.scope)) &&
|
|
214
|
-
(!options.types || options.types.includes(item.type))).slice(0, options.limit ?? 50);
|
|
215
299
|
}
|
|
216
300
|
context(task, files = [], symbols = []) {
|
|
217
|
-
const
|
|
301
|
+
const scanned = this.allKnowledge();
|
|
302
|
+
const all = scanned.filter((item) => item.status === 'active');
|
|
218
303
|
const always = all.filter((item) => item.recall === 'always');
|
|
219
304
|
const matching = all.filter((item) => item.applies_to.some((pattern) => files.some((file) => {
|
|
220
305
|
const relative = path.isAbsolute(file) ? path.relative(this.projectRoot, file) : file;
|
|
221
306
|
return path.matchesGlob(relative, pattern);
|
|
222
307
|
})));
|
|
223
|
-
const selected = [...new Map([...always, ...matching, ...this.
|
|
308
|
+
const selected = [...new Map([...always, ...matching, ...this.searchItems(scanned, [task, ...symbols].join(' '))].map((item) => [item.id, item])).values()];
|
|
224
309
|
let size = 0;
|
|
225
310
|
const items = selected.filter((item) => {
|
|
226
311
|
const length = JSON.stringify(item).length;
|
|
@@ -283,7 +368,13 @@ class BrainStore {
|
|
|
283
368
|
.filter((item) => item.scope === scope && item.type === 'review' && item.finding?.startsWith(prefix))
|
|
284
369
|
.map((item) => Number.parseInt(item.finding.slice(prefix.length), 10))
|
|
285
370
|
.filter((sequence) => Number.isSafeInteger(sequence));
|
|
286
|
-
|
|
371
|
+
if (this.knowledgeErrors.some((item) => (0, utils_1.isSamePathOrDescendant)(path.join(this.configRoot, item.source), path.join(this.scopeDirectory(scope), 'reviews')))) {
|
|
372
|
+
throw new Error('Cannot assign a review number while review files are broken. List reviews and correct the reported files first.');
|
|
373
|
+
}
|
|
374
|
+
const counters = this.reviewCounters(scope);
|
|
375
|
+
const next = Math.max(counters[prefix], 0, ...used) + 1;
|
|
376
|
+
this.reserveFinding(scope, `${prefix}${next}`);
|
|
377
|
+
return `${prefix}${next}`;
|
|
287
378
|
}
|
|
288
379
|
/** Review findings in severity then sequence order. Open findings only unless states are given. */
|
|
289
380
|
reviews(options = {}) {
|
|
@@ -294,6 +385,65 @@ class BrainStore {
|
|
|
294
385
|
.filter((item) => (options.state ?? ['open']).includes(item.state ?? 'open'))
|
|
295
386
|
.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || sequence(left) - sequence(right));
|
|
296
387
|
}
|
|
388
|
+
reviewCounters(scope) {
|
|
389
|
+
const file = this.safePath(this.scopeDirectory(scope), 'review-counters.json');
|
|
390
|
+
return fs.existsSync(file) ? reviewCountersSchema.parse(JSON.parse(fs.readFileSync(file, 'utf8'))) : { C: 0, H: 0, M: 0, L: 0 };
|
|
391
|
+
}
|
|
392
|
+
reserveFinding(scope, finding) {
|
|
393
|
+
const counters = this.reviewCounters(scope);
|
|
394
|
+
const prefix = finding[0];
|
|
395
|
+
const number = Number(finding.slice(1));
|
|
396
|
+
if (!Number.isSafeInteger(number) || number < 1)
|
|
397
|
+
throw new Error('Invalid review number');
|
|
398
|
+
counters[prefix] = Math.max(counters[prefix], number);
|
|
399
|
+
this.atomicWrite(path.join(this.scopeDirectory(scope), 'review-counters.json'), JSON.stringify(counters) + '\n');
|
|
400
|
+
}
|
|
401
|
+
closedReviews(scope) {
|
|
402
|
+
const file = this.safePath(this.scopeDirectory(scope), 'review-history.jsonl');
|
|
403
|
+
if (!fs.existsSync(file))
|
|
404
|
+
return [];
|
|
405
|
+
return fs.readFileSync(file, 'utf8').split('\n').filter((line) => line.trim()).map((line) => {
|
|
406
|
+
const record = reviewClosureSchema.parse(JSON.parse(line));
|
|
407
|
+
if (record.scope !== scope)
|
|
408
|
+
throw new Error(`Review history scope mismatch: ${file}`);
|
|
409
|
+
return record;
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
reviewHistory(options = {}) {
|
|
413
|
+
const query = options.query?.toLowerCase();
|
|
414
|
+
const items = this.scopes.filter((entry) => !options.scope || entry.scope === options.scope)
|
|
415
|
+
.flatMap((entry) => this.closedReviews(entry.scope))
|
|
416
|
+
.filter((item) => (!options.state || item.state === options.state) && (!query || [item.id, item.finding, item.title, item.reason].some((value) => value.toLowerCase().includes(query))))
|
|
417
|
+
.sort((a, b) => b.closed.localeCompare(a.closed) || a.id.localeCompare(b.id));
|
|
418
|
+
const offset = options.offset ?? 0;
|
|
419
|
+
return { items: items.slice(offset, offset + (options.limit ?? 100)), total: items.length };
|
|
420
|
+
}
|
|
421
|
+
archiveReview(item, state, reason, closed) {
|
|
422
|
+
if (!item.finding || !item.severity)
|
|
423
|
+
throw new Error('Only numbered review findings can be archived.');
|
|
424
|
+
const records = this.closedReviews(item.scope);
|
|
425
|
+
const existing = records.find((record) => record.id === item.id);
|
|
426
|
+
if (existing && (existing.revision !== item.revision || existing.state !== state))
|
|
427
|
+
throw new Error('Archived review conflicts with the current file; inspect review history before updating.');
|
|
428
|
+
const record = existing ?? { id: item.id, finding: item.finding, severity: item.severity, scope: item.scope, title: item.title, state, closed, reason, source: item.source, revision: item.revision };
|
|
429
|
+
// Persist the number and closure before removing the full finding. A retry can finish deletion.
|
|
430
|
+
this.reserveFinding(item.scope, item.finding);
|
|
431
|
+
if (!existing)
|
|
432
|
+
this.atomicWrite(path.join(this.scopeDirectory(item.scope), 'review-history.jsonl'), [...records, record].map((entry) => JSON.stringify(entry)).join('\n') + '\n');
|
|
433
|
+
fs.unlinkSync(this.safePath(item.source));
|
|
434
|
+
return record;
|
|
435
|
+
}
|
|
436
|
+
archiveReviews(scope) {
|
|
437
|
+
return this.writeLocked(() => {
|
|
438
|
+
const history = this.closedReviews(scope);
|
|
439
|
+
return this.allKnowledge().filter((item) => item.scope === scope && item.type === 'review' && item.finding)
|
|
440
|
+
.filter((item) => item.state === 'fixed' || item.state === 'wontfix' || history.some((record) => record.id === item.id))
|
|
441
|
+
.map((item) => {
|
|
442
|
+
const prior = history.find((record) => record.id === item.id);
|
|
443
|
+
return this.archiveReview(item, prior?.state ?? item.state, prior?.reason ?? item.reason ?? '', prior?.closed ?? item.updated);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
297
447
|
resolveReview(id, revision, state, reason) {
|
|
298
448
|
return this.writeLocked(() => {
|
|
299
449
|
const item = this.get(id);
|
|
@@ -301,9 +451,7 @@ class BrainStore {
|
|
|
301
451
|
throw new Error('Only review findings can be resolved.');
|
|
302
452
|
if (item.revision !== revision)
|
|
303
453
|
throw new Error('Knowledge changed; read the current revision before updating.');
|
|
304
|
-
|
|
305
|
-
this.atomicWrite(source, `---\n${(0, yaml_1.stringify)({ ...metadata, state, status: 'deprecated', updated: new Date().toISOString(), ...(reason ? { reason } : {}) })}---\n\n${content}\n`);
|
|
306
|
-
return this.get(id);
|
|
454
|
+
return this.archiveReview(item, state, reason ?? '', new Date().toISOString());
|
|
307
455
|
});
|
|
308
456
|
}
|
|
309
457
|
deprecate(id, revision, reason) {
|
|
@@ -335,8 +483,9 @@ class BrainStore {
|
|
|
335
483
|
}
|
|
336
484
|
get(id) {
|
|
337
485
|
const item = this.allKnowledge().find((entry) => entry.id === id);
|
|
486
|
+
this.assertReadable(id, this.knowledgeErrors);
|
|
338
487
|
if (!item)
|
|
339
|
-
throw new Error(`Knowledge not found in the active scopes: ${id}`);
|
|
488
|
+
throw new Error(`Knowledge not found in the active scopes: ${id}${this.knowledgeErrors.length ? '. Some files are broken; list or search knowledge for their paths and errors' : ''}`);
|
|
340
489
|
return item;
|
|
341
490
|
}
|
|
342
491
|
}
|
package/dist/brain/todos.js
CHANGED
|
@@ -26,17 +26,42 @@ exports.todoInput = zod_1.z.object({
|
|
|
26
26
|
const remoteTask = exports.todoInput.pick({ title: true, description: true, status: true, labels: true, due: true }).strip();
|
|
27
27
|
const todoMetadata = exports.todoInput.omit({ description: true }).extend({ id: zod_1.z.uuid(), created: zod_1.z.string(), updated: zod_1.z.string(), remote: remoteTask.optional() });
|
|
28
28
|
function listTodos(store) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
store.todoErrors.length = 0;
|
|
30
|
+
const sources = new Map();
|
|
31
|
+
const todos = store.scopes.flatMap(({ scope, directory }) => store.files(path.join(directory, 'todo'), { onError: (file, error) => store.todoErrors.push(store.brokenFile(file, error)) }).flatMap((file) => {
|
|
32
|
+
let id;
|
|
33
|
+
try {
|
|
34
|
+
const text = store.read(file);
|
|
35
|
+
const document = (0, store_1.readMarkdown)(text);
|
|
36
|
+
if (document.metadata && typeof document.metadata === 'object' && 'id' in document.metadata && typeof document.metadata.id === 'string')
|
|
37
|
+
id = document.metadata.id;
|
|
38
|
+
const metadata = todoMetadata.parse(document.metadata);
|
|
39
|
+
if (metadata.scope !== scope || path.basename(file) !== `${metadata.id}.md`)
|
|
40
|
+
throw new Error(`Todo metadata does not match its location: ${file}`);
|
|
41
|
+
const todo = { ...metadata, description: document.content, revision: (0, node_crypto_1.createHash)('sha256').update(text).digest('hex') };
|
|
42
|
+
sources.set(todo, file);
|
|
43
|
+
return [todo];
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
store.todoErrors.push(store.brokenFile(file, error, id));
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
36
49
|
}));
|
|
50
|
+
const counts = new Map();
|
|
51
|
+
for (const item of [...todos, ...store.todoErrors]) {
|
|
52
|
+
if (item.id)
|
|
53
|
+
counts.set(item.id, (counts.get(item.id) ?? 0) + 1);
|
|
54
|
+
}
|
|
55
|
+
return todos.filter((todo) => {
|
|
56
|
+
if (counts.get(todo.id) === 1)
|
|
57
|
+
return true;
|
|
58
|
+
store.todoErrors.push(store.brokenFile(sources.get(todo), new Error(`Duplicate todo id: ${todo.id}`), todo.id));
|
|
59
|
+
return false;
|
|
60
|
+
});
|
|
37
61
|
}
|
|
38
62
|
function getTodo(store, id) {
|
|
39
63
|
const todo = listTodos(store).find((item) => item.id === id);
|
|
64
|
+
store.assertReadable(id, store.todoErrors);
|
|
40
65
|
if (!todo)
|
|
41
66
|
throw new Error(`Todo not found: ${id}`);
|
|
42
67
|
return todo;
|
|
@@ -54,6 +79,8 @@ function addTodo(store, input) {
|
|
|
54
79
|
const existing = listTodos(store).find((item) => item.scope === value.scope && item.source?.type === value.source?.type && item.source?.external_id === value.source?.external_id);
|
|
55
80
|
if (existing)
|
|
56
81
|
return existing;
|
|
82
|
+
if (store.todoErrors.length)
|
|
83
|
+
throw new Error('Cannot deduplicate an external task while todo files are broken. List tasks and correct the reported files first.');
|
|
57
84
|
}
|
|
58
85
|
const now = new Date().toISOString();
|
|
59
86
|
return writeTodo(store, { ...value, id: (0, node_crypto_1.randomUUID)(), created: now, updated: now });
|
|
@@ -86,6 +113,8 @@ function importTodos(store, input) {
|
|
|
86
113
|
return store.writeLocked(() => {
|
|
87
114
|
const result = { imported: [], updated: [], conflicts: [] };
|
|
88
115
|
const existing = listTodos(store);
|
|
116
|
+
if (store.todoErrors.length)
|
|
117
|
+
throw new Error('Cannot import tasks while todo files are broken. List tasks and correct the reported files first.');
|
|
89
118
|
for (const item of incoming) {
|
|
90
119
|
if (!item.source)
|
|
91
120
|
throw new Error('Imported tasks require an external source');
|
package/dist/commands.js
CHANGED
|
@@ -89,15 +89,15 @@ function runTool(parsed) {
|
|
|
89
89
|
prepareConfigRoot(configRoot);
|
|
90
90
|
requireProfile(command, agentDir);
|
|
91
91
|
migrateProfileOnStart(configRoot, agentDir, false);
|
|
92
|
-
const preview = (0, renderer_1.renderProfile)(projectRoot, agentDir, true
|
|
92
|
+
const preview = (0, renderer_1.renderProfile)(projectRoot, agentDir, true);
|
|
93
93
|
ensureToolEnabled(preview.context, command);
|
|
94
94
|
if (wrapperArgs.generate) {
|
|
95
|
-
printUpdateSummary((0, renderer_1.
|
|
95
|
+
printUpdateSummary((0, renderer_1.syncRenderedProfile)(preview));
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
checkLocalAiFiles(command, projectRoot, agentDir, preview.context, wrapperArgs.local);
|
|
99
99
|
const realBinary = (0, process_1.findRealBinary)(command);
|
|
100
|
-
const rendered = (0, renderer_1.
|
|
100
|
+
const rendered = (0, renderer_1.syncRenderedProfile)(preview);
|
|
101
101
|
const runtime = rendered.runtimes[command];
|
|
102
102
|
if (!runtime) {
|
|
103
103
|
(0, utils_1.fail)(`no generated ${command} runtime found for profile ${rendered.profile}`);
|
|
@@ -186,7 +186,7 @@ function runGenerate(parsed) {
|
|
|
186
186
|
if (projectRoot === null) {
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
|
-
const agentDir = initializeProfileSource(projectRoot
|
|
189
|
+
const agentDir = initializeProfileSource(projectRoot);
|
|
190
190
|
migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
|
|
191
191
|
printUpdateSummary((0, renderer_1.syncAgentProfile)(projectRoot, agentDir));
|
|
192
192
|
}
|
|
@@ -200,7 +200,6 @@ function runSetup(parsed) {
|
|
|
200
200
|
(0, utils_1.fail)(`starter config skeleton not found: ${sourcePath}`);
|
|
201
201
|
}
|
|
202
202
|
(0, config_tree_1.copySkeletonTree)(sourcePath, configRoot, new Set(['starter']));
|
|
203
|
-
(0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
|
|
204
203
|
const agentDir = path.join(configRoot, profile);
|
|
205
204
|
migrateProfileOnStart(configRoot, agentDir, false);
|
|
206
205
|
(0, config_tree_1.copySkeletonTree)(path.join(sourcePath, 'starter', 'basic-project'), agentDir);
|
|
@@ -265,7 +264,7 @@ function runEdit(parsed) {
|
|
|
265
264
|
if (projectRoot === null) {
|
|
266
265
|
return;
|
|
267
266
|
}
|
|
268
|
-
const agentDir = initializeProfileSource(projectRoot
|
|
267
|
+
const agentDir = initializeProfileSource(projectRoot);
|
|
269
268
|
migrateProfileOnStart((0, project_1.defaultConfigRoot)(projectRoot), agentDir, false);
|
|
270
269
|
const editPath = (0, manifest_1.createDefaultLocalFile)(agentDir);
|
|
271
270
|
(0, renderer_1.syncAgentProfile)(projectRoot, agentDir);
|
|
@@ -387,7 +386,6 @@ function prepareConfigRoot(configRoot) {
|
|
|
387
386
|
(0, config_tree_1.ensureConfigRootLayout)(configRoot);
|
|
388
387
|
(0, config_tree_1.ensureConfigRootGitignore)(configRoot);
|
|
389
388
|
(0, config_tree_1.ensureDefaultGlobalTemplates)(configRoot);
|
|
390
|
-
(0, config_tree_1.ensurePortableSystemdFiles)(configRoot);
|
|
391
389
|
}
|
|
392
390
|
function activeProject(targetPath, action) {
|
|
393
391
|
const projectRoot = (0, project_1.findProjectRoot)(targetPath);
|
|
@@ -398,8 +396,8 @@ function activeProject(targetPath, action) {
|
|
|
398
396
|
process.stdout.write(`SKIP ignored ${projectRoot}\n`);
|
|
399
397
|
return null;
|
|
400
398
|
}
|
|
401
|
-
function initializeProfileSource(projectRoot
|
|
402
|
-
const configRoot = (0, project_1.defaultConfigRoot)(projectRoot
|
|
399
|
+
function initializeProfileSource(projectRoot) {
|
|
400
|
+
const configRoot = (0, project_1.defaultConfigRoot)(projectRoot);
|
|
403
401
|
const resolved = (0, project_1.resolveProfileResult)(projectRoot, configRoot, false);
|
|
404
402
|
if (!resolved.profile)
|
|
405
403
|
throw new Error(resolved.reason);
|
|
@@ -11,7 +11,7 @@ const templates_1 = require("../templates");
|
|
|
11
11
|
function buildCanonicalInstructions(env, configRoot, manifest, context, trace) {
|
|
12
12
|
const sections = [(0, templates_1.renderTemplateFile)(env, configRoot, manifest.agent.base, context, trace)];
|
|
13
13
|
for (const include of manifest.agent.includes) {
|
|
14
|
-
const includePath = (0, templates_1.resolveConfigPath)(configRoot, include, context);
|
|
14
|
+
const includePath = (0, templates_1.resolveConfigPath)(configRoot, include, context, env);
|
|
15
15
|
if (!fs.existsSync(includePath)) {
|
|
16
16
|
if (include.includes('AGENTS-MODS.md') || include.includes(constants_1.LOCAL_TEMPLATE_FILE_NAME)) {
|
|
17
17
|
continue;
|
|
@@ -45,6 +45,8 @@ function renderJsonMcp(servers, target) {
|
|
|
45
45
|
function renderTomlMcp(servers, target) {
|
|
46
46
|
const sections = [];
|
|
47
47
|
for (const [name, server] of Object.entries(servers)) {
|
|
48
|
+
if (!server.enabled)
|
|
49
|
+
continue;
|
|
48
50
|
const lines = [`[mcp_servers.${tomlKey(name)}]`];
|
|
49
51
|
if (server.transport === 'stdio') {
|
|
50
52
|
lines.push(`command = ${tomlString(server.command ?? '')}`);
|
|
@@ -64,9 +66,6 @@ function renderTomlMcp(servers, target) {
|
|
|
64
66
|
lines.push(`${target === 'codex' ? 'http_headers' : 'headers'} = ${tomlInlineTable(server.headers)}`);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
|
-
if (!server.enabled) {
|
|
68
|
-
lines.push('enabled = false');
|
|
69
|
-
}
|
|
70
69
|
sections.push(lines.join('\n'));
|
|
71
70
|
}
|
|
72
71
|
return sections.length > 0 ? `${sections.join('\n\n')}\n` : '';
|
|
@@ -7,14 +7,15 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const templates_1 = require("../templates");
|
|
9
9
|
function buildCanonicalSkills(env, configRoot, manifest, context, trace) {
|
|
10
|
+
const skillEnv = (0, templates_1.createSkillNunjucksEnv)(configRoot, context.profileDir, trace);
|
|
10
11
|
return manifest.skills.install.map((name) => {
|
|
11
12
|
const canonical = `global/skills/${name}/SKILL.md`;
|
|
12
13
|
const sourceTemplate = manifest.skills.overrides[name] ?? (fs.existsSync(path.join(configRoot, canonical)) ? canonical : `${canonical}.njk`);
|
|
13
|
-
const sourcePath = (0, templates_1.resolveConfigPath)(configRoot, sourceTemplate, context);
|
|
14
|
+
const sourcePath = (0, templates_1.resolveConfigPath)(configRoot, sourceTemplate, context, env);
|
|
14
15
|
if (!fs.existsSync(sourcePath)) {
|
|
15
16
|
throw new Error(`missing skill template for ${name}: ${sourcePath}`);
|
|
16
17
|
}
|
|
17
|
-
const renderedContent = (
|
|
18
|
+
const renderedContent = skillEnv.render(path.relative(configRoot, sourcePath), context);
|
|
18
19
|
(0, templates_1.assertNoUnexpandedTemplateVars)(`skill ${name}`, renderedContent);
|
|
19
20
|
validateRenderedSkill(renderedContent, sourcePath);
|
|
20
21
|
return { name, sourcePath, renderedContent, description: extractSkillDescription(renderedContent) };
|
package/dist/config-tree.js
CHANGED
|
@@ -3,7 +3,6 @@ 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;
|
|
7
6
|
exports.migrateOldTemplates = migrateOldTemplates;
|
|
8
7
|
exports.findLegacyProfileDirs = findLegacyProfileDirs;
|
|
9
8
|
exports.findProfileDirs = findProfileDirs;
|
|
@@ -75,27 +74,6 @@ function ensureDefaultGlobalTemplates(configRoot) {
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
|
-
function ensurePortableSystemdFiles(configRoot) {
|
|
79
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
80
|
-
const files = new Map([
|
|
81
|
-
['install-systemd-jobs.sh', { source: 'scripts/install-systemd-jobs.sh', mode: 0o755 }],
|
|
82
|
-
['update-ai-tools.sh', { source: 'scripts/update-ai-tools.sh', mode: 0o755 }],
|
|
83
|
-
['ensure-agent-brain.sh', { source: 'scripts/ensure-agent-brain.sh', mode: 0o755 }],
|
|
84
|
-
['agent-brain.service', { source: 'ops/systemd/agent-brain.service', mode: 0o644 }],
|
|
85
|
-
['ai-tools-update.service', { source: 'ops/systemd/ai-tools-update.service', mode: 0o644 }],
|
|
86
|
-
['ai-tools-update.timer', { source: 'ops/systemd/ai-tools-update.timer', mode: 0o644 }]
|
|
87
|
-
]);
|
|
88
|
-
for (const [targetName, asset] of files) {
|
|
89
|
-
const sourcePath = path.join(packageRoot, asset.source);
|
|
90
|
-
const targetPath = path.join(configRoot, targetName);
|
|
91
|
-
if (!fs.existsSync(sourcePath)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
95
|
-
fs.chmodSync(targetPath, asset.mode);
|
|
96
|
-
(0, utils_1.verbose)(`updated ${targetPath}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
77
|
function migrateOldTemplates(configRoot) {
|
|
100
78
|
const oldCodeTemplate = path.join(configRoot, 'templates', 'AGENTS-CODE.md');
|
|
101
79
|
const newCodeTemplate = path.join(configRoot, 'global', 'agents', 'code.md.njk');
|