0xray 4.0.19 → 4.0.21
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/.opencode/skills/orchestrator/SKILL.md +1 -1
- package/.xray/features.json +1 -1
- package/AGENTS-consumer.md +4 -4
- package/AGENTS.md +4 -4
- package/README.md +14 -14
- package/SKILLS.md +3 -3
- package/dist/AGENTS.md +4 -4
- package/dist/CHANGELOG.md +27 -6
- package/dist/README.md +14 -14
- package/dist/SKILLS.md +3 -3
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.js +7 -1
- package/dist/integrations/cursor/README.md +6 -5
- package/dist/integrations/cursor/hooks/after-file-edit.js +6 -3
- package/dist/integrations/cursor/hooks/after-file-edit.sh +3 -0
- package/dist/integrations/cursor/hooks/before-read-file.sh +3 -0
- package/dist/integrations/cursor/hooks/before-shell-execution.sh +3 -0
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +121 -20
- package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +3 -0
- package/dist/integrations/cursor/hooks/hooks.json +13 -3
- package/dist/integrations/cursor/hooks/pre-compact.js +39 -22
- package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +20 -15
- package/dist/integrations/cursor/hooks/pre-tool-use.sh +3 -0
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +63 -0
- package/dist/integrations/grok/hooks/grok-hook-utils.js +6 -3
- package/dist/integrations/hermes-agent/bridge.mjs +14 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +416 -37
- package/dist/integrations/hooks/station-hook-runtime.mjs +15 -0
- package/dist/integrations/hooks/station-memory-ingest.mjs +152 -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.d.ts +0 -5
- package/dist/nucleus/suit-temperament.js +18 -2
- package/dist/skills/orchestrator/SKILL.md +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/scripts/foundry/README.md +2 -2
- package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
- package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
- package/scripts/foundry/validate-release-docs.mjs +44 -0
- package/scripts/node/install-bridges.cjs +256 -10
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/skills/orchestrator/SKILL.md +1 -1
- package/xray/features.json +1 -1
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Station heat — compaction / host-swap card.
|
|
3
|
-
*
|
|
3
|
+
* STATION.md is the pickup ticket. session-boot.json is the snapshot.
|
|
4
|
+
* Heat must not paint leftover boot extras onto the card.
|
|
4
5
|
* Grok ignores SessionStart/UserPromptSubmit stdout; disk + AGENTS.md is the contract.
|
|
5
6
|
* Heat writers merge stock fields; unknown keys and ## Durable / ## Seed survive.
|
|
6
7
|
*/
|
|
7
8
|
const { execFileSync } = require("child_process");
|
|
8
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
closeSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
openSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
statSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} = require("fs");
|
|
9
20
|
const { join } = require("path");
|
|
10
21
|
|
|
11
22
|
const HOOKS_DIR = __dirname;
|
|
@@ -94,6 +105,63 @@ function readPlanLine(root) {
|
|
|
94
105
|
return readGitSubject(root);
|
|
95
106
|
}
|
|
96
107
|
|
|
108
|
+
function isStockTicket(value) {
|
|
109
|
+
const text = String(value || "").trim().toLowerCase();
|
|
110
|
+
return !text || text === "(none)" || text === "(none yet)";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readStationTicketField(root, field) {
|
|
114
|
+
const md = readExistingStationMarkdown(root);
|
|
115
|
+
const re = field === "Plan" ? /^Plan:\s*(.*)$/im : /^Intent:\s*(.*)$/im;
|
|
116
|
+
const match = md.match(re);
|
|
117
|
+
if (!match) return null;
|
|
118
|
+
const value = clipIntent(match[1]);
|
|
119
|
+
if (!value || isStockTicket(value)) return null;
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bootHeadMoved(root, existing) {
|
|
124
|
+
const live = readGitBrief(root);
|
|
125
|
+
const bootHead = existing && existing.git && existing.git.head ? String(existing.git.head) : "";
|
|
126
|
+
return Boolean(live && live.head && bootHead && bootHead !== live.head);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Live ticket beats leftover boot. Extra spoken intent still wins. */
|
|
130
|
+
function resolveHeatIntent(root, extra, existing) {
|
|
131
|
+
const incoming = clipIntent(
|
|
132
|
+
extra.intent || extra.prompt || extra.userMessage || extra.user_prompt,
|
|
133
|
+
);
|
|
134
|
+
if (incoming) return { intent: incoming, rematch: true };
|
|
135
|
+
const card = readStationTicketField(root, "Intent");
|
|
136
|
+
const pickup = clipIntent(readNotesPickup(root));
|
|
137
|
+
const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
|
|
138
|
+
const cardIsBootEcho = Boolean(card && bootIntent && card === bootIntent);
|
|
139
|
+
if (pickup && cardIsBootEcho && pickup !== bootIntent) {
|
|
140
|
+
return { intent: pickup, rematch: true };
|
|
141
|
+
}
|
|
142
|
+
if (card && !cardIsBootEcho) return { intent: card, rematch: card !== bootIntent };
|
|
143
|
+
if (pickup && pickup !== bootIntent) return { intent: pickup, rematch: true };
|
|
144
|
+
if (card) return { intent: card, rematch: false };
|
|
145
|
+
if (bootIntent) return { intent: bootIntent, rematch: false };
|
|
146
|
+
const git = readGitSubject(root);
|
|
147
|
+
return { intent: git, rematch: Boolean(git) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveHeatPlan(root, extra, existing) {
|
|
151
|
+
const incoming = clipIntent(extra.plan || extra.planLine);
|
|
152
|
+
if (incoming) return incoming;
|
|
153
|
+
const card = readStationTicketField(root, "Plan");
|
|
154
|
+
const bootPlan = typeof existing.planLine === "string" ? clipIntent(existing.planLine) : null;
|
|
155
|
+
const live = readPlanLine(root);
|
|
156
|
+
const cardIsBootEcho = Boolean(card && bootPlan && card === bootPlan);
|
|
157
|
+
if (card && !cardIsBootEcho) return card;
|
|
158
|
+
if (bootHeadMoved(root, existing) && bootPlan && live === bootPlan) {
|
|
159
|
+
return readGitSubject(root) || card || live;
|
|
160
|
+
}
|
|
161
|
+
if (card) return card;
|
|
162
|
+
return live;
|
|
163
|
+
}
|
|
164
|
+
|
|
97
165
|
function resolveRepertoireProviderModule(root) {
|
|
98
166
|
const mr = readMemoryRoutingConfig(root);
|
|
99
167
|
if (mr && typeof mr.module_path === "string" && mr.module_path) {
|
|
@@ -190,32 +258,44 @@ function readRepertoireWorking(root) {
|
|
|
190
258
|
}
|
|
191
259
|
}
|
|
192
260
|
|
|
261
|
+
function isKeywordDestName(name) {
|
|
262
|
+
const n = String(name || "").trim().toLowerCase();
|
|
263
|
+
if (!n) return true;
|
|
264
|
+
if (n.startsWith("repo-") || n.startsWith("bedrock-")) return true;
|
|
265
|
+
if (n === "test-expansion" || n === "dead-code-removal" || n === "station-heat") return true;
|
|
266
|
+
if (/^release-v?\d/.test(n) || /^stamp-/.test(n) || /^drop-stale-/.test(n)) return true;
|
|
267
|
+
if (/\b4-0-\d{2}\b/.test(n) && /npm|package-lock|release|stamp/.test(n)) return true;
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isGenericFieldObservedSignal(signal) {
|
|
272
|
+
const definition = String((signal && signal.definition) || "");
|
|
273
|
+
return /field-observed domain primitive/i.test(definition);
|
|
274
|
+
}
|
|
275
|
+
|
|
193
276
|
function stationSafeSignals(names) {
|
|
194
277
|
if (!Array.isArray(names)) return [];
|
|
195
278
|
const out = [];
|
|
196
279
|
for (const raw of names) {
|
|
197
280
|
const name = String(raw || "").trim();
|
|
198
|
-
if (!name) continue;
|
|
199
|
-
if (name.toLowerCase().startsWith("bedrock-")) continue;
|
|
281
|
+
if (!name || isKeywordDestName(name)) continue;
|
|
200
282
|
out.push(name);
|
|
201
283
|
if (out.length >= 4) break;
|
|
202
284
|
}
|
|
203
285
|
return out;
|
|
204
286
|
}
|
|
205
287
|
|
|
206
|
-
/**
|
|
207
|
-
function
|
|
288
|
+
/** Laws only. Hangar repo-* and git-slug keywords stay off the card. */
|
|
289
|
+
function preferLawHits(names, cap = 8) {
|
|
208
290
|
if (!Array.isArray(names)) return [];
|
|
209
291
|
const unique = [];
|
|
210
292
|
for (const raw of names) {
|
|
211
293
|
const name = String(raw || "").trim();
|
|
212
|
-
if (!name || name
|
|
294
|
+
if (!name || isKeywordDestName(name)) continue;
|
|
213
295
|
if (unique.includes(name)) continue;
|
|
214
296
|
unique.push(name);
|
|
215
297
|
}
|
|
216
|
-
|
|
217
|
-
const rest = unique.filter((name) => !name.startsWith("repo-"));
|
|
218
|
-
return [...subject, ...rest].slice(0, cap);
|
|
298
|
+
return unique.slice(0, cap);
|
|
219
299
|
}
|
|
220
300
|
|
|
221
301
|
function destSignalsPath(root) {
|
|
@@ -269,6 +349,52 @@ function signalNameSet(filePath) {
|
|
|
269
349
|
return new Set(readSignalRecords(filePath).map((signal) => String(signal.name).trim()));
|
|
270
350
|
}
|
|
271
351
|
|
|
352
|
+
function destLawNameSet(root) {
|
|
353
|
+
const names = new Set();
|
|
354
|
+
for (const fileName of ["curated_signals.json", "stack-overlay.json"]) {
|
|
355
|
+
for (const name of signalNameSet(repertoireDataFile(root, fileName))) {
|
|
356
|
+
if (!isKeywordDestName(name)) names.add(name);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const name of signalNameSet(destSignalsPath(root))) {
|
|
360
|
+
if (!isKeywordDestName(name)) names.add(name);
|
|
361
|
+
}
|
|
362
|
+
return names;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Drop hangar repo-* and git-slug keywords. Keep factory + stack laws. */
|
|
366
|
+
function pruneKeywordDest(root) {
|
|
367
|
+
const dest = destSignalsPath(root);
|
|
368
|
+
if (!dest || !existsSync(dest)) return { removed: 0, kept: 0 };
|
|
369
|
+
let data;
|
|
370
|
+
try {
|
|
371
|
+
data = JSON.parse(readFileSync(dest, "utf8"));
|
|
372
|
+
} catch {
|
|
373
|
+
return { removed: 0, kept: 0 };
|
|
374
|
+
}
|
|
375
|
+
if (!Array.isArray(data.signals)) return { removed: 0, kept: 0 };
|
|
376
|
+
const factory = signalNameSet(repertoireDataFile(root, "curated_signals.json"));
|
|
377
|
+
const stack = signalNameSet(repertoireDataFile(root, "stack-overlay.json"));
|
|
378
|
+
const kept = [];
|
|
379
|
+
let removed = 0;
|
|
380
|
+
for (const signal of data.signals) {
|
|
381
|
+
const name = String((signal && signal.name) || "").trim();
|
|
382
|
+
if (!name) continue;
|
|
383
|
+
const protectedLaw = factory.has(name) || stack.has(name);
|
|
384
|
+
if (!protectedLaw && (isKeywordDestName(name) || isGenericFieldObservedSignal(signal))) {
|
|
385
|
+
removed += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
kept.push(signal);
|
|
389
|
+
}
|
|
390
|
+
if (removed) {
|
|
391
|
+
data.signals = kept;
|
|
392
|
+
data.last_updated = new Date().toISOString();
|
|
393
|
+
writeFileSync(dest, `${JSON.stringify(data, null, 2)}\n`);
|
|
394
|
+
}
|
|
395
|
+
return { removed, kept: kept.length };
|
|
396
|
+
}
|
|
397
|
+
|
|
272
398
|
function mergeMissingSignals(destPath, incoming) {
|
|
273
399
|
if (!incoming.length || !existsSync(destPath)) return 0;
|
|
274
400
|
let data;
|
|
@@ -285,7 +411,12 @@ function mergeMissingSignals(destPath, incoming) {
|
|
|
285
411
|
for (const signal of incoming) {
|
|
286
412
|
const name = String(signal.name).trim();
|
|
287
413
|
if (!name || have.has(name)) continue;
|
|
288
|
-
data.signals.push(
|
|
414
|
+
data.signals.push({
|
|
415
|
+
...signal,
|
|
416
|
+
name,
|
|
417
|
+
tags: Array.isArray(signal.tags) ? signal.tags : [],
|
|
418
|
+
definition: typeof signal.definition === "string" ? signal.definition : name,
|
|
419
|
+
});
|
|
289
420
|
have.add(name);
|
|
290
421
|
added += 1;
|
|
291
422
|
}
|
|
@@ -293,7 +424,7 @@ function mergeMissingSignals(destPath, incoming) {
|
|
|
293
424
|
return added;
|
|
294
425
|
}
|
|
295
426
|
|
|
296
|
-
/** Hydrate project dest from seed + stack
|
|
427
|
+
/** Hydrate project dest from seed + stack. Subject repo-* stay off dest. */
|
|
297
428
|
function hydrateDestOnWake(root) {
|
|
298
429
|
const dest = destSignalsPath(root);
|
|
299
430
|
const seed = repertoireDataFile(root, "curated_signals.json");
|
|
@@ -302,10 +433,12 @@ function hydrateDestOnWake(root) {
|
|
|
302
433
|
copyFileSync(seed, dest);
|
|
303
434
|
}
|
|
304
435
|
if (!existsSync(dest)) return { dest: null, added: 0, destCount: 0 };
|
|
305
|
-
const added =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
436
|
+
const added = mergeMissingSignals(
|
|
437
|
+
dest,
|
|
438
|
+
readSignalRecords(repertoireDataFile(root, "stack-overlay.json")),
|
|
439
|
+
);
|
|
440
|
+
const pruned = pruneKeywordDest(root);
|
|
441
|
+
return { dest, added, destCount: countCuratedSignals(dest) || 0, pruned };
|
|
309
442
|
}
|
|
310
443
|
|
|
311
444
|
function readNotesPickup(root) {
|
|
@@ -374,7 +507,138 @@ function gitCommitSubjects(root, args) {
|
|
|
374
507
|
}
|
|
375
508
|
}
|
|
376
509
|
|
|
377
|
-
|
|
510
|
+
function destLockPath(root) {
|
|
511
|
+
return join(root, ".xray", "state", "repertoire", "dest.lock");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function sleepMs(ms) {
|
|
515
|
+
try {
|
|
516
|
+
const buf = new Int32Array(new SharedArrayBuffer(4));
|
|
517
|
+
Atomics.wait(buf, 0, 0, ms);
|
|
518
|
+
} catch {
|
|
519
|
+
/* lock retry is best-effort */
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Parallel live contexts share one project copy. Exclusive create; stale after 60s. */
|
|
524
|
+
function withDestLock(root, fn) {
|
|
525
|
+
const lockPath = destLockPath(root);
|
|
526
|
+
try {
|
|
527
|
+
mkdirSync(join(root, ".xray", "state", "repertoire"), { recursive: true });
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err && (err.code === "EACCES" || err.code === "EPERM")) return null;
|
|
530
|
+
throw err;
|
|
531
|
+
}
|
|
532
|
+
const started = Date.now();
|
|
533
|
+
while (Date.now() - started < 50000) {
|
|
534
|
+
try {
|
|
535
|
+
const fd = openSync(lockPath, "wx");
|
|
536
|
+
try {
|
|
537
|
+
writeFileSync(fd, `${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}\n`);
|
|
538
|
+
return fn();
|
|
539
|
+
} finally {
|
|
540
|
+
try {
|
|
541
|
+
closeSync(fd);
|
|
542
|
+
} catch {
|
|
543
|
+
/* leftover */
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
unlinkSync(lockPath);
|
|
547
|
+
} catch {
|
|
548
|
+
/* leftover */
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (!err || err.code !== "EEXIST") return fn();
|
|
553
|
+
try {
|
|
554
|
+
const st = statSync(lockPath);
|
|
555
|
+
if (Date.now() - st.mtimeMs > 60000) unlinkSync(lockPath);
|
|
556
|
+
} catch {
|
|
557
|
+
/* leftover */
|
|
558
|
+
}
|
|
559
|
+
sleepMs(40);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return fn();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function slugPrimitiveName(raw) {
|
|
566
|
+
const trimmed = String(raw || "").trim();
|
|
567
|
+
if (!trimmed) return null;
|
|
568
|
+
const bare = trimmed.replace(/^@[^/]+\//, "");
|
|
569
|
+
const slug = bare
|
|
570
|
+
.toLowerCase()
|
|
571
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
572
|
+
.replace(/^-+|-+$/g, "");
|
|
573
|
+
if (!/^[a-z][a-z0-9-]{2,119}$/.test(slug)) return null;
|
|
574
|
+
if (/^phase-\d/.test(slug)) return null;
|
|
575
|
+
if (/^\d/.test(slug)) return null;
|
|
576
|
+
if (
|
|
577
|
+
slug === "criteria-selection-gap" ||
|
|
578
|
+
slug === "external-norm-smuggling-risk" ||
|
|
579
|
+
slug === "model-latent-geometry-as-true-invariant"
|
|
580
|
+
) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
return slug;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function stripConventionalSubject(message) {
|
|
587
|
+
return String(message || "")
|
|
588
|
+
.replace(/^(feat|fix|chore|docs|test|refactor|perf|style|ci|build|revert)(\([^)]+\))?:\s*/i, "")
|
|
589
|
+
.trim();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Observe existing laws mentioned in git. Do not mint commit slugs. */
|
|
593
|
+
function patternsFromGit(root, commits) {
|
|
594
|
+
const text = (Array.isArray(commits) ? commits : [])
|
|
595
|
+
.map((row) => String((row && row.message) || ""))
|
|
596
|
+
.join("\n")
|
|
597
|
+
.toLowerCase();
|
|
598
|
+
const patterns = [];
|
|
599
|
+
if (!text.trim()) return patterns;
|
|
600
|
+
for (const name of destLawNameSet(root)) {
|
|
601
|
+
if (text.includes(name) || text.includes(name.replace(/-/g, " "))) {
|
|
602
|
+
patterns.push({ name, confidence: 0.7, description: "observe existing law from git" });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return patterns;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function latestSessionNewerThanGrow(root) {
|
|
609
|
+
const latest = join(root, "docs", "inference", "latest-session.json");
|
|
610
|
+
if (!existsSync(latest)) return false;
|
|
611
|
+
const working = readRepertoireWorking(root);
|
|
612
|
+
const grownAt = working && working.grow && (working.grow.at || working.updatedAt);
|
|
613
|
+
if (!grownAt) return true;
|
|
614
|
+
try {
|
|
615
|
+
return statSync(latest).mtimeMs > Date.parse(String(grownAt));
|
|
616
|
+
} catch {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Capture then grow. Skip-path for floors that already have a live card. */
|
|
622
|
+
function heatLiveMemory(root) {
|
|
623
|
+
try {
|
|
624
|
+
const mr = readMemoryRoutingConfig(root);
|
|
625
|
+
if (isExplicitMemoryRoutingOptOut(mr)) {
|
|
626
|
+
return { captured: null, grow: null };
|
|
627
|
+
}
|
|
628
|
+
const result = withDestLock(root, () => {
|
|
629
|
+
const captured = maybeCaptureSessionOnHeadMove(root);
|
|
630
|
+
const grow = captured || latestSessionNewerThanGrow(root) ? growDestOnWake(root) : null;
|
|
631
|
+
if (grow) persistRepertoireWorking(root, { grow });
|
|
632
|
+
return { captured, grow };
|
|
633
|
+
});
|
|
634
|
+
return result || { captured: null, grow: null };
|
|
635
|
+
} catch {
|
|
636
|
+
/* dest EACCES / unwritable heat root must not deny the host tool */
|
|
637
|
+
return { captured: null, grow: null };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
|
|
378
642
|
function maybeCaptureSessionOnHeadMove(root) {
|
|
379
643
|
const cfg = readSessionCaptureConfig(root);
|
|
380
644
|
if (!cfg.enabled) return null;
|
|
@@ -393,18 +657,19 @@ function maybeCaptureSessionOnHeadMove(root) {
|
|
|
393
657
|
if (commits.length < cfg.minCommits) return null;
|
|
394
658
|
const sessionId = `session-${new Date().toISOString().slice(0, 10)}-${git.head}`;
|
|
395
659
|
const approaches = commits.map((row) => row.message).filter(Boolean);
|
|
660
|
+
const span = {
|
|
661
|
+
from: stamp && stamp.head ? stamp.head : commits[commits.length - 1] ? commits[commits.length - 1].hash : git.head,
|
|
662
|
+
to: git.head,
|
|
663
|
+
};
|
|
396
664
|
const session = {
|
|
397
665
|
sessionId,
|
|
398
666
|
timestamp: new Date().toISOString(),
|
|
399
|
-
span
|
|
400
|
-
from: stamp && stamp.head ? stamp.head : `HEAD~${cfg.lookback}`,
|
|
401
|
-
to: git.head,
|
|
402
|
-
},
|
|
667
|
+
span,
|
|
403
668
|
problems: [],
|
|
404
669
|
approaches,
|
|
405
670
|
wrongTurns: [],
|
|
406
671
|
solutions: [],
|
|
407
|
-
patterns:
|
|
672
|
+
patterns: patternsFromGit(root, commits, span),
|
|
408
673
|
matched_primitives: [],
|
|
409
674
|
metrics: { commits: commits.length },
|
|
410
675
|
};
|
|
@@ -476,6 +741,26 @@ function matchStationSignalsSync(root, intent) {
|
|
|
476
741
|
}
|
|
477
742
|
}
|
|
478
743
|
|
|
744
|
+
function growDestOnWake(root) {
|
|
745
|
+
const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
|
|
746
|
+
if (!existsSync(helper)) return null;
|
|
747
|
+
try {
|
|
748
|
+
const out = execFileSync(process.execPath, [helper, root, "--grow"], {
|
|
749
|
+
encoding: "utf8",
|
|
750
|
+
timeout: 45000,
|
|
751
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
752
|
+
env: { ...process.env, REPERTOIRE_FIELD_SYNC: "0", REPERTOIRE_DEST_LOCK: "held" },
|
|
753
|
+
});
|
|
754
|
+
const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
|
|
755
|
+
const parsed = JSON.parse(line);
|
|
756
|
+
const pruned = pruneKeywordDest(root);
|
|
757
|
+
if (!parsed || typeof parsed !== "object") return { pruned };
|
|
758
|
+
return { ...parsed, pruned: pruned.removed, destCount: countCuratedSignals(destSignalsPath(root)) };
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
479
764
|
function ingestCompactFeedbackSync(root, sessionId, hookEvent, signals) {
|
|
480
765
|
if (!signals.length) return;
|
|
481
766
|
const helper = join(HOOKS_DIR, "station-memory-ingest.mjs");
|
|
@@ -501,6 +786,31 @@ function isCompactHook(extra) {
|
|
|
501
786
|
return /compact/i.test(hook);
|
|
502
787
|
}
|
|
503
788
|
|
|
789
|
+
function isCompactEventName(value) {
|
|
790
|
+
return /compact/i.test(String(value || ""));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Keep Compact boot fields after later preToolUse / HEAD rewrite. */
|
|
794
|
+
function retainCompactFields(existing, extra = {}) {
|
|
795
|
+
const incomingHook = extra && (extra.hookEvent || extra.source || extra.hook);
|
|
796
|
+
if (isCompactEventName(incomingHook) || (extra && extra.event_class)) {
|
|
797
|
+
const kept = {};
|
|
798
|
+
if (extra.hookEvent) kept.hookEvent = extra.hookEvent;
|
|
799
|
+
if (extra.conversation_id) kept.conversation_id = extra.conversation_id;
|
|
800
|
+
if (extra.generation_id) kept.generation_id = extra.generation_id;
|
|
801
|
+
if (extra.event_class) kept.event_class = extra.event_class;
|
|
802
|
+
return kept;
|
|
803
|
+
}
|
|
804
|
+
const prior = existing && typeof existing === "object" ? existing : {};
|
|
805
|
+
if (!isCompactEventName(prior.hookEvent) && !prior.event_class) return {};
|
|
806
|
+
const kept = {};
|
|
807
|
+
if (prior.hookEvent) kept.hookEvent = prior.hookEvent;
|
|
808
|
+
if (prior.conversation_id) kept.conversation_id = prior.conversation_id;
|
|
809
|
+
if (prior.generation_id) kept.generation_id = prior.generation_id;
|
|
810
|
+
if (prior.event_class) kept.event_class = prior.event_class;
|
|
811
|
+
return kept;
|
|
812
|
+
}
|
|
813
|
+
|
|
504
814
|
function formatWorkingLine(working) {
|
|
505
815
|
if (!working || typeof working !== "object") return null;
|
|
506
816
|
const matched = stationSafeSignals(working.matchedSignals);
|
|
@@ -537,8 +847,23 @@ function buildRepertoireResume(root) {
|
|
|
537
847
|
function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
538
848
|
const mr = readMemoryRoutingConfig(root);
|
|
539
849
|
const memoryOff = isExplicitMemoryRoutingOptOut(mr);
|
|
540
|
-
const
|
|
541
|
-
|
|
850
|
+
const live = memoryOff
|
|
851
|
+
? { hydrate: { dest: null, added: 0, destCount: 0 }, captured: null, grow: null }
|
|
852
|
+
: withDestLock(root, () => {
|
|
853
|
+
const hydrateInner = hydrateDestOnWake(root);
|
|
854
|
+
const capturedInner = maybeCaptureSessionOnHeadMove(root);
|
|
855
|
+
const growInner = growDestOnWake(root);
|
|
856
|
+
return { hydrate: hydrateInner, captured: capturedInner, grow: growInner };
|
|
857
|
+
});
|
|
858
|
+
const hydrate = live.hydrate;
|
|
859
|
+
const captured = live.captured;
|
|
860
|
+
const grow = live.grow;
|
|
861
|
+
const destCountAfterGrow =
|
|
862
|
+
grow && typeof grow.after === "number"
|
|
863
|
+
? grow.after
|
|
864
|
+
: existsSync(destSignalsPath(root))
|
|
865
|
+
? countCuratedSignals(destSignalsPath(root)) || 0
|
|
866
|
+
: hydrate.destCount;
|
|
542
867
|
const pickup = readNotesPickup(root);
|
|
543
868
|
const approaches = readLatestSessionApproaches(root);
|
|
544
869
|
const prevHost = typeof existing.host === "string" ? existing.host : null;
|
|
@@ -551,14 +876,12 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
551
876
|
? existing.hotSwap
|
|
552
877
|
: null;
|
|
553
878
|
const hotSwap = nextSwap || keptSwap;
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const intent = incomingIntent || (typeof existing.intent === "string" ? existing.intent : null);
|
|
879
|
+
const resolved = resolveHeatIntent(root, extra, existing);
|
|
880
|
+
const intent = resolved.intent;
|
|
881
|
+
const rematch = resolved.rematch;
|
|
558
882
|
const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
|
|
559
|
-
const rematch = Boolean(incomingIntent);
|
|
560
883
|
const git = readGitBrief(root);
|
|
561
|
-
const planLine =
|
|
884
|
+
const planLine = resolveHeatPlan(root, extra, existing);
|
|
562
885
|
const repertoireResume =
|
|
563
886
|
typeof extra.repertoireResume === "string" ? extra.repertoireResume : buildRepertoireResume(root);
|
|
564
887
|
const swapBit = hotSwap ? `hot-swap ${hotSwap.from} → ${hotSwap.to}` : `host ${host}`;
|
|
@@ -567,10 +890,13 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
567
890
|
const planBit = planLine ? `plan: ${planLine}` : "plan: (none)";
|
|
568
891
|
let matchedSignals = [];
|
|
569
892
|
if (Array.isArray(extra.matchedSignals) && extra.matchedSignals.length) {
|
|
570
|
-
matchedSignals =
|
|
893
|
+
matchedSignals = preferLawHits(extra.matchedSignals, 8);
|
|
571
894
|
}
|
|
572
895
|
const priorWorking = readRepertoireWorking(root);
|
|
573
|
-
const destCount =
|
|
896
|
+
const destCount =
|
|
897
|
+
destCountAfterGrow ||
|
|
898
|
+
hydrate.destCount ||
|
|
899
|
+
(existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
|
|
574
900
|
if (!memoryOff && !matchedSignals.length && matchText) {
|
|
575
901
|
if (
|
|
576
902
|
!rematch &&
|
|
@@ -580,10 +906,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
580
906
|
Array.isArray(priorWorking.matchedSignals) &&
|
|
581
907
|
priorWorking.matchedSignals.length
|
|
582
908
|
) {
|
|
583
|
-
matchedSignals =
|
|
909
|
+
matchedSignals = preferLawHits(priorWorking.matchedSignals, 8);
|
|
584
910
|
} else {
|
|
585
|
-
matchedSignals =
|
|
586
|
-
matchStationSignalsSync(root, rematch &&
|
|
911
|
+
matchedSignals = preferLawHits(
|
|
912
|
+
matchStationSignalsSync(root, rematch && intent ? intent : matchText),
|
|
587
913
|
8,
|
|
588
914
|
);
|
|
589
915
|
}
|
|
@@ -591,23 +917,33 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
591
917
|
if (!matchedSignals.length && priorWorking) {
|
|
592
918
|
matchedSignals = stationSafeSignals(priorWorking.matchedSignals);
|
|
593
919
|
}
|
|
920
|
+
const compactHold = retainCompactFields(existing, extra);
|
|
921
|
+
const nextHook = compactHold.hookEvent || extra.hookEvent || extra.source || null;
|
|
594
922
|
const workingSnapshot = {
|
|
595
923
|
host,
|
|
596
924
|
intent,
|
|
597
925
|
git,
|
|
598
926
|
hotSwap,
|
|
599
|
-
hookEvent: extra.hookEvent || extra.source || null,
|
|
600
927
|
memoryRouting: repertoireResume.startsWith("Repertoire: on") ? "on" : "off",
|
|
601
928
|
repertoireResume,
|
|
602
929
|
destCount,
|
|
603
930
|
};
|
|
931
|
+
if (nextHook) workingSnapshot.hookEvent = nextHook;
|
|
604
932
|
if (pickup) workingSnapshot.pickup = pickup;
|
|
605
933
|
if (matchText) workingSnapshot.matchText = matchText;
|
|
606
934
|
if (captured) workingSnapshot.sessionCapture = captured;
|
|
935
|
+
if (grow && grow.skipped) workingSnapshot.grow = grow.skipped;
|
|
936
|
+
if (grow && typeof grow.after === "number") {
|
|
937
|
+
workingSnapshot.grow = {
|
|
938
|
+
before: grow.before,
|
|
939
|
+
after: grow.after,
|
|
940
|
+
imported: grow.imported,
|
|
941
|
+
observed: Array.isArray(grow.observed) ? grow.observed.length : 0,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
607
944
|
if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
|
|
608
945
|
const opProcNames = readOpProcNames(root);
|
|
609
946
|
if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
|
|
610
|
-
const working = persistRepertoireWorking(root, workingSnapshot);
|
|
611
947
|
const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
|
|
612
948
|
if (
|
|
613
949
|
!memoryOff &&
|
|
@@ -621,6 +957,11 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
621
957
|
matchedSignals,
|
|
622
958
|
);
|
|
623
959
|
}
|
|
960
|
+
if (!memoryOff) {
|
|
961
|
+
const latePrune = pruneKeywordDest(root);
|
|
962
|
+
if (latePrune.kept) workingSnapshot.destCount = latePrune.kept;
|
|
963
|
+
}
|
|
964
|
+
const working = persistRepertoireWorking(root, workingSnapshot);
|
|
624
965
|
const workingLine = formatWorkingLine(working);
|
|
625
966
|
const workingBit = workingLine ? workingLine : "working: (none)";
|
|
626
967
|
const stationLine = `${swapBit}. ${intentBit}. ${planBit}. ${gitBit}. ${repertoireResume}. ${workingBit}`;
|
|
@@ -633,6 +974,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
633
974
|
repertoireResume,
|
|
634
975
|
workingLine,
|
|
635
976
|
stationLine,
|
|
977
|
+
...compactHold,
|
|
636
978
|
};
|
|
637
979
|
}
|
|
638
980
|
|
|
@@ -666,6 +1008,28 @@ function stripHoldNpm(line) {
|
|
|
666
1008
|
.replace(/[ \t]+$/g, "");
|
|
667
1009
|
}
|
|
668
1010
|
|
|
1011
|
+
/** Same-session skip must still rewrite when HEAD, organ count, or Hold npm diverges. */
|
|
1012
|
+
function stationBootNeedsRefresh(existing, root, host) {
|
|
1013
|
+
if (!existing || typeof existing !== "object") return true;
|
|
1014
|
+
if (host && existing.host && existing.host !== host) return true;
|
|
1015
|
+
if (!existing.suit_profile) return true;
|
|
1016
|
+
if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
|
|
1017
|
+
if (!existing.stationLine) return true;
|
|
1018
|
+
const liveGit = readGitBrief(root);
|
|
1019
|
+
const bootHead = existing.git && existing.git.head ? String(existing.git.head) : "";
|
|
1020
|
+
if (liveGit && liveGit.head && bootHead !== liveGit.head) return true;
|
|
1021
|
+
const liveResume = buildRepertoireResume(root);
|
|
1022
|
+
if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
|
|
1023
|
+
if (stationDurableHoldsNpm(root)) return true;
|
|
1024
|
+
const cardIntent = readStationTicketField(root, "Intent");
|
|
1025
|
+
const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
|
|
1026
|
+
if (cardIntent && bootIntent && cardIntent !== bootIntent) return true;
|
|
1027
|
+
const cardPlan = readStationTicketField(root, "Plan");
|
|
1028
|
+
const bootPlan = typeof existing.planLine === "string" ? clipIntent(existing.planLine) : null;
|
|
1029
|
+
if (cardPlan && bootPlan && cardPlan !== bootPlan) return true;
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
669
1033
|
function stationDurableHoldsNpm(root) {
|
|
670
1034
|
return readExistingStationMarkdown(root)
|
|
671
1035
|
.split(/\r?\n/)
|
|
@@ -793,14 +1157,29 @@ module.exports = {
|
|
|
793
1157
|
readRepertoireWorking,
|
|
794
1158
|
readOpProcNames,
|
|
795
1159
|
hydrateDestOnWake,
|
|
1160
|
+
growDestOnWake,
|
|
1161
|
+
heatLiveMemory,
|
|
1162
|
+
withDestLock,
|
|
1163
|
+
slugPrimitiveName,
|
|
1164
|
+
patternsFromGit,
|
|
796
1165
|
readNotesPickup,
|
|
797
1166
|
maybeCaptureSessionOnHeadMove,
|
|
798
1167
|
formatWorkingLine,
|
|
799
1168
|
applyStationHeat,
|
|
1169
|
+
isStockTicket,
|
|
1170
|
+
readStationTicketField,
|
|
1171
|
+
resolveHeatIntent,
|
|
1172
|
+
resolveHeatPlan,
|
|
800
1173
|
extractPreservedStationLines,
|
|
801
1174
|
mergeStationMarkdown,
|
|
802
1175
|
formatStationMarkdown,
|
|
803
1176
|
writeStationMarkdown,
|
|
804
1177
|
isHoldNpmLine,
|
|
805
1178
|
stationDurableHoldsNpm,
|
|
1179
|
+
stationBootNeedsRefresh,
|
|
1180
|
+
isKeywordDestName,
|
|
1181
|
+
pruneKeywordDest,
|
|
1182
|
+
preferLawHits,
|
|
1183
|
+
retainCompactFields,
|
|
1184
|
+
destLawNameSet,
|
|
806
1185
|
};
|
|
@@ -20,13 +20,28 @@ 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;
|
|
26
31
|
export const applyStationHeat = impl.applyStationHeat;
|
|
32
|
+
export const isStockTicket = impl.isStockTicket;
|
|
33
|
+
export const readStationTicketField = impl.readStationTicketField;
|
|
34
|
+
export const resolveHeatIntent = impl.resolveHeatIntent;
|
|
35
|
+
export const resolveHeatPlan = impl.resolveHeatPlan;
|
|
27
36
|
export const extractPreservedStationLines = impl.extractPreservedStationLines;
|
|
28
37
|
export const mergeStationMarkdown = impl.mergeStationMarkdown;
|
|
29
38
|
export const formatStationMarkdown = impl.formatStationMarkdown;
|
|
30
39
|
export const writeStationMarkdown = impl.writeStationMarkdown;
|
|
31
40
|
export const isHoldNpmLine = impl.isHoldNpmLine;
|
|
32
41
|
export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
|
|
42
|
+
export const stationBootNeedsRefresh = impl.stationBootNeedsRefresh;
|
|
43
|
+
export const isKeywordDestName = impl.isKeywordDestName;
|
|
44
|
+
export const pruneKeywordDest = impl.pruneKeywordDest;
|
|
45
|
+
export const preferLawHits = impl.preferLawHits;
|
|
46
|
+
export const retainCompactFields = impl.retainCompactFields;
|
|
47
|
+
export const destLawNameSet = impl.destLawNameSet;
|