total-recall-brain 3.13.0 → 3.13.2

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 (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -31,6 +31,9 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
31
31
  to_version: toVersion,
32
32
  status: 'pending',
33
33
  description,
34
+ title: `Migration v${fromVersion}→v${toVersion}`,
35
+ timestamp: new Date().toISOString(),
36
+ category: 'migrations',
34
37
  };
35
38
 
36
39
  try {
@@ -44,7 +47,7 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
44
47
  // Note: in a real system we'd diff and only write changed, but migration often touches all.
45
48
  for (const node of migratedNodes) {
46
49
  // In a real migration we might delete old files if slug changes, but this assumes in-place update
47
- writeNode(node, vaultDir);
50
+ await writeNode(node, vaultDir);
48
51
  }
49
52
 
50
53
  migrationRecord.status = 'applied';
@@ -60,13 +63,16 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
60
63
  logger.error('migration', `WARNING: Vault may be in an inconsistent state. Please run 'total-recall snapshot rollback ${snapshotId}' immediately to recover.`);
61
64
  throw err;
62
65
  } finally {
63
- // 3. Record the migration event
64
- const migrationsDir = path.join(vaultDir, 'system', 'migrations');
65
- if (!fs.existsSync(migrationsDir)) fs.mkdirSync(migrationsDir, { recursive: true });
66
-
67
- // Ensure slug doesn't conflict
68
- const slug = `migration-v${toVersion}-${migrationId}`;
69
- writeNode({ ...migrationRecord, slug }, migrationsDir);
66
+ // 3. Record the migration event via Operation Contract (migrations/<slug>.md)
67
+ const slug = `migration-v${toVersion}-${migrationId}`.replace(/[^a-zA-Z0-9_-]/g, '-');
68
+ await writeNode({
69
+ ...migrationRecord,
70
+ slug,
71
+ category: 'migrations',
72
+ title: migrationRecord.title || slug,
73
+ description: description || slug,
74
+ timestamp: migrationRecord.applied_at || new Date().toISOString(),
75
+ }, vaultDir);
70
76
  }
71
77
 
72
78
  return { migrationId, status: migrationRecord.status };
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import crypto from 'crypto';
4
4
  import matter from 'gray-matter';
5
5
  import { safeStringify } from './vault.mjs';
6
- import { writeNodeValidated } from './validated-write.mjs';
6
+ import { writeNodeValidatedAsync } from './validated-write.mjs';
7
7
  import { getNodes } from './vault-cache.mjs';
8
8
  import { validateMemoryNode } from './total-recall-memory-validator.mjs';
9
9
 
@@ -280,7 +280,7 @@ export async function importBundle(bundlePath, vaultDir, options = {}) {
280
280
  }
281
281
 
282
282
  try {
283
- const writeResult = writeNodeValidated(node, vaultDir, {
283
+ const writeResult = await writeNodeValidatedAsync(node, vaultDir, {
284
284
  dryRun: options.dryRun || false,
285
285
  agentRole: options.agentRole || 'admin'
286
286
  });
@@ -1,355 +1,63 @@
1
+ /**
2
+ * Total Recall Operation Contract entry points.
3
+ *
4
+ * All mutations go through the SSSS 0.9 package kernel via
5
+ * `processViaPackageKernel`. The local Stage 1–7 pipeline has been removed.
6
+ */
1
7
  import fs from 'fs';
2
8
  import path from 'path';
3
- import matter from 'gray-matter';
4
9
  import crypto from 'node:crypto';
5
- import {
6
- SSSS_SCHEMAS,
7
- OperationEnvelopeSchema,
8
- PatchEnvelopeSchema,
9
- EventEnvelopeSchema,
10
- DeleteEnvelopeSchema,
11
- } from './schema.mjs';
12
- import { atomicWrite, safeStringify } from './vault.mjs';
13
- import { logger } from './logger.mjs';
14
- import { validateMemoryNode } from './total-recall-memory-validator.mjs';
15
-
16
- const APPEND_TYPES = new Set(['conversation', 'run']);
17
- const PROTOCOL_PATHS = [
18
- 'references/ssss-spec.md',
19
- 'references/admin-protocol-evolution-policy.md',
20
- 'references/user-local-optimizer-boundary-policy.md',
21
- ];
22
- const PROTOCOL_PATH_PREFIXES = ['fixtures/'];
23
- const idempotencyCache = new Map();
24
- const IDEMPOTENCY_TTL_MS = 10 * 60 * 1000;
25
- const warmedVaults = new Set();
26
-
27
- function cacheKey(wid, key) { return `${wid}:${key}`; }
28
-
29
- function pruneExpired() {
30
- const now = Date.now();
31
- for (const [k, v] of idempotencyCache) {
32
- if (now - v.ts > IDEMPOTENCY_TTL_MS) idempotencyCache.delete(k);
33
- }
34
- }
35
-
36
- export function warmIdempotencyCache(vaultRoot, eventLogDir) {
37
- const vaultKey = path.resolve(vaultRoot);
38
- if (warmedVaults.has(vaultKey)) return;
39
- warmedVaults.add(vaultKey);
40
-
41
- const auditDir = eventLogDir || path.join(vaultRoot, '.events');
42
- const auditFile = path.join(auditDir, 'audit.jsonl');
43
- if (!fs.existsSync(auditFile)) return;
44
-
45
- try {
46
- const lines = fs.readFileSync(auditFile, 'utf8').split('\n');
47
- const now = Date.now();
48
- for (const line of lines) {
49
- if (!line.trim()) continue;
50
- try {
51
- const record = JSON.parse(line);
52
- if (record.event_type !== 'audit' || !record.payload) continue;
53
- const ts = Date.parse(record.ts);
54
- if (isNaN(ts) || now - ts > IDEMPOTENCY_TTL_MS) continue;
55
-
56
- const ck = cacheKey(record.payload.workspace_id, record.payload.idempotency_key);
57
- const response = {
58
- success: true,
59
- type: record.payload.envelope_type,
60
- operation_id: record.correlation_id,
61
- path: record.subject,
62
- committed_at: record.ts,
63
- validation: {
64
- valid: true,
65
- type: record.payload.resolved_type,
66
- errors: [],
67
- warnings: []
68
- }
69
- };
70
- idempotencyCache.set(ck, { response, ts });
71
- } catch { /* skip corrupt lines */ }
72
- }
73
- } catch (err) {
74
- logger.error('operation-validator', `Failed to warm idempotency cache: ${err.message}`);
75
- }
76
- }
77
-
78
- function isProtocolPath(fp) {
79
- for (const p of PROTOCOL_PATHS) if (fp === p || fp.endsWith(`/${p}`)) return true;
80
- for (const prefix of PROTOCOL_PATH_PREFIXES) if (fp.startsWith(prefix) || fp.includes(`/${prefix}`)) return true;
81
- if (fp.endsWith('schema.mjs') || fp.endsWith('schema.spec.mjs')) return true;
82
- return false;
83
- }
84
-
85
- function buildRepair(zodError) {
86
- return { field_errors: (zodError.issues || []).map(i => ({ field: i.path.join('.') || '(root)', issue: i.message })) };
87
- }
88
-
89
- function resolveVfsPath(vaultRoot, vfsPath) {
90
- const cleaned = vfsPath.replace(/^\/+/, '');
91
- const resolved = path.resolve(vaultRoot, cleaned);
92
- if (!resolved.startsWith(path.resolve(vaultRoot))) throw new Error(`Path traversal: ${vfsPath}`);
93
- return resolved;
94
- }
95
-
96
- function makeErrorResponse(type, opId, p, errors, resolvedType = undefined, repairEntries = undefined) {
97
- const response = {
98
- success: false,
99
- type,
100
- operation_id: opId,
101
- path: p,
102
- committed_at: null,
103
- validation: { valid: false, type: resolvedType, errors, warnings: [] }
104
- };
105
- if (repairEntries) response.repair = { field_errors: repairEntries };
106
- return response;
107
- }
108
-
109
- function getActorRole(envelope, options) {
110
- return options.agentRole || envelope.actor?.role || null;
111
- }
112
-
113
- function loadRolePermissions(vaultRoot, role) {
114
- if (role === 'system') return ['*:*'];
115
- if (role === 'admin') return ['write:*', 'read:*'];
116
- if (role === 'optimizer') return ['write:memory'];
117
- if (!role) return [];
118
-
119
- const roleFile = path.join(vaultRoot, 'roles', role, 'ROLE.md');
120
- if (!fs.existsSync(roleFile)) return [];
121
-
122
- try {
123
- const { data } = matter(fs.readFileSync(roleFile, 'utf8'));
124
- return Array.isArray(data.permissions) ? data.permissions : [];
125
- } catch {
126
- return [];
127
- }
128
- }
10
+ import { processViaPackageKernel } from './ssss-kernel-bridge.mjs';
129
11
 
130
- function hasPermission(permissions, requiredPerm) {
131
- const [, permType] = requiredPerm.split(':');
132
- return permissions.includes('*:*') ||
133
- permissions.includes('write:*') ||
134
- permissions.includes(`*:${permType}`) ||
135
- permissions.includes(requiredPerm);
12
+ /**
13
+ * @deprecated Sync API removed. Use {@link processOperationAsync}.
14
+ */
15
+ export function processOperation(_envelope, _vaultRoot, _options = {}) {
16
+ throw new Error(
17
+ 'processOperation (sync) was removed. Use processOperationAsync — all mutations go through the SSSS 0.9 package kernel.',
18
+ );
136
19
  }
137
20
 
138
21
  /**
139
- * Process an SSSS operation through the full §6.3 pipeline.
140
- * @param {object} envelope - The operation envelope.
141
- * @param {string} vaultRoot - Absolute path to the vault directory.
142
- * @param {object} [options] - { agentRole, leaseStore, eventLogDir }
143
- * @returns {object} OperationResponse (§6.4).
22
+ * Primary Operation Contract entry for Total Recall.
23
+ * Always routes through `@ssss/cli` package kernel + TR host adapters.
144
24
  */
145
- export function processOperation(envelope, vaultRoot, options = {}) {
146
- const { leaseStore, eventLogDir } = options;
147
- const operationId = crypto.randomUUID();
148
- const warnings = [];
149
- const actorRole = getActorRole(envelope, options);
150
-
151
- // Stage 1: Envelope Validation
152
- const schemaMap = { operation: OperationEnvelopeSchema, patch: PatchEnvelopeSchema, event: EventEnvelopeSchema, delete: DeleteEnvelopeSchema };
153
- const envSchema = schemaMap[envelope.type];
154
- if (!envSchema) return makeErrorResponse(envelope.type || 'unknown', operationId, envelope.path || '', [`Unknown envelope type: ${envelope.type}`]);
155
- const envResult = envSchema.safeParse(envelope);
156
- if (!envResult.success) return { ...makeErrorResponse(envelope.type, operationId, envelope.path || '', envResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`)), repair: buildRepair(envResult.error) };
157
-
158
- // Warm idempotency cache from audit logs if not already done
159
- warmIdempotencyCache(vaultRoot, eventLogDir);
160
-
161
- // Stage 2: Idempotency
162
- pruneExpired();
163
- const ck = cacheKey(envelope.workspace_id, envelope.idempotency_key);
164
- const cached = idempotencyCache.get(ck);
165
- if (cached) return { ...cached.response, replay: cached.response };
166
-
167
- // Stage 3: Authorization
168
- if (isProtocolPath(envelope.path) && actorRole !== 'admin' && actorRole !== 'system') return makeErrorResponse(envelope.type, operationId, envelope.path, [`Protocol path '${envelope.path}' requires admin role.`]);
169
-
170
- // Stage 4: Lease Check
171
- if (leaseStore) {
172
- const lc = checkLease(envelope, leaseStore);
173
- if (!lc.ok) return makeErrorResponse(envelope.type, operationId, envelope.path, [lc.error]);
174
- }
175
-
176
- // Stage 5: Content Validation
177
- let resolvedType = null;
178
- const errors = [];
179
-
180
- if (envelope.type === 'operation' || envelope.type === 'patch') {
181
- try {
182
- let fmData;
183
- if (envelope.type === 'operation') {
184
- fmData = matter(envelope.content).data;
185
- } else {
186
- const absPath = resolveVfsPath(vaultRoot, envelope.path);
187
- if (!fs.existsSync(absPath)) { errors.push(`Target file does not exist for patch: ${envelope.path}`); }
188
- else {
189
- const { data, content: body } = matter(fs.readFileSync(absPath, 'utf8'));
190
- fmData = { ...data, ...envelope.patches };
191
- if (envelope.patches.__body__ !== undefined && APPEND_TYPES.has(data.type)) {
192
- const eb = body.trim(), nb = String(envelope.patches.__body__).trim();
193
- if (eb && !nb.startsWith(eb)) errors.push(`Append-type '${data.type}' does not allow rewriting existing records.`);
194
- }
195
- }
196
- }
197
- if (fmData && errors.length === 0) {
198
- resolvedType = fmData.type;
199
- if (!resolvedType) errors.push('Missing required frontmatter field: type');
200
- else if (!SSSS_SCHEMAS[resolvedType]) errors.push(`Unknown SSSS type: '${resolvedType}'.`);
201
- else {
202
- if (resolvedType === 'memory' && envelope.path.includes('.agent/memory-vault/')) {
203
- const vResult = validateMemoryNode(fmData);
204
- if (!vResult.success) vResult.errors.forEach(e => errors.push(e));
205
- } else {
206
- const r = SSSS_SCHEMAS[resolvedType].safeParse(fmData);
207
- if (!r.success) r.error.issues.forEach(i => errors.push(`${i.path.join('.')}: ${i.message}`));
208
- }
209
-
210
- if (envelope.type === 'operation' && APPEND_TYPES.has(resolvedType)) {
211
- const ap = resolveVfsPath(vaultRoot, envelope.path);
212
- if (fs.existsSync(ap)) {
213
- const eb = matter(fs.readFileSync(ap, 'utf8')).content.trim();
214
- const nb = matter(envelope.content).content.trim();
215
- if (eb && !nb.startsWith(eb)) errors.push(`Append-type '${resolvedType}' does not allow rewriting existing records.`);
216
- }
217
- }
218
- if (actorRole === 'optimizer' && resolvedType === 'memory') {
219
- if (fmData.priority === 'absolute') warnings.push('Optimizer writing Tier 1 (priority: absolute) node.');
220
- if (fmData.immutable === true) warnings.push('Optimizer writing immutable node.');
221
- }
222
- if (resolvedType === 'schema-proposal' && fmData.status === 'accepted') {
223
- if (actorRole !== 'admin') errors.push('Only admin may accept schema-proposals.');
224
- if (!fmData.reviewed_by) errors.push('Accepted schema-proposals must have reviewed_by.');
225
- }
226
- }
227
- }
228
- } catch (err) { errors.push(`Content parsing error: ${err.message}`); }
229
- }
230
- if (envelope.type === 'event') { try { JSON.parse(envelope.content); } catch { errors.push('Event content must be valid JSON.'); } }
231
-
232
- if (envelope.type === 'delete') {
233
- const absPath = resolveVfsPath(vaultRoot, envelope.path);
234
- if (!fs.existsSync(absPath)) { errors.push(`Target file does not exist for delete: ${envelope.path}`); }
235
- else {
236
- const { data } = matter(fs.readFileSync(absPath, 'utf8'));
237
- resolvedType = data.type || null;
238
- // §6.2: append-type documents are immutable and may not be deleted.
239
- if (APPEND_TYPES.has(resolvedType)) errors.push(`Append-type '${resolvedType}' may not be deleted.`);
240
- }
241
- }
242
-
243
- if (errors.length === 0) {
244
- const permType = envelope.type === 'event' ? 'event' : resolvedType;
245
- const requiredPerm = `write:${permType}`;
246
- const permissions = loadRolePermissions(vaultRoot, actorRole);
247
- if (!hasPermission(permissions, requiredPerm)) {
248
- errors.push(`Access denied: role '${actorRole || '(none)'}' lacks permission '${requiredPerm}'.`);
249
- }
250
- }
251
-
252
- const isValid = errors.length === 0;
253
- if (envelope.dry_run || !isValid) {
254
- const resp = { success: isValid, type: envelope.type, operation_id: operationId, path: envelope.path, committed_at: null, dry_run: !!envelope.dry_run, validation: { valid: isValid, type: resolvedType, errors, warnings } };
255
- if (!isValid) resp.repair = {
256
- field_errors: errors.map(e => {
257
- if (e.startsWith('Access denied:')) {
258
- return {
259
- field: 'actor.role',
260
- issue: actorRole ? 'Insufficient permissions.' : 'Missing actor.role — the envelope was not authorized by a verified identity.'
261
- };
262
- }
263
- return { field: e.split(': ')[0] || '(unknown)', issue: e };
264
- })
265
- };
266
- return resp;
267
- }
268
-
269
- // Stage 6: Commit
270
- const committedAt = new Date().toISOString();
271
- try {
272
- const absPath = resolveVfsPath(vaultRoot, envelope.path);
273
- const dir = path.dirname(absPath);
274
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
275
- if (envelope.type === 'operation') {
276
- let contentToWrite = envelope.content;
277
- try {
278
- const parsed = matter(contentToWrite);
279
- if (parsed.data && parsed.data.type === 'memory') {
280
- parsed.data.updated = committedAt;
281
- parsed.data.last_accessed = committedAt;
282
- contentToWrite = safeStringify(parsed.content, parsed.data);
283
- }
284
- } catch {
285
- // Fallback to raw content if parsing fails
286
- }
287
- atomicWrite(absPath, contentToWrite);
288
- }
289
- else if (envelope.type === 'patch') {
290
- const { data, content: body } = matter(fs.readFileSync(absPath, 'utf8'));
291
- const merged = { ...data };
292
- for (const [k, v] of Object.entries(envelope.patches)) { if (k !== '__body__') merged[k] = v; }
293
- if (merged.type === 'memory') {
294
- merged.updated = committedAt;
295
- merged.last_accessed = committedAt;
296
- }
297
- const newBody = envelope.patches.__body__ !== undefined ? (APPEND_TYPES.has(data.type) ? body + '\n' + envelope.patches.__body__ : envelope.patches.__body__) : body;
298
- atomicWrite(absPath, safeStringify(newBody, merged));
299
- } else if (envelope.type === 'event') {
300
- const logDir = eventLogDir || path.join(vaultRoot, '.events');
301
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
302
- fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'operation-audit', ts: committedAt, path: envelope.path, content: envelope.content, idempotency_key: envelope.idempotency_key }) + '\n');
303
- } else if (envelope.type === 'delete') {
304
- fs.rmSync(absPath, { force: true });
305
- // §6.2: a delete emits an auditable deletion event so history is never lost.
306
- const logDir = eventLogDir || path.join(vaultRoot, '.events');
307
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
308
- fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'delete', ts: committedAt, path: envelope.path, resolved_type: resolvedType, idempotency_key: envelope.idempotency_key }) + '\n');
309
- }
310
- } catch (err) {
311
- logger.error('operation-validator', `Commit failed: ${err.message}`);
312
- return makeErrorResponse(envelope.type, operationId, envelope.path, [`Commit failed: ${err.message}`]);
313
- }
314
-
315
- // Stage 7: Audit
316
- try {
317
- const auditDir = eventLogDir || path.join(vaultRoot, '.events');
318
- if (!fs.existsSync(auditDir)) fs.mkdirSync(auditDir, { recursive: true });
319
- fs.appendFileSync(path.join(auditDir, 'audit.jsonl'), JSON.stringify({ event_id: crypto.randomUUID(), event_type: 'audit', correlation_id: operationId, ts: committedAt, subject: envelope.path, payload: { envelope_type: envelope.type, idempotency_key: envelope.idempotency_key, workspace_id: envelope.workspace_id, agent_role: actorRole, resolved_type: resolvedType } }) + '\n');
320
- } catch (err) { logger.error('operation-validator', `Audit failed: ${err.message}`); }
321
-
322
- const response = { success: true, type: envelope.type, operation_id: operationId, path: envelope.path, committed_at: committedAt, validation: { valid: true, type: resolvedType, errors: [], warnings } };
323
- idempotencyCache.set(ck, { response, ts: Date.now() });
324
- return response;
25
+ export async function processOperationAsync(envelope, vaultRoot, options = {}) {
26
+ return processViaPackageKernel(envelope, vaultRoot, {
27
+ ...options,
28
+ agentRole: options.agentRole || envelope?.actor?.role || 'admin',
29
+ });
325
30
  }
326
31
 
327
- function checkLease(envelope, leaseStore) {
328
- const lp = path.join(leaseStore, envelope.workspace_id, `${envelope.path.replace(/\//g, '__')}.lease.json`);
329
- if (!fs.existsSync(lp)) return { ok: true };
330
- try {
331
- const lease = JSON.parse(fs.readFileSync(lp, 'utf8'));
332
- if (new Date(lease.expires_at) < new Date()) { fs.unlinkSync(lp); return { ok: true }; }
333
- if (!envelope.lease_id) return { ok: false, error: `Path '${envelope.path}' is leased (${lease.lease_id}). Supply lease_id.` };
334
- if (envelope.lease_id !== lease.lease_id) return { ok: false, error: `Lease mismatch: got '${envelope.lease_id}', expected '${lease.lease_id}'.` };
335
- return { ok: true };
336
- } catch { try { fs.unlinkSync(lp); } catch { } return { ok: true }; }
337
- }
32
+ // ─── Lease helpers (host-local filesystem lease store, used by tests/API) ────
338
33
 
339
34
  export function acquireLease(workspaceId, vfsPath, leaseStore, ttlMs = 30000) {
340
35
  const dir = path.join(leaseStore, workspaceId);
341
36
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
342
37
  const lp = path.join(dir, `${vfsPath.replace(/\//g, '__')}.lease.json`);
343
- if (fs.existsSync(lp)) { try { const e = JSON.parse(fs.readFileSync(lp, 'utf8')); if (new Date(e.expires_at) >= new Date()) return { error: `Path '${vfsPath}' already leased.` }; } catch { } }
38
+ if (fs.existsSync(lp)) {
39
+ try {
40
+ const e = JSON.parse(fs.readFileSync(lp, 'utf8'));
41
+ if (new Date(e.expires_at) >= new Date()) return { error: `Path '${vfsPath}' already leased.` };
42
+ } catch { /* replace corrupt lease */ }
43
+ }
344
44
  const leaseId = crypto.randomUUID();
345
45
  const expiresAt = new Date(Date.now() + ttlMs).toISOString();
346
- fs.writeFileSync(lp, JSON.stringify({ lease_id: leaseId, path: vfsPath, workspace_id: workspaceId, expires_at: expiresAt }, null, 2));
46
+ fs.writeFileSync(lp, JSON.stringify({
47
+ lease_id: leaseId, path: vfsPath, workspace_id: workspaceId, expires_at: expiresAt,
48
+ }, null, 2));
347
49
  return { lease_id: leaseId, expires_at: expiresAt };
348
50
  }
349
51
 
350
52
  export function releaseLease(workspaceId, vfsPath, leaseId, leaseStore) {
351
53
  const lp = path.join(leaseStore, workspaceId, `${vfsPath.replace(/\//g, '__')}.lease.json`);
352
54
  if (!fs.existsSync(lp)) return { released: true };
353
- try { const e = JSON.parse(fs.readFileSync(lp, 'utf8')); if (e.lease_id !== leaseId) return { error: 'Lease ID mismatch.' }; fs.unlinkSync(lp); return { released: true }; }
354
- catch (err) { return { error: err.message }; }
55
+ try {
56
+ const e = JSON.parse(fs.readFileSync(lp, 'utf8'));
57
+ if (e.lease_id !== leaseId) return { error: 'Lease ID mismatch.' };
58
+ fs.unlinkSync(lp);
59
+ return { released: true };
60
+ } catch (err) {
61
+ return { error: err.message };
62
+ }
355
63
  }
@@ -18,6 +18,13 @@ export function createProposal(category, summary, targetPath = null, rationale =
18
18
  return {
19
19
  type: 'proposal',
20
20
  proposal_id: proposalId,
21
+ slug: proposalId,
22
+ // Vault folder for proposal documents (distinct from proposal.category topic)
23
+ // prepareNodeForContract falls back to category=proposals when absent; set explicitly.
24
+ // Note: SSSS field `category` here is the proposal topic (memory-cleanup, etc.).
25
+ title: summary || proposalId,
26
+ description: rationale || summary || 'Optimizer proposal',
27
+ timestamp: new Date().toISOString(),
21
28
  category,
22
29
  status: 'draft',
23
30
  target_path: targetPath,
@@ -219,7 +219,7 @@ export async function runPostMortem(sessionPath, { vaultDir, inboxDir, runtimeCo
219
219
  // ─── Draft Node Writers ─────────────────────────────────────────────────────────
220
220
 
221
221
  function writeDraftNode(inboxDir, { title, body, category, layer, tags, sourceSession }) {
222
- const slug = `pm-${category}-${crypto.randomBytes(4).toString('hex')}`;
222
+ const slug = `pm-${category}-${crypto.createHash('md5').update(sourceSession + title).digest('hex').slice(0, 8)}`;
223
223
  const now = new Date().toISOString();
224
224
 
225
225
  const frontmatter = {
@@ -270,7 +270,7 @@ function writeDraftNode(inboxDir, { title, body, category, layer, tags, sourceSe
270
270
  }
271
271
 
272
272
  function writeSkillGapTask(queueDir, { topic, description, sourceSession }) {
273
- const slug = `skill-gap-${topic}-${crypto.randomBytes(3).toString('hex')}`;
273
+ const slug = `skill-gap-${crypto.createHash('md5').update(topic).digest('hex').slice(0, 6)}`;
274
274
  const data = {
275
275
  type: 'task',
276
276
  slug,