@geraldmaron/construct 1.0.23 → 1.0.24

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 (52) hide show
  1. package/README.md +0 -2
  2. package/bin/construct +15 -215
  3. package/lib/embed/inbox.mjs +6 -3
  4. package/lib/embed/recommendation-store.mjs +7 -289
  5. package/lib/embed/reconcile.mjs +2 -2
  6. package/lib/hooks/config-protection.mjs +4 -4
  7. package/lib/hooks/session-reflect.mjs +5 -1
  8. package/lib/intake/git-queue.mjs +195 -0
  9. package/lib/intake/queue.mjs +9 -16
  10. package/lib/mcp/tools/storage.mjs +2 -3
  11. package/lib/mcp-catalog.json +3 -3
  12. package/lib/mcp-manager.mjs +59 -3
  13. package/lib/observation-store.mjs +38 -166
  14. package/lib/orchestration/runtime.mjs +3 -2
  15. package/lib/reconcile/index.mjs +0 -2
  16. package/lib/service-manager.mjs +38 -256
  17. package/lib/setup.mjs +26 -426
  18. package/lib/status.mjs +3 -6
  19. package/lib/storage/admin.mjs +48 -325
  20. package/lib/storage/backend.mjs +10 -57
  21. package/lib/storage/hybrid-query.mjs +15 -196
  22. package/lib/storage/sync.mjs +36 -177
  23. package/lib/storage/vector-client.mjs +256 -235
  24. package/lib/strategy-store.mjs +35 -286
  25. package/lib/worker/entrypoint.mjs +6 -14
  26. package/package.json +6 -5
  27. package/platforms/claude/settings.template.json +0 -7
  28. package/scripts/sync-specialists.mjs +46 -12
  29. package/specialists/prompts/cx-qa.md +1 -1
  30. package/specialists/registry.json +0 -8
  31. package/templates/docs/construct_guide.md +1 -1
  32. package/db/schema/001_init.sql +0 -40
  33. package/db/schema/002_pgvector.sql +0 -182
  34. package/db/schema/003_intake.sql +0 -47
  35. package/db/schema/003_observation_reconciliation.sql +0 -14
  36. package/db/schema/004_recommendations.sql +0 -46
  37. package/db/schema/005_strategy.sql +0 -21
  38. package/db/schema/006_graph.sql +0 -24
  39. package/db/schema/007_tags.sql +0 -30
  40. package/db/schema/008_skill_usage.sql +0 -24
  41. package/db/schema/009_scheduler.sql +0 -14
  42. package/db/schema/010_cx_scores.sql +0 -51
  43. package/lib/intake/postgres-queue.mjs +0 -240
  44. package/lib/reconcile/postgres-namespace.mjs +0 -102
  45. package/lib/services/local-postgres.mjs +0 -15
  46. package/lib/storage/backup.mjs +0 -347
  47. package/lib/storage/migrations.mjs +0 -187
  48. package/lib/storage/postgres-backup.mjs +0 -124
  49. package/lib/storage/sql-store.mjs +0 -45
  50. package/lib/storage/store-version.mjs +0 -115
  51. package/lib/storage/unified-storage.mjs +0 -550
  52. package/lib/storage/vector-store.mjs +0 -100
@@ -36,11 +36,10 @@
36
36
  import path from 'node:path';
37
37
 
38
38
  import { FilesystemIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir } from './filesystem-queue.mjs';
39
- import { PostgresIntakeQueue } from './postgres-queue.mjs';
39
+ import { GitIntakeQueue } from './git-queue.mjs';
40
40
  import { getDeploymentMode } from '../deployment-mode.mjs';
41
- import { createSqlClient } from '../storage/backend.mjs';
42
41
 
43
- export { FilesystemIntakeQueue, PostgresIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
42
+ export { FilesystemIntakeQueue, GitIntakeQueue, queueRoot, pendingDir, processedDir, skippedDir };
44
43
 
45
44
  export const INTAKE_QUEUE_BACKEND_ENV_KEY = 'CONSTRUCT_INTAKE_QUEUE_BACKEND';
46
45
  export const INTAKE_PROJECT_ENV_KEY = 'CONSTRUCT_PROJECT_NAME';
@@ -48,9 +47,9 @@ export const INTAKE_TENANT_ENV_KEY = 'CONSTRUCT_TENANT_ID';
48
47
 
49
48
  function resolveBackend(env) {
50
49
  const override = env?.[INTAKE_QUEUE_BACKEND_ENV_KEY];
51
- if (override === 'filesystem' || override === 'postgres') return override;
50
+ if (override === 'filesystem' || override === 'git') return override;
52
51
  const mode = getDeploymentMode(env);
53
- return mode === 'solo' ? 'filesystem' : 'postgres';
52
+ return mode === 'solo' ? 'filesystem' : 'git';
54
53
  }
55
54
 
56
55
  function resolveProject(rootDir, env) {
@@ -62,22 +61,16 @@ function resolveProject(rootDir, env) {
62
61
  /**
63
62
  * Create an IntakeQueue instance for the given project root.
64
63
  * Backend selection: CONSTRUCT_INTAKE_QUEUE_BACKEND override wins; otherwise
65
- * solo mode → filesystem, team/enterprise → postgres. The postgres
66
- * backend pulls its connection from createSqlClient(env) and uses
67
- * CONSTRUCT_PROJECT_NAME / CONSTRUCT_TENANT_ID to scope queries; an
68
- * explicit `sql` and `project` in opts override both.
64
+ * solo mode → filesystem, team/enterprise → git. The git
65
+ * backend uses the filesystem and git for state synchronization.
69
66
  */
70
67
  export function createIntakeQueue(rootDir, env = process.env, opts = {}) {
71
68
  const backend = opts.backend || resolveBackend(env);
72
69
  if (backend === 'filesystem') return new FilesystemIntakeQueue(rootDir);
73
- if (backend === 'postgres') {
74
- const sql = opts.sql ?? createSqlClient(env);
75
- if (!sql) {
76
- throw new Error('Postgres intake queue requires a configured DATABASE_URL (or sql client). Run `construct init` or set CONSTRUCT_INTAKE_QUEUE_BACKEND=filesystem.');
77
- }
70
+ if (backend === 'git' || backend === 'postgres') {
71
+ // We treat 'postgres' as an alias for 'git' now to avoid breaking existing configs
78
72
  const project = opts.project ?? resolveProject(rootDir, env);
79
- const tenantId = opts.tenantId ?? env?.[INTAKE_TENANT_ENV_KEY] ?? null;
80
- return new PostgresIntakeQueue({ sql, project, tenantId });
73
+ return new GitIntakeQueue({ project, rootDir });
81
74
  }
82
75
  throw new Error(`Unknown intake queue backend: ${backend}`);
83
76
  }
@@ -37,14 +37,13 @@ export async function storageReset(args) {
37
37
  return resetStorage(cwd, {
38
38
  env: process.env,
39
39
  project,
40
- resetSql: args.reset_sql !== false,
41
40
  resetVector: args.reset_vector !== false,
42
41
  resetIngested: args.reset_ingested === true,
43
42
  confirm: true,
44
43
  });
45
44
  }
46
45
 
47
- export function deleteIngestedArtifactsTool(args) {
46
+ export async function deleteIngestedArtifactsTool(args) {
48
47
  const cwd = args.cwd ? resolve(String(args.cwd)) : process.cwd();
49
48
  if (args.confirm !== true) {
50
49
  return { error: 'delete_ingested_artifacts requires confirm=true' };
@@ -53,7 +52,7 @@ export function deleteIngestedArtifactsTool(args) {
53
52
  ? args.files.map((value) => String(value))
54
53
  : [];
55
54
  try {
56
- return deleteIngestedArtifacts(cwd, { files, confirm: true });
55
+ return await deleteIngestedArtifacts(cwd, { files, confirm: true });
57
56
  } catch (error) {
58
57
  return { error: error.message ?? String(error) };
59
58
  }
@@ -85,12 +85,12 @@
85
85
  "id": "playwright",
86
86
  "name": "Playwright",
87
87
  "category": "optional",
88
- "description": "Browser automation for E2E testing and web research.",
89
- "package": "@playwright/mcp@latest",
88
+ "description": "Browser automation for E2E testing and web research. Opt-in via `construct mcp add playwright` — not synced by default (downloads browser binaries on first use).",
89
+ "package": "@playwright/mcp@0.0.75",
90
90
  "command": "npx",
91
91
  "args": [
92
92
  "-y",
93
- "@playwright/mcp@latest"
93
+ "@playwright/mcp@0.0.75"
94
94
  ],
95
95
  "env": {},
96
96
  "requiredEnv": [],
@@ -33,6 +33,56 @@ function getClaudeSettingsPath() {
33
33
  return join(homedir(), '.claude', 'settings.json');
34
34
  }
35
35
 
36
+ // VS Code's "MCP: Open User Configuration" edits a per-profile mcp.json keyed
37
+ // on top-level `servers`; Cursor uses ~/.cursor/mcp.json keyed on `mcpServers`.
38
+ // construct sync writes both surfaces, so remove must clean them too or a
39
+ // removed MCP lingers there.
40
+
41
+ function getVSCodeUserMcpPaths(home = homedir()) {
42
+ const plat = platform();
43
+ if (plat === 'darwin') {
44
+ return [
45
+ join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'),
46
+ join(home, 'Library', 'Application Support', 'Code - Insiders', 'User', 'mcp.json'),
47
+ ];
48
+ }
49
+ if (plat === 'win32') {
50
+ const appData = process.env.APPDATA ?? join(home, 'AppData', 'Roaming');
51
+ return [join(appData, 'Code', 'User', 'mcp.json'), join(appData, 'Code - Insiders', 'User', 'mcp.json')];
52
+ }
53
+ return [
54
+ join(home, '.config', 'Code', 'User', 'mcp.json'),
55
+ join(home, '.config', 'Code - Insiders', 'User', 'mcp.json'),
56
+ ];
57
+ }
58
+
59
+ function getCursorMcpPath(home = homedir()) {
60
+ return join(home, '.cursor', 'mcp.json');
61
+ }
62
+
63
+ function mcpFileHasEntry(file, key, id) {
64
+ if (!existsSync(file)) return false;
65
+ try {
66
+ return Boolean(JSON.parse(readFileSync(file, 'utf8'))?.[key]?.[id]);
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function removeMcpFromJsonFile(file, key, id) {
73
+ if (!existsSync(file)) return false;
74
+ let config;
75
+ try {
76
+ config = JSON.parse(readFileSync(file, 'utf8'));
77
+ } catch {
78
+ return false;
79
+ }
80
+ if (!config?.[key] || !(id in config[key])) return false;
81
+ delete config[key][id];
82
+ writeFileSync(file, JSON.stringify(config, null, 2) + '\n');
83
+ return true;
84
+ }
85
+
36
86
  function openUrl(url) {
37
87
  try {
38
88
  const opener = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
@@ -426,10 +476,14 @@ export function cmdMcpRemove(id) {
426
476
  const openCodeState = readOpenCodeConfig();
427
477
  const oc = openCodeState.config ?? {};
428
478
  const openCodeId = getOpenCodeMcpId(id);
479
+ const vscodePaths = getVSCodeUserMcpPaths();
480
+ const cursorPath = getCursorMcpPath();
429
481
  const hasClaudeEntry = Boolean(settings.mcpServers?.[id]);
430
482
  const hasOpenCodeEntry = Boolean(oc.mcp?.[openCodeId] || oc.mcp?.[id] || (id === 'memory' && oc.mcp?.cass));
431
483
  const hasCodexEntry = readCodexConfig().includes(`[mcp_servers.${id}]`) || readCodexConfig().includes(`[mcp_servers.${tomlString(id)}]`);
432
- if (!hasClaudeEntry && !hasOpenCodeEntry && !hasCodexEntry) {
484
+ const hasVscodeEntry = vscodePaths.some((p) => mcpFileHasEntry(p, 'servers', id));
485
+ const hasCursorEntry = mcpFileHasEntry(cursorPath, 'mcpServers', id);
486
+ if (!hasClaudeEntry && !hasOpenCodeEntry && !hasCodexEntry && !hasVscodeEntry && !hasCursorEntry) {
433
487
  console.log(`${id} is not installed. Nothing to remove.`);
434
488
  return;
435
489
  }
@@ -448,9 +502,11 @@ export function cmdMcpRemove(id) {
448
502
  if (openCodeState.config || hasOpenCodeEntry) saveOpenCodeConfig(oc);
449
503
  else if (openCodeState.file && existsSync(openCodeState.file)) rmSync(openCodeState.file, { force: true });
450
504
  removeCodexMcp(id);
505
+ for (const p of vscodePaths) removeMcpFromJsonFile(p, 'servers', id);
506
+ removeMcpFromJsonFile(cursorPath, 'mcpServers', id);
451
507
 
452
- console.log(`✓ ${name} removed from Claude Code, OpenCode, and Codex config`);
453
- console.log('Restart Claude Code, OpenCode, or Codex to deactivate it.');
508
+ console.log(`✓ ${name} removed from Claude Code, OpenCode, Codex, VS Code, and Cursor config`);
509
+ console.log('Restart the editor to deactivate it.');
454
510
  }
455
511
 
456
512
  /**
@@ -4,31 +4,36 @@
4
4
  * Stores distilled insights that specialists learn during work:
5
5
  * - patterns, anti-patterns, dependency relationships, decisions, insights
6
6
  * - each scoped to a role (cx-engineer, cx-architect, etc.)
7
- * - vector-indexed for semantic search via hashing-bow-v1
7
+ * - vector-indexed for semantic search via embedded LanceDB
8
8
  *
9
9
  * Storage layout:
10
10
  * .cx/observations/index.json — lightweight listing for fast filtering
11
11
  * .cx/observations/<id>.json — full observation record
12
- * .cx/observations/vectors.json — local vector index for semantic search
13
12
  */
14
13
  import crypto from 'node:crypto';
15
14
  import fs from 'node:fs';
16
15
  import path from 'node:path';
17
- import { cosineSimilarity, rankByBm25 } from './storage/embeddings.mjs';
18
- import { embedSync as embedText } from './storage/embeddings-legacy.mjs';
19
16
  import { embedText as embedTextEngine } from './storage/embeddings-engine.mjs';
20
17
  import { VectorClient } from './storage/vector-client.mjs';
21
- import { withFileLockSync } from './storage/file-lock.mjs';
22
18
  import { ensureCxDir } from './project-init-shared.mjs';
23
19
 
24
20
  const OBS_DIR = '.cx/observations';
25
21
  const INDEX_FILE = 'index.json';
26
- const VECTORS_FILE = 'vectors.json';
27
22
  const MAX_INDEX = 1000;
28
23
 
29
- // The exact text fed to the embedder, and its content hash. addObservation and
30
- // the reconciliation pass MUST derive both the same way, so the (content_hash,
31
- // model) fingerprint they compare is computed identically.
24
+ // VectorClient resolves its store from CONSTRUCT_LANCEDB_PATH and otherwise
25
+ // falls back to process.cwd(). Observations are scoped to rootDir, so when no
26
+ // explicit path is configured (tests, unmanaged checkouts) the vector store
27
+ // must follow rootDir — not the caller's cwd — or a reader in a different cwd
28
+ // than the writer sees an empty table. A configured path (managed installs)
29
+ // is left untouched so the home-global store keeps working.
30
+
31
+ function vectorClientFor(rootDir) {
32
+ const env = process.env.CONSTRUCT_LANCEDB_PATH
33
+ ? process.env
34
+ : { ...process.env, CONSTRUCT_LANCEDB_PATH: path.join(rootDir, '.cx', 'lancedb') };
35
+ return new VectorClient({ env });
36
+ }
32
37
 
33
38
  export function observationSearchText(obs) {
34
39
  return [obs.summary, obs.content, ...(obs.tags || [])].filter(Boolean).join(' ');
@@ -40,9 +45,6 @@ export function observationContentHash(text) {
40
45
  const MAX_SUMMARY = 500;
41
46
  const MAX_CONTENT = 2000;
42
47
  const MAX_TAGS = 10;
43
- // Size guardrail: surfaces in `construct doctor` when the observation tree
44
- // exceeds this byte cap. Doesn't drop data — the operator decides whether to
45
- // run `construct memory consolidate` (which prunes archive) or raise the cap.
46
48
  const MAX_BYTES = 50 * 1024 * 1024;
47
49
 
48
50
  const VALID_CATEGORIES = new Set([
@@ -50,8 +52,6 @@ const VALID_CATEGORIES = new Set([
50
52
  ]);
51
53
 
52
54
  function ensureDir(dir, rootDir = null) {
53
- // Preserves the construct context invariant: any code that creates a .cx/
54
- // subdirectory also initializes context.md from the project template.
55
55
  if (rootDir) ensureCxDir(rootDir);
56
56
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
57
57
  }
@@ -64,10 +64,6 @@ function indexPath(rootDir) {
64
64
  return path.join(obsDir(rootDir), INDEX_FILE);
65
65
  }
66
66
 
67
- function vectorsPath(rootDir) {
68
- return path.join(obsDir(rootDir), VECTORS_FILE);
69
- }
70
-
71
67
  function generateId() {
72
68
  const ts = Date.now().toString(36);
73
69
  const rand = crypto.randomBytes(4).toString('hex');
@@ -132,30 +128,6 @@ function writeIndex(rootDir, entries) {
132
128
  return { dropped };
133
129
  }
134
130
 
135
- function readVectors(rootDir) {
136
- const p = vectorsPath(rootDir);
137
- if (!fs.existsSync(p)) return [];
138
- try {
139
- return JSON.parse(fs.readFileSync(p, 'utf8'));
140
- } catch {
141
- return [];
142
- }
143
- }
144
-
145
- function writeVectors(rootDir, records) {
146
- ensureDir(obsDir(rootDir), rootDir);
147
- const dropped = Math.max(0, records.length - MAX_INDEX);
148
- if (dropped > 0) {
149
- logCapDrop(rootDir, 'observation-vectors', dropped, records.length);
150
- }
151
- fs.writeFileSync(vectorsPath(rootDir), JSON.stringify(records, null, 2) + '\n');
152
- return { dropped };
153
- }
154
-
155
- /**
156
- * Add a new observation and vectorize it for semantic search.
157
- * Uses SQL + neural embeddings when available, falls back to local JSON + hashing.
158
- */
159
131
  export async function addObservation(rootDir, {
160
132
  role = 'unknown',
161
133
  category = 'insight',
@@ -175,10 +147,9 @@ export async function addObservation(rootDir, {
175
147
  const clampedSummary = clamp(String(summary), MAX_SUMMARY);
176
148
  const clampedContent = clamp(String(content), MAX_CONTENT);
177
149
  const clampedTags = (Array.isArray(tags) ? tags : []).slice(0, MAX_TAGS).map(String);
178
- // Structured metadata: opt-in. Capped at 2 KB stringified so a misuse can't
179
- // bloat the per-id JSON. Plain object only; rejects arrays and functions.
180
150
  const clampedExtras = sanitizeExtras(extras);
181
151
 
152
+ const effectiveProject = project ? String(project) : rootDir.split('/').pop();
182
153
  const record = {
183
154
  id,
184
155
  role: String(role),
@@ -186,7 +157,7 @@ export async function addObservation(rootDir, {
186
157
  summary: clampedSummary,
187
158
  content: clampedContent,
188
159
  tags: clampedTags,
189
- project: project ? String(project) : null,
160
+ project: effectiveProject,
190
161
  confidence: Math.max(0, Math.min(1, Number(confidence) || 0.8)),
191
162
  source: source || null,
192
163
  gitSha: gitSha ? String(gitSha).slice(0, 40) : null,
@@ -195,37 +166,30 @@ export async function addObservation(rootDir, {
195
166
  updatedAt: now,
196
167
  };
197
168
 
198
- // Lock the per-store index so concurrent writers (CLI + hooks) cannot
199
- // corrupt the index/vectors JSON pair. The observation JSON file is
200
- // unique-per-id and doesn't need the lock; the shared structures do.
201
169
  ensureDir(obsDir(rootDir), rootDir);
202
170
  fs.writeFileSync(
203
171
  path.join(obsDir(rootDir), `${id}.json`),
204
172
  JSON.stringify(record, null, 2) + '\n',
205
173
  );
206
174
 
207
- let indexDropped = 0;
208
- withFileLockSync(indexPath(rootDir), () => {
209
- const index = readIndex(rootDir);
210
- index.unshift({
211
- id,
212
- role: record.role,
213
- category: record.category,
214
- summary: record.summary,
215
- project: record.project,
216
- createdAt: now,
217
- });
218
- indexDropped = writeIndex(rootDir, index).dropped;
175
+ const index = readIndex(rootDir);
176
+ index.unshift({
177
+ id,
178
+ role: record.role,
179
+ category: record.category,
180
+ summary: record.summary,
181
+ project: record.project,
182
+ createdAt: now,
219
183
  });
184
+ const indexDropped = writeIndex(rootDir, index).dropped;
220
185
  if (indexDropped > 0) {
221
186
  record.capDropped = indexDropped;
222
187
  process.stderr.write(`[observation-store] observation cap reached: ${indexDropped} oldest entries evicted (cap=${MAX_INDEX})\n`);
223
188
  }
224
189
 
225
- // Try SQL storage with neural embeddings
226
190
  try {
227
- const client = new VectorClient();
228
- if (await client.isHealthy() && await client.isPgvectorEnabled()) {
191
+ const client = vectorClientFor(rootDir);
192
+ if (await client.isHealthy()) {
229
193
  const searchText = observationSearchText(record);
230
194
  const { embedding, model } = await embedTextEngine(searchText);
231
195
  await client.storeObservation({
@@ -234,34 +198,15 @@ export async function addObservation(rootDir, {
234
198
  contentHash: observationContentHash(searchText),
235
199
  model,
236
200
  });
237
- await client.close();
238
- return record;
239
201
  }
240
202
  await client.close();
241
203
  } catch (err) {
242
- // A reachable backend that still fails to store (dimension drift, auth, transient
243
- // network) must not look identical to "no backend configured" — surface it, then
244
- // preserve the observation in the local index so the write never silently vanishes.
245
-
246
- process.stderr.write(`[observation-store] SQL store failed; kept observation in local index: ${err?.message || err}\n`);
204
+ process.stderr.write(`[observation-store] vector store failed; kept observation in local index: ${err?.message || err}\n`);
247
205
  }
248
206
 
249
- // Fallback: local JSON vector index with hashing-bow-v1.
250
- const searchText = [record.summary, record.content, ...record.tags].filter(Boolean).join(' ');
251
- const embedding = embedText(searchText);
252
- withFileLockSync(vectorsPath(rootDir), () => {
253
- const vectors = readVectors(rootDir);
254
- vectors.push({ id, embedding, role: record.role, category: record.category, project: record.project });
255
- writeVectors(rootDir, vectors.slice(-MAX_INDEX));
256
- });
257
-
258
207
  return record;
259
208
  }
260
209
 
261
- /**
262
- * Search observations using SQL + neural embeddings when available,
263
- * falling back to hybrid cosine + BM25 on local JSON.
264
- */
265
210
  export async function searchObservations(rootDir, query, {
266
211
  role = null,
267
212
  category = null,
@@ -270,16 +215,15 @@ export async function searchObservations(rootDir, query, {
270
215
  } = {}) {
271
216
  if (!query) return [];
272
217
 
273
- // Try SQL + neural embeddings first
274
218
  try {
275
- const client = new VectorClient();
276
- if (await client.isHealthy() && await client.isPgvectorEnabled()) {
219
+ const client = vectorClientFor(rootDir);
220
+ if (await client.isHealthy()) {
277
221
  const { embedding } = await embedTextEngine(String(query));
278
222
  const results = await client.searchObservations({
279
223
  project: project || rootDir.split('/').pop(),
280
224
  queryEmbedding: embedding,
281
225
  limit,
282
- minSimilarity: 0.1,
226
+ minSimilarity: 0.01,
283
227
  role,
284
228
  category,
285
229
  });
@@ -288,6 +232,7 @@ export async function searchObservations(rootDir, query, {
288
232
  id: r.id,
289
233
  role: r.role,
290
234
  category: r.category,
235
+ project: r.project,
291
236
  summary: r.summary,
292
237
  content: r.content,
293
238
  tags: r.tags || [],
@@ -299,65 +244,12 @@ export async function searchObservations(rootDir, query, {
299
244
  }
300
245
  await client.close();
301
246
  } catch {
302
- // Fall through to local JSON search
247
+ // If Vector search fails, return empty since fallback is removed.
303
248
  }
304
249
 
305
- // Fallback: local JSON + hashing-bow-v1
306
- const queryEmbedding = embedText(String(query));
307
- let vectors = readVectors(rootDir);
308
-
309
- if (role) vectors = vectors.filter((v) => v.role === role);
310
- if (category) vectors = vectors.filter((v) => v.category === category);
311
- if (project) vectors = vectors.filter((v) => v.project === project);
312
-
313
- // Cosine pass
314
- const cosineScored = vectors
315
- .map((v) => ({
316
- id: v.id,
317
- score: cosineSimilarity(queryEmbedding, v.embedding || []),
318
- }))
319
- .filter(({ score }) => score > 0.05);
320
-
321
- // Load full records for BM25 pass
322
- const candidateIds = new Set(cosineScored.map((v) => v.id));
323
- const recentIndex = readIndex(rootDir);
324
- const filtered = role ? recentIndex.filter((e) => e.role === role) : recentIndex;
325
- const filteredCat = category ? filtered.filter((e) => e.category === category) : filtered;
326
- const filteredProj = project ? filteredCat.filter((e) => e.project === project) : filteredCat;
327
- for (const entry of filteredProj.slice(0, Math.max(limit * 3, 30))) candidateIds.add(entry.id);
328
-
329
- const candidateRecords = [...candidateIds]
330
- .map((id) => getObservation(rootDir, id))
331
- .filter(Boolean)
332
- .map((r) => ({ ...r, text: [r.summary, r.content, ...(r.tags || [])].filter(Boolean).join(' ') }));
333
-
334
- const bm25Scored = rankByBm25(candidateRecords, query, { limit: limit * 2 });
335
-
336
- // Merge: highest score wins per id
337
- const scoreMap = new Map();
338
- for (const { id, score } of cosineScored) {
339
- scoreMap.set(id, score);
340
- }
341
- for (const item of bm25Scored) {
342
- const prev = scoreMap.get(item.id) || 0;
343
- const bm25Max = bm25Scored[0]?.score || 1;
344
- const normalized = Math.min(item.score / bm25Max, 1);
345
- scoreMap.set(item.id, Math.max(prev, normalized));
346
- }
347
-
348
- return [...scoreMap.entries()]
349
- .sort((a, b) => b[1] - a[1])
350
- .slice(0, limit)
351
- .map(([id, score]) => {
352
- const record = getObservation(rootDir, id);
353
- return record ? { ...record, score } : null;
354
- })
355
- .filter(Boolean);
250
+ return [];
356
251
  }
357
252
 
358
- /**
359
- * List observations from the index with optional filters.
360
- */
361
253
  export function listObservations(rootDir, {
362
254
  role = null,
363
255
  category = null,
@@ -371,9 +263,6 @@ export function listObservations(rootDir, {
371
263
  return entries.slice(0, limit);
372
264
  }
373
265
 
374
- /**
375
- * Load a full observation record by ID.
376
- */
377
266
  export function getObservation(rootDir, id) {
378
267
  const filePath = path.join(obsDir(rootDir), `${id}.json`);
379
268
  if (!fs.existsSync(filePath)) return null;
@@ -384,31 +273,17 @@ export function getObservation(rootDir, id) {
384
273
  }
385
274
  }
386
275
 
387
- /**
388
- * Delete an observation by id.
389
- */
390
276
  export function deleteObservation(rootDir, id) {
391
277
  const filePath = path.join(obsDir(rootDir), `${id}.json`);
392
278
  if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true });
393
279
 
394
- withFileLockSync(indexPath(rootDir), () => {
395
- const index = readIndex(rootDir);
396
- const filtered = index.filter((e) => e.id !== id);
397
- if (filtered.length !== index.length) writeIndex(rootDir, filtered);
398
- });
399
-
400
- withFileLockSync(vectorsPath(rootDir), () => {
401
- const vectors = readVectors(rootDir);
402
- const filteredVec = vectors.filter((v) => v.id !== id);
403
- if (filteredVec.length !== vectors.length) writeVectors(rootDir, filteredVec);
404
- });
280
+ const index = readIndex(rootDir);
281
+ const filtered = index.filter((e) => e.id !== id);
282
+ if (filtered.length !== index.length) writeIndex(rootDir, filtered);
405
283
 
406
284
  return true;
407
285
  }
408
286
 
409
- /**
410
- * Count observations, optionally filtered.
411
- */
412
287
  export function countObservations(rootDir, { role = null, project = null } = {}) {
413
288
  let entries = readIndex(rootDir);
414
289
  if (role) entries = entries.filter((e) => e.role === role);
@@ -416,9 +291,6 @@ export function countObservations(rootDir, { role = null, project = null } = {})
416
291
  return entries.length;
417
292
  }
418
293
 
419
- // On-disk byte size of the observation tree (live + archive) at rootDir.
420
- // Backs the `Observation size` doctor surface so size pressure shows up
421
- // independently of count-based caps.
422
294
  export function getObservationsSize(rootDir) {
423
295
  const root = obsDir(rootDir);
424
296
  if (!fs.existsSync(root)) return 0;
@@ -439,4 +311,4 @@ export function getObservationsSize(rootDir) {
439
311
  export function checkObservationsSize(rootDir, { cap = MAX_BYTES } = {}) {
440
312
  const size = getObservationsSize(rootDir);
441
313
  return { size, cap, ok: size <= cap };
442
- }
314
+ }
@@ -147,6 +147,7 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
147
147
  const {
148
148
  request: text = '', workflowType = null, requestedStrategy = 'auto', useConstruct = true,
149
149
  host = null, hostModel = null, hostProvider = null, fileCount = 0, moduleCount = 0,
150
+ workerBackend: explicitWorkerBackend = null,
150
151
  } = request;
151
152
 
152
153
  const config = loadConfig(cwd, env);
@@ -161,7 +162,7 @@ export async function planRun(request = {}, { env = process.env, cwd = process.c
161
162
  const traceId = newTraceId();
162
163
  const runId = newRunId();
163
164
  const now = new Date().toISOString();
164
- const workerBackend = resolveWorkerBackend({ config });
165
+ const workerBackend = resolveWorkerBackend({ explicit: explicitWorkerBackend, config });
165
166
  const chainOfThought = resolveChainOfThought({ config });
166
167
 
167
168
  // Construct owns a specialist task sequence only when the contract resolves to
@@ -358,7 +359,7 @@ export function hostAdapterMetadata(run) {
358
359
  degradationReason: e.degradationReason,
359
360
  selectedProvider: e.selectedProvider,
360
361
  selectedModel: e.selectedModel,
361
- tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, reasoning: t.reasoning ?? null })),
362
+ tasks: (run.tasks || []).map((t) => ({ id: t.id, role: t.role, status: t.status, executor: t.executor, reasoning: t.reasoning ?? null, output: t.output ?? null, error: t.error ?? null })),
362
363
  warnings: run.warnings || [],
363
364
  semantics: run.semantics,
364
365
  executionSemantics: run.executionSemantics,
@@ -35,7 +35,6 @@ import legacyDoctrineStrip from './legacy-doctrine-strip.mjs';
35
35
  import legacyGuideDecommit from './legacy-guide-decommit.mjs';
36
36
  import mcpEntryReconcile from './mcp-entry-reconcile.mjs';
37
37
  import adapterPrune from './adapter-prune.mjs';
38
- import postgresNamespace from './postgres-namespace.mjs';
39
38
 
40
39
  export const RECONCILIATIONS = [
41
40
  legacySkillsCleanup,
@@ -45,7 +44,6 @@ export const RECONCILIATIONS = [
45
44
  legacyGuideDecommit,
46
45
  mcpEntryReconcile,
47
46
  adapterPrune,
48
- postgresNamespace,
49
47
  ];
50
48
 
51
49
  const STAMP_FILE = 'reconcile.json';