0xray 4.0.40 → 4.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/agents/api-design.yml +31 -0
  3. package/.opencode/agents/architect.yml +15 -0
  4. package/.opencode/agents/architecture-patterns.yml +14 -0
  5. package/.opencode/agents/auto-format.yml +14 -0
  6. package/.opencode/agents/backend-engineer.yml +12 -0
  7. package/.opencode/agents/boot-orchestrator.yml +14 -0
  8. package/.opencode/agents/bug-triage-specialist.yml +15 -0
  9. package/.opencode/agents/code-analyzer.yml +12 -0
  10. package/.opencode/agents/code-reviewer.yml +14 -0
  11. package/.opencode/agents/content-creator.yml +12 -0
  12. package/.opencode/agents/database-engineer.yml +12 -0
  13. package/.opencode/agents/devops-engineer.yml +12 -0
  14. package/.opencode/agents/framework-compliance-audit.yml +14 -0
  15. package/.opencode/agents/frontend-engineer.yml +12 -0
  16. package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  17. package/.opencode/agents/git-workflow.yml +15 -0
  18. package/.opencode/agents/growth-strategist.yml +12 -0
  19. package/.opencode/agents/hermes-agent.yml +12 -0
  20. package/.opencode/agents/inference-improve.yml +12 -0
  21. package/.opencode/agents/lint.yml +14 -0
  22. package/.opencode/agents/log-monitor.yml +12 -0
  23. package/.opencode/agents/mobile-developer.yml +12 -0
  24. package/.opencode/agents/model-health-check.yml +12 -0
  25. package/.opencode/agents/multimodal-looker.yml +12 -0
  26. package/.opencode/agents/performance-analysis.yml +12 -0
  27. package/.opencode/agents/performance-engineer.yml +12 -0
  28. package/.opencode/agents/performance-optimization.yml +12 -0
  29. package/.opencode/agents/processor-pipeline.yml +14 -0
  30. package/.opencode/agents/project-analysis.yml +12 -0
  31. package/.opencode/agents/refactorer.yml +12 -0
  32. package/.opencode/agents/researcher.yml +12 -0
  33. package/.opencode/agents/security-auditor.yml +12 -0
  34. package/.opencode/agents/security-scan.yml +12 -0
  35. package/.opencode/agents/seo-consultant.yml +12 -0
  36. package/.opencode/agents/session-management.yml +12 -0
  37. package/.opencode/agents/state-manager.yml +12 -0
  38. package/.opencode/agents/storyteller.yml +12 -0
  39. package/.opencode/agents/strategist.yml +12 -0
  40. package/.opencode/agents/tech-writer.yml +12 -0
  41. package/.opencode/agents/testing-best-practices.yml +12 -0
  42. package/.opencode/agents/testing-lead.yml +12 -0
  43. package/.opencode/agents/ui-ux-design.yml +12 -0
  44. package/.opencode/codex.codex +8 -0
  45. package/.opencode/commands/auto-format.md +99 -0
  46. package/.opencode/commands/auto-summary-capture.md +90 -0
  47. package/.opencode/commands/dependency-audit.md +184 -0
  48. package/.opencode/commands/enforcer-daily-scan.md +137 -0
  49. package/.opencode/commands/framework-compliance-audit.md +205 -0
  50. package/.opencode/commands/interactive-validator.md +75 -0
  51. package/.opencode/commands/job-summary-logger.md +68 -0
  52. package/.opencode/commands/lint.md +11 -0
  53. package/.opencode/commands/mode-switch.md +95 -0
  54. package/.opencode/commands/model-health-check.md +186 -0
  55. package/.opencode/commands/performance-analysis.md +144 -0
  56. package/.opencode/commands/pre-commit-introspection.md +185 -0
  57. package/.opencode/commands/pre-commit-introspection.sh +133 -0
  58. package/.opencode/commands/security-scan.md +157 -0
  59. package/.opencode/commands/sisyphus-validation.md +128 -0
  60. package/.opencode/commands/summary-logger.md +83 -0
  61. package/.opencode/enforcer-config.json +282 -0
  62. package/.opencode/workflows/post-deployment-audit.yml +123 -0
  63. package/.xray/features.json +1 -1
  64. package/dist/CHANGELOG.md +16 -0
  65. package/dist/integrations/grok/hooks/grok-hook-utils.js +21 -1
  66. package/dist/integrations/grok/hooks/session-start.js +2 -20
  67. package/dist/integrations/hooks/goggles-mcp.mjs +36 -4
  68. package/dist/integrations/hooks/goggles-pipeline.mjs +55 -15
  69. package/dist/integrations/hooks/goggles-planes.json +9 -3
  70. package/dist/integrations/hooks/plates.cjs +22 -0
  71. package/dist/integrations/hooks/session-note-index.cjs +298 -0
  72. package/dist/integrations/hooks/session-note-index.mjs +22 -0
  73. package/dist/integrations/hooks/sleeve.cjs +335 -0
  74. package/dist/integrations/hooks/sleeve.mjs +18 -0
  75. package/dist/integrations/hooks/station-hook-runtime.cjs +205 -9
  76. package/dist/integrations/hooks/wake-cascade.cjs +102 -0
  77. package/dist/integrations/hooks/wake-cascade.mjs +16 -0
  78. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  79. package/dist/memory-routing/plates.d.ts +1 -1
  80. package/dist/memory-routing/plates.js +1 -0
  81. package/docs-site/docs/plates/activity-log.md +1 -1
  82. package/docs-site/docs/plates/boot.md +2 -0
  83. package/docs-site/docs/plates/centrifuge.md +30 -0
  84. package/docs-site/docs/plates/cursor-compact.md +1 -1
  85. package/docs-site/docs/plates/glossary.md +1 -2
  86. package/docs-site/docs/plates/goggles.md +4 -0
  87. package/docs-site/docs/plates/governance.md +2 -0
  88. package/docs-site/docs/plates/grok-compact.md +1 -1
  89. package/docs-site/docs/plates/grokbot.md +1 -1
  90. package/docs-site/docs/plates/host-pack.md +2 -0
  91. package/docs-site/docs/plates/house.md +1 -1
  92. package/docs-site/docs/plates/index.md +3 -1
  93. package/docs-site/docs/plates/inference-files.md +1 -1
  94. package/docs-site/docs/plates/kept-line.md +1 -1
  95. package/docs-site/docs/plates/kits.md +2 -0
  96. package/docs-site/docs/plates/lens-gate.md +1 -1
  97. package/docs-site/docs/plates/lens-page.md +1 -1
  98. package/docs-site/docs/plates/live-mesh.md +1 -1
  99. package/docs-site/docs/plates/memory-recall.md +4 -0
  100. package/docs-site/docs/plates/notes-page.md +2 -1
  101. package/docs-site/docs/plates/orchestration.md +2 -0
  102. package/docs-site/docs/plates/package-face.md +1 -1
  103. package/docs-site/docs/plates/payload-heat.md +1 -1
  104. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  105. package/docs-site/docs/plates/pre-tool.md +1 -1
  106. package/docs-site/docs/plates/processor.md +2 -0
  107. package/docs-site/docs/plates/record-map.md +2 -1
  108. package/docs-site/docs/plates/reflection-page.md +1 -1
  109. package/docs-site/docs/plates/reporting.md +2 -0
  110. package/docs-site/docs/plates/review.md +1 -1
  111. package/docs-site/docs/plates/routing.md +2 -0
  112. package/docs-site/docs/plates/session-capture.md +1 -1
  113. package/docs-site/docs/plates/site-manual.md +1 -1
  114. package/docs-site/docs/plates/stamp-plate.md +1 -1
  115. package/docs-site/docs/plates/station-card.md +4 -1
  116. package/docs-site/docs/plates/station-heat.md +1 -1
  117. package/docs-site/docs/plates/suit-organs.md +1 -1
  118. package/docs-site/docs/plates/suit-settings.md +1 -1
  119. package/docs-site/docs/plates/suit-wear.md +3 -1
  120. package/docs-site/docs/plates/suit.md +2 -0
  121. package/docs-site/docs/plates/trail-state.md +1 -1
  122. package/docs-site/docs/plates/work-fresh.md +1 -1
  123. package/docs-site/docs/plates/write-home.md +1 -1
  124. package/package.json +1 -1
  125. package/scripts/node/bridge-mcp-wiring.cjs +37 -4
  126. package/scripts/node/install-bridges.cjs +172 -19
  127. package/scripts/node/setup.cjs +2 -46
  128. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  129. package/xray/features.json +1 -1
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Sleeve: on means the previous ticket came back, the worn plate already
3
+ * matched the package, that ticket already matched the working record,
4
+ * and inspect has passed for the skills on disk.
5
+ * Session start and compact run the mill. A later prompt reads the report.
6
+ */
7
+ const { execFileSync } = require("child_process");
8
+ const { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } = require("fs");
9
+ const { dirname, join } = require("path");
10
+
11
+ const STITCHES = ["loop", "domain", "state", "foundry"];
12
+ const SLEEVE_PLANES = [
13
+ "record-map",
14
+ "station-card",
15
+ "notes-page",
16
+ "memory-recall",
17
+ "suit-wear",
18
+ "suit-organs",
19
+ "suit-settings",
20
+ "trail-state",
21
+ ];
22
+ const FOUNDRY_FILES = ["cli.mjs", "mill-root.mjs", "inspect.mjs", "reconcile-version.mjs"];
23
+
24
+ function isSleevePlane(id) {
25
+ return SLEEVE_PLANES.includes(String(id || ""));
26
+ }
27
+
28
+ function stitch(name, on, at) {
29
+ return {
30
+ name,
31
+ on: Boolean(on),
32
+ at: Array.isArray(at) ? at.filter((item) => typeof item === "string" && item) : [],
33
+ };
34
+ }
35
+
36
+ function readJson(file) {
37
+ try {
38
+ return JSON.parse(readFileSync(file, "utf8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function stationText(root) {
45
+ const file = join(root, ".xray", "state", "STATION.md");
46
+ if (!existsSync(file)) return "";
47
+ try {
48
+ return readFileSync(file, "utf8");
49
+ } catch {
50
+ return "";
51
+ }
52
+ }
53
+
54
+ function fieldValue(text, label) {
55
+ const line = String(text || "").split(/\r?\n/).find((row) => row.startsWith(`${label}: `));
56
+ return line ? line.slice(label.length + 2).trim() : "";
57
+ }
58
+
59
+ function ticketIntent(root) {
60
+ return fieldValue(stationText(root), "Intent");
61
+ }
62
+
63
+ function ticketHost(root) {
64
+ const line = fieldValue(stationText(root), "Host");
65
+ return line.split(/\s+/)[0] || "";
66
+ }
67
+
68
+ function workingFile(root) {
69
+ return readJson(join(root, ".xray", "state", "repertoire-working.json"));
70
+ }
71
+
72
+ function oneLine(value) {
73
+ return String(value || "").replace(/\s+/g, " ").trim().slice(0, 80);
74
+ }
75
+
76
+ function loopStitch(root) {
77
+ const card = ticketIntent(root);
78
+ const working = workingFile(root);
79
+ const prior = working && typeof working.priorIntent === "string" ? working.priorIntent : "";
80
+ const kept = working && typeof working.intent === "string" ? working.intent : "";
81
+ const returned = Boolean(prior && kept && card && prior === kept && card === kept);
82
+ return stitch("loop", returned, returned ? [oneLine(kept)] : []);
83
+ }
84
+
85
+ function plateBody(raw) {
86
+ return String(raw || "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
87
+ }
88
+
89
+ function domainStitch(root) {
90
+ const intent = ticketIntent(root);
91
+ if (!intent) return stitch("domain", false, []);
92
+ let plates = null;
93
+ try {
94
+ plates = require("./plates.cjs");
95
+ } catch {
96
+ return stitch("domain", false, []);
97
+ }
98
+ let plate = null;
99
+ try {
100
+ plate = plates.recallPlate(intent);
101
+ } catch {
102
+ plate = null;
103
+ }
104
+ if (!plate || !plate.id || !plate.body) return stitch("domain", false, []);
105
+ const worn = join(root, ".xray", "state", "plates", `${plate.id}.md`);
106
+ if (!existsSync(worn)) return stitch("domain", false, []);
107
+ let raw = "";
108
+ try {
109
+ raw = readFileSync(worn, "utf8");
110
+ } catch {
111
+ return stitch("domain", false, []);
112
+ }
113
+ if (plateBody(raw) !== plate.body) return stitch("domain", false, []);
114
+ return stitch("domain", true, [plate.id]);
115
+ }
116
+
117
+ function stateStitch(root) {
118
+ const card = ticketIntent(root);
119
+ const host = ticketHost(root);
120
+ const working = workingFile(root);
121
+ const priorCard = working && typeof working.priorIntent === "string" ? working.priorIntent : "";
122
+ const priorHost = working && typeof working.priorHost === "string" ? working.priorHost : "";
123
+ const priorKept = working && typeof working.priorWorkingIntent === "string" ? working.priorWorkingIntent : "";
124
+ const priorHeld = working && typeof working.priorWorkingHost === "string" ? working.priorWorkingHost : "";
125
+ const agreed = Boolean(
126
+ priorCard && priorHost && priorKept && priorHeld
127
+ && priorCard === priorKept
128
+ && priorHost === priorHeld
129
+ && card === priorCard
130
+ && host === priorHost,
131
+ );
132
+ return stitch("state", agreed, agreed ? [".xray/state/STATION.md"] : []);
133
+ }
134
+
135
+ function foundryDir(root) {
136
+ const candidates = [
137
+ join(root, "scripts", "foundry"),
138
+ join(root, "node_modules", "0xray", "scripts", "foundry"),
139
+ ];
140
+ for (const dir of candidates) {
141
+ if (FOUNDRY_FILES.every((name) => existsSync(join(dir, name)))) return dir;
142
+ }
143
+ return "";
144
+ }
145
+
146
+ function millPath(root) {
147
+ return join(root, ".xray", "state", "sleeve-mill.json");
148
+ }
149
+
150
+ function packageVersion(dir) {
151
+ let current = dir;
152
+ for (let depth = 0; depth < 6; depth += 1) {
153
+ const pkg = readJson(join(current, "package.json"));
154
+ if (pkg && typeof pkg.version === "string" && pkg.version) return pkg.version;
155
+ const parent = dirname(current);
156
+ if (parent === current) break;
157
+ current = parent;
158
+ }
159
+ return "";
160
+ }
161
+
162
+ function gitHead(root) {
163
+ try {
164
+ return execFileSync("git", ["rev-parse", "--short", "HEAD"], {
165
+ cwd: root,
166
+ encoding: "utf8",
167
+ timeout: 2000,
168
+ stdio: ["ignore", "pipe", "ignore"],
169
+ }).trim();
170
+ } catch {
171
+ return "";
172
+ }
173
+ }
174
+
175
+ function wornSkillFingerprint(root) {
176
+ const homes = [
177
+ ".opencode/skills",
178
+ ".grok/plugins/0xray/skills",
179
+ ".hermes/plugins/xray-hermes/skills",
180
+ ".openclaw/skills",
181
+ ];
182
+ const names = [];
183
+ for (const rel of homes) {
184
+ const dir = join(root, rel);
185
+ if (!existsSync(dir)) continue;
186
+ let entries = [];
187
+ try {
188
+ entries = readdirSync(dir, { withFileTypes: true });
189
+ } catch {
190
+ continue;
191
+ }
192
+ for (const entry of entries) {
193
+ if (!entry.isDirectory()) continue;
194
+ if (!existsSync(join(dir, entry.name, "SKILL.md"))) continue;
195
+ names.push(`${rel}/${entry.name}`);
196
+ }
197
+ }
198
+ names.sort();
199
+ return names.join(",");
200
+ }
201
+
202
+ function millStamp(root, dir) {
203
+ return `${packageVersion(dir)}|${gitHead(root)}|${wornSkillFingerprint(root)}`;
204
+ }
205
+
206
+ function runInspect(root, cli) {
207
+ let out = "";
208
+ try {
209
+ out = execFileSync(process.execPath, [cli, "inspect", "--skip-live"], {
210
+ cwd: root,
211
+ env: {
212
+ ...process.env,
213
+ FOUNDRY_ROOT: root,
214
+ },
215
+ encoding: "utf8",
216
+ timeout: 20000,
217
+ stdio: ["ignore", "pipe", "pipe"],
218
+ });
219
+ } catch (err) {
220
+ if (err && (err.killed || err.signal === "SIGTERM")) {
221
+ return stitch("foundry", false, ["inspect-timeout"]);
222
+ }
223
+ out = err && err.stdout ? String(err.stdout) : "";
224
+ }
225
+ try {
226
+ return inspectStitch(JSON.parse(out));
227
+ } catch {
228
+ return stitch("foundry", false, ["inspect"]);
229
+ }
230
+ }
231
+
232
+ function inspectStitch(report) {
233
+ if (report && report.ok === true) return stitch("foundry", true, ["inspect --skip-live"]);
234
+ const failed = report && Array.isArray(report.failed) ? report.failed.map(String) : [];
235
+ const blocking = failed.filter((id) => id !== "machinePlugin");
236
+ if (failed.length > 0 && blocking.length === 0) {
237
+ return stitch("foundry", true, ["inspect --skip-live"]);
238
+ }
239
+ if (!blocking.length) return stitch("foundry", false, ["inspect"]);
240
+ return stitch("foundry", false, blocking);
241
+ }
242
+
243
+ function writeMill(root, result, stamp) {
244
+ const dest = millPath(root);
245
+ mkdirSync(dirname(dest), { recursive: true });
246
+ writeFileSync(dest, `${JSON.stringify({
247
+ ok: result.on,
248
+ failed: result.on ? [] : result.at,
249
+ stamp,
250
+ ranAt: new Date().toISOString(),
251
+ })}\n`);
252
+ }
253
+
254
+ function runFoundryMill(root) {
255
+ const dir = foundryDir(root);
256
+ if (!dir) {
257
+ const missing = stitch("foundry", false, []);
258
+ writeMill(root, missing, "");
259
+ return missing;
260
+ }
261
+ const stamp = millStamp(root, dir);
262
+ const result = runInspect(root, join(dir, "cli.mjs"));
263
+ writeMill(root, result, stamp);
264
+ return result;
265
+ }
266
+
267
+ function foundryStitch(root) {
268
+ const saved = readJson(millPath(root));
269
+ if (!saved || typeof saved !== "object") return stitch("foundry", false, []);
270
+ const dir = foundryDir(root);
271
+ const stamp = dir ? millStamp(root, dir) : "";
272
+ if (!stamp || saved.stamp !== stamp) return stitch("foundry", false, saved.stamp ? ["stale"] : []);
273
+ if (saved.ok === true) return stitch("foundry", true, ["inspect --skip-live"]);
274
+ const failed = Array.isArray(saved.failed) ? saved.failed.map(String) : [];
275
+ // The wear leaves ~/.grok/plugins/0xray in place and scrubs it. That check is not the mill.
276
+ const blocking = failed.filter((id) => id !== "machinePlugin");
277
+ if (failed.length > 0 && blocking.length === 0) {
278
+ return stitch("foundry", true, ["inspect --skip-live"]);
279
+ }
280
+ return stitch("foundry", false, blocking.length ? blocking : failed);
281
+ }
282
+
283
+ function readSleeve(root) {
284
+ const stitches = [
285
+ loopStitch(root),
286
+ domainStitch(root),
287
+ stateStitch(root),
288
+ foundryStitch(root),
289
+ ];
290
+ const missing = stitches.filter((row) => !row.on).map((row) => row.name);
291
+ return { on: missing.length === 0, missing, stitches };
292
+ }
293
+
294
+ function rowsOf(sleeve) {
295
+ const listed = sleeve && Array.isArray(sleeve.stitches) ? sleeve.stitches : [];
296
+ return STITCHES.map((name) => {
297
+ const found = listed.find((row) => row && row.name === name);
298
+ return found ? stitch(name, found.on, found.at) : stitch(name, false, []);
299
+ });
300
+ }
301
+
302
+ function formatSleevePointer(sleeve) {
303
+ if (sleeve && sleeve.on) return "Sleeve: on";
304
+ const missing = sleeve && Array.isArray(sleeve.missing) && sleeve.missing.length
305
+ ? STITCHES.filter((name) => sleeve.missing.includes(name))
306
+ : rowsOf(sleeve).filter((row) => !row.on).map((row) => row.name);
307
+ const names = missing.length ? missing : STITCHES.slice();
308
+ return `Sleeve: off ${names.join(", ")}`;
309
+ }
310
+
311
+ function stitchText(row) {
312
+ if (row.at.length) return row.at.join(", ");
313
+ return row.on ? "on" : "off";
314
+ }
315
+
316
+ function labelOf(name) {
317
+ return name ? name.charAt(0).toUpperCase() + name.slice(1) : "";
318
+ }
319
+
320
+ function formatSleeveReading(sleeve) {
321
+ const lines = [formatSleevePointer(sleeve)];
322
+ for (const row of rowsOf(sleeve)) lines.push(`${labelOf(row.name)}: ${stitchText(row)}`);
323
+ return lines.join("\n");
324
+ }
325
+
326
+ module.exports = {
327
+ STITCHES,
328
+ SLEEVE_PLANES,
329
+ isSleevePlane,
330
+ readSleeve,
331
+ runFoundryMill,
332
+ inspectStitch,
333
+ formatSleevePointer,
334
+ formatSleeveReading,
335
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ESM face for sleeve.cjs.
3
+ */
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join } from "node:path";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const impl = require(join(dirname(fileURLToPath(import.meta.url)), "sleeve.cjs"));
10
+
11
+ export const STITCHES = impl.STITCHES;
12
+ export const SLEEVE_PLANES = impl.SLEEVE_PLANES;
13
+ export const isSleevePlane = impl.isSleevePlane;
14
+ export const readSleeve = impl.readSleeve;
15
+ export const runFoundryMill = impl.runFoundryMill;
16
+ export const inspectStitch = impl.inspectStitch;
17
+ export const formatSleevePointer = impl.formatSleevePointer;
18
+ export const formatSleeveReading = impl.formatSleeveReading;
@@ -20,7 +20,10 @@ const {
20
20
  const { join, resolve } = require("path");
21
21
 
22
22
  const HOOKS_DIR = __dirname;
23
- const { plateStockLine, recallPlate, stampPlateIfMissing } = require("./plates.cjs");
23
+ const { ensureWornPlate, plateStockLine, recallPlate } = require("./plates.cjs");
24
+ const { rememberSessionNote, retainProjectNotes } = require("./session-note-index.cjs");
25
+ const { formatCascadePointer, readWakeCascade } = require("./wake-cascade.cjs");
26
+ const { formatSleevePointer, readSleeve, runFoundryMill } = require("./sleeve.cjs");
24
27
 
25
28
  const INTENT_MAX = 240;
26
29
 
@@ -111,6 +114,12 @@ function isStockTicket(value) {
111
114
  return !text || text === "(none)" || text === "(none yet)";
112
115
  }
113
116
 
117
+ function readStationHostToken(root) {
118
+ const md = readExistingStationMarkdown(root);
119
+ const match = md.match(/^Host:\s*(\S+)/m);
120
+ return match ? match[1] : "";
121
+ }
122
+
114
123
  function readStationTicketField(root, field) {
115
124
  const md = readExistingStationMarkdown(root);
116
125
  const re = field === "Plan" ? /^Plan:\s*(.*)$/im : /^Intent:\s*(.*)$/im;
@@ -136,9 +145,20 @@ function resolveHeatIntent(root, extra, existing) {
136
145
  const pickup = clipIntent(readNotesPickup(root));
137
146
  const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
138
147
  const cardIsBootEcho = Boolean(card && bootIntent && card === bootIntent);
139
- const hook = String(extra.hookEvent || extra.source || "");
140
- const keepsTicket = /user_prompt|compact/i.test(hook);
141
- const compact = /compact/i.test(hook);
148
+ const named = String(extra.hookEvent || "");
149
+ const channel = String(extra.hookEvent || extra.source || "");
150
+ const keepsTicket = /user_prompt|compact/i.test(channel);
151
+ const compact = /compact/i.test(channel);
152
+ let receipt = null;
153
+ try {
154
+ receipt = readSleeve(root);
155
+ } catch {
156
+ receipt = null;
157
+ }
158
+ // A proved receipt keeps the ticket. A new session must not restate it.
159
+ if (receipt && receipt.on && card && /^(session_start|session-start)$/i.test(named)) {
160
+ return { intent: card, rematch: false, kept: true };
161
+ }
142
162
  // A chat sentence or a compact summary does not replace a ticket already on disk.
143
163
  if (incoming && !(keepsTicket && (card || pickup))) {
144
164
  return { intent: incoming, rematch: true };
@@ -520,6 +540,7 @@ function stampNotesPickup(root, line) {
520
540
  : body
521
541
  ? `${marker}\n\n${body.replace(/^\n+/, "")}`
522
542
  : `${marker}\n`;
543
+ rememberSessionNote(root, text, { source: "NOTES.md", slot: "pickup" });
523
544
  if (next === body) return text;
524
545
  mkdirSync(join(root, ".xray", "state"), { recursive: true });
525
546
  writeFileSync(dest, next.endsWith("\n") ? next : `${next}\n`);
@@ -973,6 +994,7 @@ function buildRepertoireResume(root) {
973
994
  }
974
995
 
975
996
  function applyStationHeat(root, host, extra = {}, existing = {}) {
997
+ retainProjectNotes(root);
976
998
  const mr = readMemoryRoutingConfig(root);
977
999
  const memoryOff = isExplicitMemoryRoutingOptOut(mr);
978
1000
  const promptHook = isPromptHook(extra);
@@ -1005,6 +1027,8 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
1005
1027
  ? existing.hotSwap
1006
1028
  : null;
1007
1029
  const hotSwap = nextSwap || keptSwap;
1030
+ const priorIntent = readStationTicketField(root, "Intent");
1031
+ const priorHost = readStationHostToken(root);
1008
1032
  const resolved = resolveHeatIntent(root, extra, existing);
1009
1033
  const intent = resolved.intent;
1010
1034
  const rematch = resolved.rematch;
@@ -1018,7 +1042,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
1018
1042
  }
1019
1043
  const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
1020
1044
  const git = readGitBrief(root);
1021
- const planLine = resolveHeatPlan(root, extra, existing);
1045
+ let planLine = resolveHeatPlan(root, extra, existing);
1046
+ if (priorIntent && intent && intent !== priorIntent && !(extra.plan || extra.planLine)) {
1047
+ planLine = null;
1048
+ }
1022
1049
  // A passed string is the previous card. Count the project file after this wake.
1023
1050
  const repertoireResume = buildRepertoireResume(root);
1024
1051
  const swapBit = hotSwap ? `hot-swap ${hotSwap.from} → ${hotSwap.to}` : `host ${host}`;
@@ -1056,11 +1083,20 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
1056
1083
  if (!matchedSignals.length && priorWorking) {
1057
1084
  matchedSignals = stationSafeSignals(priorWorking.matchedSignals);
1058
1085
  }
1086
+ if (resolved.kept) {
1087
+ matchedSignals = preferLawHits(priorWorking && priorWorking.matchedSignals, 8);
1088
+ }
1059
1089
  const compactHold = retainCompactFields(existing, extra);
1060
1090
  const nextHook = compactHold.hookEvent || extra.hookEvent || extra.source || null;
1061
1091
  const workingSnapshot = {
1062
1092
  host,
1063
1093
  intent,
1094
+ priorIntent: priorIntent || null,
1095
+ priorHost: priorHost || null,
1096
+ priorWorkingIntent: priorWorking && typeof priorWorking.intent === "string" ? priorWorking.intent : null,
1097
+ priorWorkingHost: priorWorking && typeof priorWorking.host === "string"
1098
+ ? (priorWorking.host.split(/\s+/)[0] || null)
1099
+ : null,
1064
1100
  git,
1065
1101
  hotSwap,
1066
1102
  memoryRouting: repertoireResume.startsWith("Repertoire: on") ? "on" : "off",
@@ -1074,6 +1110,13 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
1074
1110
  const growReceipt = workingGrowSnapshot(grow);
1075
1111
  if (growReceipt) workingSnapshot.grow = growReceipt;
1076
1112
  if (matchedSignals.length) workingSnapshot.matchedSignals = matchedSignals.slice(0, 8);
1113
+ const cascade = readWakeCascade(root, { laws: matchedSignals });
1114
+ workingSnapshot.cascade = {
1115
+ plane: cascade.plane,
1116
+ laws: cascade.laws,
1117
+ notes: cascade.notes.length,
1118
+ };
1119
+ const cascadeLine = formatCascadePointer(cascade);
1077
1120
  const opProcNames = readOpProcNames(root);
1078
1121
  if (opProcNames.length) workingSnapshot.opProcNames = opProcNames;
1079
1122
  const pickupChanged = Boolean(pickup && (!priorWorking || priorWorking.pickup !== pickup));
@@ -1106,8 +1149,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
1106
1149
  repertoireResume,
1107
1150
  workingLine,
1108
1151
  stationLine,
1152
+ cascadeLine,
1109
1153
  ...(notesLine ? { notesLine } : {}),
1110
1154
  ...compactHold,
1155
+ arrivedHook: extra && extra.hookEvent ? String(extra.hookEvent) : "",
1111
1156
  };
1112
1157
  }
1113
1158
 
@@ -1122,7 +1167,12 @@ const STOCK_STATION_PREFIXES = [
1122
1167
  "working:",
1123
1168
  "plate:",
1124
1169
  "library:",
1170
+ "cascade:",
1171
+ "sleeve:",
1125
1172
  "notes:",
1173
+ "plane:",
1174
+ "judgement:",
1175
+ "payments:",
1126
1176
  ];
1127
1177
 
1128
1178
  /** Stock design map. Exact lines so a later heat does not preserve a second copy. */
@@ -1318,6 +1368,11 @@ function formatStationMarkdown(fields) {
1318
1368
  const plateLine = plateStockLine(fields.intent);
1319
1369
  if (plateLine) lines.push(plateLine);
1320
1370
  lines.push("Library: record-map — .agents/skills/record-map/SKILL.md");
1371
+ if (fields.cascadeLine) lines.push(fields.cascadeLine);
1372
+ if (fields.sleeveLine) lines.push(fields.sleeveLine);
1373
+ lines.push(`Plane: ${fields.plane || "(none)"}`);
1374
+ lines.push(`Judgement: ${fields.judgement || "start"}`);
1375
+ lines.push(`Payments: ${fields.payments === "on" ? "on" : "off"}`);
1321
1376
  if (fields.notesLine) lines.push(fields.notesLine);
1322
1377
  lines.push("");
1323
1378
  lines.push(...DESIGN_MAP_LINES);
@@ -1338,19 +1393,160 @@ function readExistingStationMarkdown(root) {
1338
1393
  }
1339
1394
  }
1340
1395
 
1396
+
1397
+
1398
+ function ticketIsPayment(intent) {
1399
+ return /\b(pay|payment|payments|x402|invoice)\b/i.test(String(intent || ""));
1400
+ }
1401
+
1402
+ function readHeldPlane(root) {
1403
+ try {
1404
+ const data = JSON.parse(readFileSync(join(root, ".xray", "state", "goggles-plane.json"), "utf8"));
1405
+ return data && typeof data.plane === "string" ? data.plane : "";
1406
+ } catch {
1407
+ return "";
1408
+ }
1409
+ }
1410
+
1411
+ function missKey(proved) {
1412
+ const missing = proved && Array.isArray(proved.missing) ? proved.missing.slice() : [];
1413
+ const rows = proved && Array.isArray(proved.stitches) ? proved.stitches : [];
1414
+ const at = {};
1415
+ for (const row of rows) {
1416
+ if (!row || !missing.includes(row.name)) continue;
1417
+ at[row.name] = Array.isArray(row.at) ? row.at.slice() : [];
1418
+ }
1419
+ return JSON.stringify({ missing, at });
1420
+ }
1421
+
1422
+ function judgementLine(order, saved, proved) {
1423
+ const wake = order && order.wake ? order.wake : "start";
1424
+ if (wake === "repair") {
1425
+ const prior = saved && Number.isFinite(saved.retries) ? saved.retries : 0;
1426
+ const key = missKey(proved);
1427
+ if (saved && saved.missKey === key && prior > 0) return `retry ${prior}`;
1428
+ const next = prior + 1;
1429
+ if (saved) {
1430
+ saved.retries = next;
1431
+ saved.missKey = key;
1432
+ }
1433
+ return `retry ${next}`;
1434
+ }
1435
+ if (saved) {
1436
+ saved.retries = 0;
1437
+ delete saved.missKey;
1438
+ }
1439
+ if (wake === "continue" || wake === "stop" || wake === "start") return wake;
1440
+ return "start";
1441
+ }
1442
+
1443
+ function isMissPlan(line) {
1444
+ return typeof line === "string" && line.startsWith("The work is ");
1445
+ }
1446
+
1447
+ function missWork(missing) {
1448
+ const words = { loop: "the ticket", domain: "the plate", state: "the record", foundry: "the mill" };
1449
+ const parts = (Array.isArray(missing) ? missing : []).map((name) => words[name]).filter(Boolean);
1450
+ if (parts.length === 1) return `The work is ${parts[0]}.`;
1451
+ if (parts.length > 1) {
1452
+ const last = parts[parts.length - 1];
1453
+ return `The work is ${parts.slice(0, -1).join(", ")} and ${last}.`;
1454
+ }
1455
+ return "The work is the suit.";
1456
+ }
1457
+
1458
+ function wakeOrder(saved, proved, fields) {
1459
+ const missing = proved && Array.isArray(proved.missing) ? proved.missing : [];
1460
+ const sameJob = Boolean(saved && saved.priorIntent && saved.intent === saved.priorIntent);
1461
+ const suitMiss = missing.some((name) => name === "domain" || name === "foundry" || name === "state");
1462
+ if (proved && proved.on) {
1463
+ const paused = saved && typeof saved.pausedPlan === "string" ? saved.pausedPlan : "";
1464
+ if (isMissPlan(fields && fields.planLine) && saved && Object.prototype.hasOwnProperty.call(saved, "pausedPlan")) {
1465
+ return { wake: "continue", planLine: paused, pausedPlan: undefined };
1466
+ }
1467
+ return { wake: "continue", pausedPlan: undefined };
1468
+ }
1469
+ if (!saved || !saved.priorIntent) return { wake: "start" };
1470
+ if (saved.intent && saved.priorIntent && saved.intent !== saved.priorIntent) {
1471
+ return { wake: "stop", pausedPlan: undefined };
1472
+ }
1473
+ if (sameJob && !missing.includes("loop") && suitMiss) {
1474
+ const current = fields && fields.planLine ? String(fields.planLine) : "";
1475
+ const paused = isMissPlan(current)
1476
+ ? (typeof saved.pausedPlan === "string" ? saved.pausedPlan : "")
1477
+ : current;
1478
+ return { wake: "repair", planLine: missWork(missing), pausedPlan: paused };
1479
+ }
1480
+ return { wake: "start" };
1481
+ }
1482
+
1341
1483
  function writeStationMarkdown(root, fields) {
1342
1484
  try {
1343
1485
  const dir = join(root, ".xray", "state");
1344
1486
  mkdirSync(dir, { recursive: true });
1487
+ const arrivedKnown = Boolean(fields && Object.prototype.hasOwnProperty.call(fields, "arrivedHook"));
1488
+ const wake = arrivedKnown
1489
+ ? String(fields.arrivedHook || "")
1490
+ : String((fields && fields.hookEvent) || "");
1491
+ if (/^(session_start|session-start|pre_compact|post_compact)$/i.test(wake)) {
1492
+ try {
1493
+ runFoundryMill(root);
1494
+ } catch {
1495
+ /* the card still writes when the mill does not */
1496
+ }
1497
+ }
1498
+ const dest = stationMarkdownPath(root);
1499
+ const base = { ...(fields || {}) };
1500
+ delete base.sleeveLine;
1501
+ writeFileSync(dest, mergeStationMarkdown(formatStationMarkdown(base), readExistingStationMarkdown(root)));
1502
+ // Solar phase is not this judgement. The moral overlay cannot pass alone.
1503
+ fields.plane = readHeldPlane(root);
1504
+ fields.payments = ticketIsPayment(fields.intent) ? "on" : "off";
1505
+ let sleeveLine = "Sleeve: off loop, domain, state, foundry";
1506
+ try {
1507
+ const proved = readSleeve(root);
1508
+ sleeveLine = formatSleevePointer(proved);
1509
+ const workingPath = join(root, ".xray", "state", "repertoire-working.json");
1510
+ if (existsSync(workingPath)) {
1511
+ const saved = JSON.parse(readFileSync(workingPath, "utf8"));
1512
+ if (saved && typeof saved === "object") {
1513
+ const order = wakeOrder(saved, proved, fields);
1514
+ fields.wake = order.wake;
1515
+ if (Object.prototype.hasOwnProperty.call(order, "planLine")) {
1516
+ fields.planLine = order.planLine;
1517
+ }
1518
+ fields.judgement = judgementLine(order, saved, proved);
1519
+ saved.wake = order.wake;
1520
+ saved.retries = saved.retries || 0;
1521
+ saved.judgement = fields.judgement;
1522
+ saved.plane = fields.plane;
1523
+ saved.payments = fields.payments;
1524
+ if (order.pausedPlan !== undefined) saved.pausedPlan = order.pausedPlan;
1525
+ else delete saved.pausedPlan;
1526
+ saved.sleeve = {
1527
+ on: proved.on,
1528
+ missing: proved.missing,
1529
+ stitches: proved.stitches,
1530
+ };
1531
+ writeFileSync(workingPath, `${JSON.stringify(saved)}\n`);
1532
+ }
1533
+ }
1534
+ } catch {
1535
+ /* the card still writes; the sleeve stays off */
1536
+ }
1537
+ writeFileSync(
1538
+ dest,
1539
+ mergeStationMarkdown(
1540
+ formatStationMarkdown({ ...(fields || {}), sleeveLine }),
1541
+ readExistingStationMarkdown(root),
1542
+ ),
1543
+ );
1345
1544
  try {
1346
1545
  const plate = recallPlate(fields && fields.intent);
1347
- if (plate) stampPlateIfMissing(root, plate.id);
1546
+ if (plate) ensureWornPlate(root, plate.id);
1348
1547
  } catch {
1349
1548
  /* a missing plate doc must not block the card */
1350
1549
  }
1351
- const dest = stationMarkdownPath(root);
1352
- const next = mergeStationMarkdown(formatStationMarkdown(fields), readExistingStationMarkdown(root));
1353
- writeFileSync(dest, next);
1354
1550
  return dest;
1355
1551
  } catch {
1356
1552
  return null;