0xray 4.0.36 → 4.0.38

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 (169) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.xray/features.json +1 -1
  3. package/dist/CHANGELOG.md +28 -0
  4. package/dist/analytics/emerging-pattern-detector.d.ts +12 -1
  5. package/dist/analytics/emerging-pattern-detector.js +37 -12
  6. package/dist/analytics/pattern-learning-engine.js +1 -9
  7. package/dist/analytics/predictive-analytics.js +51 -33
  8. package/dist/analytics/prompt-pattern-analyzer.d.ts +1 -0
  9. package/dist/analytics/prompt-pattern-analyzer.js +39 -20
  10. package/dist/analytics/routing-performance-analyzer.d.ts +2 -0
  11. package/dist/analytics/routing-performance-analyzer.js +89 -37
  12. package/dist/analytics/simple-pattern-analyzer.d.ts +4 -0
  13. package/dist/analytics/simple-pattern-analyzer.js +66 -28
  14. package/dist/architect/architect-tools.d.ts +3 -0
  15. package/dist/architect/architect-tools.js +22 -0
  16. package/dist/cli/commands/grok-install.js +1 -1
  17. package/dist/cli/commands/hermes-install.js +11 -5
  18. package/dist/cli/commands/openclaw-install.js +13 -7
  19. package/dist/cli/commands/opencode-install.js +7 -4
  20. package/dist/cli/commands/suit-monitor.d.ts +30 -0
  21. package/dist/cli/commands/suit-monitor.js +128 -0
  22. package/dist/cli/index.js +52 -15
  23. package/dist/cli/suit-pulse.d.ts +29 -0
  24. package/dist/cli/suit-pulse.js +270 -0
  25. package/dist/delegation/agent-delegator.d.ts +4 -2
  26. package/dist/delegation/agent-delegator.js +46 -27
  27. package/dist/delegation/agent-expertise.js +2 -1
  28. package/dist/delegation/analytics/outcome-tracker.d.ts +6 -0
  29. package/dist/delegation/analytics/outcome-tracker.js +17 -3
  30. package/dist/delegation/ast-code-parser.d.ts +5 -0
  31. package/dist/delegation/ast-code-parser.js +53 -48
  32. package/dist/delegation/codebase-context-analyzer.d.ts +3 -7
  33. package/dist/delegation/codebase-context-analyzer.js +188 -159
  34. package/dist/delegation/session-coordinator.js +3 -2
  35. package/dist/enforcement/loaders/codex-loader.js +18 -14
  36. package/dist/enforcement/validators/architecture-validators.js +115 -9
  37. package/dist/governance/codex-policy.service.d.ts +12 -2
  38. package/dist/governance/codex-policy.service.js +91 -16
  39. package/dist/integrations/grok/hooks/session-start.js +6 -6
  40. package/dist/integrations/hooks/goggles-pipeline.mjs +169 -28
  41. package/dist/integrations/hooks/goggles-planes.json +172 -8
  42. package/dist/integrations/hooks/plates.cjs +138 -0
  43. package/dist/integrations/hooks/station-hook-runtime.cjs +63 -5
  44. package/dist/integrations/hooks/write-home.cjs +59 -0
  45. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  46. package/dist/mcps/architect-tools.server.d.ts +6 -0
  47. package/dist/mcps/architect-tools.server.js +19 -18
  48. package/dist/mcps/orchestrator/server.d.ts +4 -3
  49. package/dist/mcps/orchestrator/server.js +29 -17
  50. package/dist/memory-routing/lookup-cache.d.ts +8 -0
  51. package/dist/memory-routing/lookup-cache.js +72 -0
  52. package/dist/memory-routing/plates.d.ts +1 -1
  53. package/dist/memory-routing/plates.js +26 -0
  54. package/dist/memory-routing/provider-loader.js +3 -0
  55. package/dist/metrics/agent-metrics.d.ts +14 -5
  56. package/dist/metrics/agent-metrics.js +362 -224
  57. package/dist/nucleus/synthesis-consult-receipt.js +38 -16
  58. package/dist/orchestrator/enhanced-multi-agent-orchestrator.d.ts +5 -0
  59. package/dist/orchestrator/enhanced-multi-agent-orchestrator.js +9 -0
  60. package/dist/orchestrator/multi-agent-orchestration-coordinator.js +1 -2
  61. package/dist/orchestrator/orchestrator.js +1 -2
  62. package/dist/processors/implementations/agents-md-validation-processor.d.ts +0 -1
  63. package/dist/processors/implementations/agents-md-validation-processor.js +0 -11
  64. package/dist/processors/implementations/console-log-guard-processor.js +39 -50
  65. package/dist/processors/implementations/performance-budget-processor.d.ts +0 -5
  66. package/dist/processors/implementations/performance-budget-processor.js +29 -38
  67. package/dist/processors/implementations/storytelling-trigger-processor.d.ts +5 -0
  68. package/dist/processors/implementations/storytelling-trigger-processor.js +69 -16
  69. package/dist/processors/implementations/test-auto-creation-processor.js +39 -36
  70. package/dist/reporting/framework-reporting-system.d.ts +3 -0
  71. package/dist/reporting/framework-reporting-system.js +61 -8
  72. package/dist/reporting/log-parser.js +188 -105
  73. package/dist/reporting/metrics.js +42 -18
  74. package/dist/security/prompt-security-validator.js +8 -7
  75. package/dist/security/security-auditor.d.ts +4 -0
  76. package/dist/security/security-auditor.js +132 -57
  77. package/dist/security/security-hardener.d.ts +2 -0
  78. package/dist/security/security-hardener.js +31 -6
  79. package/dist/security/security-headers.d.ts +2 -0
  80. package/dist/security/security-headers.js +23 -21
  81. package/dist/security/security-scanner.js +18 -4
  82. package/dist/session/session-cleanup-manager.d.ts +2 -0
  83. package/dist/session/session-cleanup-manager.js +29 -8
  84. package/dist/session/session-monitor.d.ts +14 -4
  85. package/dist/session/session-monitor.js +208 -106
  86. package/dist/session/session-state-manager.d.ts +11 -0
  87. package/dist/session/session-state-manager.js +90 -8
  88. package/docs-site/docs/plates/activity-log.md +38 -0
  89. package/docs-site/docs/plates/cursor-compact.md +35 -0
  90. package/docs-site/docs/plates/grok-compact.md +35 -0
  91. package/docs-site/docs/plates/index.md +25 -3
  92. package/docs-site/docs/plates/inference-files.md +35 -0
  93. package/docs-site/docs/plates/kept-line.md +35 -0
  94. package/docs-site/docs/plates/lens-gate.md +35 -0
  95. package/docs-site/docs/plates/lens-page.md +35 -0
  96. package/docs-site/docs/plates/notes-page.md +35 -0
  97. package/docs-site/docs/plates/package-face.md +35 -0
  98. package/docs-site/docs/plates/payload-heat.md +35 -0
  99. package/docs-site/docs/plates/pickup-stamp.md +35 -0
  100. package/docs-site/docs/plates/pre-tool.md +35 -0
  101. package/docs-site/docs/plates/record-map.md +41 -0
  102. package/docs-site/docs/plates/reflection-page.md +35 -0
  103. package/docs-site/docs/plates/review.md +57 -0
  104. package/docs-site/docs/plates/session-capture.md +38 -0
  105. package/docs-site/docs/plates/site-manual.md +35 -0
  106. package/docs-site/docs/plates/stamp-plate.md +35 -0
  107. package/docs-site/docs/plates/station-card.md +35 -0
  108. package/docs-site/docs/plates/station-heat.md +35 -0
  109. package/docs-site/docs/plates/suit-organs.md +38 -0
  110. package/docs-site/docs/plates/suit-settings.md +35 -0
  111. package/docs-site/docs/plates/suit-wear.md +37 -0
  112. package/docs-site/docs/plates/trail-state.md +35 -0
  113. package/docs-site/docs/plates/work-fresh.md +35 -0
  114. package/docs-site/docs/plates/write-home.md +38 -0
  115. package/package.json +3 -2
  116. package/scripts/node/install-bridges.cjs +302 -78
  117. package/scripts/node/postinstall.cjs +14 -3
  118. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  119. package/src/mcps/architect-tools.server.ts +25 -20
  120. package/src/mcps/orchestrator/server.ts +33 -19
  121. package/xray/features.json +1 -1
  122. package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +0 -1
  123. package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +0 -1
  124. package/vendor/@0xray/repertoire/dist/governance/ontological-trap-enforcer.d.ts.map +0 -1
  125. package/vendor/@0xray/repertoire/dist/governance/ontological-trap-enforcer.js.map +0 -1
  126. package/vendor/@0xray/repertoire/dist/index.d.ts.map +0 -1
  127. package/vendor/@0xray/repertoire/dist/index.js.map +0 -1
  128. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.d.ts.map +0 -1
  129. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.js.map +0 -1
  130. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-parser.d.ts.map +0 -1
  131. package/vendor/@0xray/repertoire/dist/ingestion/groover-log-parser.js.map +0 -1
  132. package/vendor/@0xray/repertoire/dist/ingestion/orchestrator-feedback-ingester.d.ts.map +0 -1
  133. package/vendor/@0xray/repertoire/dist/ingestion/orchestrator-feedback-ingester.js.map +0 -1
  134. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +0 -1
  135. package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +0 -1
  136. package/vendor/@0xray/repertoire/dist/mcp/server.d.ts.map +0 -1
  137. package/vendor/@0xray/repertoire/dist/mcp/server.js.map +0 -1
  138. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.d.ts.map +0 -1
  139. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.js.map +0 -1
  140. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/capability-enhancer.d.ts.map +0 -1
  141. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/capability-enhancer.js.map +0 -1
  142. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +0 -1
  143. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +0 -1
  144. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-agent-affinity.d.ts.map +0 -1
  145. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-agent-affinity.js.map +0 -1
  146. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +0 -1
  147. package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +0 -1
  148. package/vendor/@0xray/repertoire/dist/paths.d.ts.map +0 -1
  149. package/vendor/@0xray/repertoire/dist/paths.js.map +0 -1
  150. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +0 -1
  151. package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +0 -1
  152. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +0 -1
  153. package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +0 -1
  154. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts.map +0 -1
  155. package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js.map +0 -1
  156. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.d.ts.map +0 -1
  157. package/vendor/@0xray/repertoire/dist/registry/confidence-decay.js.map +0 -1
  158. package/vendor/@0xray/repertoire/dist/registry/signal-prune.d.ts.map +0 -1
  159. package/vendor/@0xray/repertoire/dist/registry/signal-prune.js.map +0 -1
  160. package/vendor/@0xray/repertoire/dist/synthesis/concrete-sections.d.ts.map +0 -1
  161. package/vendor/@0xray/repertoire/dist/synthesis/concrete-sections.js.map +0 -1
  162. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts.map +0 -1
  163. package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js.map +0 -1
  164. package/vendor/@0xray/repertoire/dist/synthesis/primitive-confidence-aggregator.d.ts.map +0 -1
  165. package/vendor/@0xray/repertoire/dist/synthesis/primitive-confidence-aggregator.js.map +0 -1
  166. package/vendor/@0xray/repertoire/dist/synthesis/synthesis-prompt-builder.d.ts.map +0 -1
  167. package/vendor/@0xray/repertoire/dist/synthesis/synthesis-prompt-builder.js.map +0 -1
  168. package/vendor/@0xray/repertoire/dist/types.d.ts.map +0 -1
  169. package/vendor/@0xray/repertoire/dist/types.js.map +0 -1
@@ -278,6 +278,108 @@ export class ErrorResolutionValidator extends BaseValidator {
278
278
  return this.createSuccessResult("Error resolution patterns are properly implemented");
279
279
  }
280
280
  }
281
+ /** Code points matched by a non-unicode `\s` in this Node. */
282
+ function isCallWhitespace(ch) {
283
+ const c = ch.charCodeAt(0);
284
+ if (c <= 32) {
285
+ return c === 9 || c === 10 || c === 11 || c === 12 || c === 13 || c === 32;
286
+ }
287
+ if (c >= 0x2000 && c <= 0x200a)
288
+ return true;
289
+ return (c === 0xa0 ||
290
+ c === 0x1680 ||
291
+ c === 0x2028 ||
292
+ c === 0x2029 ||
293
+ c === 0x202f ||
294
+ c === 0x205f ||
295
+ c === 0x3000 ||
296
+ c === 0xfeff);
297
+ }
298
+ /** Per-name scans are cheaper below this; one automaton wins above it. */
299
+ const CALL_SCAN_NAME_THRESHOLD = 48;
300
+ /**
301
+ * Counts `name\\s*\\(` for every name.
302
+ * Suffix hits count (`foo` inside `myfoo(`), matching a separate global regex per name.
303
+ */
304
+ function countNameCallSites(code, names) {
305
+ if (names.length < CALL_SCAN_NAME_THRESHOLD) {
306
+ return names.map((funcName) => (code.match(new RegExp(`${funcName}\\s*\\(`, "g")) || []).length);
307
+ }
308
+ const counts = new Array(names.length).fill(0);
309
+ if (code.length === 0)
310
+ return counts;
311
+ const nodes = [{ next: new Map(), fail: 0, out: [] }];
312
+ for (let nameIndex = 0; nameIndex < names.length; nameIndex++) {
313
+ const name = names[nameIndex] ?? "";
314
+ let state = 0;
315
+ for (let i = 0; i < name.length; i++) {
316
+ const ch = name.charAt(i);
317
+ const current = nodes[state];
318
+ if (!current)
319
+ break;
320
+ let next = current.next.get(ch);
321
+ if (next === undefined) {
322
+ next = nodes.length;
323
+ nodes.push({ next: new Map(), fail: 0, out: [] });
324
+ current.next.set(ch, next);
325
+ }
326
+ state = next;
327
+ }
328
+ nodes[state]?.out.push(nameIndex);
329
+ }
330
+ const queue = [];
331
+ const root = nodes[0];
332
+ if (!root)
333
+ return counts;
334
+ for (const next of root.next.values())
335
+ queue.push(next);
336
+ let head = 0;
337
+ while (head < queue.length) {
338
+ const currentIndex = queue[head++];
339
+ const current = currentIndex === undefined ? undefined : nodes[currentIndex];
340
+ if (!current)
341
+ continue;
342
+ for (const [ch, childIndex] of current.next) {
343
+ queue.push(childIndex);
344
+ let fail = current.fail;
345
+ while (fail !== 0 && !nodes[fail]?.next.has(ch)) {
346
+ fail = nodes[fail]?.fail ?? 0;
347
+ }
348
+ const down = nodes[fail]?.next.get(ch);
349
+ const child = nodes[childIndex];
350
+ if (!child)
351
+ continue;
352
+ child.fail = down !== undefined && down !== childIndex ? down : 0;
353
+ const failOut = nodes[child.fail]?.out;
354
+ if (failOut && failOut.length > 0) {
355
+ child.out = child.out.concat(failOut);
356
+ }
357
+ }
358
+ }
359
+ let state = 0;
360
+ for (let i = 0; i < code.length; i++) {
361
+ const ch = code.charAt(i);
362
+ while (state !== 0 && !nodes[state]?.next.has(ch)) {
363
+ state = nodes[state]?.fail ?? 0;
364
+ }
365
+ const down = nodes[state]?.next.get(ch);
366
+ state = down === undefined ? 0 : down;
367
+ const outs = nodes[state]?.out;
368
+ if (!outs || outs.length === 0)
369
+ continue;
370
+ let k = i + 1;
371
+ while (k < code.length && isCallWhitespace(code.charAt(k)))
372
+ k++;
373
+ if (code.charAt(k) !== "(")
374
+ continue;
375
+ for (const nameIndex of outs) {
376
+ const current = counts[nameIndex];
377
+ if (current !== undefined)
378
+ counts[nameIndex] = current + 1;
379
+ }
380
+ }
381
+ return counts;
382
+ }
281
383
  /**
282
384
  * Validates loop safety (Codex Term #8).
283
385
  * Prevents infinite loops by checking for proper termination conditions.
@@ -322,15 +424,19 @@ export class LoopSafetyValidator extends BaseValidator {
322
424
  // Check for recursion without base case detection (basic)
323
425
  const functionMatches = newCode.match(/function\s+\w+\s*\([^)]*\)/g);
324
426
  if (functionMatches) {
325
- const functionNames = functionMatches
326
- .map((match) => {
327
- const nameMatch = match.match(/function\s+(\w+)/);
328
- return nameMatch ? nameMatch[1] : null;
329
- })
330
- .filter(Boolean);
331
- for (const funcName of functionNames) {
332
- // Check if function calls itself (basic recursion detection)
333
- const selfCalls = (newCode.match(new RegExp(`${funcName}\\s*\\(`, "g")) || []).length;
427
+ const functionNames = [];
428
+ for (const match of functionMatches) {
429
+ const name = match.match(/function\s+(\w+)/)?.[1];
430
+ if (name)
431
+ functionNames.push(name);
432
+ }
433
+ const callCounts = countNameCallSites(newCode, functionNames);
434
+ for (let i = 0; i < functionNames.length; i++) {
435
+ const funcName = functionNames[i];
436
+ if (!funcName)
437
+ continue;
438
+ // Same count a per-name /name\s*\(/g scan would return, including suffix hits.
439
+ const selfCalls = callCounts[i] ?? 0;
334
440
  if (selfCalls > 1) {
335
441
  // More than just the function definition
336
442
  // Allow recursive functions with proper base cases (edge case)
@@ -15,7 +15,8 @@
15
15
  * - Returns ActiveCodexSnapshot compatible with the existing get_active_codex MCP tool.
16
16
  * - Provides getTermCount() with safe 60-term fallback (preserves prior behavior of bypasses).
17
17
  * - Full frameworkLogger discipline on every load/decision/error.
18
- * - No caching in v1 skeleton (additive later); no mutation; no enforcement.
18
+ * - Reuses one parsed codex while that file still wins and its mtime and size are unchanged.
19
+ * - No mutation of the cached codex; no enforcement.
19
20
  *
20
21
  * First wired consumer: src/mcps/enforcer-tools.server.ts (getCodexTermCount bypass removed;
21
22
  * now delegates through this service).
@@ -34,9 +35,18 @@ import type { ActiveCodexSnapshot, ICodexPolicyProvider } from './governance-typ
34
35
  */
35
36
  export declare class CodexPolicyService implements ICodexPolicyProvider {
36
37
  private readonly component;
38
+ /**
39
+ * Winning parse. Invalid when a higher-priority path appears or mtime/size changes,
40
+ * so the next check still parses the bytes a fresh load would have read.
41
+ */
42
+ private codexCache;
43
+ private candidateKey;
44
+ private fileIdentity;
45
+ private cachedRaw;
37
46
  /**
38
47
  * Internal loader: resolves candidates, picks first existing, reads + parses.
39
- * Always logs via frameworkLogger (success, error, fallback).
48
+ * Repeat calls skip the read and JSON.parse while the winning file is unchanged.
49
+ * Logs via frameworkLogger on load, error, and fallback (not on a cache hit).
40
50
  */
41
51
  private loadRaw;
42
52
  private computeTermCount;
@@ -15,7 +15,8 @@
15
15
  * - Returns ActiveCodexSnapshot compatible with the existing get_active_codex MCP tool.
16
16
  * - Provides getTermCount() with safe 60-term fallback (preserves prior behavior of bypasses).
17
17
  * - Full frameworkLogger discipline on every load/decision/error.
18
- * - No caching in v1 skeleton (additive later); no mutation; no enforcement.
18
+ * - Reuses one parsed codex while that file still wins and its mtime and size are unchanged.
19
+ * - No mutation of the cached codex; no enforcement.
19
20
  *
20
21
  * First wired consumer: src/mcps/enforcer-tools.server.ts (getCodexTermCount bypass removed;
21
22
  * now delegates through this service).
@@ -29,51 +30,125 @@
29
30
  */
30
31
  import { frameworkLogger } from '../core/framework-logger.js';
31
32
  import { resolveCodexPath } from '../core/config-paths.js';
32
- import { existsSync } from 'fs';
33
+ import { existsSync, statSync } from 'fs';
33
34
  import * as fs from 'fs/promises';
35
+ /** mtimeNs when the stat has it. mtimeMs only when it does not. */
36
+ function codexClock(fileStat) {
37
+ const nanos = fileStat.mtimeNs;
38
+ if (typeof nanos === 'bigint')
39
+ return nanos.toString();
40
+ if (typeof nanos === 'number' && Number.isFinite(nanos))
41
+ return String(nanos);
42
+ if (typeof fileStat.mtimeMs === 'number' && Number.isFinite(fileStat.mtimeMs)) {
43
+ return String(fileStat.mtimeMs);
44
+ }
45
+ return null;
46
+ }
34
47
  /**
35
48
  * Canonical implementation of the Governance Codex/Policy provider.
36
49
  * Single owner of the "first read" of any codex.json for the framework.
37
50
  */
38
51
  export class CodexPolicyService {
39
52
  component = 'codex-policy-service';
53
+ /**
54
+ * Winning parse. Invalid when a higher-priority path appears or mtime/size changes,
55
+ * so the next check still parses the bytes a fresh load would have read.
56
+ */
57
+ codexCache = null;
58
+ candidateKey(candidates) {
59
+ return JSON.stringify(candidates);
60
+ }
61
+ fileIdentity(sourcePath) {
62
+ try {
63
+ const fileStat = statSync(sourcePath);
64
+ if (!fileStat.isFile())
65
+ return null;
66
+ const clock = codexClock(fileStat);
67
+ if (clock === null)
68
+ return null;
69
+ return `${clock}:${fileStat.size}`;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ cachedRaw(candidates) {
76
+ const cached = this.codexCache;
77
+ if (!cached || cached.candidateKey !== this.candidateKey(candidates))
78
+ return null;
79
+ for (let i = 0; i < cached.index; i++) {
80
+ const higher = candidates[i];
81
+ if (higher && existsSync(higher))
82
+ return null;
83
+ }
84
+ const identity = this.fileIdentity(cached.sourcePath);
85
+ if (!identity || identity !== cached.identity)
86
+ return null;
87
+ return cached.raw;
88
+ }
40
89
  /**
41
90
  * Internal loader: resolves candidates, picks first existing, reads + parses.
42
- * Always logs via frameworkLogger (success, error, fallback).
91
+ * Repeat calls skip the read and JSON.parse while the winning file is unchanged.
92
+ * Logs via frameworkLogger on load, error, and fallback (not on a cache hit).
43
93
  */
44
94
  async loadRaw() {
45
- const candidates = resolveCodexPath();
46
- const resolvedPath = Array.isArray(candidates)
47
- ? candidates.find((p) => existsSync(p)) || candidates[0] || null
48
- : candidates || null;
49
- if (!resolvedPath || !existsSync(resolvedPath)) {
95
+ const resolved = resolveCodexPath();
96
+ const candidates = Array.isArray(resolved)
97
+ ? resolved
98
+ : resolved
99
+ ? [resolved]
100
+ : [];
101
+ const hit = this.cachedRaw(candidates);
102
+ if (hit)
103
+ return hit;
104
+ const index = candidates.findIndex((candidate) => existsSync(candidate));
105
+ const indexed = index >= 0 ? candidates[index] : undefined;
106
+ const sourcePath = indexed ?? candidates[0] ?? null;
107
+ if (index < 0 || !sourcePath) {
108
+ this.codexCache = null;
50
109
  await frameworkLogger.log(this.component, 'codex-not-found', 'warning', {
51
110
  candidates,
52
- resolvedPath,
111
+ resolvedPath: sourcePath,
53
112
  message: 'No codex.json found in standard locations; using fallback behavior',
54
113
  });
55
- return { path: resolvedPath, data: this.getBuiltinFallback(), isFallback: true };
114
+ return { path: sourcePath, data: this.getBuiltinFallback(), isFallback: true };
56
115
  }
57
116
  try {
58
- const content = await fs.readFile(resolvedPath, 'utf-8');
117
+ const before = this.fileIdentity(sourcePath);
118
+ const content = await fs.readFile(sourcePath, 'utf-8');
59
119
  const data = JSON.parse(content);
60
120
  const termCount = this.computeTermCount(data);
121
+ const raw = { path: sourcePath, data, isFallback: false };
122
+ const after = this.fileIdentity(sourcePath);
123
+ if (before && after && before === after) {
124
+ this.codexCache = {
125
+ candidateKey: this.candidateKey(candidates),
126
+ index,
127
+ sourcePath,
128
+ identity: after,
129
+ raw,
130
+ };
131
+ }
132
+ else {
133
+ this.codexCache = null;
134
+ }
61
135
  await frameworkLogger.log(this.component, 'codex-loaded', 'success', {
62
- source: resolvedPath,
136
+ source: sourcePath,
63
137
  version: data?.version || 'unknown',
64
138
  termCount,
65
139
  lastUpdated: data?.lastUpdated,
66
140
  });
67
- return { path: resolvedPath, data, isFallback: false };
141
+ return raw;
68
142
  }
69
143
  catch (error) {
144
+ this.codexCache = null;
70
145
  const msg = error instanceof Error ? error.message : String(error);
71
146
  await frameworkLogger.log(this.component, 'codex-load-error', 'error', {
72
- source: resolvedPath,
147
+ source: sourcePath,
73
148
  error: msg,
74
149
  });
75
150
  // Safe fallback (never break callers)
76
- return { path: resolvedPath, data: this.getBuiltinFallback(), isFallback: true };
151
+ return { path: sourcePath, data: this.getBuiltinFallback(), isFallback: true };
77
152
  }
78
153
  }
79
154
  computeTermCount(data) {
@@ -121,7 +196,7 @@ export class CodexPolicyService {
121
196
  ? 'Governance CodexPolicyService — builtin fallback (no external codex.json resolved)'
122
197
  : 'Returned via Governance CodexPolicyService — V2 Single Source of Truth (S02-REAL)',
123
198
  dynamo_required: true,
124
- ...(includeRaw ? { codex: data } : {}),
199
+ ...(includeRaw ? { codex: structuredClone(data) } : {}),
125
200
  };
126
201
  await frameworkLogger.log(this.component, 'get-current-codex', 'info', {
127
202
  source: path,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Grok SessionStart / UserPromptSubmit / PreCompact / PostCompact.
4
- * Grok ignores stdout here; station heat is session-boot.json + STATION.md.
4
+ * Compact prints nothing. Station heat is session-boot.json + STATION.md.
5
5
  */
6
6
 
7
7
  import {
@@ -148,9 +148,10 @@ async function main() {
148
148
  : '0xray/grok-session-start';
149
149
 
150
150
  const intent = extractIntent(event);
151
- const matchedSignals =
152
- HOOK_EVENT === 'user_prompt_submit' ? [] : await matchStationSignals(eventRoot, intent);
153
151
  const compact = COMPACT_EVENTS.has(HOOK_EVENT);
152
+ // The compact payload is the whole conversation. Do not match it and do not print it.
153
+ const matchedSignals =
154
+ compact || HOOK_EVENT === 'user_prompt_submit' ? [] : await matchStationSignals(eventRoot, intent);
154
155
  let freshnessLine = null;
155
156
  if (!compact) {
156
157
  try {
@@ -164,8 +165,7 @@ async function main() {
164
165
  const payload = buildSessionBootPayload(eventRoot, source, {
165
166
  hookEvent: HOOK_EVENT,
166
167
  sessionId: event.sessionId || process.env.GROK_SESSION_ID || null,
167
- ...(intent ? { intent } : {}),
168
- ...(!compact && intent ? { cardText: intent } : {}),
168
+ ...(!compact && intent ? { intent, cardText: intent } : {}),
169
169
  ...(matchedSignals.length ? { matchedSignals } : {}),
170
170
  ...(freshnessLine ? { freshnessLine } : {}),
171
171
  });
@@ -195,7 +195,7 @@ async function main() {
195
195
  /* non-blocking pipeline facets */
196
196
  }
197
197
 
198
- console.log(JSON.stringify(payload));
198
+ if (!compact) process.stdout.write(`${JSON.stringify(payload)}\n`);
199
199
  process.exit(0);
200
200
  } catch (err) {
201
201
  appendHookActivity(root, 'grok-session-start', 'session-boot-error', 'error', {
@@ -4,7 +4,11 @@
4
4
  * The view keeps every field for the next look.
5
5
  * A held plane stays quiet. A leave is denied.
6
6
  * On this host a search is grep, glob, or a shell that runs one.
7
+ * A shell that reads a project source file is a search.
7
8
  * The lens file stays open. One later read may go on, then the next read stops.
9
+ * An opened source file that no lens lists is remembered.
10
+ * The next search stops until that file is on a lens and its plate is on disk.
11
+ * The hook does not author the plate.
8
12
  */
9
13
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
10
14
  import { createRequire } from 'node:module';
@@ -14,7 +18,7 @@ import { fileURLToPath } from 'node:url';
14
18
  const FIELD_ORDER = ['plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
15
19
  const HERE = dirname(fileURLToPath(import.meta.url));
16
20
  const WORN_PLANES = ['dichotomy', 'syncopate', 'synthesis', 'digest', 'triage', 'loop'];
17
- const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting'];
21
+ const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page'];
18
22
  const CARD_FLAVORS = ['digest', 'triage'];
19
23
  const SCOPE_ZOOM = ['ecosystem', 'part', 'one flow', 'one artifact'];
20
24
 
@@ -58,6 +62,25 @@ function organDeny(reason) {
58
62
  return { gate: 'goggles', decision: 'deny', reason };
59
63
  }
60
64
 
65
+ function plateIdsIn(spoken) {
66
+ const ids = [];
67
+ const re = /docs-site\/docs\/plates\/([a-z0-9-]+)\.md/ig;
68
+ let match;
69
+ while ((match = re.exec(String(spoken || '')))) ids.push(match[1]);
70
+ return ids;
71
+ }
72
+
73
+ function associatedPlates(plane) {
74
+ try {
75
+ const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
76
+ const entry = data[plane];
77
+ const listed = entry && Array.isArray(entry.plates) ? entry.plates.map(String) : [];
78
+ return new Set([plane, ...listed]);
79
+ } catch {
80
+ return new Set([plane]);
81
+ }
82
+ }
83
+
61
84
  function planeWord(name, text) {
62
85
  return new RegExp(`(?:^|[^A-Za-z0-9-])${String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:[^A-Za-z0-9-]|$)`, 'i').test(String(text || ''));
63
86
  }
@@ -75,8 +98,13 @@ export function organStop(held, action) {
75
98
  const paths = (action?.paths || []).map(String).filter(Boolean);
76
99
  const tool = String(action?.tool || '');
77
100
  const spoken = [text, ...paths].join('\n');
78
- if (/docs-site\/docs\/plates\/[a-z0-9-]+\.md/i.test(spoken)) {
79
- return organDeny(`The reading is ${plane}. That drawing is not the plane.`);
101
+ const drawn = plateIdsIn(spoken);
102
+ if (drawn.length) {
103
+ const allowed = associatedPlates(plane);
104
+ const foreign = drawn.filter((id) => !allowed.has(id));
105
+ if (foreign.length) {
106
+ return organDeny(`The reading is ${plane}. That drawing is not the plane.`);
107
+ }
80
108
  }
81
109
  const otherFile = otherPlaneFile(plane, spoken);
82
110
  if (otherFile) return organDeny(`The reading is ${plane}. The action is ${otherFile}.`);
@@ -252,6 +280,7 @@ function cardView(plane, scope, flavor, root) {
252
280
  from: plane.id === 'ground' ? '' : (plane.from || 'ground'),
253
281
  digest: plane.digest || '',
254
282
  plate: plane.plate || '',
283
+ plates: Array.isArray(plane.plates) ? plane.plates : [],
255
284
  entry: plane.entry || '',
256
285
  exit: plane.exit || '',
257
286
  files: listed,
@@ -268,12 +297,15 @@ function cardView(plane, scope, flavor, root) {
268
297
  return view;
269
298
  }
270
299
 
271
- function formatCardText(view) {
300
+ function cardRows(view) {
272
301
  const rows = [
273
302
  ['Plane', view.plane],
274
303
  ['From', view.from],
275
304
  ['Digest', view.digest],
276
305
  ['Plate', view.plate],
306
+ ];
307
+ if (Array.isArray(view.plates) && view.plates.length) rows.push(['Plates', view.plates.join(', ')]);
308
+ rows.push(
277
309
  ['Entry', view.entry],
278
310
  ['Exit', view.exit],
279
311
  ['Files', view.files.join(', ')],
@@ -281,7 +313,12 @@ function formatCardText(view) {
281
313
  ['Setup', view.setup],
282
314
  ['Teardown', view.teardown],
283
315
  ['Worn', view.worn],
284
- ];
316
+ );
317
+ return rows;
318
+ }
319
+
320
+ function formatCardText(view) {
321
+ const rows = cardRows(view);
285
322
  let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
286
323
  if (view.flavor === 'triage') {
287
324
  const lines = [];
@@ -295,19 +332,7 @@ function formatCardText(view) {
295
332
  }
296
333
 
297
334
  export function formatCardPane(view) {
298
- const rows = [
299
- ['Plane', view.plane],
300
- ['From', view.from],
301
- ['Digest', view.digest],
302
- ['Plate', view.plate],
303
- ['Entry', view.entry],
304
- ['Exit', view.exit],
305
- ['Files', view.files.join(', ')],
306
- ['Skills', view.skills],
307
- ['Setup', view.setup],
308
- ['Teardown', view.teardown],
309
- ['Worn', view.worn],
310
- ];
335
+ const rows = cardRows(view);
311
336
  if (view.flavor === 'triage') {
312
337
  if (Array.isArray(view.empty) && view.empty.length) rows.push(['Empty', view.empty.join(', ')]);
313
338
  if (view.exam) rows.push(['', view.exam]);
@@ -524,6 +549,7 @@ export function assemblePlane(id, platesDir) {
524
549
  files,
525
550
  unpathed: extra.unpathed || null,
526
551
  skills: extra.skills || '',
552
+ plates: Array.isArray(extra.plates) ? extra.plates.map(String) : [],
527
553
  setup: extra.setup || sectionOf(src.body, 'Setup') || (flow && flow.setup) || '',
528
554
  teardown: extra.teardown || sectionOf(src.body, 'Teardown') || (flow && flow.teardown) || '',
529
555
  worn: extra.worn || '',
@@ -746,11 +772,117 @@ function opensLensFile(text) {
746
772
  return mapFiles().some((file) => spoken.includes(file));
747
773
  }
748
774
 
775
+ const SOURCE_FILE = /(?:^|[^A-Za-z0-9_])((?:src|scripts|docs-site|\.agents|grok-bot)\/[\w./-]*\.[A-Za-z0-9]+)(?=$|[^A-Za-z0-9_./-])/g;
776
+ const SOURCE_READER = /(?:^|[^A-Za-z0-9_])(?:python3|node|cat|head|tail|sed|less|more|bat|awk)(?=$|[^A-Za-z0-9_])/;
777
+ const STAMP_TARGET = /docs-site\/docs\/plates\/[a-z0-9-]+\.md|goggles-planes\.json|\.agents\/skills\/[a-z0-9-]+\/SKILL\.md|src\/memory-routing\/plates\.ts|src\/integrations\/hooks\/plates\.cjs|docs-site\/sidebars\.ts|docs-site\/docs\/plates\/index\.md/;
778
+
779
+ function isStampPath(file) {
780
+ return STAMP_TARGET.test(String(file || '').replace(/\\/g, '/'));
781
+ }
782
+
783
+ function openedSourcePaths(text) {
784
+ const spoken = `\n${String(text || '').replace(/\\/g, '/')}`;
785
+ const found = [];
786
+ for (const match of spoken.matchAll(SOURCE_FILE)) {
787
+ const file = match[1];
788
+ if (!file || isStampPath(file) || found.includes(file)) continue;
789
+ found.push(file);
790
+ }
791
+ return found;
792
+ }
793
+
794
+ function shellReadsSource(text) {
795
+ const spoken = String(text || '');
796
+ return SOURCE_READER.test(spoken) && openedSourcePaths(spoken).length > 0;
797
+ }
798
+
799
+ function isLensWrite(toolName) {
800
+ return /write|edit|replace/i.test(String(toolName || ''));
801
+ }
802
+
803
+ function debtFile(root) {
804
+ return join(root, '.xray', 'state', 'goggles-lens-debt.json');
805
+ }
806
+
807
+ function loadDebt(root) {
808
+ try {
809
+ const data = JSON.parse(readFileSync(debtFile(root), 'utf8'));
810
+ return Array.isArray(data.paths) ? data.paths.filter((file) => typeof file === 'string') : [];
811
+ } catch {
812
+ return [];
813
+ }
814
+ }
815
+
816
+ function saveDebt(root, paths) {
817
+ const unique = [];
818
+ for (const file of paths) {
819
+ if (!unique.includes(file)) unique.push(file);
820
+ }
821
+ const dest = debtFile(root);
822
+ if (!unique.length) {
823
+ try {
824
+ unlinkSync(dest);
825
+ } catch {
826
+ /* already clear */
827
+ }
828
+ return;
829
+ }
830
+ mkdirSync(dirname(dest), { recursive: true });
831
+ writeFileSync(dest, `${JSON.stringify({ paths: unique })}\n`);
832
+ }
833
+
834
+ function lensRecords() {
835
+ try {
836
+ const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
837
+ const records = [];
838
+ if (!data || typeof data !== 'object') return records;
839
+ for (const [name, entry] of Object.entries(data)) {
840
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
841
+ if (!Array.isArray(entry.files)) continue;
842
+ records.push([name, entry.files.filter((file) => typeof file === 'string')]);
843
+ }
844
+ return records;
845
+ } catch {
846
+ return [];
847
+ }
848
+ }
849
+
850
+ function plateIsOnDisk(root, name) {
851
+ const beside = findPlatesDir(HERE);
852
+ if (beside && existsSync(join(beside, `${name}.md`))) return true;
853
+ return existsSync(join(root, 'docs-site', 'docs', 'plates', `${name}.md`));
854
+ }
855
+
856
+ function lensCovers(root, file) {
857
+ for (const [name, files] of lensRecords()) {
858
+ if (!files.includes(file)) continue;
859
+ if (name === 'ground' || plateIsOnDisk(root, name)) return true;
860
+ }
861
+ return false;
862
+ }
863
+
864
+ function unsettledDebt(root) {
865
+ const prior = loadDebt(root);
866
+ const kept = prior.filter((file) => !lensCovers(root, file));
867
+ if (kept.length !== prior.length) saveDebt(root, kept);
868
+ return kept;
869
+ }
870
+
871
+ function rememberOpened(root, text) {
872
+ const fresh = openedSourcePaths(text).filter((file) => !lensCovers(root, file));
873
+ if (!fresh.length) return;
874
+ saveDebt(root, [...loadDebt(root), ...fresh]);
875
+ }
876
+
877
+ function debtReason(paths) {
878
+ return `Opened ${paths.join(', ')} has no lens.`;
879
+ }
880
+
749
881
  function researchCall(toolName, text) {
750
882
  const tool = String(toolName || '');
751
883
  const spoken = String(text || '');
752
884
  if (/researcher|explorer|deep[- ]?research/i.test(tool) || DEEP_SEARCH.test(tool)) return true;
753
- if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken))) return true;
885
+ if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken) || shellReadsSource(spoken))) return true;
754
886
  if (/^(grep|glob)$/i.test(tool)) return true;
755
887
  return /^read_file$/i.test(tool) && !opensLensFile(spoken);
756
888
  }
@@ -807,24 +939,33 @@ function stampPlane(root, id) {
807
939
  }
808
940
  }
809
941
 
810
- /** A search stays stopped. The lens file stays open. One later read may go on, then the next read stops. */
942
+ /** Name one plane and the tool continues. Name none, or more than one, and the search stops. */
811
943
  export function lensBeforeResearch(root, toolName, text) {
812
944
  const spoken = String(text || '');
813
- if (!researchCall(toolName, spoken)) return null;
945
+ const openDebt = unsettledDebt(root);
946
+ const research = researchCall(toolName, spoken);
947
+ if (openDebt.length && research) {
948
+ return { gate: 'lens', decision: 'deny', reason: debtReason(openDebt) };
949
+ }
950
+ if (openDebt.length && isLensWrite(toolName) && !isStampPath(spoken)) {
951
+ return { gate: 'lens', decision: 'deny', reason: debtReason(openDebt) };
952
+ }
953
+ if (!research) return null;
814
954
  const names = CARD_PLANES.filter((name) => intentWords(spoken).includes(name));
955
+ if (names.length === 1) {
956
+ stampPlane(root, names[0]);
957
+ rememberOpened(root, spoken);
958
+ return null;
959
+ }
815
960
  if (/^read_file$/i.test(String(toolName || ''))) {
816
961
  if (loadReadPass(root) === 'armed') {
817
962
  saveReadPass(root, true);
818
963
  return null;
819
964
  }
820
- if (loadReadPass(root) !== 'spent') {
821
- if (names.length === 1) stampPlane(root, names[0]);
822
- saveReadPass(root, false);
823
- }
824
- return { gate: 'lens', decision: 'deny', reason: lensPage(root, names) };
965
+ if (loadReadPass(root) !== 'spent') saveReadPass(root, false);
966
+ return { gate: 'lens', decision: 'deny', reason: 'Name one plane.' };
825
967
  }
826
- if (names.length === 1) stampPlane(root, names[0]);
827
- return { gate: 'lens', decision: 'deny', reason: lensPage(root, names) };
968
+ return { gate: 'lens', decision: 'deny', reason: 'Name one plane.' };
828
969
  }
829
970
 
830
971
  function seenFields(plane, root) {