0xray 4.0.19 → 4.0.20
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/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/dist/CHANGELOG.md +13 -0
- package/dist/integrations/cursor/hooks/after-file-edit.js +6 -3
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +40 -20
- package/dist/integrations/cursor/hooks/pre-compact.js +30 -22
- package/dist/integrations/cursor/hooks/pre-tool-use.js +5 -4
- package/dist/integrations/grok/hooks/grok-hook-utils.js +3 -1
- package/dist/integrations/hermes-agent/bridge.mjs +14 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +238 -11
- package/dist/integrations/hooks/station-hook-runtime.mjs +6 -0
- package/dist/integrations/hooks/station-memory-ingest.mjs +153 -4
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/nucleus/delegation-gate.js +10 -11
- package/dist/nucleus/suit-temperament.js +5 -1
- package/package.json +1 -1
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
package/.grok-plugin/plugin.json
CHANGED
package/.xray/features.json
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,8 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
9
|
### ✨ Features
|
|
10
|
+
- **Cleanup is memory** — `applyStationHeat` grows the project copy when Repertoire is on: kernel sessions, sibling `repo-*` names, and NOTES/CLEANSE diary heat. Groover field stays off. Opt-out still skips seed and grow. Station stays the pickup card. Not a new MCP/skill.
|
|
11
|
+
- **Live context memory** — capture then grow. Session `patterns` come from real git (commit slugs + structural names), not a leftover catalog. Cursor skip / afterFileEdit, Grok heat, Hermes pre-process, and OpenClaw pre-tool share one dest lock so parallel seats write one project copy. Not a new MCP/skill.
|
|
10
12
|
- **Vendored Repertoire 0.2.5** — `vendor/@0xray/repertoire` is the published 0.2.5 package (overlays included). Wear replaces an older leftover in `node_modules`. A newer user-installed copy is left alone. Boot reads worn `node_modules`, then vendor, then a sibling checkout last. Repertoire stays optional (`enabled: false` + `provider: repertoire`); Station is the memory when it is off.
|
|
11
13
|
- **Cursor fifth wear** — `install-bridges.cjs` fastens project `.cursor/hooks.json` (preToolUse / preCompact / afterFileEdit) so Cursor Cloud heats Station and gates tools. Leaves an existing file alone. Not a fifth chat TUI. No new MCP/skill surface. Pack gate (`REQUIRED_PACK_PATHS`) now requires `dist/integrations/cursor/hooks/{hooks.json,pre-tool-use.js,pre-compact.js,after-file-edit.js,cursor-hook-utils.js,cursor-usage-receipt.js}` so a published tarball cannot ship a dead consumer command. `preToolUse` / `afterFileEdit` rewrite the card when git HEAD or Repertoire count diverges — Ticket / Durable / Seed stay. OP-PROC stays in the hook, not on Station.
|
|
12
14
|
|
|
@@ -35,6 +37,17 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
|
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
|
40
|
+
## [4.0.20] - 2026-09-22
|
|
41
|
+
|
|
42
|
+
### 🔄 Changes
|
|
43
|
+
|
|
44
|
+
### 🐛 Bug Fixes
|
|
45
|
+
- fix: rewrite session-boot when HEAD moves — first-wins skip froze metal (same session still heats dest)
|
|
46
|
+
- fix: grow dest from live git on every floor (a7685eb09)
|
|
47
|
+
- fix: heat grows the project copy — cleanup is memory (be10c82eb)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
38
51
|
## [4.0.19] - 2026-09-22
|
|
39
52
|
|
|
40
53
|
### ✨ Features
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
appendHookActivity,
|
|
9
|
+
cursorHeatRoots,
|
|
9
10
|
cursorSessionId,
|
|
10
11
|
cursorWorkspaceRoot,
|
|
11
12
|
ensureCursorSessionBoot,
|
|
@@ -17,9 +18,11 @@ async function main() {
|
|
|
17
18
|
try {
|
|
18
19
|
const event = await readStdinJson();
|
|
19
20
|
const eventRoot = cursorWorkspaceRoot(event);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const sessionId = cursorSessionId(event);
|
|
22
|
+
let bootPath = null;
|
|
23
|
+
for (const root of cursorHeatRoots(event)) {
|
|
24
|
+
bootPath = ensureCursorSessionBoot(root, '0xray/cursor-after-file-edit-boot', { sessionId });
|
|
25
|
+
}
|
|
23
26
|
appendHookActivity(eventRoot, 'cursor-after-file-edit', 'session-boot', 'info', {
|
|
24
27
|
bootPath,
|
|
25
28
|
file: event.file_path || event.path || null,
|
|
@@ -23,13 +23,12 @@ import {
|
|
|
23
23
|
} from '../../hooks/delegation-gate-runtime.mjs';
|
|
24
24
|
import { appendHookActivity } from '../../grok/hooks/grok-hook-activity.js';
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
heatLiveMemory,
|
|
27
27
|
maybeCaptureSessionOnHeadMove,
|
|
28
|
-
|
|
29
|
-
stationDurableHoldsNpm,
|
|
28
|
+
stationBootNeedsRefresh,
|
|
30
29
|
} from '../../hooks/station-hook-runtime.mjs';
|
|
31
30
|
|
|
32
|
-
export { maybeCaptureSessionOnHeadMove };
|
|
31
|
+
export { heatLiveMemory, maybeCaptureSessionOnHeadMove };
|
|
33
32
|
|
|
34
33
|
export const CURSOR_HOST = 'cursor';
|
|
35
34
|
export const EVENT_CLASS_HOST = 'cursor-host-precompact';
|
|
@@ -77,6 +76,40 @@ export function cursorWorkspaceRoot(event = {}) {
|
|
|
77
76
|
);
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
function addHeatRoot(out, seen, value) {
|
|
80
|
+
if (!value) return;
|
|
81
|
+
const resolved = path.resolve(String(value));
|
|
82
|
+
if (seen.has(resolved)) return;
|
|
83
|
+
seen.add(resolved);
|
|
84
|
+
out.push(resolved);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Every live context that already has a Station card — cwd plus mill when they differ. */
|
|
88
|
+
export function cursorHeatRoots(event = {}) {
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const out = [];
|
|
91
|
+
addHeatRoot(out, seen, event.cwd);
|
|
92
|
+
addHeatRoot(out, seen, event.workspaceRoot);
|
|
93
|
+
const listed = event.workspace_roots || event.workspaceRoots;
|
|
94
|
+
if (Array.isArray(listed)) {
|
|
95
|
+
for (const row of listed) {
|
|
96
|
+
if (typeof row === 'string') addHeatRoot(out, seen, row);
|
|
97
|
+
else if (row && typeof row === 'object' && row.path) addHeatRoot(out, seen, row.path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
addHeatRoot(out, seen, process.env.CURSOR_PROJECT_DIR);
|
|
101
|
+
const mill = process.env.XRAY_AI_PATH;
|
|
102
|
+
if (mill) {
|
|
103
|
+
const resolved = path.resolve(String(mill));
|
|
104
|
+
const millHasCard =
|
|
105
|
+
fs.existsSync(path.join(resolved, '.xray', 'state', 'STATION.md')) ||
|
|
106
|
+
fs.existsSync(path.join(resolved, '.xray', 'features.json'));
|
|
107
|
+
if (millHasCard) addHeatRoot(out, seen, resolved);
|
|
108
|
+
}
|
|
109
|
+
if (out.length === 0) addHeatRoot(out, seen, cursorWorkspaceRoot(event));
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
export function cursorSessionId(event = {}) {
|
|
81
114
|
return (
|
|
82
115
|
event.conversation_id ||
|
|
@@ -141,18 +174,7 @@ export function classifyPreCompactEvent(event = {}, argv = process.argv) {
|
|
|
141
174
|
}
|
|
142
175
|
|
|
143
176
|
export function cursorBootNeedsRefresh(existing, root) {
|
|
144
|
-
|
|
145
|
-
if (existing.host !== CURSOR_HOST) return true;
|
|
146
|
-
if (!existing.suit_profile) return true;
|
|
147
|
-
if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
|
|
148
|
-
if (!existing.stationLine) return true;
|
|
149
|
-
const liveGit = readGitBrief(root);
|
|
150
|
-
const bootHead = existing.git && existing.git.head ? String(existing.git.head) : '';
|
|
151
|
-
if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
|
|
152
|
-
const liveResume = buildRepertoireResume(root);
|
|
153
|
-
if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
|
|
154
|
-
if (stationDurableHoldsNpm(root)) return true;
|
|
155
|
-
return false;
|
|
177
|
+
return stationBootNeedsRefresh(existing, root, CURSOR_HOST);
|
|
156
178
|
}
|
|
157
179
|
|
|
158
180
|
export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-use-boot', extra = {}) {
|
|
@@ -161,7 +183,7 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
|
|
|
161
183
|
try {
|
|
162
184
|
const existing = JSON.parse(fs.readFileSync(bootPath, 'utf8'));
|
|
163
185
|
if (!cursorBootNeedsRefresh(existing, root)) {
|
|
164
|
-
|
|
186
|
+
heatLiveMemory(root);
|
|
165
187
|
return bootPath;
|
|
166
188
|
}
|
|
167
189
|
} catch {
|
|
@@ -172,9 +194,7 @@ export function ensureCursorSessionBoot(root, source = '0xray/cursor-pre-tool-us
|
|
|
172
194
|
host: CURSOR_HOST,
|
|
173
195
|
...extra,
|
|
174
196
|
});
|
|
175
|
-
|
|
176
|
-
maybeCaptureSessionOnHeadMove(root);
|
|
177
|
-
return written;
|
|
197
|
+
return writeSessionBoot(root, payload) || bootPath;
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
export function writeCursorPrecompactReceipt(root, fields) {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
appendHookActivity,
|
|
10
10
|
buildSessionBootPayload,
|
|
11
11
|
classifyPreCompactEvent,
|
|
12
|
+
cursorHeatRoots,
|
|
12
13
|
cursorSessionId,
|
|
13
14
|
cursorWorkspaceRoot,
|
|
14
15
|
readStdinJson,
|
|
@@ -40,33 +41,40 @@ async function main() {
|
|
|
40
41
|
const eventRoot = cursorWorkspaceRoot(event);
|
|
41
42
|
const sessionId = cursorSessionId(event);
|
|
42
43
|
const intent = extractIntent(event);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
44
|
+
let bootPath = null;
|
|
45
|
+
let receiptPath = null;
|
|
46
|
+
let usagePath = null;
|
|
47
|
+
let stationLine = null;
|
|
48
|
+
for (const root of cursorHeatRoots(event)) {
|
|
49
|
+
const payload = buildSessionBootPayload(root, '0xray/cursor-compact', {
|
|
50
|
+
host: 'cursor',
|
|
51
|
+
hookEvent: 'pre_compact',
|
|
52
|
+
event_class: eventClass,
|
|
53
|
+
sessionId,
|
|
54
|
+
...(intent ? { intent } : {}),
|
|
55
|
+
});
|
|
56
|
+
bootPath = writeSessionBoot(root, payload);
|
|
57
|
+
stationLine = payload.stationLine;
|
|
58
|
+
receiptPath = writeCursorPrecompactReceipt(root, {
|
|
59
|
+
event_class: eventClass,
|
|
60
|
+
hookEvent: 'pre_compact',
|
|
61
|
+
trigger: event.trigger || 'synthetic',
|
|
62
|
+
sessionId,
|
|
63
|
+
bootPath,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
usagePath = writeCursorUsageReceipt(root, {
|
|
67
|
+
eventClass,
|
|
68
|
+
sessionId,
|
|
69
|
+
usage: hostUsageFromPreCompactEvent(event),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
64
72
|
appendHookActivity(eventRoot, 'cursor-pre-compact', 'station-written', 'success', {
|
|
65
73
|
bootPath,
|
|
66
74
|
receiptPath,
|
|
67
75
|
usagePath,
|
|
68
76
|
event_class: eventClass,
|
|
69
|
-
stationLine
|
|
77
|
+
stationLine,
|
|
70
78
|
});
|
|
71
79
|
console.log(
|
|
72
80
|
JSON.stringify({
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
appendHookActivity,
|
|
10
10
|
checkCodexPatterns,
|
|
11
11
|
checkFullTestSuite,
|
|
12
|
+
cursorHeatRoots,
|
|
12
13
|
cursorSessionId,
|
|
13
14
|
cursorToolContext,
|
|
14
15
|
cursorWorkspaceRoot,
|
|
@@ -47,16 +48,16 @@ async function main() {
|
|
|
47
48
|
try {
|
|
48
49
|
const event = await readStdinJson();
|
|
49
50
|
const eventRoot = cursorWorkspaceRoot(event);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const sessionId = cursorSessionId(event);
|
|
52
|
+
for (const root of cursorHeatRoots(event)) {
|
|
53
|
+
ensureCursorSessionBoot(root, '0xray/cursor-pre-tool-use-boot', { sessionId });
|
|
54
|
+
}
|
|
53
55
|
|
|
54
56
|
const features = loadFeatures(eventRoot);
|
|
55
57
|
const gateFeatures = loadDelegationGateFeatures(eventRoot, 'cursor');
|
|
56
58
|
const ctx = cursorToolContext(event);
|
|
57
59
|
toolName = ctx.toolName;
|
|
58
60
|
const { content, cmd, toolInput } = ctx;
|
|
59
|
-
const sessionId = cursorSessionId(event);
|
|
60
61
|
|
|
61
62
|
const gateBlock = evaluatePreToolGate(toolName, toolInput, {
|
|
62
63
|
projectRoot: eventRoot,
|
|
@@ -306,10 +306,12 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
|
|
|
306
306
|
...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
|
|
307
307
|
...(userAsideBoot ?? {}),
|
|
308
308
|
sessionId,
|
|
309
|
-
timestamp: new Date().toISOString(),
|
|
310
309
|
source,
|
|
311
310
|
...extra,
|
|
312
311
|
...heat,
|
|
312
|
+
hook: source,
|
|
313
|
+
source,
|
|
314
|
+
timestamp: new Date().toISOString(),
|
|
313
315
|
};
|
|
314
316
|
}
|
|
315
317
|
|
|
@@ -468,6 +468,20 @@ async function handlePreProcess(input, projectRoot, logDir) {
|
|
|
468
468
|
const { tool, args } = input;
|
|
469
469
|
const startTime = Date.now();
|
|
470
470
|
|
|
471
|
+
try {
|
|
472
|
+
const gate = await import('../hooks/delegation-gate-runtime.mjs');
|
|
473
|
+
if (typeof gate.maybeHeatHostStation === 'function') {
|
|
474
|
+
gate.maybeHeatHostStation(projectRoot, 'hermes', {
|
|
475
|
+
source: '0xray/hermes-pre-process',
|
|
476
|
+
sessionId: input.sessionId || 'hermes',
|
|
477
|
+
hookEvent: 'pre_tool_use',
|
|
478
|
+
...(input.intent ? { intent: input.intent } : {}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
/* live memory is best-effort — quality gate still runs */
|
|
483
|
+
}
|
|
484
|
+
|
|
471
485
|
logToolEvent(logDir, "start", tool, args);
|
|
472
486
|
logToActivity(logDir, `pre-process: tool=${tool}`);
|
|
473
487
|
|
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
* Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
|
|
6
6
|
*/
|
|
7
7
|
const { execFileSync } = require("child_process");
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
closeSync,
|
|
10
|
+
copyFileSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
openSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
statSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} = require("fs");
|
|
9
19
|
const { join } = require("path");
|
|
10
20
|
|
|
11
21
|
const HOOKS_DIR = __dirname;
|
|
@@ -285,7 +295,12 @@ function mergeMissingSignals(destPath, incoming) {
|
|
|
285
295
|
for (const signal of incoming) {
|
|
286
296
|
const name = String(signal.name).trim();
|
|
287
297
|
if (!name || have.has(name)) continue;
|
|
288
|
-
data.signals.push(
|
|
298
|
+
data.signals.push({
|
|
299
|
+
...signal,
|
|
300
|
+
name,
|
|
301
|
+
tags: Array.isArray(signal.tags) ? signal.tags : [],
|
|
302
|
+
definition: typeof signal.definition === "string" ? signal.definition : name,
|
|
303
|
+
});
|
|
289
304
|
have.add(name);
|
|
290
305
|
added += 1;
|
|
291
306
|
}
|
|
@@ -374,7 +389,151 @@ function gitCommitSubjects(root, args) {
|
|
|
374
389
|
}
|
|
375
390
|
}
|
|
376
391
|
|
|
377
|
-
|
|
392
|
+
function destLockPath(root) {
|
|
393
|
+
return join(root, ".xray", "state", "repertoire", "dest.lock");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function sleepMs(ms) {
|
|
397
|
+
try {
|
|
398
|
+
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
399
|
+
Atomics.wait(buf, 0, 0, ms);
|
|
400
|
+
} catch {
|
|
401
|
+
/* lock retry is best-effort */
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Parallel live contexts share one project copy. Exclusive create; stale after 60s. */
|
|
406
|
+
function withDestLock(root, fn) {
|
|
407
|
+
const lockPath = destLockPath(root);
|
|
408
|
+
mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
|
|
409
|
+
const started = Date.now();
|
|
410
|
+
while (Date.now() - started < 50000) {
|
|
411
|
+
try {
|
|
412
|
+
const fd = openSync(lockPath, "wx");
|
|
413
|
+
try {
|
|
414
|
+
writeFileSync(fd, `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}\n`);
|
|
415
|
+
return fn();
|
|
416
|
+
} finally {
|
|
417
|
+
try {
|
|
418
|
+
closeSync(fd);
|
|
419
|
+
} catch {
|
|
420
|
+
/* leftover */
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
unlinkSync(lockPath);
|
|
424
|
+
} catch {
|
|
425
|
+
/* leftover */
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
if (!err || err.code !== "EEXIST") return fn();
|
|
430
|
+
try {
|
|
431
|
+
const st = statSync(lockPath);
|
|
432
|
+
if (Date.now() - st.mtimeMs > 60000) unlinkSync(lockPath);
|
|
433
|
+
} catch {
|
|
434
|
+
/* leftover */
|
|
435
|
+
}
|
|
436
|
+
sleepMs(40);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return fn();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function slugPrimitiveName(raw) {
|
|
443
|
+
const trimmed = String(raw || "").trim();
|
|
444
|
+
if (!trimmed) return null;
|
|
445
|
+
const bare = trimmed.replace(/^@[^/]+\//, "");
|
|
446
|
+
const slug = bare
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
449
|
+
.replace(/^-+|-+$/g, "");
|
|
450
|
+
if (!/^[a-z][a-z0-9-]{2,119}$/.test(slug)) return null;
|
|
451
|
+
if (/^phase-\d/.test(slug)) return null;
|
|
452
|
+
if (/^\d/.test(slug)) return null;
|
|
453
|
+
if (
|
|
454
|
+
slug === "criteria-selection-gap" ||
|
|
455
|
+
slug === "external-norm-smuggling-risk" ||
|
|
456
|
+
slug === "model-latent-geometry-as-true-invariant"
|
|
457
|
+
) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
return slug;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function stripConventionalSubject(message) {
|
|
464
|
+
return String(message || "")
|
|
465
|
+
.replace(/^(feat|fix|chore|docs|test|refactor|perf|style|ci|build|revert)(\([^)]+\))?:\s*/i, "")
|
|
466
|
+
.trim();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Real git → dest names. Not a leftover catalog. Kernel structural names when the diff shows them. */
|
|
470
|
+
function patternsFromGit(root, commits, span) {
|
|
471
|
+
const patterns = [];
|
|
472
|
+
const seen = new Set();
|
|
473
|
+
const add = (raw, confidence, description) => {
|
|
474
|
+
const slug = slugPrimitiveName(raw);
|
|
475
|
+
if (!slug || seen.has(slug)) return;
|
|
476
|
+
seen.add(slug);
|
|
477
|
+
patterns.push({ name: slug, confidence, description: description || String(raw) });
|
|
478
|
+
};
|
|
479
|
+
for (const row of commits) {
|
|
480
|
+
add(stripConventionalSubject(row.message), 0.55, row.message);
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const from = span && span.from ? String(span.from) : "";
|
|
484
|
+
const to = span && span.to ? String(span.to) : "HEAD";
|
|
485
|
+
if (from && !from.startsWith("HEAD~")) {
|
|
486
|
+
const out = execFileSync("git", ["diff", "--name-status", `${from}..${to}`], {
|
|
487
|
+
cwd: root,
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
timeout: 4000,
|
|
490
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
491
|
+
});
|
|
492
|
+
const lines = String(out).split("\n").filter(Boolean);
|
|
493
|
+
if (lines.some((line) => /\.(test|spec)\.[jt]sx?|__tests__|\/tests\//.test(line))) {
|
|
494
|
+
add("test-expansion", 0.55, "git test files");
|
|
495
|
+
}
|
|
496
|
+
if (lines.some((line) => /^D[\t ]/.test(line))) {
|
|
497
|
+
add("dead-code-removal", 0.55, "git deletions");
|
|
498
|
+
}
|
|
499
|
+
if (lines.some((line) => /hooks\/station|session-capture|semantic-patterns/.test(line))) {
|
|
500
|
+
add("station-heat", 0.55, "heat path");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
/* structural names are extra — commit slugs still grow dest */
|
|
505
|
+
}
|
|
506
|
+
return patterns;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function latestSessionNewerThanGrow(root) {
|
|
510
|
+
const latest = join(root, "docs", "inference", "latest-session.json");
|
|
511
|
+
if (!existsSync(latest)) return false;
|
|
512
|
+
const working = readRepertoireWorking(root);
|
|
513
|
+
const grownAt = working && working.grow && (working.grow.at || working.updatedAt);
|
|
514
|
+
if (!grownAt) return true;
|
|
515
|
+
try {
|
|
516
|
+
return statSync(latest).mtimeMs > Date.parse(String(grownAt));
|
|
517
|
+
} catch {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Capture then grow. Skip-path for floors that already have a live card. */
|
|
523
|
+
function heatLiveMemory(root) {
|
|
524
|
+
const mr = readMemoryRoutingConfig(root);
|
|
525
|
+
if (isExplicitMemoryRoutingOptOut(mr)) {
|
|
526
|
+
return { captured: null, grow: null };
|
|
527
|
+
}
|
|
528
|
+
return withDestLock(root, () => {
|
|
529
|
+
const captured = maybeCaptureSessionOnHeadMove(root);
|
|
530
|
+
const grow = captured || latestSessionNewerThanGrow(root) ? growDestOnWake(root) : null;
|
|
531
|
+
if (grow) persistRepertoireWorking(root, { grow });
|
|
532
|
+
return { captured, grow };
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
|
|
378
537
|
function maybeCaptureSessionOnHeadMove(root) {
|
|
379
538
|
const cfg = readSessionCaptureConfig(root);
|
|
380
539
|
if (!cfg.enabled) return null;
|
|
@@ -393,18 +552,19 @@ function maybeCaptureSessionOnHeadMove(root) {
|
|
|
393
552
|
if (commits.length < cfg.minCommits) return null;
|
|
394
553
|
const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
|
|
395
554
|
const approaches = commits.map((row) => row.message).filter(Boolean);
|
|
555
|
+
const span = {
|
|
556
|
+
from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
|
|
557
|
+
to: git.head,
|
|
558
|
+
};
|
|
396
559
|
const session = {
|
|
397
560
|
sessionId,
|
|
398
561
|
timestamp: new Date().toISOString(),
|
|
399
|
-
span
|
|
400
|
-
from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
|
|
401
|
-
to: git.head,
|
|
402
|
-
},
|
|
562
|
+
span,
|
|
403
563
|
problems: [],
|
|
404
564
|
approaches,
|
|
405
565
|
wrongTurns: [],
|
|
406
566
|
solutions: [],
|
|
407
|
-
patterns:
|
|
567
|
+
patterns: patternsFromGit(root, commits, span),
|
|
408
568
|
matched_primitives: [],
|
|
409
569
|
metrics: { commits: commits.length },
|
|
410
570
|
};
|
|
@@ -476,6 +636,24 @@ function matchStationSignalsSync(root, intent) {
|
|
|
476
636
|
}
|
|
477
637
|
}
|
|
478
638
|
|
|
639
|
+
function growDestOnWake(root) {
|
|
640
|
+
const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
|
|
641
|
+
if (!existsSync(helper)) return null;
|
|
642
|
+
try {
|
|
643
|
+
const out = execFileSync(process.execPath, [helper, root, "--grow"], {
|
|
644
|
+
encoding: "utf8",
|
|
645
|
+
timeout: 45000,
|
|
646
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
647
|
+
env: { ...process.env, REPERTOIRE_FIELD_SYNC: "0", REPERTOIRE_DEST_LOCK: "held" },
|
|
648
|
+
});
|
|
649
|
+
const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
|
|
650
|
+
const parsed = JSON.parse(line);
|
|
651
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
479
657
|
function ingestCompactFeedbackSync(root, sessionId, hookEvent, signals) {
|
|
480
658
|
if (!signals.length) return;
|
|
481
659
|
const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
|
|
@@ -537,8 +715,23 @@ function buildRepertoireResume(root) {
|
|
|
537
715
|
function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
538
716
|
const mr = readMemoryRoutingConfig(root);
|
|
539
717
|
const memoryOff = isExplicitMemoryRoutingOptOut(mr);
|
|
540
|
-
const
|
|
541
|
-
|
|
718
|
+
const live = memoryOff
|
|
719
|
+
? { hydrate: { dest: null, added: 0, destCount: 0 }, captured: null, grow: null }
|
|
720
|
+
: withDestLock(root, () => {
|
|
721
|
+
const hydrateInner = hydrateDestOnWake(root);
|
|
722
|
+
const capturedInner = maybeCaptureSessionOnHeadMove(root);
|
|
723
|
+
const growInner = growDestOnWake(root);
|
|
724
|
+
return { hydrate: hydrateInner, captured: capturedInner, grow: growInner };
|
|
725
|
+
});
|
|
726
|
+
const hydrate = live.hydrate;
|
|
727
|
+
const captured = live.captured;
|
|
728
|
+
const grow = live.grow;
|
|
729
|
+
const destCountAfterGrow =
|
|
730
|
+
grow && typeof grow.after === "number"
|
|
731
|
+
? grow.after
|
|
732
|
+
: existsSync(destSignalsPath(root))
|
|
733
|
+
? countCuratedSignals(destSignalsPath(root)) || 0
|
|
734
|
+
: hydrate.destCount;
|
|
542
735
|
const pickup = readNotesPickup(root);
|
|
543
736
|
const approaches = readLatestSessionApproaches(root);
|
|
544
737
|
const prevHost = typeof existing.host === "string" ? existing.host : null;
|
|
@@ -570,7 +763,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
570
763
|
matchedSignals = preferSubjectHits(extra.matchedSignals, 8);
|
|
571
764
|
}
|
|
572
765
|
const priorWorking = readRepertoireWorking(root);
|
|
573
|
-
const destCount =
|
|
766
|
+
const destCount =
|
|
767
|
+
destCountAfterGrow ||
|
|
768
|
+
hydrate.destCount ||
|
|
769
|
+
(existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
|
|
574
770
|
if (!memoryOff && !matchedSignals.length && matchText) {
|
|
575
771
|
if (
|
|
576
772
|
!rematch &&
|
|
@@ -604,6 +800,15 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
604
800
|
if (pickup) workingSnapshot.pickup = pickup;
|
|
605
801
|
if (matchText) workingSnapshot.matchText = matchText;
|
|
606
802
|
if (captured) workingSnapshot.sessionCapture = captured;
|
|
803
|
+
if (grow && grow.skipped) workingSnapshot.grow = grow.skipped;
|
|
804
|
+
if (grow && typeof grow.after === "number") {
|
|
805
|
+
workingSnapshot.grow = {
|
|
806
|
+
before: grow.before,
|
|
807
|
+
after: grow.after,
|
|
808
|
+
imported: grow.imported,
|
|
809
|
+
observed: Array.isArray(grow.observed) ? grow.observed.length : 0,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
607
812
|
if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
|
|
608
813
|
const opProcNames = readOpProcNames(root);
|
|
609
814
|
if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
|
|
@@ -666,6 +871,22 @@ function stripHoldNpm(line) {
|
|
|
666
871
|
.replace(/[ \t]+$/g, "");
|
|
667
872
|
}
|
|
668
873
|
|
|
874
|
+
/** Same-session skip must still rewrite when HEAD, organ count, or Hold npm diverges. */
|
|
875
|
+
function stationBootNeedsRefresh(existing, root, host) {
|
|
876
|
+
if (!existing || typeof existing !== "object") return true;
|
|
877
|
+
if (host && existing.host && existing.host !== host) return true;
|
|
878
|
+
if (!existing.suit_profile) return true;
|
|
879
|
+
if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
|
|
880
|
+
if (!existing.stationLine) return true;
|
|
881
|
+
const liveGit = readGitBrief(root);
|
|
882
|
+
const bootHead = existing.git && existing.git.head ? String(existing.git.head) : "";
|
|
883
|
+
if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
|
|
884
|
+
const liveResume = buildRepertoireResume(root);
|
|
885
|
+
if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
|
|
886
|
+
if (stationDurableHoldsNpm(root)) return true;
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
|
|
669
890
|
function stationDurableHoldsNpm(root) {
|
|
670
891
|
return readExistingStationMarkdown(root)
|
|
671
892
|
.split(/\r?\n/)
|
|
@@ -793,6 +1014,11 @@ module.exports = {
|
|
|
793
1014
|
readRepertoireWorking,
|
|
794
1015
|
readOpProcNames,
|
|
795
1016
|
hydrateDestOnWake,
|
|
1017
|
+
growDestOnWake,
|
|
1018
|
+
heatLiveMemory,
|
|
1019
|
+
withDestLock,
|
|
1020
|
+
slugPrimitiveName,
|
|
1021
|
+
patternsFromGit,
|
|
796
1022
|
readNotesPickup,
|
|
797
1023
|
maybeCaptureSessionOnHeadMove,
|
|
798
1024
|
formatWorkingLine,
|
|
@@ -803,4 +1029,5 @@ module.exports = {
|
|
|
803
1029
|
writeStationMarkdown,
|
|
804
1030
|
isHoldNpmLine,
|
|
805
1031
|
stationDurableHoldsNpm,
|
|
1032
|
+
stationBootNeedsRefresh,
|
|
806
1033
|
};
|
|
@@ -20,6 +20,11 @@ export const persistRepertoireWorking = impl.persistRepertoireWorking;
|
|
|
20
20
|
export const readRepertoireWorking = impl.readRepertoireWorking;
|
|
21
21
|
export const readOpProcNames = impl.readOpProcNames;
|
|
22
22
|
export const hydrateDestOnWake = impl.hydrateDestOnWake;
|
|
23
|
+
export const growDestOnWake = impl.growDestOnWake;
|
|
24
|
+
export const heatLiveMemory = impl.heatLiveMemory;
|
|
25
|
+
export const withDestLock = impl.withDestLock;
|
|
26
|
+
export const slugPrimitiveName = impl.slugPrimitiveName;
|
|
27
|
+
export const patternsFromGit = impl.patternsFromGit;
|
|
23
28
|
export const readNotesPickup = impl.readNotesPickup;
|
|
24
29
|
export const maybeCaptureSessionOnHeadMove = impl.maybeCaptureSessionOnHeadMove;
|
|
25
30
|
export const formatWorkingLine = impl.formatWorkingLine;
|
|
@@ -30,3 +35,4 @@ export const formatStationMarkdown = impl.formatStationMarkdown;
|
|
|
30
35
|
export const writeStationMarkdown = impl.writeStationMarkdown;
|
|
31
36
|
export const isHoldNpmLine = impl.isHoldNpmLine;
|
|
32
37
|
export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
|
|
38
|
+
export const stationBootNeedsRefresh = impl.stationBootNeedsRefresh;
|
|
@@ -1,12 +1,161 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Station organ write — compact ingest + wake grow.
|
|
4
|
+
* Grow uses the worn RepertoireService. Groover field stays off.
|
|
4
5
|
*/
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
7
9
|
|
|
8
10
|
const root = process.argv[2];
|
|
9
|
-
const
|
|
11
|
+
const mode = process.argv[3] || 'station';
|
|
12
|
+
|
|
13
|
+
function readRouting(projectRoot) {
|
|
14
|
+
let routing = { enabled: false, provider: 'null' };
|
|
15
|
+
const featuresPath = join(projectRoot, '.xray', 'features.json');
|
|
16
|
+
if (existsSync(featuresPath)) {
|
|
17
|
+
try {
|
|
18
|
+
routing = JSON.parse(readFileSync(featuresPath, 'utf8')).memory_routing || routing;
|
|
19
|
+
} catch {
|
|
20
|
+
/* leftover */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return routing;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isExplicitOptOut(routing) {
|
|
27
|
+
return routing && routing.enabled === false && routing.provider === 'repertoire';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function organServiceCandidates(projectRoot, routing) {
|
|
31
|
+
const configured = routing.module_path
|
|
32
|
+
? routing.module_path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(routing.module_path)
|
|
33
|
+
? routing.module_path
|
|
34
|
+
: join(projectRoot, routing.module_path)
|
|
35
|
+
: null;
|
|
36
|
+
const fromProvider = configured
|
|
37
|
+
? configured.replace(/provider\/memory-routing-provider\.js$/, 'RepertoireService.js')
|
|
38
|
+
: null;
|
|
39
|
+
const raw = [
|
|
40
|
+
fromProvider,
|
|
41
|
+
join(projectRoot, 'node_modules', '@0xray', 'repertoire', 'dist', 'RepertoireService.js'),
|
|
42
|
+
join(projectRoot, 'vendor', '@0xray', 'repertoire', 'dist', 'RepertoireService.js'),
|
|
43
|
+
join(projectRoot, '..', 'repertoire', 'dist', 'RepertoireService.js'),
|
|
44
|
+
].filter(Boolean);
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const dest of raw) {
|
|
48
|
+
if (seen.has(dest) || !existsSync(dest)) continue;
|
|
49
|
+
seen.add(dest);
|
|
50
|
+
out.push(dest);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureDestShape(destSignals) {
|
|
56
|
+
if (!existsSync(destSignals)) return;
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(readFileSync(destSignals, 'utf8'));
|
|
59
|
+
if (!Array.isArray(data.signals)) return;
|
|
60
|
+
let dirty = false;
|
|
61
|
+
for (const signal of data.signals) {
|
|
62
|
+
if (!signal) continue;
|
|
63
|
+
if (!Array.isArray(signal.tags)) {
|
|
64
|
+
signal.tags = [];
|
|
65
|
+
dirty = true;
|
|
66
|
+
}
|
|
67
|
+
if (typeof signal.definition !== 'string') {
|
|
68
|
+
signal.definition = String(signal.name || '');
|
|
69
|
+
dirty = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (dirty) writeFileSync(destSignals, `${JSON.stringify(data, null, 2)}\n`);
|
|
73
|
+
} catch {
|
|
74
|
+
/* leftover */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readCleanupDiary(projectRoot) {
|
|
79
|
+
const files = [
|
|
80
|
+
join(projectRoot, '.xray', 'state', 'NOTES.md'),
|
|
81
|
+
join(projectRoot, '.xray', 'state', 'CLEANSE-LIST.md'),
|
|
82
|
+
join(projectRoot, 'docs', 'archive', 'README.md'),
|
|
83
|
+
];
|
|
84
|
+
const parts = [];
|
|
85
|
+
const sources = [];
|
|
86
|
+
for (const dest of files) {
|
|
87
|
+
if (!existsSync(dest)) continue;
|
|
88
|
+
try {
|
|
89
|
+
parts.push(readFileSync(dest, 'utf8'));
|
|
90
|
+
sources.push(dest);
|
|
91
|
+
} catch {
|
|
92
|
+
/* leftover */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { text: parts.join('\n'), sources };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function growDest(projectRoot) {
|
|
99
|
+
const routing = readRouting(projectRoot);
|
|
100
|
+
if (isExplicitOptOut(routing)) {
|
|
101
|
+
process.stdout.write(`${JSON.stringify({ skipped: 'opt-out' })}\n`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const dest of organServiceCandidates(projectRoot, routing)) {
|
|
105
|
+
try {
|
|
106
|
+
const mod = await import(pathToFileURL(dest).href);
|
|
107
|
+
const RepertoireService = mod.RepertoireService;
|
|
108
|
+
if (typeof RepertoireService !== 'function') continue;
|
|
109
|
+
const pathsMod = await import(pathToFileURL(dest.replace(/RepertoireService\.js$/, 'paths.js')).href);
|
|
110
|
+
const destSignals = join(projectRoot, '.xray', 'state', 'repertoire', 'curated_signals.json');
|
|
111
|
+
ensureDestShape(destSignals);
|
|
112
|
+
const service = new RepertoireService({
|
|
113
|
+
projectRoot,
|
|
114
|
+
signalsPath: destSignals,
|
|
115
|
+
syncXray: false,
|
|
116
|
+
syncField: false,
|
|
117
|
+
});
|
|
118
|
+
ensureDestShape(destSignals);
|
|
119
|
+
const before = service.signalsManager.load().signals.length;
|
|
120
|
+
const sessions = service.syncXrayMemory();
|
|
121
|
+
const siblings = service.syncWorkspaceRepos();
|
|
122
|
+
const kernel =
|
|
123
|
+
typeof pathsMod.collectKernelDiaryText === 'function'
|
|
124
|
+
? pathsMod.collectKernelDiaryText(projectRoot)
|
|
125
|
+
: { text: '', sources: [] };
|
|
126
|
+
const cleanup = readCleanupDiary(projectRoot);
|
|
127
|
+
const diary = service.heatKernelDiary({
|
|
128
|
+
text: [kernel.text, cleanup.text].filter(Boolean).join('\n'),
|
|
129
|
+
sources: [...(kernel.sources || []), ...cleanup.sources],
|
|
130
|
+
});
|
|
131
|
+
const after = service.signalsManager.load().signals.length;
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
`${JSON.stringify({
|
|
134
|
+
before,
|
|
135
|
+
after,
|
|
136
|
+
imported: sessions.imported,
|
|
137
|
+
observed: siblings.observed,
|
|
138
|
+
heated: diary.heated,
|
|
139
|
+
})}\n`,
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
} catch {
|
|
143
|
+
/* try next organ */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(`${JSON.stringify({ skipped: 'no-organ' })}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!root) {
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mode === '--grow') {
|
|
154
|
+
await growDest(root);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sessionId = mode;
|
|
10
159
|
const hookEvent = process.argv[4] || 'post_compact';
|
|
11
160
|
let signals = [];
|
|
12
161
|
try {
|
|
@@ -14,7 +163,7 @@ try {
|
|
|
14
163
|
} catch {
|
|
15
164
|
signals = [];
|
|
16
165
|
}
|
|
17
|
-
if (!
|
|
166
|
+
if (!Array.isArray(signals) || signals.length === 0) {
|
|
18
167
|
process.exit(0);
|
|
19
168
|
}
|
|
20
169
|
|
|
@@ -462,17 +462,16 @@ export function evaluateConstitutionGate(toolName, toolInput, ctx) {
|
|
|
462
462
|
return { allow: true };
|
|
463
463
|
}
|
|
464
464
|
export function evaluatePreToolGate(toolName, toolInput, ctx) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
465
|
+
const host = ctx.host || 'generic';
|
|
466
|
+
try {
|
|
467
|
+
maybeHeatHostStation(ctx.projectRoot, host, {
|
|
468
|
+
source: `0xray/${host}-pre-tool`,
|
|
469
|
+
sessionId: ctx.sessionId || host,
|
|
470
|
+
hookEvent: ctx.hookEvent || 'pre_tool_use',
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
/* station heat is best-effort — constitution still runs */
|
|
476
475
|
}
|
|
477
476
|
const constitution = evaluateConstitutionGate(toolName, toolInput, ctx);
|
|
478
477
|
if (!constitution.allow)
|
|
@@ -111,7 +111,11 @@ export function maybeHeatHostStation(projectRoot, host, extra = {}) {
|
|
|
111
111
|
const existingSessionId = concreteSessionId(existing.sessionId);
|
|
112
112
|
const sameHost = existing.host === host;
|
|
113
113
|
const sameSession = existingSessionId !== null && sessionId !== null && existingSessionId === sessionId;
|
|
114
|
-
if (sameHost &&
|
|
114
|
+
if (sameHost &&
|
|
115
|
+
sameSession &&
|
|
116
|
+
fs.existsSync(cardPath) &&
|
|
117
|
+
!stationHeat.stationBootNeedsRefresh(existing, projectRoot, host)) {
|
|
118
|
+
stationHeat.heatLiveMemory(projectRoot);
|
|
115
119
|
return bootPath;
|
|
116
120
|
}
|
|
117
121
|
return writeSuitSessionBoot(projectRoot, host, extra);
|
package/package.json
CHANGED