0xray 4.0.20 → 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 +16 -8
- 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.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 +93 -12
- 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 +9 -0
- package/dist/integrations/cursor/hooks/pre-compact.sh +3 -0
- package/dist/integrations/cursor/hooks/pre-tool-use.js +16 -12
- 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 +3 -2
- package/dist/integrations/hooks/station-hook-runtime.cjs +222 -70
- package/dist/integrations/hooks/station-hook-runtime.mjs +9 -0
- package/dist/integrations/hooks/station-memory-ingest.mjs +1 -2
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/nucleus/suit-temperament.d.ts +0 -5
- package/dist/nucleus/suit-temperament.js +13 -1
- 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,6 +1,7 @@
|
|
|
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
|
*/
|
|
@@ -104,6 +105,63 @@ function readPlanLine(root) {
|
|
|
104
105
|
return readGitSubject(root);
|
|
105
106
|
}
|
|
106
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
|
+
|
|
107
165
|
function resolveRepertoireProviderModule(root) {
|
|
108
166
|
const mr = readMemoryRoutingConfig(root);
|
|
109
167
|
if (mr && typeof mr.module_path === "string" && mr.module_path) {
|
|
@@ -200,32 +258,44 @@ function readRepertoireWorking(root) {
|
|
|
200
258
|
}
|
|
201
259
|
}
|
|
202
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
|
+
|
|
203
276
|
function stationSafeSignals(names) {
|
|
204
277
|
if (!Array.isArray(names)) return [];
|
|
205
278
|
const out = [];
|
|
206
279
|
for (const raw of names) {
|
|
207
280
|
const name = String(raw || "").trim();
|
|
208
|
-
if (!name) continue;
|
|
209
|
-
if (name.toLowerCase().startsWith("bedrock-")) continue;
|
|
281
|
+
if (!name || isKeywordDestName(name)) continue;
|
|
210
282
|
out.push(name);
|
|
211
283
|
if (out.length >= 4) break;
|
|
212
284
|
}
|
|
213
285
|
return out;
|
|
214
286
|
}
|
|
215
287
|
|
|
216
|
-
/**
|
|
217
|
-
function
|
|
288
|
+
/** Laws only. Hangar repo-* and git-slug keywords stay off the card. */
|
|
289
|
+
function preferLawHits(names, cap = 8) {
|
|
218
290
|
if (!Array.isArray(names)) return [];
|
|
219
291
|
const unique = [];
|
|
220
292
|
for (const raw of names) {
|
|
221
293
|
const name = String(raw || "").trim();
|
|
222
|
-
if (!name || name
|
|
294
|
+
if (!name || isKeywordDestName(name)) continue;
|
|
223
295
|
if (unique.includes(name)) continue;
|
|
224
296
|
unique.push(name);
|
|
225
297
|
}
|
|
226
|
-
|
|
227
|
-
const rest = unique.filter((name) => !name.startsWith("repo-"));
|
|
228
|
-
return [...subject, ...rest].slice(0, cap);
|
|
298
|
+
return unique.slice(0, cap);
|
|
229
299
|
}
|
|
230
300
|
|
|
231
301
|
function destSignalsPath(root) {
|
|
@@ -279,6 +349,52 @@ function signalNameSet(filePath) {
|
|
|
279
349
|
return new Set(readSignalRecords(filePath).map((signal) => String(signal.name).trim()));
|
|
280
350
|
}
|
|
281
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
|
+
|
|
282
398
|
function mergeMissingSignals(destPath, incoming) {
|
|
283
399
|
if (!incoming.length || !existsSync(destPath)) return 0;
|
|
284
400
|
let data;
|
|
@@ -308,7 +424,7 @@ function mergeMissingSignals(destPath, incoming) {
|
|
|
308
424
|
return added;
|
|
309
425
|
}
|
|
310
426
|
|
|
311
|
-
/** Hydrate project dest from seed + stack
|
|
427
|
+
/** Hydrate project dest from seed + stack. Subject repo-* stay off dest. */
|
|
312
428
|
function hydrateDestOnWake(root) {
|
|
313
429
|
const dest = destSignalsPath(root);
|
|
314
430
|
const seed = repertoireDataFile(root, "curated_signals.json");
|
|
@@ -317,10 +433,12 @@ function hydrateDestOnWake(root) {
|
|
|
317
433
|
copyFileSync(seed, dest);
|
|
318
434
|
}
|
|
319
435
|
if (!existsSync(dest)) return { dest: null, added: 0, destCount: 0 };
|
|
320
|
-
const added =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 };
|
|
324
442
|
}
|
|
325
443
|
|
|
326
444
|
function readNotesPickup(root) {
|
|
@@ -405,7 +523,12 @@ function sleepMs(ms) {
|
|
|
405
523
|
/** Parallel live contexts share one project copy. Exclusive create; stale after 60s. */
|
|
406
524
|
function withDestLock(root, fn) {
|
|
407
525
|
const lockPath = destLockPath(root);
|
|
408
|
-
|
|
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
|
+
}
|
|
409
532
|
const started = Date.now();
|
|
410
533
|
while (Date.now() - started < 50000) {
|
|
411
534
|
try {
|
|
@@ -466,42 +589,18 @@ function stripConventionalSubject(message) {
|
|
|
466
589
|
.trim();
|
|
467
590
|
}
|
|
468
591
|
|
|
469
|
-
/**
|
|
470
|
-
function patternsFromGit(root, commits
|
|
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();
|
|
471
598
|
const patterns = [];
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
}
|
|
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" });
|
|
502
603
|
}
|
|
503
|
-
} catch {
|
|
504
|
-
/* structural names are extra — commit slugs still grow dest */
|
|
505
604
|
}
|
|
506
605
|
return patterns;
|
|
507
606
|
}
|
|
@@ -521,16 +620,22 @@ function latestSessionNewerThanGrow(root) {
|
|
|
521
620
|
|
|
522
621
|
/** Capture then grow. Skip-path for floors that already have a live card. */
|
|
523
622
|
function heatLiveMemory(root) {
|
|
524
|
-
|
|
525
|
-
|
|
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 */
|
|
526
637
|
return { captured: null, grow: null };
|
|
527
638
|
}
|
|
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
639
|
}
|
|
535
640
|
|
|
536
641
|
/** Every floor that heats. Dedup per HEAD. Patterns come from git, not a leftover catalog. */
|
|
@@ -648,7 +753,9 @@ function growDestOnWake(root) {
|
|
|
648
753
|
});
|
|
649
754
|
const line = String(out).trim().split("\n").filter(Boolean).at(-1) || "{}";
|
|
650
755
|
const parsed = JSON.parse(line);
|
|
651
|
-
|
|
756
|
+
const pruned = pruneKeywordDest(root);
|
|
757
|
+
if (!parsed || typeof parsed !== "object") return { pruned };
|
|
758
|
+
return { ...parsed, pruned: pruned.removed, destCount: countCuratedSignals(destSignalsPath(root)) };
|
|
652
759
|
} catch {
|
|
653
760
|
return null;
|
|
654
761
|
}
|
|
@@ -679,6 +786,31 @@ function isCompactHook(extra) {
|
|
|
679
786
|
return /compact/i.test(hook);
|
|
680
787
|
}
|
|
681
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
|
+
|
|
682
814
|
function formatWorkingLine(working) {
|
|
683
815
|
if (!working || typeof working !== "object") return null;
|
|
684
816
|
const matched = stationSafeSignals(working.matchedSignals);
|
|
@@ -744,14 +876,12 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
744
876
|
? existing.hotSwap
|
|
745
877
|
: null;
|
|
746
878
|
const hotSwap = nextSwap || keptSwap;
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
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;
|
|
751
882
|
const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
|
|
752
|
-
const rematch = Boolean(incomingIntent);
|
|
753
883
|
const git = readGitBrief(root);
|
|
754
|
-
const planLine =
|
|
884
|
+
const planLine = resolveHeatPlan(root, extra, existing);
|
|
755
885
|
const repertoireResume =
|
|
756
886
|
typeof extra.repertoireResume === "string" ? extra.repertoireResume : buildRepertoireResume(root);
|
|
757
887
|
const swapBit = hotSwap ? `hot-swap ${hotSwap.from} → ${hotSwap.to}` : `host ${host}`;
|
|
@@ -760,7 +890,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
760
890
|
const planBit = planLine ? `plan: ${planLine}` : "plan: (none)";
|
|
761
891
|
let matchedSignals = [];
|
|
762
892
|
if (Array.isArray(extra.matchedSignals) && extra.matchedSignals.length) {
|
|
763
|
-
matchedSignals =
|
|
893
|
+
matchedSignals = preferLawHits(extra.matchedSignals, 8);
|
|
764
894
|
}
|
|
765
895
|
const priorWorking = readRepertoireWorking(root);
|
|
766
896
|
const destCount =
|
|
@@ -776,10 +906,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
776
906
|
Array.isArray(priorWorking.matchedSignals) &&
|
|
777
907
|
priorWorking.matchedSignals.length
|
|
778
908
|
) {
|
|
779
|
-
matchedSignals =
|
|
909
|
+
matchedSignals = preferLawHits(priorWorking.matchedSignals, 8);
|
|
780
910
|
} else {
|
|
781
|
-
matchedSignals =
|
|
782
|
-
matchStationSignalsSync(root, rematch &&
|
|
911
|
+
matchedSignals = preferLawHits(
|
|
912
|
+
matchStationSignalsSync(root, rematch && intent ? intent : matchText),
|
|
783
913
|
8,
|
|
784
914
|
);
|
|
785
915
|
}
|
|
@@ -787,16 +917,18 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
787
917
|
if (!matchedSignals.length && priorWorking) {
|
|
788
918
|
matchedSignals = stationSafeSignals(priorWorking.matchedSignals);
|
|
789
919
|
}
|
|
920
|
+
const compactHold = retainCompactFields(existing, extra);
|
|
921
|
+
const nextHook = compactHold.hookEvent || extra.hookEvent || extra.source || null;
|
|
790
922
|
const workingSnapshot = {
|
|
791
923
|
host,
|
|
792
924
|
intent,
|
|
793
925
|
git,
|
|
794
926
|
hotSwap,
|
|
795
|
-
hookEvent: extra.hookEvent || extra.source || null,
|
|
796
927
|
memoryRouting: repertoireResume.startsWith("Repertoire: on") ? "on" : "off",
|
|
797
928
|
repertoireResume,
|
|
798
929
|
destCount,
|
|
799
930
|
};
|
|
931
|
+
if (nextHook) workingSnapshot.hookEvent = nextHook;
|
|
800
932
|
if (pickup) workingSnapshot.pickup = pickup;
|
|
801
933
|
if (matchText) workingSnapshot.matchText = matchText;
|
|
802
934
|
if (captured) workingSnapshot.sessionCapture = captured;
|
|
@@ -812,7 +944,6 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
812
944
|
if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
|
|
813
945
|
const opProcNames = readOpProcNames(root);
|
|
814
946
|
if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
|
|
815
|
-
const working = persistRepertoireWorking(root, workingSnapshot);
|
|
816
947
|
const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
|
|
817
948
|
if (
|
|
818
949
|
!memoryOff &&
|
|
@@ -826,6 +957,11 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
826
957
|
matchedSignals,
|
|
827
958
|
);
|
|
828
959
|
}
|
|
960
|
+
if (!memoryOff) {
|
|
961
|
+
const latePrune = pruneKeywordDest(root);
|
|
962
|
+
if (latePrune.kept) workingSnapshot.destCount = latePrune.kept;
|
|
963
|
+
}
|
|
964
|
+
const working = persistRepertoireWorking(root, workingSnapshot);
|
|
829
965
|
const workingLine = formatWorkingLine(working);
|
|
830
966
|
const workingBit = workingLine ? workingLine : "working: (none)";
|
|
831
967
|
const stationLine = `${swapBit}. ${intentBit}. ${planBit}. ${gitBit}. ${repertoireResume}. ${workingBit}`;
|
|
@@ -838,6 +974,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
|
|
|
838
974
|
repertoireResume,
|
|
839
975
|
workingLine,
|
|
840
976
|
stationLine,
|
|
977
|
+
...compactHold,
|
|
841
978
|
};
|
|
842
979
|
}
|
|
843
980
|
|
|
@@ -884,6 +1021,12 @@ function stationBootNeedsRefresh(existing, root, host) {
|
|
|
884
1021
|
const liveResume = buildRepertoireResume(root);
|
|
885
1022
|
if (liveResume && existing.repertoireResume && liveResume !== existing.repertoireResume) return true;
|
|
886
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;
|
|
887
1030
|
return false;
|
|
888
1031
|
}
|
|
889
1032
|
|
|
@@ -1023,6 +1166,10 @@ module.exports = {
|
|
|
1023
1166
|
maybeCaptureSessionOnHeadMove,
|
|
1024
1167
|
formatWorkingLine,
|
|
1025
1168
|
applyStationHeat,
|
|
1169
|
+
isStockTicket,
|
|
1170
|
+
readStationTicketField,
|
|
1171
|
+
resolveHeatIntent,
|
|
1172
|
+
resolveHeatPlan,
|
|
1026
1173
|
extractPreservedStationLines,
|
|
1027
1174
|
mergeStationMarkdown,
|
|
1028
1175
|
formatStationMarkdown,
|
|
@@ -1030,4 +1177,9 @@ module.exports = {
|
|
|
1030
1177
|
isHoldNpmLine,
|
|
1031
1178
|
stationDurableHoldsNpm,
|
|
1032
1179
|
stationBootNeedsRefresh,
|
|
1180
|
+
isKeywordDestName,
|
|
1181
|
+
pruneKeywordDest,
|
|
1182
|
+
preferLawHits,
|
|
1183
|
+
retainCompactFields,
|
|
1184
|
+
destLawNameSet,
|
|
1033
1185
|
};
|
|
@@ -29,6 +29,10 @@ export const readNotesPickup = impl.readNotesPickup;
|
|
|
29
29
|
export const maybeCaptureSessionOnHeadMove = impl.maybeCaptureSessionOnHeadMove;
|
|
30
30
|
export const formatWorkingLine = impl.formatWorkingLine;
|
|
31
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;
|
|
32
36
|
export const extractPreservedStationLines = impl.extractPreservedStationLines;
|
|
33
37
|
export const mergeStationMarkdown = impl.mergeStationMarkdown;
|
|
34
38
|
export const formatStationMarkdown = impl.formatStationMarkdown;
|
|
@@ -36,3 +40,8 @@ export const writeStationMarkdown = impl.writeStationMarkdown;
|
|
|
36
40
|
export const isHoldNpmLine = impl.isHoldNpmLine;
|
|
37
41
|
export const stationDurableHoldsNpm = impl.stationDurableHoldsNpm;
|
|
38
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;
|
|
@@ -118,7 +118,6 @@ async function growDest(projectRoot) {
|
|
|
118
118
|
ensureDestShape(destSignals);
|
|
119
119
|
const before = service.signalsManager.load().signals.length;
|
|
120
120
|
const sessions = service.syncXrayMemory();
|
|
121
|
-
const siblings = service.syncWorkspaceRepos();
|
|
122
121
|
const kernel =
|
|
123
122
|
typeof pathsMod.collectKernelDiaryText === 'function'
|
|
124
123
|
? pathsMod.collectKernelDiaryText(projectRoot)
|
|
@@ -134,7 +133,7 @@ async function growDest(projectRoot) {
|
|
|
134
133
|
before,
|
|
135
134
|
after,
|
|
136
135
|
imported: sessions.imported,
|
|
137
|
-
observed:
|
|
136
|
+
observed: 0,
|
|
138
137
|
heated: diary.heated,
|
|
139
138
|
})}\n`,
|
|
140
139
|
);
|
|
@@ -27,11 +27,6 @@ export declare function resolveSuitProfile(raw: SuitTemperamentConfig | undefine
|
|
|
27
27
|
* A Hermes/OpenCode host ignores a leftover Grok boot (and vice versa).
|
|
28
28
|
*/
|
|
29
29
|
export declare function resolveRuntimeSuitProfile(projectRoot: string, host?: SuitHost): SuitProfile;
|
|
30
|
-
/**
|
|
31
|
-
* Write station heat when this host/session has no live card yet.
|
|
32
|
-
* OpenClaw has no SessionStart — PreToolUse is the session boundary.
|
|
33
|
-
* Install/init cards without a session id are not a live session.
|
|
34
|
-
*/
|
|
35
30
|
export declare function maybeHeatHostStation(projectRoot: string, host: SuitHost, extra?: Record<string, unknown>): string | null;
|
|
36
31
|
/** Merge temperament fields into session-boot.json for the host that just started. */
|
|
37
32
|
export declare function writeSuitSessionBoot(projectRoot: string, host: SuitHost, extra?: Record<string, unknown>): string;
|
|
@@ -95,7 +95,13 @@ function concreteSessionId(value) {
|
|
|
95
95
|
* OpenClaw has no SessionStart — PreToolUse is the session boundary.
|
|
96
96
|
* Install/init cards without a session id are not a live session.
|
|
97
97
|
*/
|
|
98
|
+
function isCursorWorkspaceWrapper(projectRoot) {
|
|
99
|
+
return (fs.existsSync(path.join(projectRoot, 'repos', 'xray')) ||
|
|
100
|
+
fs.existsSync(path.join(projectRoot, 'repos', 'repertoire')));
|
|
101
|
+
}
|
|
98
102
|
export function maybeHeatHostStation(projectRoot, host, extra = {}) {
|
|
103
|
+
if (isCursorWorkspaceWrapper(projectRoot))
|
|
104
|
+
return null;
|
|
99
105
|
const bootPath = path.join(projectRoot, '.xray', 'state', 'session-boot.json');
|
|
100
106
|
const cardPath = path.join(projectRoot, '.xray', 'state', 'STATION.md');
|
|
101
107
|
let existing = {};
|
|
@@ -124,8 +130,14 @@ export function maybeHeatHostStation(projectRoot, host, extra = {}) {
|
|
|
124
130
|
export function writeSuitSessionBoot(projectRoot, host, extra = {}) {
|
|
125
131
|
const profile = resolveSuitProfile(loadSuitTemperamentRaw(projectRoot), host);
|
|
126
132
|
const stateDir = path.join(projectRoot, '.xray', 'state');
|
|
127
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
128
133
|
const bootPath = path.join(stateDir, 'session-boot.json');
|
|
134
|
+
try {
|
|
135
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* Cloud workspace wrapper is often unwritable — fail open */
|
|
139
|
+
return bootPath;
|
|
140
|
+
}
|
|
129
141
|
let existing = {};
|
|
130
142
|
if (fs.existsSync(bootPath)) {
|
|
131
143
|
try {
|
|
@@ -76,5 +76,5 @@ The seven rules are the engine. The cadence is the time signature. Disk SSOT: `g
|
|
|
76
76
|
- **Close:** loop until Station is done. Compact: Station survives the cut. Overlay dest names are OP-PROC — reload `repertoire-working.json` `opProcNames`, not Station.
|
|
77
77
|
- **Board:** critic Strict comments; forge implements; lead stays main thread.
|
|
78
78
|
- **Codex 69:** rewire this skill and existing ops. Do not add a new `SKILL.md` to hold the cadence.
|
|
79
|
-
- **Ship matrix:** `LEAD-CADENCE.md` Decision matrix — A PR+CI · B pack · C `release:docs-check` · D `foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first.
|
|
79
|
+
- **Ship matrix:** `LEAD-CADENCE.md` Decision matrix — A PR+CI · B pack · C `release:docs-check` (`reconcile --check`; no patch stamps on guides) · D `foundry release --i-mean-it`. Green CI is not ship. Spend / credentials stay Ask-first. Patch number lives in `package.json` + CHANGELOG.
|
|
80
80
|
- **Metamorphosis:** LLM → OP-PROC is in-context worn seats (same critic, same dummy, resume only). Not more bots.
|
package/llms.txt
CHANGED
|
@@ -35,7 +35,7 @@ Registered in `.mcp.json` as `npx -y 0xray mcp <cmd>`:
|
|
|
35
35
|
| `xray-code-review` | Proposal quality |
|
|
36
36
|
| `xray-architect-tools` | System design |
|
|
37
37
|
|
|
38
|
-
Not an 8th `xray-*` server. Repertoire is
|
|
38
|
+
Not an 8th `xray-*` server. **Repertoire is preferred** — vendored 0.2.5 ships on (`npx @0xray/repertoire mcp`). Dest is named laws, not hangar `repo-*` or git slugs. Survive compact is **Station** (Read `.xray/state/STATION.md`). Station is the ticket, not a substitute for Repertoire. Do not pin 0.1.8.
|
|
39
39
|
|
|
40
40
|
## Codex 69 (always on)
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0xray",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.21",
|
|
4
4
|
"description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"license": "MIT",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"consumer:smoke": "node scripts/node/consumer-install-smoke.mjs",
|
|
125
125
|
"pack:tmp-proof": "node scripts/node/pack-tmp-suit-proof.mjs",
|
|
126
126
|
"release:gate": "node scripts/foundry/release-gate.mjs",
|
|
127
|
-
"release:docs-check": "node scripts/foundry/validate-release-docs.mjs",
|
|
127
|
+
"release:docs-check": "node scripts/foundry/reconcile-version.mjs --check && node scripts/foundry/validate-release-docs.mjs",
|
|
128
128
|
"pre-publish-guard": "node scripts/node/pre-publish-guard.js",
|
|
129
129
|
"safe-publish": "npm run prepare-consumer && npm run build",
|
|
130
130
|
"release:npm": "node scripts/foundry/release.mjs --publish-only",
|
|
@@ -60,7 +60,7 @@ After mill plant is fastened, overlay their plant (defaults; remap in `foundry.j
|
|
|
60
60
|
|
|
61
61
|
**Mill-fill is law:** their keys/files win; mill names they did not plant stay unless they overlay the same name. JSON facets always merge. Skill overlay is project dirs on every TUI floor (OpenCode, Grok, Hermes, OpenClaw). YML agents stay OpenCode. CLI re-wear calls mint **once** after mill copies. Isolated HOME is passwd home, not `os.homedir()`. CI report is `.xray/foundry-ci-report.json` (`.opencode/logs` shim).
|
|
62
62
|
|
|
63
|
-
**Default mill plant:** `mill` + `inspect` (inspect AI work). Fasten is **foundry-plant/0**: builtin plant is mill+inspect only (files from this package `plant/`). Fastens a suit, not an empty one. Not 45/42 costume. `foundry.json` `"costume": true` copies that dump.
|
|
63
|
+
**Default mill plant:** `mill` + `inspect` (inspect AI work). Fasten is **foundry-plant/0**: builtin plant is mill+inspect only (files from this package `plant/`). Fastens a suit, not an empty one. Not 45/42 costume. `foundry.json` `"costume": true` copies that dump. **Repertoire is preferred** (vendored, dest = laws). PPE stays worn.
|
|
64
64
|
|
|
65
65
|
**foundry-plant/0:** `foundry.json` `"plant": "@0xray/blip"` (aliases `"plant": "blip"` / `"plant": "sound"`) fastens whatever that mill package declares and ships in its own `plant/`. Organs (looker, vibe, mixer) live in the mill. Adding an organ is a mill publish, not a foundry catalog PR. CLI shims `foundry blip|sound` still exist on foundry 0.1.11+; fasten is the new cut. Foundry **0.1.12**.
|
|
66
66
|
|
|
@@ -76,7 +76,7 @@ A friend would hear: Blips should look like Rippel living motions with sound, no
|
|
|
76
76
|
|
|
77
77
|
**Shop plant:** `shop-extract`, `shop-witness`, `shop-pin` are first-class with mill plant. Inspect does not treat those worn names as a costume dump. `foundry.json` `"shopPlant"` names more shops (array), a plant dir (string), or `{ "skills": [...], "dir": "..." }`. This is not `"costume": true` and does not copy 45/42.
|
|
78
78
|
|
|
79
|
-
**
|
|
79
|
+
**Mill target (0xray postinstall, not this mill package):** `install-bridges.cjs` does not mill npm global prefix or npx `_npx` as a consumer. Isolated HOME skips machine `~/.grok`. Shared HOME Grok last-mile is project `.grok/plugins/0xray` (two seats do not clobber machine plugin). Nested mill ships in the 0xray tarball (foundry-plant/0; blip/sound drawers in `@0xray/blip`; inspect dest matches `resolveGrokPluginDests`).
|
|
80
80
|
|
|
81
81
|
## Publish this mill
|
|
82
82
|
|
|
@@ -24,6 +24,10 @@ export const REQUIRED_PACK_PATHS = [
|
|
|
24
24
|
"dist/integrations/cursor/hooks/after-file-edit.js",
|
|
25
25
|
"dist/integrations/cursor/hooks/cursor-hook-utils.js",
|
|
26
26
|
"dist/integrations/cursor/hooks/cursor-usage-receipt.js",
|
|
27
|
+
"dist/integrations/cursor/hooks/xray-cloud-hook.sh",
|
|
28
|
+
"dist/integrations/cursor/hooks/pre-tool-use.sh",
|
|
29
|
+
"dist/integrations/cursor/hooks/pre-compact.sh",
|
|
30
|
+
"dist/integrations/cursor/hooks/after-file-edit.sh",
|
|
27
31
|
];
|
|
28
32
|
|
|
29
33
|
export function packedFilePaths(files) {
|
|
@@ -11,7 +11,7 @@ The mill **fastens** a suit. Default mill plant is **inspect** (inspecting AI wo
|
|
|
11
11
|
|
|
12
12
|
**Mill-fill:** their keys/files win. Mill plant names they did not overlay stay. JSON always merges.
|
|
13
13
|
|
|
14
|
-
**Factory:** Repertoire
|
|
14
|
+
**Factory:** **Repertoire is preferred** (vendored 0.2.5 ships on; dest = named laws). Seven generated MCP. Constitution (PPE) stays worn. Not a fifth MCP. Not 5.0.
|
|
15
15
|
|
|
16
16
|
**Costume:** `foundry.json` `"costume": true` copies the 45/42 mill costume.
|
|
17
17
|
|