@jhizzard/termdeck 0.18.0 → 1.0.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/package.json +1 -1
- package/packages/cli/src/init-mnestra.js +40 -1
- package/packages/cli/src/init-rumen.js +337 -29
- package/packages/client/public/app.js +131 -1
- package/packages/server/src/agent-adapters/claude.js +55 -0
- package/packages/server/src/agent-adapters/codex.js +82 -0
- package/packages/server/src/agent-adapters/gemini.js +57 -0
- package/packages/server/src/agent-adapters/grok.js +82 -0
- package/packages/server/src/index.js +132 -0
- package/packages/server/src/setup/audit-upgrade.js +302 -0
- package/packages/server/src/setup/index.js +2 -1
- package/packages/server/src/setup/mnestra-migrations/013_reclassify_uncertain.sql +39 -0
- package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +46 -0
- package/packages/server/src/setup/mnestra-migrations/015_source_agent.sql +51 -0
- package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +117 -0
- package/packages/server/src/setup/preconditions.js +36 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +6 -2
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -2
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const dns = require('dns');
|
|
12
|
+
const { spawn: spawnChild } = require('child_process');
|
|
12
13
|
const { v4: uuidv4 } = require('uuid');
|
|
13
14
|
const { createCachedLookup, createFailureLogger } = require('./rumen-pool-resilience');
|
|
14
15
|
|
|
@@ -128,6 +129,97 @@ function readTermdeckSecretsForPty() {
|
|
|
128
129
|
// Test hook — clear the cache between tests that mutate the on-disk file.
|
|
129
130
|
function _resetTermdeckSecretsCache() { _termdeckSecretsCache = null; }
|
|
130
131
|
|
|
132
|
+
// Sprint 50 T1 — Per-agent SessionEnd hook trigger.
|
|
133
|
+
//
|
|
134
|
+
// `_spawnSessionEndHookImpl` is the production spawn path; tests swap it
|
|
135
|
+
// out via `_setSpawnSessionEndHookImplForTesting` to capture the
|
|
136
|
+
// payload + arguments deterministically. The reason this indirection
|
|
137
|
+
// exists rather than mocking `child_process.spawn`: `node:test` doesn't
|
|
138
|
+
// run detached + stdio:['pipe','ignore','ignore'] children inside the
|
|
139
|
+
// test runner (verified — direct spawn with the same options fails to
|
|
140
|
+
// even invoke the script's first line). Mocking `child_process` would
|
|
141
|
+
// require module-level mocking which the runner doesn't support out of
|
|
142
|
+
// the box. A single-function injection keeps the surface tiny.
|
|
143
|
+
function _defaultSpawnSessionEndHookImpl(hookPath, payload, env) {
|
|
144
|
+
const child = spawnChild('node', [hookPath], {
|
|
145
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
146
|
+
detached: true,
|
|
147
|
+
env,
|
|
148
|
+
});
|
|
149
|
+
child.on('error', (err) => {
|
|
150
|
+
console.error('[onPanelClose] hook spawn error:', err && err.message ? err.message : err);
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
child.stdin.write(JSON.stringify(payload));
|
|
154
|
+
child.stdin.end();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error('[onPanelClose] hook stdin write failed:', err && err.message ? err.message : err);
|
|
157
|
+
}
|
|
158
|
+
child.unref();
|
|
159
|
+
return child;
|
|
160
|
+
}
|
|
161
|
+
let _spawnSessionEndHookImpl = _defaultSpawnSessionEndHookImpl;
|
|
162
|
+
function _setSpawnSessionEndHookImplForTesting(fn) {
|
|
163
|
+
_spawnSessionEndHookImpl = typeof fn === 'function' ? fn : _defaultSpawnSessionEndHookImpl;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fires when a panel's PTY exits. Routes through the adapter registry's
|
|
167
|
+
// new `resolveTranscriptPath` field (10th adapter field, Sprint 50) and
|
|
168
|
+
// invokes the bundled `~/.claude/hooks/memory-session-end.js` with the
|
|
169
|
+
// right payload so Codex / Gemini / Grok panels write a `session_summary`
|
|
170
|
+
// row the same way Claude Code already does.
|
|
171
|
+
//
|
|
172
|
+
// Skip rules (in order):
|
|
173
|
+
// 1. Claude — its own SessionEnd hook (registered in
|
|
174
|
+
// ~/.claude/settings.json) ingests Claude rows. Double-firing here
|
|
175
|
+
// would either insert two rows per session or race the Claude hook.
|
|
176
|
+
// 2. Adapters without `resolveTranscriptPath` — older adapters or types
|
|
177
|
+
// not in the registry (shell, python-server, one-shot). No-op.
|
|
178
|
+
// 3. `resolveTranscriptPath` returns null — adapter declares no
|
|
179
|
+
// transcript exists for this session (panel never sent a turn).
|
|
180
|
+
// 4. ~/.claude/hooks/memory-session-end.js missing — user hasn't
|
|
181
|
+
// installed the TermDeck stack hook. No-op.
|
|
182
|
+
//
|
|
183
|
+
// Fail-soft contract: any error logs to stderr and exits cleanly. Never
|
|
184
|
+
// blocks panel teardown — the spawn is fire-and-forget (detached + unref).
|
|
185
|
+
//
|
|
186
|
+
// `source_agent` is included in the payload (T2 consumes it via the new
|
|
187
|
+
// `memory_items.source_agent` column). T1 just passes the value; if T2
|
|
188
|
+
// hasn't migrated the column yet at the moment of first fire, Supabase
|
|
189
|
+
// rejects the row and the hook logs `supabase-insert-failed: HTTP 4xx`.
|
|
190
|
+
async function onPanelClose(session) {
|
|
191
|
+
try {
|
|
192
|
+
if (!session || !session.meta) return;
|
|
193
|
+
const adapter = AGENT_ADAPTERS[session.meta.type]
|
|
194
|
+
|| Object.values(AGENT_ADAPTERS).find((a) => a.sessionType === session.meta.type);
|
|
195
|
+
if (!adapter) return;
|
|
196
|
+
if (adapter.sessionType === 'claude-code') return;
|
|
197
|
+
if (typeof adapter.resolveTranscriptPath !== 'function') return;
|
|
198
|
+
|
|
199
|
+
const transcriptPath = await adapter.resolveTranscriptPath(session);
|
|
200
|
+
if (!transcriptPath) return;
|
|
201
|
+
|
|
202
|
+
const hookPath = path.join(os.homedir(), '.claude', 'hooks', 'memory-session-end.js');
|
|
203
|
+
if (!fs.existsSync(hookPath)) return;
|
|
204
|
+
|
|
205
|
+
const payload = {
|
|
206
|
+
transcript_path: transcriptPath,
|
|
207
|
+
cwd: session.meta.cwd,
|
|
208
|
+
session_id: session.id,
|
|
209
|
+
sessionType: adapter.sessionType,
|
|
210
|
+
// Sprint 50 — T2 consumes this via the new memory_items.source_agent column.
|
|
211
|
+
source_agent: adapter.name,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
_spawnSessionEndHookImpl(hookPath, payload, {
|
|
215
|
+
...process.env,
|
|
216
|
+
...readTermdeckSecretsForPty(),
|
|
217
|
+
});
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.error('[onPanelClose] error:', err && err.message ? err.message : err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
131
223
|
// Sprint 37 T3 — lazy resolution of T2's CLI modules. The orchestration-preview
|
|
132
224
|
// helper is decoupled from T2's templates.js / init-project.js; we resolve
|
|
133
225
|
// them here and pass them into the helper. If a module is missing (e.g.
|
|
@@ -926,6 +1018,15 @@ function createServer(config) {
|
|
|
926
1018
|
|
|
927
1019
|
// Fire-and-forget session log (T2.5)
|
|
928
1020
|
writeSessionLog({ session, config, db, getSessionHistory });
|
|
1021
|
+
|
|
1022
|
+
// Sprint 50 T1 — fire the bundled SessionEnd hook for non-Claude
|
|
1023
|
+
// panels so Codex / Gemini / Grok /exits write to Mnestra the way
|
|
1024
|
+
// Claude Code already does. onPanelClose handles dispatch +
|
|
1025
|
+
// skip-claude + skip-when-no-transcript. Fire-and-forget; any
|
|
1026
|
+
// error logs and never blocks teardown.
|
|
1027
|
+
onPanelClose(session).catch((err) => {
|
|
1028
|
+
console.error('[onPanelClose] async error:', err && err.message ? err.message : err);
|
|
1029
|
+
});
|
|
929
1030
|
});
|
|
930
1031
|
|
|
931
1032
|
// Wire command logging to SQLite + RAG
|
|
@@ -1324,6 +1425,9 @@ function createServer(config) {
|
|
|
1324
1425
|
// • binary — canonical command name; client matches `^binary\b` (i)
|
|
1325
1426
|
// • costBand — 'free' | 'pay-per-token' | 'subscription' (Sprint 46
|
|
1326
1427
|
// surfaces this in PLANNING.md cost annotations)
|
|
1428
|
+
// • displayName — Sprint 50 T3: human-readable label for launcher buttons
|
|
1429
|
+
// and panel headers. Backwards-compat: existing clients
|
|
1430
|
+
// that ignore the field continue to work unchanged.
|
|
1327
1431
|
// Functions / RegExps are NOT serialized — match logic lives client-side
|
|
1328
1432
|
// and uses the binary as the prefix anchor. Adapter-specific shorthand
|
|
1329
1433
|
// (e.g. `cc` → `claude`) is normalized in app.js before this lookup.
|
|
@@ -1333,6 +1437,29 @@ function createServer(config) {
|
|
|
1333
1437
|
sessionType: a.sessionType,
|
|
1334
1438
|
binary: a.spawn && a.spawn.binary,
|
|
1335
1439
|
costBand: a.costBand,
|
|
1440
|
+
displayName: a.displayName || a.name,
|
|
1441
|
+
}));
|
|
1442
|
+
res.json(list);
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
// GET /api/agents - Sprint 50 T3: richer adapter projection used by the
|
|
1446
|
+
// dashboard launcher to render one button per registered agent and by the
|
|
1447
|
+
// mixed-agent dogfood inject script to discover available agents. Adds
|
|
1448
|
+
// the full spawn descriptor (binary + defaultArgs) so callers don't need
|
|
1449
|
+
// to re-derive it from the binary alone. Coexists with /api/agent-adapters
|
|
1450
|
+
// (kept stable for the launcher-resolver client contract).
|
|
1451
|
+
app.get('/api/agents', (req, res) => {
|
|
1452
|
+
const list = Object.values(AGENT_ADAPTERS).map((a) => ({
|
|
1453
|
+
name: a.name,
|
|
1454
|
+
sessionType: a.sessionType,
|
|
1455
|
+
displayName: a.displayName || a.name,
|
|
1456
|
+
spawn: {
|
|
1457
|
+
binary: (a.spawn && a.spawn.binary) || a.name,
|
|
1458
|
+
defaultArgs: (a.spawn && Array.isArray(a.spawn.defaultArgs))
|
|
1459
|
+
? a.spawn.defaultArgs.slice()
|
|
1460
|
+
: [],
|
|
1461
|
+
},
|
|
1462
|
+
costBand: a.costBand,
|
|
1336
1463
|
}));
|
|
1337
1464
|
res.json(list);
|
|
1338
1465
|
});
|
|
@@ -2228,4 +2355,9 @@ module.exports = {
|
|
|
2228
2355
|
// Sprint 48 T4 — exported for unit testing the secrets.env → PTY env merge.
|
|
2229
2356
|
readTermdeckSecretsForPty,
|
|
2230
2357
|
_resetTermdeckSecretsCache,
|
|
2358
|
+
// Sprint 50 T1 — exported for unit testing the per-agent SessionEnd
|
|
2359
|
+
// hook trigger (skip-claude, no-transcript, no-hook-installed,
|
|
2360
|
+
// payload shape, fire-and-forget).
|
|
2361
|
+
onPanelClose,
|
|
2362
|
+
_setSpawnSessionEndHookImplForTesting,
|
|
2231
2363
|
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Sprint 51.5 T1 — schema-introspection audit-upgrade.
|
|
2
|
+
//
|
|
3
|
+
// Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
|
|
4
|
+
// surfaced Class A — schema drift. The user upgraded npm packages but the
|
|
5
|
+
// database stayed frozen at first-kickstart: graph-inference Edge Function
|
|
6
|
+
// never deployed, vault key never created, Mnestra migrations 009-015 + TD
|
|
7
|
+
// Rumen 003 never applied. Both init wizards correctly apply their bundled
|
|
8
|
+
// migrations on a fresh install, but neither one diffs an existing install
|
|
9
|
+
// against the bundled migration set. After `npm install -g @latest`, the
|
|
10
|
+
// npm packages are current and the database is whatever it was the day the
|
|
11
|
+
// project was first kickstarted.
|
|
12
|
+
//
|
|
13
|
+
// auditUpgrade() runs at the top of `termdeck init --mnestra` and
|
|
14
|
+
// `termdeck init --rumen` re-runs. For each known schema artifact it:
|
|
15
|
+
// 1. Probes for presence via a single information_schema / pg_catalog query.
|
|
16
|
+
// 2. If absent, applies the bundled migration that creates that artifact.
|
|
17
|
+
// 3. Logs every probe + apply result so the wizard can report what changed.
|
|
18
|
+
//
|
|
19
|
+
// `dryRun: true` returns the missing[] list without applying — exposed so
|
|
20
|
+
// `mnestra doctor` (Sprint 51.5 T2) can render the same drift detection
|
|
21
|
+
// without committing changes.
|
|
22
|
+
//
|
|
23
|
+
// What this file IS: a cheap, additive, idempotent diff applier. Every probe
|
|
24
|
+
// is a single SQL statement. Every applied migration is idempotent
|
|
25
|
+
// (`ADD COLUMN IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`,
|
|
26
|
+
// `ALTER ... SCHEDULE`, `cron.unschedule + cron.schedule`).
|
|
27
|
+
//
|
|
28
|
+
// What this file is NOT: a migration-tracking-table approach. That's the
|
|
29
|
+
// durable answer (deferred to Sprint 52+) — it self-heals all future drift
|
|
30
|
+
// but requires a backfill pass for existing installs. v1.0.1 takes the
|
|
31
|
+
// cheap path: probe-as-source-of-truth.
|
|
32
|
+
//
|
|
33
|
+
// Out of scope for v1.0.1: Edge Function deploy via Management API, vault
|
|
34
|
+
// secret creation. The bundled `init-rumen.js::deployFunctions` already
|
|
35
|
+
// re-deploys both rumen-tick and graph-inference on every `init --rumen`
|
|
36
|
+
// re-run, so a user who runs the v1.0.1 hotfix instructions
|
|
37
|
+
// (`npm install -g @jhizzard/termdeck@1.0.1 && termdeck init --rumen`)
|
|
38
|
+
// gets the function deploys + vault clone via the existing flow. This
|
|
39
|
+
// module's job is to land the SQL artifacts cheaply, on either re-run path.
|
|
40
|
+
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
const path = require('path');
|
|
44
|
+
|
|
45
|
+
const migrations = require('./migrations');
|
|
46
|
+
const { applyTemplating } = require('./migration-templating');
|
|
47
|
+
|
|
48
|
+
// Probe → apply mapping. Order matters: dependencies (e.g., M-013 audit
|
|
49
|
+
// columns) come after the tables they touch. Cron schedule probes go last
|
|
50
|
+
// because they need pg_cron + pg_net which migration 002 takes for granted.
|
|
51
|
+
//
|
|
52
|
+
// Migration 012 (project_tag_re_taxonomy) is intentionally NOT in this set:
|
|
53
|
+
// it is pure DML (UPDATE rows WHERE project='chopin-nashville') with no
|
|
54
|
+
// schema artifact to introspect. Re-applying is safe (idempotent on already-
|
|
55
|
+
// retagged rows) but auto-applying every audit cycle would scan memory_items
|
|
56
|
+
// 8 times for no schema benefit. Migration 012 still ships in the bundled
|
|
57
|
+
// set and is applied by the existing init-mnestra `applyMigrations` loop on
|
|
58
|
+
// any wizard re-run.
|
|
59
|
+
//
|
|
60
|
+
// Migration 011 (project_tag_backfill) is similarly out of scope (DML).
|
|
61
|
+
// Migration 008 (legacy_rag_tables) is opt-in (rag.enabled toggle) and
|
|
62
|
+
// already creates schema with IF NOT EXISTS guards in the fresh-install
|
|
63
|
+
// path — not a drift candidate.
|
|
64
|
+
const PROBES = Object.freeze([
|
|
65
|
+
{
|
|
66
|
+
name: 'memory_relationships.weight',
|
|
67
|
+
kind: 'mnestra',
|
|
68
|
+
migrationFile: '009_memory_relationship_metadata.sql',
|
|
69
|
+
probeSql:
|
|
70
|
+
"select 1 as present from information_schema.columns " +
|
|
71
|
+
"where table_schema = 'public' " +
|
|
72
|
+
" and table_name = 'memory_relationships' " +
|
|
73
|
+
" and column_name = 'weight' limit 1",
|
|
74
|
+
presentWhen: 'rowReturned'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'memory_recall_graph rpc',
|
|
78
|
+
kind: 'mnestra',
|
|
79
|
+
migrationFile: '010_memory_recall_graph.sql',
|
|
80
|
+
probeSql:
|
|
81
|
+
"select 1 as present from pg_proc " +
|
|
82
|
+
"where proname = 'memory_recall_graph' limit 1",
|
|
83
|
+
presentWhen: 'rowReturned'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'memory_items.reclassified_by',
|
|
87
|
+
kind: 'mnestra',
|
|
88
|
+
migrationFile: '013_reclassify_uncertain.sql',
|
|
89
|
+
probeSql:
|
|
90
|
+
"select 1 as present from information_schema.columns " +
|
|
91
|
+
"where table_schema = 'public' " +
|
|
92
|
+
" and table_name = 'memory_items' " +
|
|
93
|
+
" and column_name = 'reclassified_by' limit 1",
|
|
94
|
+
presentWhen: 'rowReturned'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
// Brad's 2026-04-28 incident: service_role had no INSERT on memory_items
|
|
98
|
+
// because the project's default-privileges-in-schema-public defaults had
|
|
99
|
+
// been tightened (Supabase auto-grants didn't fire). Migration 014 lays
|
|
100
|
+
// down explicit grants. Re-applying on a project where auto-grants did
|
|
101
|
+
// fire is a no-op.
|
|
102
|
+
name: 'service_role explicit grant on memory_items',
|
|
103
|
+
kind: 'mnestra',
|
|
104
|
+
migrationFile: '014_explicit_grants.sql',
|
|
105
|
+
probeSql:
|
|
106
|
+
"select has_table_privilege('service_role', 'public.memory_items', 'INSERT') as present",
|
|
107
|
+
presentWhen: 'boolColumnTrue'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'memory_items.source_agent',
|
|
111
|
+
kind: 'mnestra',
|
|
112
|
+
migrationFile: '015_source_agent.sql',
|
|
113
|
+
probeSql:
|
|
114
|
+
"select 1 as present from information_schema.columns " +
|
|
115
|
+
"where table_schema = 'public' " +
|
|
116
|
+
" and table_name = 'memory_items' " +
|
|
117
|
+
" and column_name = 'source_agent' limit 1",
|
|
118
|
+
presentWhen: 'rowReturned'
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'rumen-tick cron schedule',
|
|
122
|
+
kind: 'rumen',
|
|
123
|
+
migrationFile: '002_pg_cron_schedule.sql',
|
|
124
|
+
templated: true,
|
|
125
|
+
probeSql:
|
|
126
|
+
"select 1 as present from cron.job where jobname = 'rumen-tick' limit 1",
|
|
127
|
+
presentWhen: 'rowReturned'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'graph-inference-tick cron schedule',
|
|
131
|
+
kind: 'rumen',
|
|
132
|
+
migrationFile: '003_graph_inference_schedule.sql',
|
|
133
|
+
templated: true,
|
|
134
|
+
probeSql:
|
|
135
|
+
"select 1 as present from cron.job where jobname = 'graph-inference-tick' limit 1",
|
|
136
|
+
presentWhen: 'rowReturned'
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
// Find the bundled migration file for a probe target. Returns the absolute
|
|
141
|
+
// path or null. `mnestra` looks under bundled mnestra-migrations; `rumen`
|
|
142
|
+
// looks under bundled rumen/migrations. Both kinds prefer the bundled copy
|
|
143
|
+
// (matches the listMnestraMigrations / listRumenMigrations convention from
|
|
144
|
+
// v0.6.8 — bundled FIRST, then the @jhizzard/<pkg> node_modules fallback).
|
|
145
|
+
function resolveMigrationFile(target, files) {
|
|
146
|
+
const wanted = target.migrationFile;
|
|
147
|
+
return files.find((f) => path.basename(f) === wanted) || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Run a probe and decide present/absent based on the probe's contract.
|
|
151
|
+
async function probeOne(pgClient, target) {
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
result = await pgClient.query(target.probeSql);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// A probe failure (e.g., schema doesn't exist yet — `cron.job` on a
|
|
157
|
+
// project without pg_cron) means the artifact is absent. Record the
|
|
158
|
+
// raw error so a caller can distinguish "absent because never installed"
|
|
159
|
+
// from "absent because we can't even check." Either way the right
|
|
160
|
+
// response is to attempt apply — which will surface the real error
|
|
161
|
+
// (e.g., "extension pg_cron is not installed") with full context.
|
|
162
|
+
return { present: false, probeError: err.message };
|
|
163
|
+
}
|
|
164
|
+
const rows = (result && result.rows) || [];
|
|
165
|
+
if (target.presentWhen === 'rowReturned') {
|
|
166
|
+
return { present: rows.length > 0 };
|
|
167
|
+
}
|
|
168
|
+
if (target.presentWhen === 'boolColumnTrue') {
|
|
169
|
+
return { present: Boolean(rows[0] && rows[0].present === true) };
|
|
170
|
+
}
|
|
171
|
+
// Defensive default: any returned row counts as present.
|
|
172
|
+
return { present: rows.length > 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Apply a single migration file. Templated migrations route through
|
|
176
|
+
// applyTemplating() so the cron schedule body never sees the raw
|
|
177
|
+
// `<project-ref>` placeholder. Brad 2026-05-03 takeaway #5 (bonus): the
|
|
178
|
+
// fresh-install path at init-rumen.js:472-505 already does this; the
|
|
179
|
+
// audit-upgrade path MUST mirror it. Tests in audit-upgrade.test.js guard
|
|
180
|
+
// against future bypass.
|
|
181
|
+
async function applyOne(pgClient, target, files, { projectRef, readFileImpl }) {
|
|
182
|
+
const file = resolveMigrationFile(target, files);
|
|
183
|
+
if (!file) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`audit-upgrade: bundled migration file not found for ${target.name} ` +
|
|
186
|
+
`(expected ${target.migrationFile}). The bundled migration set may be ` +
|
|
187
|
+
`out of sync — re-publish the package or run scripts/sync-rumen-functions.sh.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const raw = readFileImpl(file);
|
|
191
|
+
const sql = target.templated
|
|
192
|
+
? applyTemplating(raw, { projectRef })
|
|
193
|
+
: raw;
|
|
194
|
+
await pgClient.query(sql);
|
|
195
|
+
return { file: path.basename(file) };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Public API.
|
|
199
|
+
//
|
|
200
|
+
// Inputs:
|
|
201
|
+
// pgClient — open node-postgres Client (caller owns the lifecycle).
|
|
202
|
+
// projectRef — required when any templated migration is in the probe set
|
|
203
|
+
// (i.e., the rumen cron schedules). The applyTemplating
|
|
204
|
+
// helper will throw if it sees a `<project-ref>` placeholder
|
|
205
|
+
// and projectRef is missing — surfaced via errors[].
|
|
206
|
+
// dryRun — when true, probes only; skips apply. applied stays empty.
|
|
207
|
+
// probes — optional override for the probe set (test injection point).
|
|
208
|
+
// Defaults to PROBES.
|
|
209
|
+
// _migrations — optional override for the migrations module (test
|
|
210
|
+
// injection). Lets tests stub listMnestraMigrations /
|
|
211
|
+
// listRumenMigrations / readFile.
|
|
212
|
+
//
|
|
213
|
+
// Returns:
|
|
214
|
+
// {
|
|
215
|
+
// probed: string[] — every target name we tried to probe
|
|
216
|
+
// present: string[] — targets whose probe came back present
|
|
217
|
+
// missing: string[] — targets whose probe came back absent
|
|
218
|
+
// applied: string[] — targets the audit applied this run
|
|
219
|
+
// (empty when dryRun=true)
|
|
220
|
+
// skipped: string[] — targets we couldn't apply (e.g., missing
|
|
221
|
+
// projectRef on a templated migration)
|
|
222
|
+
// errors: Array<{ name, error }> — apply or probe errors (probe errors
|
|
223
|
+
// only surface here when subsequent
|
|
224
|
+
// apply ALSO fails)
|
|
225
|
+
// }
|
|
226
|
+
//
|
|
227
|
+
// Idempotent: a second run reports `applied=[]` because every probe will
|
|
228
|
+
// come back present. All shipped migrations are themselves idempotent
|
|
229
|
+
// (ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
|
|
230
|
+
// cron.unschedule + cron.schedule, GRANT … TO service_role).
|
|
231
|
+
async function auditUpgrade({
|
|
232
|
+
pgClient,
|
|
233
|
+
projectRef,
|
|
234
|
+
dryRun = false,
|
|
235
|
+
probes,
|
|
236
|
+
_migrations
|
|
237
|
+
} = {}) {
|
|
238
|
+
if (!pgClient || typeof pgClient.query !== 'function') {
|
|
239
|
+
throw new Error('auditUpgrade: pgClient with .query() is required');
|
|
240
|
+
}
|
|
241
|
+
const targets = probes || PROBES;
|
|
242
|
+
const mig = _migrations || migrations;
|
|
243
|
+
|
|
244
|
+
// Resolve once: the bundled migration sets stay constant for the duration
|
|
245
|
+
// of a single audit run.
|
|
246
|
+
const mnestraFiles = mig.listMnestraMigrations();
|
|
247
|
+
const rumenFiles = mig.listRumenMigrations();
|
|
248
|
+
|
|
249
|
+
const probed = [];
|
|
250
|
+
const present = [];
|
|
251
|
+
const missing = [];
|
|
252
|
+
const applied = [];
|
|
253
|
+
const skipped = [];
|
|
254
|
+
const errors = [];
|
|
255
|
+
|
|
256
|
+
for (const target of targets) {
|
|
257
|
+
probed.push(target.name);
|
|
258
|
+
const probeResult = await probeOne(pgClient, target);
|
|
259
|
+
if (probeResult.present) {
|
|
260
|
+
present.push(target.name);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
missing.push(target.name);
|
|
264
|
+
|
|
265
|
+
if (dryRun) continue;
|
|
266
|
+
|
|
267
|
+
const files = target.kind === 'rumen' ? rumenFiles : mnestraFiles;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
await applyOne(pgClient, target, files, {
|
|
271
|
+
projectRef,
|
|
272
|
+
readFileImpl: mig.readFile
|
|
273
|
+
});
|
|
274
|
+
applied.push(target.name);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
// Surface but don't abort. One missing artifact failing to apply (e.g.,
|
|
277
|
+
// pg_cron extension not enabled) shouldn't block the rest of the audit
|
|
278
|
+
// from running. The wizard will report the whole audit summary at the
|
|
279
|
+
// end so the user can address each failure individually.
|
|
280
|
+
errors.push({
|
|
281
|
+
name: target.name,
|
|
282
|
+
error: err && err.message ? err.message : String(err)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// skipped[] reserved for v1.0.2: targets the audit deliberately doesn't
|
|
288
|
+
// attempt (e.g., when projectRef is missing for a templated migration we
|
|
289
|
+
// currently let applyTemplating throw → errors[]; future versions may
|
|
290
|
+
// pre-skip those into skipped[]).
|
|
291
|
+
return { probed, present, missing, applied, skipped, errors };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
auditUpgrade,
|
|
296
|
+
PROBES,
|
|
297
|
+
// Test surface — kept exported so audit-upgrade.test.js can pin probe
|
|
298
|
+
// selection / apply pathway behavior without needing a live pg client.
|
|
299
|
+
_probeOne: probeOne,
|
|
300
|
+
_applyOne: applyOne,
|
|
301
|
+
_resolveMigrationFile: resolveMigrationFile
|
|
302
|
+
};
|
|
@@ -13,5 +13,6 @@ module.exports = {
|
|
|
13
13
|
migrationTemplating: require('./migration-templating'),
|
|
14
14
|
pgRunner: require('./pg-runner'),
|
|
15
15
|
migrationRunner: require('./migration-runner'),
|
|
16
|
-
preconditions: require('./preconditions')
|
|
16
|
+
preconditions: require('./preconditions'),
|
|
17
|
+
auditUpgrade: require('./audit-upgrade')
|
|
17
18
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- 013_reclassify_uncertain.sql
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 41 (T4) — Audit-trail columns for the LLM-classification pass that
|
|
4
|
+
-- finishes the chopin-nashville taxonomy cleanup.
|
|
5
|
+
--
|
|
6
|
+
-- Background:
|
|
7
|
+
-- Sprint 41 T2's deterministic re-tag (`012_project_tag_re_taxonomy.sql`)
|
|
8
|
+
-- handles every chopin-nashville row whose content has a clear keyword or
|
|
9
|
+
-- path signal. The residue — rows with no clear signal — gets classified
|
|
10
|
+
-- by `scripts/reclassify-chopin-nashville.js` which calls Haiku 4.5 in
|
|
11
|
+
-- batches of 20 and writes back per-row tag decisions.
|
|
12
|
+
--
|
|
13
|
+
-- Some of those LLM decisions will be "this row really IS chopin-nashville
|
|
14
|
+
-- competition work — leave the tag." Without an audit stamp the script
|
|
15
|
+
-- can't distinguish "row the LLM voted to keep" from "row the LLM hasn't
|
|
16
|
+
-- seen yet" — every re-run would re-ask Haiku about the same rows
|
|
17
|
+
-- indefinitely. The stamp also gives a one-line audit trail
|
|
18
|
+
-- (`SELECT count(*) FROM memory_items WHERE reclassified_by = '...'`).
|
|
19
|
+
--
|
|
20
|
+
-- Idempotent: safe to re-run. ADD COLUMN IF NOT EXISTS — no-op if already
|
|
21
|
+
-- applied.
|
|
22
|
+
--
|
|
23
|
+
-- Constraints:
|
|
24
|
+
-- Both columns nullable. Only rows the script touches get stamped; every
|
|
25
|
+
-- other row stays untouched. There is no foreign key, no NOT NULL, no
|
|
26
|
+
-- default — these are pure audit metadata.
|
|
27
|
+
|
|
28
|
+
alter table memory_items
|
|
29
|
+
add column if not exists reclassified_by text,
|
|
30
|
+
add column if not exists reclassified_at timestamptz;
|
|
31
|
+
|
|
32
|
+
-- Lightweight partial index — useful for `count(*) WHERE reclassified_by = ...`
|
|
33
|
+
-- audit queries and for the script's own idempotency filter. Keeps the index
|
|
34
|
+
-- small (only stamped rows are indexed) so it doesn't cost anything on the
|
|
35
|
+
-- vast majority of memory_items rows that stay untouched.
|
|
36
|
+
|
|
37
|
+
create index if not exists memory_items_reclassified_by_idx
|
|
38
|
+
on memory_items(reclassified_by)
|
|
39
|
+
where reclassified_by is not null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- Mnestra v0.3.2 — explicit GRANTs to make installs deterministic
|
|
2
|
+
--
|
|
3
|
+
-- Prior migrations relied on Supabase's auto-grant default, which
|
|
4
|
+
-- auto-grants public-schema privileges to service_role / authenticated /
|
|
5
|
+
-- anon when (a) the creating role is `postgres` AND (b) the project's
|
|
6
|
+
-- default privileges in schema public haven't been tightened. On any
|
|
7
|
+
-- Supabase project where one of those preconditions failed, every
|
|
8
|
+
-- Mnestra install landed in the same broken state:
|
|
9
|
+
--
|
|
10
|
+
-- memory_remember(...) → "Memory skipped: ..." (silent — see remember.ts)
|
|
11
|
+
-- memory_status → Total active memories: 0
|
|
12
|
+
-- memory_recall(...) → "Search error: permission denied for table memory_items"
|
|
13
|
+
--
|
|
14
|
+
-- Root cause: `service_role` had no SELECT/INSERT/UPDATE/DELETE on
|
|
15
|
+
-- memory_items, memory_sessions, memory_relationships, and no EXECUTE
|
|
16
|
+
-- on match_memories / memory_hybrid_search / expand_memory_neighborhood.
|
|
17
|
+
-- PostgREST checks table-level privileges before evaluating RLS, so
|
|
18
|
+
-- service_role's bypassrls attribute does not help.
|
|
19
|
+
--
|
|
20
|
+
-- Reported and root-caused by Brad Heath 2026-04-28 against project
|
|
21
|
+
-- ref rrzkceirgciiqgeefvbe; fix verified end-to-end on his install
|
|
22
|
+
-- before being upstreamed here.
|
|
23
|
+
--
|
|
24
|
+
-- This migration is idempotent and safe on greenfield projects where
|
|
25
|
+
-- the auto-grant default already fired (the GRANTs become no-ops).
|
|
26
|
+
|
|
27
|
+
-- ── Tables: service_role is Mnestra's only direct connection role.
|
|
28
|
+
|
|
29
|
+
grant select, insert, update, delete on all tables in schema public
|
|
30
|
+
to service_role;
|
|
31
|
+
|
|
32
|
+
-- ── Functions / RPCs: convention from migrations 006 and 010 is to
|
|
33
|
+
-- grant execute to all three Supabase roles. Apply schema-wide so
|
|
34
|
+
-- future RPCs inherit without another migration.
|
|
35
|
+
|
|
36
|
+
grant execute on all functions in schema public
|
|
37
|
+
to service_role, authenticated, anon;
|
|
38
|
+
|
|
39
|
+
-- ── Default privileges: any future tables/functions created in
|
|
40
|
+
-- schema public automatically inherit the same grants.
|
|
41
|
+
|
|
42
|
+
alter default privileges in schema public
|
|
43
|
+
grant select, insert, update, delete on tables to service_role;
|
|
44
|
+
|
|
45
|
+
alter default privileges in schema public
|
|
46
|
+
grant execute on functions to service_role, authenticated, anon;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- Mnestra v0.4.0 — source_agent provenance column on memory_items
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 50 T2 (TermDeck). Adds an LLM-provenance tag to every memory row
|
|
4
|
+
-- so future memory_recall callers can filter or trust-weight by the agent
|
|
5
|
+
-- that produced the row (Claude / Codex / Gemini / Grok / orchestrator).
|
|
6
|
+
--
|
|
7
|
+
-- Why now:
|
|
8
|
+
-- Sprint 49 (mixed-agent dogfood, 2026-05-02) surfaced a trust-fundamental
|
|
9
|
+
-- gap. Each lane's panel produced real work; only Claude's hook wrote to
|
|
10
|
+
-- Mnestra. Sprint 50 closes both halves of that gap — T1 fires the hook
|
|
11
|
+
-- for every adapter at panel close (write-side); T2 (this migration) adds
|
|
12
|
+
-- the read-side ability to filter by source. Without this column,
|
|
13
|
+
-- memory_recall returns a careful Claude observation alongside (e.g.) a
|
|
14
|
+
-- Gemini-produced timestamp claim, with no way to tell them apart at the
|
|
15
|
+
-- recall consumer. See docs/MULTI-AGENT-MEMORY-ARCHITECTURE.md
|
|
16
|
+
-- § Deliverable 2 in the TermDeck repo for the full design.
|
|
17
|
+
--
|
|
18
|
+
-- Backwards compatibility:
|
|
19
|
+
-- Historical rows stay NULL (no destructive default backfill on archived
|
|
20
|
+
-- data). The recall filter treats NULL as "unknown agent" — rows with
|
|
21
|
+
-- NULL source_agent are excluded from a filtered recall and included in
|
|
22
|
+
-- an unfiltered one.
|
|
23
|
+
--
|
|
24
|
+
-- Exception: pre-Sprint-50 session_summary rows came exclusively from
|
|
25
|
+
-- Claude Code's SessionEnd hook (only Claude shipped a hook system before
|
|
26
|
+
-- Sprint 50 T1 added per-agent triggers). Backfill those to 'claude' so
|
|
27
|
+
-- they remain reachable via source_agents=['claude']. Other source_types
|
|
28
|
+
-- (fact / decision / preference / bug_fix / architecture / code_context)
|
|
29
|
+
-- came from a mix of MCP tools and the rag-system extractor — no clean
|
|
30
|
+
-- single-agent attribution exists for them, so they stay NULL.
|
|
31
|
+
--
|
|
32
|
+
-- Idempotent: ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS,
|
|
33
|
+
-- and the backfill UPDATE skips rows already populated.
|
|
34
|
+
|
|
35
|
+
alter table memory_items
|
|
36
|
+
add column if not exists source_agent text;
|
|
37
|
+
|
|
38
|
+
create index if not exists idx_memory_items_source_agent
|
|
39
|
+
on memory_items (source_agent)
|
|
40
|
+
where source_agent is not null;
|
|
41
|
+
|
|
42
|
+
comment on column memory_items.source_agent is
|
|
43
|
+
'Agent that produced this memory: claude|codex|gemini|grok|orchestrator|NULL (historical or unknown). Populated by the SessionEnd hook from Sprint 50 onward; NULL for pre-Sprint-50 rows except session_summary which were always Claude (backfilled).';
|
|
44
|
+
|
|
45
|
+
-- Backfill historical session_summary rows. These came from Claude Code's
|
|
46
|
+
-- SessionEnd hook (only Claude shipped a hook system before Sprint 50 T1).
|
|
47
|
+
-- Idempotent — re-running this UPDATE on already-tagged rows is a no-op.
|
|
48
|
+
update memory_items
|
|
49
|
+
set source_agent = 'claude'
|
|
50
|
+
where source_type = 'session_summary'
|
|
51
|
+
and source_agent is null;
|