@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.
Files changed (68) hide show
  1. package/README.md +67 -0
  2. package/fixtures/team/book.json +47 -0
  3. package/fixtures/team/business.json +47 -0
  4. package/fixtures/team/content.json +47 -0
  5. package/fixtures/team/design.json +47 -0
  6. package/fixtures/team/mixed.json +59 -0
  7. package/fixtures/team/research.json +47 -0
  8. package/fixtures/team/software.json +47 -0
  9. package/package.json +1 -9
  10. package/src/.registry-meta-key.pem +3 -0
  11. package/src/active-extension-writer.js +142 -0
  12. package/src/blackboard.js +360 -0
  13. package/src/cli-run.js +91 -0
  14. package/src/codex-agents.js +177 -0
  15. package/src/compute/extract.js +3 -0
  16. package/src/compute/fts5.js +4 -4
  17. package/src/compute/graph-lock.js +0 -2
  18. package/src/compute/migrations/003-tier-semantic.js +3 -3
  19. package/src/compute/runner.js +44 -15
  20. package/src/compute/schema.sql +1 -1
  21. package/src/cross-orchestrator-cli.js +974 -13
  22. package/src/cross-orchestrator.js +9 -1
  23. package/src/dashboard-client.html +353 -1
  24. package/src/dashboard-server.js +318 -2
  25. package/src/design-intelligence.js +721 -0
  26. package/src/dispatch/colon-syntax.js +31 -3
  27. package/src/dispatch/domain-manifest.js +251 -0
  28. package/src/dispatch/extension.js +637 -0
  29. package/src/dispatch/override.js +221 -0
  30. package/src/dispatch-planner.js +1 -0
  31. package/src/dream/runner.mjs +3 -3
  32. package/src/extension-installer.js +1269 -0
  33. package/src/extension-manifest-schema.js +301 -0
  34. package/src/extension-permission-check.mjs +79 -0
  35. package/src/extension-registry.js +619 -0
  36. package/src/extension-signer.js +905 -0
  37. package/src/gate-result-formatter.js +95 -0
  38. package/src/gate-result-schema.js +274 -0
  39. package/src/gate-result.js +195 -0
  40. package/src/intent-router.js +2 -0
  41. package/src/lib/npm-view.js +1 -0
  42. package/src/memory/fts5.js +3 -3
  43. package/src/memory/migrations/002-tier-semantic.js +2 -2
  44. package/src/memory/staleness.js +1 -1
  45. package/src/memory/tier-promotion.js +6 -6
  46. package/src/memory/tokenize.js +1 -1
  47. package/src/memory-feedback.js +372 -0
  48. package/src/override-manifest-schema.js +146 -0
  49. package/src/override-resolver.js +699 -0
  50. package/src/override-use-registry.js +307 -0
  51. package/src/overrides/presets/academic.md +101 -0
  52. package/src/overrides/presets/book.md +87 -0
  53. package/src/overrides/presets/campaign.md +95 -0
  54. package/src/overrides/presets/screenplay.md +99 -0
  55. package/src/recovery/checkpoint.js +191 -0
  56. package/src/redactor.js +2 -0
  57. package/src/runtime-mediator.js +207 -0
  58. package/src/sandbox.js +17 -3
  59. package/src/server.js +94 -2
  60. package/src/swarm/dispatch-prompt.js +154 -0
  61. package/src/swarm/planner.js +399 -0
  62. package/src/swarm/review.js +136 -0
  63. package/src/swarm/worktree.js +239 -0
  64. package/src/team/generator.js +119 -0
  65. package/src/team/schemas.js +341 -0
  66. package/src/trident/dispatch.js +47 -0
  67. package/src/update-check.js +1 -1
  68. package/src/vectors.js +7 -8
@@ -0,0 +1,239 @@
1
+ // Conservative git worktree support for prepared swarm tasks.
2
+ //
3
+ // This module never dispatches agents and never auto-resolves merge conflicts.
4
+ // It creates isolated worktrees for code-heavy tasks after the task lifecycle
5
+ // has prepared/started them.
6
+
7
+ import { existsSync, mkdirSync, realpathSync, rmSync } from 'node:fs';
8
+ import { isAbsolute, join, relative, resolve } from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { appendBlackboardEvent, listBlackboardTasks, updateBlackboardTask } from '../blackboard.js';
11
+ import { createCheckpoint } from '../recovery/checkpoint.js';
12
+
13
+ export function createTaskWorktree(projectRoot, taskId, options = {}) {
14
+ const root = resolve(projectRoot);
15
+ const task = findTask(root, taskId);
16
+ if (!task.ok) return task;
17
+ if (task.task.status !== 'in_progress' && !options.allowReady) {
18
+ return { ok: false, error: 'task-not-in-progress', task: task.task };
19
+ }
20
+ if (!options.force && !isCodeHeavyTask(task.task)) return { ok: false, error: 'non-code-task', task: task.task };
21
+ if (!isGitRepo(root)) return { ok: false, error: 'not-git-repo' };
22
+
23
+ const dirty = git(root, ['status', '--porcelain']);
24
+ if (dirty.status !== 0) return { ok: false, error: 'git-status-unavailable', stderr: dirty.stderr };
25
+ const dirtyUserFiles = dirtyLines(dirty.stdout);
26
+ if (dirtyUserFiles.length && !options.allowDirty) {
27
+ return { ok: false, error: 'dirty-worktree', detail: dirtyUserFiles };
28
+ }
29
+
30
+ const baseRef = options.baseRef || 'HEAD';
31
+ const worktreeDir = join(root, '.ijfw', 'worktrees');
32
+ const worktreePath = join(worktreeDir, safeTaskId(taskId));
33
+ if (existsSync(worktreePath)) return { ok: false, error: 'worktree-exists', path: worktreePath };
34
+ mkdirSync(worktreeDir, { recursive: true, mode: 0o700 });
35
+
36
+ createCheckpoint(root, 'before-worktree-create', { actor: 'ijfw', message: taskId });
37
+ const branch = options.branch || `ijfw/${safeTaskId(taskId)}`;
38
+ const result = git(root, ['worktree', 'add', '-b', branch, worktreePath, baseRef]);
39
+ if (result.status !== 0) return { ok: false, error: 'worktree-add-failed', stderr: result.stderr, stdout: result.stdout };
40
+
41
+ const updated = updateBlackboardTask(root, taskId, {
42
+ worktree: { path: worktreePath, branch, base_ref: baseRef, status: 'created' },
43
+ });
44
+ appendBlackboardEvent(root, {
45
+ type: 'worktree.created',
46
+ actor: 'ijfw',
47
+ task_id: taskId,
48
+ artifact_ids: task.task.artifact_ids || [],
49
+ message: worktreePath,
50
+ data: { branch, baseRef },
51
+ });
52
+ return { ok: true, task: updated.task, path: worktreePath, branch };
53
+ }
54
+
55
+ export function listTaskWorktrees(projectRoot) {
56
+ const root = resolve(projectRoot);
57
+ const tasks = (listBlackboardTasks(root).tasks || []).filter((task) => task.worktree);
58
+ return {
59
+ ok: true,
60
+ worktrees: tasks.map((task) => ({
61
+ task_id: task.id,
62
+ path: task.worktree.path,
63
+ branch: task.worktree.branch,
64
+ status: task.worktree.status,
65
+ })),
66
+ };
67
+ }
68
+
69
+ export function integrateTaskWorktree(projectRoot, taskId, options = {}) {
70
+ const root = resolve(projectRoot);
71
+ const task = findTask(root, taskId);
72
+ if (!task.ok) return task;
73
+ const wt = task.task.worktree;
74
+ if (!wt?.path) return { ok: false, error: 'no-worktree', task: task.task };
75
+ const validation = validateTaskWorktree(root, wt);
76
+ if (!validation.ok) return validation;
77
+ if (!existsSync(validation.path)) return { ok: false, error: 'worktree-missing', path: validation.path };
78
+
79
+ const wtDirty = git(validation.path, ['status', '--porcelain']);
80
+ if (wtDirty.status !== 0) return { ok: false, error: 'worktree-status-unavailable', stderr: wtDirty.stderr };
81
+ if (wtDirty.stdout.trim()) return { ok: false, error: 'worktree-has-uncommitted-changes', detail: wtDirty.stdout.trim().split('\n') };
82
+
83
+ createCheckpoint(root, 'before-worktree-integrate', { actor: 'ijfw', message: taskId });
84
+ const merge = git(root, ['merge', '--no-ff', '--no-edit', validation.branch]);
85
+ if (merge.status !== 0) {
86
+ updateBlackboardTask(root, taskId, {
87
+ worktree: { ...wt, status: 'merge-blocked', last_error: merge.stderr || merge.stdout },
88
+ });
89
+ appendBlackboardEvent(root, {
90
+ type: 'worktree.integrate.blocked',
91
+ actor: 'ijfw',
92
+ task_id: taskId,
93
+ artifact_ids: task.task.artifact_ids || [],
94
+ message: 'Merge blocked; preserve worktree for inspection',
95
+ data: { stderr: merge.stderr, stdout: merge.stdout },
96
+ });
97
+ return { ok: false, error: 'merge-blocked', stderr: merge.stderr, stdout: merge.stdout };
98
+ }
99
+
100
+ const updated = updateBlackboardTask(root, taskId, {
101
+ worktree: { ...wt, path: validation.path, branch: validation.branch, status: 'integrated', integrated_at: new Date().toISOString() },
102
+ });
103
+ appendBlackboardEvent(root, {
104
+ type: 'worktree.integrated',
105
+ actor: 'ijfw',
106
+ task_id: taskId,
107
+ artifact_ids: task.task.artifact_ids || [],
108
+ message: validation.path,
109
+ });
110
+ if (options.cleanup) cleanupTaskWorktree(root, taskId, { force: true });
111
+ return { ok: true, task: updated.task };
112
+ }
113
+
114
+ export function cleanupTaskWorktree(projectRoot, taskId, options = {}) {
115
+ const root = resolve(projectRoot);
116
+ const task = findTask(root, taskId);
117
+ if (!task.ok) return task;
118
+ const wt = task.task.worktree;
119
+ if (!wt?.path) return { ok: false, error: 'no-worktree', task: task.task };
120
+ const validation = validateTaskWorktree(root, wt);
121
+ if (!validation.ok) return validation;
122
+ if (wt.status !== 'integrated' && !options.force) {
123
+ return { ok: false, error: 'worktree-not-cleanup-ready', status: wt.status };
124
+ }
125
+
126
+ const remove = git(root, ['worktree', 'remove', options.force ? '--force' : '', validation.path].filter(Boolean));
127
+ if (remove.status !== 0 && existsSync(validation.path)) return { ok: false, error: 'worktree-remove-failed', stderr: remove.stderr, stdout: remove.stdout };
128
+ if (existsSync(validation.path) && options.force) rmSync(validation.path, { recursive: true, force: true });
129
+ const updated = updateBlackboardTask(root, taskId, {
130
+ worktree: { ...wt, path: validation.path, branch: validation.branch, status: 'cleaned', cleaned_at: new Date().toISOString() },
131
+ });
132
+ appendBlackboardEvent(root, {
133
+ type: 'worktree.cleaned',
134
+ actor: 'ijfw',
135
+ task_id: taskId,
136
+ artifact_ids: task.task.artifact_ids || [],
137
+ message: validation.path,
138
+ });
139
+ return { ok: true, task: updated.task };
140
+ }
141
+
142
+ function findTask(projectRoot, taskId) {
143
+ const tasks = listBlackboardTasks(projectRoot).tasks || [];
144
+ const task = tasks.find((item) => item.id === taskId);
145
+ if (!task) return { ok: false, error: 'task-not-found' };
146
+ return { ok: true, task };
147
+ }
148
+
149
+ function isGitRepo(projectRoot) {
150
+ return git(projectRoot, ['rev-parse', '--is-inside-work-tree']).stdout.trim() === 'true';
151
+ }
152
+
153
+ function isCodeHeavyTask(task) {
154
+ const text = [
155
+ ...(task.paths || []),
156
+ ...(task.artifact_ids || []),
157
+ task.owner || '',
158
+ task.title || '',
159
+ ].join(' ').toLowerCase();
160
+ return /\.(js|jsx|ts|tsx|py|go|rs|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|mjs|cjs)\b/.test(text)
161
+ || /\b(app|api|module|test|runtime|src|code|software|engineer)\b/.test(text);
162
+ }
163
+
164
+ function validateTaskWorktree(projectRoot, wt) {
165
+ const root = resolve(projectRoot);
166
+ const worktreesRoot = resolve(root, '.ijfw', 'worktrees');
167
+ const worktreePath = resolve(root, String(wt.path || ''));
168
+ const pathRel = relative(worktreesRoot, worktreePath);
169
+ if (!pathRel || pathRel.startsWith('..') || isAbsolute(pathRel)) {
170
+ return { ok: false, error: 'invalid-worktree-path', path: wt.path };
171
+ }
172
+
173
+ const branch = String(wt.branch || '').trim();
174
+ if (!branch.startsWith('ijfw/')) {
175
+ return { ok: false, error: 'invalid-worktree-branch', branch: wt.branch };
176
+ }
177
+
178
+ const listed = git(root, ['worktree', 'list', '--porcelain']);
179
+ if (listed.status !== 0) {
180
+ return { ok: true, path: worktreePath, branch, warning: 'worktree-list-unavailable' };
181
+ }
182
+ const worktrees = parseWorktreeList(listed.stdout);
183
+ const canonicalWorktreePath = canonicalPath(worktreePath);
184
+ const match = worktrees.some((item) => item.path === canonicalWorktreePath && item.branch === branch);
185
+ if (!match) {
186
+ return { ok: false, error: 'worktree-metadata-mismatch', path: worktreePath, branch };
187
+ }
188
+ return { ok: true, path: worktreePath, branch };
189
+ }
190
+
191
+ function parseWorktreeList(stdout) {
192
+ const items = [];
193
+ let current = null;
194
+ for (const line of String(stdout || '').split('\n')) {
195
+ if (line.startsWith('worktree ')) {
196
+ if (current) items.push(current);
197
+ current = { path: canonicalPath(line.slice('worktree '.length).trim()), branch: null };
198
+ } else if (current && line.startsWith('branch ')) {
199
+ current.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
200
+ } else if (line === '' && current) {
201
+ items.push(current);
202
+ current = null;
203
+ }
204
+ }
205
+ if (current) items.push(current);
206
+ return items;
207
+ }
208
+
209
+ function canonicalPath(path) {
210
+ try {
211
+ return realpathSync.native(path);
212
+ } catch {
213
+ return resolve(path);
214
+ }
215
+ }
216
+
217
+ function safeTaskId(taskId) {
218
+ return String(taskId).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-|-$/g, '') || 'task';
219
+ }
220
+
221
+ function git(cwd, args) {
222
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
223
+ return {
224
+ status: res.status ?? 1,
225
+ stdout: res.stdout || '',
226
+ stderr: res.stderr || (res.error ? res.error.message : ''),
227
+ };
228
+ }
229
+
230
+ function dirtyLines(stdout) {
231
+ return String(stdout || '')
232
+ .split('\n')
233
+ .map((line) => line.trimEnd())
234
+ .filter(Boolean)
235
+ .filter((line) => {
236
+ const path = line.slice(3).trim();
237
+ return !(path === '.ijfw' || path.startsWith('.ijfw/'));
238
+ });
239
+ }
@@ -0,0 +1,119 @@
1
+ // Team Assembly 2.0 generator.
2
+ //
3
+ // Creates project-local agent markdown plus machine-readable charter/workflow
4
+ // contracts from archetype fixtures. This is the first executable slice; later
5
+ // waves can replace fixture selection with richer brief-aware synthesis.
6
+
7
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
8
+ import { basename, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { writeAtomic } from '../lib/atomic-io.js';
11
+ import { syncCodexAgents } from '../codex-agents.js';
12
+ import { detect } from '../project-type-detector.js';
13
+ import { assertValidTeamBundle, validateTeamCharter, validateWorkflowManifest } from './schemas.js';
14
+
15
+ const FIXTURE_DIR = resolve(fileURLToPath(new URL('../../fixtures/team/', import.meta.url)));
16
+ const SUPPORTED_ARCHETYPES = new Set(['software', 'design', 'content', 'book', 'research', 'business', 'mixed']);
17
+
18
+ export function detectTeamArchetype(projectRoot = process.cwd(), options = {}) {
19
+ if (options.archetype) return normalizeArchetype(options.archetype);
20
+ const detected = detect(projectRoot, { maxFiles: options.maxFiles || 4000, c9Available: false });
21
+ return normalizeArchetype(detected.primary_type || detected.type);
22
+ }
23
+
24
+ export function loadTeamTemplate(archetype) {
25
+ const normalized = normalizeArchetype(archetype);
26
+ const path = join(FIXTURE_DIR, `${normalized}.json`);
27
+ const bundle = JSON.parse(readFileSync(path, 'utf8'));
28
+ assertValidTeamBundle(bundle);
29
+ return structuredClone(bundle);
30
+ }
31
+
32
+ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
33
+ const root = resolve(projectRoot);
34
+ const archetype = detectTeamArchetype(root, options);
35
+ const bundle = loadTeamTemplate(archetype);
36
+ const teamName = options.teamName || `${basename(root) || archetype}-team`;
37
+
38
+ bundle.charter.team_name = teamName;
39
+ bundle.charter.generated_at = new Date().toISOString();
40
+ bundle.charter.source_archetype = archetype;
41
+ bundle.workflow.generated_at = bundle.charter.generated_at;
42
+ bundle.workflow.source_archetype = archetype;
43
+
44
+ const teamDir = join(root, '.ijfw', 'team');
45
+ const agentsDir = join(root, '.ijfw', 'agents');
46
+ mkdirSync(teamDir, { recursive: true, mode: 0o700 });
47
+ mkdirSync(agentsDir, { recursive: true, mode: 0o700 });
48
+
49
+ const charterPath = join(teamDir, 'charter.json');
50
+ const workflowPath = join(teamDir, 'workflow.json');
51
+ if (!options.force && (existsSync(charterPath) || existsSync(workflowPath))) {
52
+ return { ok: false, error: 'exists', teamDir, agentsDir, archetype };
53
+ }
54
+
55
+ writeAtomic(charterPath, `${JSON.stringify(bundle.charter, null, 2)}\n`, { mode: 0o600 });
56
+ writeAtomic(workflowPath, `${JSON.stringify(bundle.workflow, null, 2)}\n`, { mode: 0o600 });
57
+
58
+ const agentFiles = [];
59
+ for (const role of bundle.charter.roles) {
60
+ const agentPath = join(agentsDir, `${role.name}.md`);
61
+ writeAtomic(agentPath, renderAgent(role, bundle), { mode: 0o600 });
62
+ agentFiles.push(agentPath);
63
+ }
64
+ const codexAgents = syncCodexAgents(root, { bundle });
65
+
66
+ return {
67
+ ok: true,
68
+ archetype,
69
+ team_name: teamName,
70
+ teamDir,
71
+ agentsDir,
72
+ charterPath,
73
+ workflowPath,
74
+ agentFiles,
75
+ codexAgents,
76
+ };
77
+ }
78
+
79
+ export function readTeamAssembly(projectRoot = process.cwd()) {
80
+ const root = resolve(projectRoot);
81
+ const charterPath = join(root, '.ijfw', 'team', 'charter.json');
82
+ const workflowPath = join(root, '.ijfw', 'team', 'workflow.json');
83
+ const agentsDir = join(root, '.ijfw', 'agents');
84
+ const charter = existsSync(charterPath) ? JSON.parse(readFileSync(charterPath, 'utf8')) : null;
85
+ const workflow = existsSync(workflowPath) ? JSON.parse(readFileSync(workflowPath, 'utf8')) : null;
86
+ const agents = existsSync(agentsDir)
87
+ ? readdirSync(agentsDir).filter((file) => file.endsWith('.md')).sort()
88
+ : [];
89
+ const charterValidation = charter ? validateTeamCharter(charter) : { ok: false, errors: ['missing charter'] };
90
+ const workflowValidation = workflow ? validateWorkflowManifest(workflow, charter) : { ok: false, errors: ['missing workflow'] };
91
+ return {
92
+ ok: Boolean(charter && workflow && charterValidation.ok && workflowValidation.ok),
93
+ charterPath,
94
+ workflowPath,
95
+ agentsDir,
96
+ agents,
97
+ charter,
98
+ workflow,
99
+ validation: {
100
+ charter: charterValidation,
101
+ workflow: workflowValidation,
102
+ },
103
+ };
104
+ }
105
+
106
+ function normalizeArchetype(value) {
107
+ const archetype = String(value || '').toLowerCase();
108
+ return SUPPORTED_ARCHETYPES.has(archetype) ? archetype : 'mixed';
109
+ }
110
+
111
+ function renderAgent(role, bundle) {
112
+ const owned = role.owns.map((item) => `- ${item.artifact_type}: ${(item.paths || item.refs || []).join(', ')}`).join('\n');
113
+ const reviews = role.reviews.map((item) => `- ${item.artifact_type}: ${item.criteria.join(', ')}`).join('\n') || '- None';
114
+ const phases = role.phase_scope.join(', ');
115
+ const sections = role.handoff.required_sections.join(', ');
116
+ const archetypes = bundle.charter.project_archetypes.join(', ');
117
+
118
+ return `---\nname: ${role.name}\nmodel: ${role.model}\neffort: ${role.effort || 'medium'}\ndescription: ${role.name} for ${archetypes} projects; owns ${role.owns.map((item) => item.artifact_type).join(', ')}.\nallowed-tools: Read, Write, Edit, Bash\n---\n\n# ${role.name}\n\nYou are part of this project's IJFW team assembly.\n\nProject archetypes: ${archetypes}\nRole type: ${role.role_type}\nPhase scope: ${phases}\n\n## Owns\n\n${owned}\n\n## Reviews\n\n${reviews}\n\n## Coordination\n\n- Claim required: ${role.coordination.claim_required ? 'yes' : 'no'}\n- Parallel safe: ${role.coordination.parallel_safe ? 'yes' : 'no'}\n- Conflicts with: ${role.coordination.conflicts_with.length ? role.coordination.conflicts_with.join(', ') : 'none'}\n\n## Handoff\n\nFormat: ${role.handoff.format}\nRequired sections: ${sections}\n\nRecord claims, findings, blockers, and decisions in .ijfw/blackboard/ when swarm execution is active.\n`;
119
+ }