@ijfw/memory-server 1.3.0 → 1.4.1
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/README.md +67 -0
- package/fixtures/team/book.json +47 -0
- package/fixtures/team/business.json +47 -0
- package/fixtures/team/content.json +47 -0
- package/fixtures/team/design.json +47 -0
- package/fixtures/team/mixed.json +59 -0
- package/fixtures/team/research.json +47 -0
- package/fixtures/team/software.json +47 -0
- package/package.json +1 -9
- package/src/.registry-meta-key.pem +3 -0
- package/src/active-extension-writer.js +142 -0
- package/src/blackboard.js +360 -0
- package/src/cli-run.js +91 -0
- package/src/codex-agents.js +177 -0
- package/src/compute/extract.js +3 -0
- package/src/compute/fts5.js +4 -4
- package/src/compute/graph-lock.js +0 -2
- package/src/compute/migrations/003-tier-semantic.js +3 -3
- package/src/compute/runner.js +44 -15
- package/src/compute/schema.sql +1 -1
- package/src/cross-orchestrator-cli.js +974 -13
- package/src/cross-orchestrator.js +9 -1
- package/src/dashboard-client.html +353 -1
- package/src/dashboard-server.js +318 -2
- package/src/design-intelligence.js +721 -0
- package/src/dispatch/colon-syntax.js +31 -3
- package/src/dispatch/domain-manifest.js +251 -0
- package/src/dispatch/extension.js +637 -0
- package/src/dispatch/override.js +221 -0
- package/src/dispatch-planner.js +1 -0
- package/src/dream/runner.mjs +3 -3
- package/src/extension-installer.js +1269 -0
- package/src/extension-manifest-schema.js +301 -0
- package/src/extension-permission-check.mjs +79 -0
- package/src/extension-registry.js +619 -0
- package/src/extension-signer.js +905 -0
- package/src/gate-result-formatter.js +95 -0
- package/src/gate-result-schema.js +274 -0
- package/src/gate-result.js +195 -0
- package/src/intent-router.js +2 -0
- package/src/lib/npm-view.js +1 -0
- package/src/memory/fts5.js +3 -3
- package/src/memory/migrations/002-tier-semantic.js +2 -2
- package/src/memory/staleness.js +1 -1
- package/src/memory/tier-promotion.js +6 -6
- package/src/memory/tokenize.js +1 -1
- package/src/memory-feedback.js +372 -0
- package/src/override-manifest-schema.js +146 -0
- package/src/override-resolver.js +699 -0
- package/src/override-use-registry.js +307 -0
- package/src/overrides/presets/academic.md +101 -0
- package/src/overrides/presets/book.md +87 -0
- package/src/overrides/presets/campaign.md +95 -0
- package/src/overrides/presets/screenplay.md +99 -0
- package/src/recovery/checkpoint.js +191 -0
- package/src/redactor.js +2 -0
- package/src/runtime-mediator.js +207 -0
- package/src/sandbox.js +17 -3
- package/src/server.js +94 -2
- package/src/swarm/dispatch-prompt.js +154 -0
- package/src/swarm/planner.js +399 -0
- package/src/swarm/review.js +136 -0
- package/src/swarm/worktree.js +239 -0
- package/src/team/generator.js +119 -0
- package/src/team/schemas.js +341 -0
- package/src/trident/dispatch.js +47 -0
- package/src/update-check.js +1 -1
- package/src/vectors.js +7 -8
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Project-local blackboard coordination for IJFW swarm work.
|
|
2
|
+
//
|
|
3
|
+
// Runtime state lives under <project>/.ijfw/blackboard/. It is deliberately
|
|
4
|
+
// small and dependency-free: tasks/claims are atomic JSON, notes are append-only
|
|
5
|
+
// JSONL, and handoff is plain markdown.
|
|
6
|
+
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { writeAtomic, readSafe, withLock } from './lib/atomic-io.js';
|
|
10
|
+
|
|
11
|
+
export const BLACKBOARD_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
export function blackboardPaths(projectRoot = process.cwd()) {
|
|
14
|
+
const root = resolve(projectRoot);
|
|
15
|
+
const dir = join(root, '.ijfw', 'blackboard');
|
|
16
|
+
return {
|
|
17
|
+
root,
|
|
18
|
+
dir,
|
|
19
|
+
lock: join(dir, '.lock'),
|
|
20
|
+
tasks: join(dir, 'tasks.json'),
|
|
21
|
+
claims: join(dir, 'claims.json'),
|
|
22
|
+
findings: join(dir, 'findings.jsonl'),
|
|
23
|
+
decisions: join(dir, 'decisions.jsonl'),
|
|
24
|
+
blockers: join(dir, 'blockers.jsonl'),
|
|
25
|
+
notes: join(dir, 'notes.jsonl'),
|
|
26
|
+
events: join(dir, 'events.jsonl'),
|
|
27
|
+
handoff: join(dir, 'handoff.md'),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nowIso() {
|
|
32
|
+
return new Date().toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureDir(paths) {
|
|
36
|
+
if (!existsSync(paths.dir)) mkdirSync(paths.dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function defaultTasks() {
|
|
40
|
+
return { version: BLACKBOARD_VERSION, tasks: [], updated_at: nowIso() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultClaims() {
|
|
44
|
+
return { version: BLACKBOARD_VERSION, claims: [], updated_at: nowIso() };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validTasks(data) {
|
|
48
|
+
return data && data.version === BLACKBOARD_VERSION && Array.isArray(data.tasks);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validClaims(data) {
|
|
52
|
+
return data && data.version === BLACKBOARD_VERSION && Array.isArray(data.claims);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readJson(path, fallback, validator) {
|
|
56
|
+
const res = readSafe(path, validator);
|
|
57
|
+
if (res.ok) return { ok: true, data: res.data };
|
|
58
|
+
return { ok: false, data: fallback(), error: res.error, message: res.message };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeJson(path, data) {
|
|
62
|
+
data.updated_at = nowIso();
|
|
63
|
+
return writeAtomic(path, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readJsonl(path, limit = 5) {
|
|
67
|
+
try {
|
|
68
|
+
if (!existsSync(path)) return [];
|
|
69
|
+
const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
|
|
70
|
+
return lines.slice(-limit).map((line) => {
|
|
71
|
+
try { return JSON.parse(line); } catch { return { malformed: true, raw: line }; }
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function appendJsonlUnlocked(path, entry) {
|
|
79
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function appendJsonl(paths, path, entry) {
|
|
84
|
+
return withLock(paths.lock, () => appendJsonlUnlocked(path, entry)).result ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function blackboardEventEntry(input) {
|
|
88
|
+
return {
|
|
89
|
+
ts: nowIso(),
|
|
90
|
+
type: String(input.type || 'event'),
|
|
91
|
+
actor: input.actor || input.owner || 'ijfw',
|
|
92
|
+
task_id: input.task_id || null,
|
|
93
|
+
artifact_ids: Array.isArray(input.artifact_ids) ? input.artifact_ids : [],
|
|
94
|
+
message: input.message || null,
|
|
95
|
+
data: input.data || {},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function initBlackboard(projectRoot = process.cwd()) {
|
|
100
|
+
const paths = blackboardPaths(projectRoot);
|
|
101
|
+
ensureDir(paths);
|
|
102
|
+
if (!existsSync(paths.tasks)) writeJson(paths.tasks, defaultTasks());
|
|
103
|
+
if (!existsSync(paths.claims)) writeJson(paths.claims, defaultClaims());
|
|
104
|
+
for (const p of [paths.findings, paths.decisions, paths.blockers, paths.notes, paths.events]) {
|
|
105
|
+
if (!existsSync(p)) writeFileSync(p, '', { encoding: 'utf8', mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
if (!existsSync(paths.handoff)) {
|
|
108
|
+
writeFileSync(paths.handoff, '# IJFW Blackboard Handoff\n\nNo active handoff.\n', { encoding: 'utf8', mode: 0o600 });
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, dir: paths.dir };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readBlackboard(projectRoot = process.cwd()) {
|
|
114
|
+
const paths = blackboardPaths(projectRoot);
|
|
115
|
+
const tasks = readJson(paths.tasks, defaultTasks, validTasks);
|
|
116
|
+
const claims = readJson(paths.claims, defaultClaims, validClaims);
|
|
117
|
+
return {
|
|
118
|
+
paths,
|
|
119
|
+
tasks,
|
|
120
|
+
claims,
|
|
121
|
+
recent: {
|
|
122
|
+
findings: readJsonl(paths.findings),
|
|
123
|
+
decisions: readJsonl(paths.decisions),
|
|
124
|
+
blockers: readJsonl(paths.blockers),
|
|
125
|
+
notes: readJsonl(paths.notes),
|
|
126
|
+
events: readJsonl(paths.events),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function appendBlackboardEvent(projectRoot, input) {
|
|
132
|
+
const paths = blackboardPaths(projectRoot);
|
|
133
|
+
ensureDir(paths);
|
|
134
|
+
const entry = blackboardEventEntry(input);
|
|
135
|
+
const written = appendJsonl(paths, paths.events, entry);
|
|
136
|
+
if (!written) return { ok: false, error: 'locked' };
|
|
137
|
+
return { ok: true, entry: written };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function writeBlackboardTasks(projectRoot, tasks, options = {}) {
|
|
141
|
+
const paths = blackboardPaths(projectRoot);
|
|
142
|
+
ensureDir(paths);
|
|
143
|
+
return withLock(paths.lock, () => {
|
|
144
|
+
const current = readJson(paths.tasks, defaultTasks, validTasks).data;
|
|
145
|
+
const next = {
|
|
146
|
+
version: BLACKBOARD_VERSION,
|
|
147
|
+
tasks: options.replace ? [] : current.tasks.slice(),
|
|
148
|
+
updated_at: nowIso(),
|
|
149
|
+
};
|
|
150
|
+
const incoming = tasks.map((task) => ({
|
|
151
|
+
...task,
|
|
152
|
+
updated_at: task.updated_at || nowIso(),
|
|
153
|
+
}));
|
|
154
|
+
const incomingIds = new Set(incoming.map((task) => task.id));
|
|
155
|
+
next.tasks = next.tasks.filter((task) => !incomingIds.has(task.id));
|
|
156
|
+
next.tasks.push(...incoming);
|
|
157
|
+
writeJson(paths.tasks, next);
|
|
158
|
+
return { ok: true, written: incoming.length, total: next.tasks.length };
|
|
159
|
+
}).result ?? { ok: false, error: 'locked' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function listBlackboardTasks(projectRoot = process.cwd()) {
|
|
163
|
+
const state = readBlackboard(projectRoot);
|
|
164
|
+
const tasks = state.tasks.data.tasks || [];
|
|
165
|
+
return { ok: state.tasks.ok, tasks, error: state.tasks.error };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function updateBlackboardTask(projectRoot, taskId, patch) {
|
|
169
|
+
const paths = blackboardPaths(projectRoot);
|
|
170
|
+
ensureDir(paths);
|
|
171
|
+
return withLock(paths.lock, () => {
|
|
172
|
+
const current = readJson(paths.tasks, defaultTasks, validTasks).data;
|
|
173
|
+
const index = current.tasks.findIndex((task) => task.id === taskId);
|
|
174
|
+
if (index === -1) return { ok: false, error: 'task-not-found' };
|
|
175
|
+
const before = current.tasks[index];
|
|
176
|
+
const next = {
|
|
177
|
+
...before,
|
|
178
|
+
...patch,
|
|
179
|
+
updated_at: nowIso(),
|
|
180
|
+
};
|
|
181
|
+
current.tasks[index] = next;
|
|
182
|
+
writeJson(paths.tasks, current);
|
|
183
|
+
return { ok: true, task: next, previous: before };
|
|
184
|
+
}).result ?? { ok: false, error: 'locked' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function activeClaims(claimsData) {
|
|
188
|
+
return claimsData.claims.filter((claim) => claim.status === 'active');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function claimArtifactId(claim) {
|
|
192
|
+
return claim.artifact_id || claim.artifact;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function claimAgent(claim) {
|
|
196
|
+
return claim.agent || claim.owner;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizePaths(paths) {
|
|
200
|
+
if (!paths) return [];
|
|
201
|
+
if (Array.isArray(paths)) return paths.map(String).filter(Boolean);
|
|
202
|
+
return String(paths).split(',').map((p) => p.trim()).filter(Boolean);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function commonPrefixBeforeGlob(pattern) {
|
|
206
|
+
const idx = pattern.search(/[*?[\]{}]/);
|
|
207
|
+
return idx === -1 ? pattern : pattern.slice(0, idx);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function pathsOverlap(a, b) {
|
|
211
|
+
if (!a.length || !b.length) return false;
|
|
212
|
+
for (const left of a) {
|
|
213
|
+
for (const right of b) {
|
|
214
|
+
if (left === right) return true;
|
|
215
|
+
const lp = commonPrefixBeforeGlob(left);
|
|
216
|
+
const rp = commonPrefixBeforeGlob(right);
|
|
217
|
+
if (lp && right.startsWith(lp)) return true;
|
|
218
|
+
if (rp && left.startsWith(rp)) return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function claimConflicts(existing, next) {
|
|
225
|
+
return activeClaims(existing).filter((claim) => {
|
|
226
|
+
if (claimAgent(claim) === next.agent) return false;
|
|
227
|
+
if (claimArtifactId(claim) === next.artifact_id) return true;
|
|
228
|
+
return pathsOverlap(claim.paths || [], next.paths || []);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function claimArtifact(projectRoot, input) {
|
|
233
|
+
const paths = blackboardPaths(projectRoot);
|
|
234
|
+
ensureDir(paths);
|
|
235
|
+
return withLock(paths.lock, () => {
|
|
236
|
+
const current = readJson(paths.claims, defaultClaims, validClaims).data;
|
|
237
|
+
const artifactId = String(input.artifact_id || input.artifact || '').trim();
|
|
238
|
+
const agent = String(input.agent || input.owner || '').trim();
|
|
239
|
+
const next = {
|
|
240
|
+
id: input.id || `${artifactId}:${agent}`,
|
|
241
|
+
artifact_id: artifactId,
|
|
242
|
+
agent,
|
|
243
|
+
paths: normalizePaths(input.paths),
|
|
244
|
+
status: 'active',
|
|
245
|
+
claimed_at: nowIso(),
|
|
246
|
+
note: input.note ? String(input.note) : undefined,
|
|
247
|
+
};
|
|
248
|
+
if (!next.artifact_id) return { ok: false, error: 'artifact-required' };
|
|
249
|
+
if (!next.agent) return { ok: false, error: 'owner-required' };
|
|
250
|
+
|
|
251
|
+
const conflicts = claimConflicts(current, next);
|
|
252
|
+
if (conflicts.length) return { ok: false, error: 'conflict', conflicts };
|
|
253
|
+
|
|
254
|
+
current.claims = current.claims.filter((claim) => !(claimArtifactId(claim) === next.artifact_id && claimAgent(claim) === next.agent));
|
|
255
|
+
current.claims.push(next);
|
|
256
|
+
writeJson(paths.claims, current);
|
|
257
|
+
appendJsonlUnlocked(paths.events, blackboardEventEntry({
|
|
258
|
+
type: 'claim.acquired',
|
|
259
|
+
actor: next.agent,
|
|
260
|
+
artifact_ids: [next.artifact_id],
|
|
261
|
+
message: `Claimed ${next.artifact_id}`,
|
|
262
|
+
data: { paths: next.paths },
|
|
263
|
+
}));
|
|
264
|
+
return { ok: true, claim: next };
|
|
265
|
+
}).result ?? { ok: false, error: 'locked' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function releaseClaim(projectRoot, input) {
|
|
269
|
+
const paths = blackboardPaths(projectRoot);
|
|
270
|
+
ensureDir(paths);
|
|
271
|
+
return withLock(paths.lock, () => {
|
|
272
|
+
const current = readJson(paths.claims, defaultClaims, validClaims).data;
|
|
273
|
+
const artifactId = String(input.artifact_id || input.artifact || '').trim();
|
|
274
|
+
const agent = input.agent || input.owner ? String(input.agent || input.owner).trim() : null;
|
|
275
|
+
if (!artifactId) return { ok: false, error: 'artifact-required' };
|
|
276
|
+
|
|
277
|
+
let released = 0;
|
|
278
|
+
current.claims = current.claims.map((claim) => {
|
|
279
|
+
if (claimArtifactId(claim) !== artifactId) return claim;
|
|
280
|
+
if (agent && claimAgent(claim) !== agent) return claim;
|
|
281
|
+
if (claim.status !== 'active') return claim;
|
|
282
|
+
released += 1;
|
|
283
|
+
return { ...claim, status: 'released', released_at: nowIso() };
|
|
284
|
+
});
|
|
285
|
+
writeJson(paths.claims, current);
|
|
286
|
+
if (released > 0) {
|
|
287
|
+
appendJsonlUnlocked(paths.events, blackboardEventEntry({
|
|
288
|
+
type: 'claim.released',
|
|
289
|
+
actor: agent || 'ijfw',
|
|
290
|
+
artifact_ids: [artifactId],
|
|
291
|
+
message: `Released ${released} claim(s) for ${artifactId}`,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, released };
|
|
295
|
+
}).result ?? { ok: false, error: 'locked' };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function addBlackboardNote(projectRoot, input) {
|
|
299
|
+
const paths = blackboardPaths(projectRoot);
|
|
300
|
+
ensureDir(paths);
|
|
301
|
+
const kind = input.kind || 'note';
|
|
302
|
+
const targets = {
|
|
303
|
+
note: paths.notes,
|
|
304
|
+
finding: paths.findings,
|
|
305
|
+
decision: paths.decisions,
|
|
306
|
+
blocker: paths.blockers,
|
|
307
|
+
};
|
|
308
|
+
const target = targets[kind];
|
|
309
|
+
if (!target) return { ok: false, error: 'unknown-kind' };
|
|
310
|
+
const entry = {
|
|
311
|
+
kind,
|
|
312
|
+
author: input.author || input.owner || 'unknown',
|
|
313
|
+
artifact: input.artifact || null,
|
|
314
|
+
message: String(input.message || '').trim(),
|
|
315
|
+
ts: nowIso(),
|
|
316
|
+
};
|
|
317
|
+
if (!entry.message) return { ok: false, error: 'message-required' };
|
|
318
|
+
const written = appendJsonl(paths, target, entry);
|
|
319
|
+
if (!written) return { ok: false, error: 'locked' };
|
|
320
|
+
return { ok: true, entry: written };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function writeHandoff(projectRoot, body) {
|
|
324
|
+
const paths = blackboardPaths(projectRoot);
|
|
325
|
+
ensureDir(paths);
|
|
326
|
+
const text = String(body || '').trim();
|
|
327
|
+
if (!text) return { ok: false, error: 'handoff-required' };
|
|
328
|
+
writeAtomic(paths.handoff, `${text}\n`, { mode: 0o600 });
|
|
329
|
+
return { ok: true, path: paths.handoff };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function blackboardStatus(projectRoot = process.cwd()) {
|
|
333
|
+
const paths = blackboardPaths(projectRoot);
|
|
334
|
+
const state = readBlackboard(projectRoot);
|
|
335
|
+
const claims = state.claims.data.claims || [];
|
|
336
|
+
const tasks = state.tasks.data.tasks || [];
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
dir: paths.dir,
|
|
340
|
+
initialized: existsSync(paths.dir),
|
|
341
|
+
tasks: {
|
|
342
|
+
total: tasks.length,
|
|
343
|
+
open: tasks.filter((task) => !['done', 'cancelled'].includes(task.status)).length,
|
|
344
|
+
},
|
|
345
|
+
claims: {
|
|
346
|
+
total: claims.length,
|
|
347
|
+
active: activeClaims({ claims }).length,
|
|
348
|
+
active_items: activeClaims({ claims }).map((claim) => ({
|
|
349
|
+
artifact_id: claimArtifactId(claim),
|
|
350
|
+
agent: claimAgent(claim),
|
|
351
|
+
paths: claim.paths || [],
|
|
352
|
+
})),
|
|
353
|
+
},
|
|
354
|
+
recent: state.recent,
|
|
355
|
+
health: {
|
|
356
|
+
tasks: state.tasks.ok ? 'ok' : state.tasks.error,
|
|
357
|
+
claims: state.claims.ok ? 'ok' : state.claims.error,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
package/src/cli-run.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli-run.js
|
|
4
|
+
*
|
|
5
|
+
* IJFW v1.4.0 / W6/S5 -- terminal shim for the colon-syntax dispatch surface.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists:
|
|
8
|
+
* The session-start hook needs a way to fire `domain-manifest:load` (and
|
|
9
|
+
* `extension:deploy-lazy` from W6/S12) from bash without depending on the
|
|
10
|
+
* long-lived MCP server. A 30-line shim that imports dispatchRun directly
|
|
11
|
+
* keeps the dependency chain trivial: bash -> node -> dispatch/*.js.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node cli-run.js <namespace>:<command> [--project-root <dir>] [args...]
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* node cli-run.js domain-manifest:load --project-root /path/to/proj
|
|
18
|
+
* node cli-run.js extension:deploy-lazy --project-root /path/to/proj
|
|
19
|
+
*
|
|
20
|
+
* Contract:
|
|
21
|
+
* - Always exits 0 on a successful dispatch (even when the dispatched
|
|
22
|
+
* command reports ok:false -- that's a *result*, not a shim failure).
|
|
23
|
+
* - Exits 2 on argv-shape errors (missing colon expression).
|
|
24
|
+
* - Exits 3 on a thrown error inside the dispatcher.
|
|
25
|
+
* - Prints the JSON-stringified result to stdout. stderr stays empty on
|
|
26
|
+
* the happy path so the session-start log isn't polluted.
|
|
27
|
+
*
|
|
28
|
+
* Discipline:
|
|
29
|
+
* - Built-in Node only. No new deps.
|
|
30
|
+
* - ESM. ASCII strings only.
|
|
31
|
+
* - Detached fire-and-forget safe: the caller pipes stdin/stdout/stderr
|
|
32
|
+
* to /dev/null and the shim never needs an interactive TTY.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { parseColonCommand, dispatchRun } from './dispatch/colon-syntax.js';
|
|
36
|
+
|
|
37
|
+
function parseFlags(argv) {
|
|
38
|
+
// argv layout: [colonExpr, ...rest] -- rest may interleave flags and
|
|
39
|
+
// positional args. We only consume --project-root <value> here; anything
|
|
40
|
+
// else is passed through as part of the colon-expr args via space-join.
|
|
41
|
+
const out = { projectRoot: null, rest: [] };
|
|
42
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
43
|
+
const tok = argv[i];
|
|
44
|
+
if (tok === '--project-root') {
|
|
45
|
+
out.projectRoot = argv[i + 1] || null;
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (typeof tok === 'string' && tok.startsWith('--project-root=')) {
|
|
50
|
+
out.projectRoot = tok.slice('--project-root='.length);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
out.rest.push(tok);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const argv = process.argv.slice(2);
|
|
60
|
+
if (argv.length === 0) {
|
|
61
|
+
process.stderr.write('cli-run: usage: node cli-run.js <namespace>:<command> [--project-root <dir>] [args...]\n');
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
const colonExpr = argv[0];
|
|
65
|
+
const tail = argv.slice(1);
|
|
66
|
+
const { projectRoot, rest } = parseFlags(tail);
|
|
67
|
+
|
|
68
|
+
// Glue any positional args back onto the colon expression so the
|
|
69
|
+
// dispatcher's existing arg-parsing handles them. e.g.
|
|
70
|
+
// cli-run domain-manifest:load extra args -> parseColonCommand sees
|
|
71
|
+
// "domain-manifest:load extra args"
|
|
72
|
+
const expr = rest.length ? `${colonExpr} ${rest.join(' ')}` : colonExpr;
|
|
73
|
+
const parsed = parseColonCommand(expr);
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
process.stderr.write(`cli-run: could not parse "${colonExpr}" as <namespace>:<command>\n`);
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await dispatchRun(parsed, {
|
|
81
|
+
projectRoot: projectRoot || process.env.IJFW_PROJECT_DIR || process.cwd(),
|
|
82
|
+
});
|
|
83
|
+
process.stdout.write(JSON.stringify(result == null ? { ok: false, error: 'dispatch returned null (unknown namespace)' } : result) + '\n');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
process.stderr.write(`cli-run: dispatch threw: ${err && err.message ? err.message : String(err)}\n`);
|
|
87
|
+
process.exit(3);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { writeAtomic } from './lib/atomic-io.js';
|
|
4
|
+
|
|
5
|
+
export function renderCodexAgentToml(role, bundle = {}) {
|
|
6
|
+
assertRole(role);
|
|
7
|
+
const charter = bundle.charter || bundle;
|
|
8
|
+
const workflow = bundle.workflow || null;
|
|
9
|
+
const archetypes = list(charter.project_archetypes).length ? list(charter.project_archetypes) : ['mixed'];
|
|
10
|
+
const agentName = codexAgentName(role.name);
|
|
11
|
+
const description = renderDescription(role, archetypes);
|
|
12
|
+
const instructions = renderDeveloperInstructions(role, { archetypes, workflow });
|
|
13
|
+
const lines = [
|
|
14
|
+
`name = ${tomlString(agentName)}`,
|
|
15
|
+
`description = ${tomlString(description)}`,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const codexConfig = role.codex && typeof role.codex === 'object' ? role.codex : {};
|
|
19
|
+
if (typeof codexConfig.model === 'string' && codexConfig.model.trim()) {
|
|
20
|
+
lines.push(`model = ${tomlString(codexConfig.model.trim())}`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof codexConfig.model_reasoning_effort === 'string' && codexConfig.model_reasoning_effort.trim()) {
|
|
23
|
+
lines.push(`model_reasoning_effort = ${tomlString(codexConfig.model_reasoning_effort.trim())}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
lines.push(`developer_instructions = ${tomlMultiline(instructions)}`);
|
|
27
|
+
return `${lines.join('\n')}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function syncCodexAgents(projectRoot = process.cwd(), options = {}) {
|
|
31
|
+
const root = resolve(projectRoot);
|
|
32
|
+
const bundle = options.bundle || readTeamBundle(root);
|
|
33
|
+
if (!bundle?.charter?.roles?.length) {
|
|
34
|
+
return { ok: false, error: 'missing-team-charter', agentsDir: join(root, '.codex', 'agents'), agentFiles: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const agentsDir = join(root, '.codex', 'agents');
|
|
38
|
+
mkdirSync(agentsDir, { recursive: true, mode: 0o700 });
|
|
39
|
+
|
|
40
|
+
const agentFiles = [];
|
|
41
|
+
for (const role of bundle.charter.roles) {
|
|
42
|
+
const agentPath = join(agentsDir, `${codexAgentFilename(role.name)}.toml`);
|
|
43
|
+
writeAtomic(agentPath, renderCodexAgentToml(role, bundle), { mode: 0o600 });
|
|
44
|
+
agentFiles.push(agentPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
agentsDir,
|
|
50
|
+
agentFiles,
|
|
51
|
+
count: agentFiles.length,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readTeamBundle(root) {
|
|
56
|
+
const charterPath = join(root, '.ijfw', 'team', 'charter.json');
|
|
57
|
+
if (!existsSync(charterPath)) return null;
|
|
58
|
+
const workflowPath = join(root, '.ijfw', 'team', 'workflow.json');
|
|
59
|
+
return {
|
|
60
|
+
charter: JSON.parse(readFileSync(charterPath, 'utf8')),
|
|
61
|
+
workflow: existsSync(workflowPath) ? JSON.parse(readFileSync(workflowPath, 'utf8')) : null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderDescription(role, archetypes) {
|
|
66
|
+
const owns = list(role.owns).map((item) => item.artifact_type).filter(Boolean);
|
|
67
|
+
const owned = owns.length ? owns.join(', ') : 'assigned artifacts';
|
|
68
|
+
return `${role.name} for ${archetypes.join(', ')} projects; owns ${owned}.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderDeveloperInstructions(role, context) {
|
|
72
|
+
const owns = renderArtifactRefs(role.owns, '- No primary ownership declared.');
|
|
73
|
+
const reviews = renderReviews(role.reviews);
|
|
74
|
+
const phaseScope = list(role.phase_scope).join(', ') || 'project workflow';
|
|
75
|
+
const archetypes = context.archetypes.join(', ');
|
|
76
|
+
const artifacts = renderWorkflowArtifacts(role.name, context.workflow);
|
|
77
|
+
const conflicts = list(role.coordination?.conflicts_with).join(', ') || 'none declared';
|
|
78
|
+
const claimRequired = role.coordination?.claim_required ? 'yes' : 'no';
|
|
79
|
+
const parallelSafe = role.coordination?.parallel_safe ? 'yes' : 'no';
|
|
80
|
+
const sections = list(role.handoff?.required_sections).join(', ') || 'changed_artifacts, verification, risks';
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
`You are the ${role.name} Codex custom agent for this IJFW Team Assembly.`,
|
|
84
|
+
'',
|
|
85
|
+
`Role type: ${role.role_type}`,
|
|
86
|
+
`Project archetypes: ${archetypes}`,
|
|
87
|
+
`Phase scope: ${phaseScope}`,
|
|
88
|
+
'',
|
|
89
|
+
'Work project-agnostically. Treat artifacts as whatever this project uses: source files, documents, designs, datasets, plans, research notes, workflows, diagrams, or other durable project outputs.',
|
|
90
|
+
'',
|
|
91
|
+
'Owns:',
|
|
92
|
+
owns,
|
|
93
|
+
'',
|
|
94
|
+
'Reviews:',
|
|
95
|
+
reviews,
|
|
96
|
+
'',
|
|
97
|
+
'Workflow artifacts assigned to this role:',
|
|
98
|
+
artifacts,
|
|
99
|
+
'',
|
|
100
|
+
'Blackboard coordination:',
|
|
101
|
+
`- Claim required: ${claimRequired}`,
|
|
102
|
+
`- Parallel safe: ${parallelSafe}`,
|
|
103
|
+
`- Conflicts with: ${conflicts}`,
|
|
104
|
+
'- Before editing or producing durable output, coordinate through the IJFW blackboard when swarm execution is active.',
|
|
105
|
+
'- Start assigned swarm work with: ijfw swarm start <task-id>',
|
|
106
|
+
'- Complete finished swarm work with: ijfw swarm complete <task-id>',
|
|
107
|
+
'- Report blocked swarm work with: ijfw swarm block <task-id> --message <why>',
|
|
108
|
+
'- Record findings, decisions, and blockers in .ijfw/blackboard/ through IJFW commands when relevant.',
|
|
109
|
+
'',
|
|
110
|
+
'Safety:',
|
|
111
|
+
'- You are not alone in this repo. Never revert user changes or another agent\'s changes unless the parent session explicitly asks for that exact revert.',
|
|
112
|
+
'- Keep edits scoped to your claimed artifacts and paths. If ownership is unclear, stop and report the ambiguity.',
|
|
113
|
+
'- Preserve existing project conventions and validate the behavior or artifact quality you changed.',
|
|
114
|
+
'',
|
|
115
|
+
'Handoff:',
|
|
116
|
+
`- Format: ${role.handoff?.format || 'markdown'}`,
|
|
117
|
+
`- Required sections: ${sections}`,
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderArtifactRefs(refs, fallback) {
|
|
122
|
+
const lines = list(refs).map((item) => {
|
|
123
|
+
const targets = list(item.paths).concat(list(item.refs));
|
|
124
|
+
const targetText = targets.length ? targets.join(', ') : 'project-defined locations';
|
|
125
|
+
return `- ${item.artifact_type || 'artifact'}: ${targetText}`;
|
|
126
|
+
});
|
|
127
|
+
return lines.length ? lines.join('\n') : fallback;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderReviews(refs) {
|
|
131
|
+
const lines = list(refs).map((item) => {
|
|
132
|
+
const criteria = list(item.criteria).join(', ') || 'quality, correctness, fit';
|
|
133
|
+
return `- ${item.artifact_type || 'artifact'}: ${criteria}`;
|
|
134
|
+
});
|
|
135
|
+
return lines.length ? lines.join('\n') : '- No review responsibility declared.';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderWorkflowArtifacts(roleName, workflow) {
|
|
139
|
+
const artifacts = list(workflow?.artifacts).filter((artifact) => {
|
|
140
|
+
return artifact.owner === roleName || list(artifact.reviewers).includes(roleName);
|
|
141
|
+
});
|
|
142
|
+
if (!artifacts.length) return '- No workflow artifact mapping found; follow the role owns/reviews contract.';
|
|
143
|
+
return artifacts.map((artifact) => {
|
|
144
|
+
const relationship = artifact.owner === roleName ? 'owns' : 'reviews';
|
|
145
|
+
const targets = list(artifact.paths).concat(list(artifact.refs));
|
|
146
|
+
const targetText = targets.length ? ` (${targets.join(', ')})` : '';
|
|
147
|
+
return `- ${artifact.id}: ${relationship} ${artifact.type}${targetText}`;
|
|
148
|
+
}).join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function assertRole(role) {
|
|
152
|
+
if (!role || typeof role !== 'object') throw new TypeError('role must be an object');
|
|
153
|
+
if (!role.name || typeof role.name !== 'string') throw new TypeError('role.name is required');
|
|
154
|
+
if (!role.role_type || typeof role.role_type !== 'string') throw new TypeError('role.role_type is required');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function codexAgentName(value) {
|
|
158
|
+
const name = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
159
|
+
return name || 'ijfw_agent';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function codexAgentFilename(value) {
|
|
163
|
+
const name = String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
164
|
+
return name || 'ijfw-agent';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tomlString(value) {
|
|
168
|
+
return JSON.stringify(String(value));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function tomlMultiline(value) {
|
|
172
|
+
return `"""\n${String(value).replace(/"""/g, '\\"\\"\\"')}\n"""`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function list(value) {
|
|
176
|
+
return Array.isArray(value) ? value : [];
|
|
177
|
+
}
|
package/src/compute/extract.js
CHANGED
|
@@ -101,6 +101,7 @@ const DOTFILE_RE = new RegExp(
|
|
|
101
101
|
// Windows path: drive letter + (\\ or \) + chain. The fixture body
|
|
102
102
|
// contains DOUBLE backslashes; expected name uses SINGLE backslashes.
|
|
103
103
|
// Match doubled-backslash form, then normalize to single backslash on emit.
|
|
104
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- extractor input is capped by caller; character classes are bounded to non-whitespace path segments.
|
|
104
105
|
const WINDOWS_PATH_RE = /(?<![\w])([A-Za-z]:(?:\\\\[^\s\\]+)+\.[a-zA-Z][a-zA-Z0-9]{0,8})(?![\w])/g;
|
|
105
106
|
|
|
106
107
|
// Bare basename with extension (no path). Conservative: requires the
|
|
@@ -141,6 +142,7 @@ const DUNDER_BARE_RE = /\b(__[a-z][a-z0-9_]*)\b/g;
|
|
|
141
142
|
const REACT_HOOK_RE = /\b(use[A-Z][A-Za-z0-9]*)\b/g;
|
|
142
143
|
|
|
143
144
|
// --- identifier regex --------------------------------------------------
|
|
145
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- token extractor regex is linear over capped text and uses bounded identifier classes.
|
|
144
146
|
const UPPER_SNAKE_RE = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
|
|
145
147
|
const PASCAL_BARE_RE = /\b([A-Z][a-z][A-Za-z0-9]*|I[A-Z][a-z][A-Za-z0-9]*)\b/g;
|
|
146
148
|
|
|
@@ -171,6 +173,7 @@ const HTTP_EXPLICIT_RE = /\bHTTP[_ ]?([1-5]\d{2})\b/g;
|
|
|
171
173
|
const EXCEPTION_RE = /\b([A-Z][a-z][A-Za-z0-9]*(?:Exception|Error))\b/g;
|
|
172
174
|
|
|
173
175
|
// --- decision regex ----------------------------------------------------
|
|
176
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- token extractor regex is linear over capped text and uses bounded slug segments.
|
|
174
177
|
const D_PREFIX_RE = /\b(d-[a-z][a-z0-9]{3,}(?:-[a-z0-9]+)+)\b/g;
|
|
175
178
|
const HASH_DECISION_RE = /#decision:([a-z][a-z0-9-]+)/g;
|
|
176
179
|
const ADR_NUMERIC_RE = /\b(ADR-\d{4})\b/g;
|
package/src/compute/fts5.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Integrity discipline:
|
|
16
16
|
// - openDb() -- enforces schema version; refuses downgrade.
|
|
17
17
|
// - safeWrite() -- runs `redactSecrets()` over `body` and `topic` BEFORE
|
|
18
|
-
// inserting (D-PILLAR-SPEC
|
|
18
|
+
// inserting (D-PILLAR-SPEC section 12 ingest scrub gate), then
|
|
19
19
|
// inserts inside a transaction and runs PRAGMA quick_check
|
|
20
20
|
// after each insert; throws IntegrityError on anything
|
|
21
21
|
// other than 'ok'. The scrub default is on; setting
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// by bm25 rank.
|
|
26
26
|
// - closeDb() -- clean close; suppresses double-close errors.
|
|
27
27
|
//
|
|
28
|
-
// Security model (D-PILLAR-SPEC
|
|
28
|
+
// Security model (D-PILLAR-SPEC section 12, real fix-wave C3):
|
|
29
29
|
// Secrets are scrubbed at the observation-ingest boundary. By the time a
|
|
30
30
|
// row reaches the FTS index, the entity extractor, or the kg layer, all
|
|
31
31
|
// `redactSecrets`-recognised tokens have been replaced with
|
|
@@ -40,7 +40,7 @@ import { runMigrations, highestKnownVersion, SchemaVersionError } from './migrat
|
|
|
40
40
|
import { autoIndexGraphFromBody } from './graph-auto-index.js';
|
|
41
41
|
import { redactSecrets } from '../redactor.js';
|
|
42
42
|
|
|
43
|
-
// D-PILLAR-SPEC
|
|
43
|
+
// D-PILLAR-SPEC section 12 ingest scrub gate. Default-on; the only escape hatch
|
|
44
44
|
// is the IJFW_INGEST_SCRUB=0 env var, which exists for local debugging
|
|
45
45
|
// (e.g. asserting raw body shape in a fixture) and is NOT a shipping
|
|
46
46
|
// posture. Read on every safeWrite call so test harnesses can flip it
|
|
@@ -271,7 +271,7 @@ export function safeWrite(db, table, row) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
// D-PILLAR-SPEC
|
|
274
|
+
// D-PILLAR-SPEC section 12 ingest scrub gate. Replace `body` and `topic` with
|
|
275
275
|
// their redacted forms BEFORE the INSERT runs, so the FTS index, the
|
|
276
276
|
// entity extractor (D2), and any downstream reader only ever see the
|
|
277
277
|
// scrubbed text. This applies to every `safeWrite` regardless of table
|