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.
- package/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/dist/CHANGELOG.md +28 -0
- package/dist/analytics/emerging-pattern-detector.d.ts +12 -1
- package/dist/analytics/emerging-pattern-detector.js +37 -12
- package/dist/analytics/pattern-learning-engine.js +1 -9
- package/dist/analytics/predictive-analytics.js +51 -33
- package/dist/analytics/prompt-pattern-analyzer.d.ts +1 -0
- package/dist/analytics/prompt-pattern-analyzer.js +39 -20
- package/dist/analytics/routing-performance-analyzer.d.ts +2 -0
- package/dist/analytics/routing-performance-analyzer.js +89 -37
- package/dist/analytics/simple-pattern-analyzer.d.ts +4 -0
- package/dist/analytics/simple-pattern-analyzer.js +66 -28
- package/dist/architect/architect-tools.d.ts +3 -0
- package/dist/architect/architect-tools.js +22 -0
- package/dist/cli/commands/grok-install.js +1 -1
- package/dist/cli/commands/hermes-install.js +11 -5
- package/dist/cli/commands/openclaw-install.js +13 -7
- package/dist/cli/commands/opencode-install.js +7 -4
- package/dist/cli/commands/suit-monitor.d.ts +30 -0
- package/dist/cli/commands/suit-monitor.js +128 -0
- package/dist/cli/index.js +52 -15
- package/dist/cli/suit-pulse.d.ts +29 -0
- package/dist/cli/suit-pulse.js +270 -0
- package/dist/delegation/agent-delegator.d.ts +4 -2
- package/dist/delegation/agent-delegator.js +46 -27
- package/dist/delegation/agent-expertise.js +2 -1
- package/dist/delegation/analytics/outcome-tracker.d.ts +6 -0
- package/dist/delegation/analytics/outcome-tracker.js +17 -3
- package/dist/delegation/ast-code-parser.d.ts +5 -0
- package/dist/delegation/ast-code-parser.js +53 -48
- package/dist/delegation/codebase-context-analyzer.d.ts +3 -7
- package/dist/delegation/codebase-context-analyzer.js +188 -159
- package/dist/delegation/session-coordinator.js +3 -2
- package/dist/enforcement/loaders/codex-loader.js +18 -14
- package/dist/enforcement/validators/architecture-validators.js +115 -9
- package/dist/governance/codex-policy.service.d.ts +12 -2
- package/dist/governance/codex-policy.service.js +91 -16
- package/dist/integrations/grok/hooks/session-start.js +6 -6
- package/dist/integrations/hooks/goggles-pipeline.mjs +169 -28
- package/dist/integrations/hooks/goggles-planes.json +172 -8
- package/dist/integrations/hooks/plates.cjs +138 -0
- package/dist/integrations/hooks/station-hook-runtime.cjs +63 -5
- package/dist/integrations/hooks/write-home.cjs +59 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/mcps/architect-tools.server.d.ts +6 -0
- package/dist/mcps/architect-tools.server.js +19 -18
- package/dist/mcps/orchestrator/server.d.ts +4 -3
- package/dist/mcps/orchestrator/server.js +29 -17
- package/dist/memory-routing/lookup-cache.d.ts +8 -0
- package/dist/memory-routing/lookup-cache.js +72 -0
- package/dist/memory-routing/plates.d.ts +1 -1
- package/dist/memory-routing/plates.js +26 -0
- package/dist/memory-routing/provider-loader.js +3 -0
- package/dist/metrics/agent-metrics.d.ts +14 -5
- package/dist/metrics/agent-metrics.js +362 -224
- package/dist/nucleus/synthesis-consult-receipt.js +38 -16
- package/dist/orchestrator/enhanced-multi-agent-orchestrator.d.ts +5 -0
- package/dist/orchestrator/enhanced-multi-agent-orchestrator.js +9 -0
- package/dist/orchestrator/multi-agent-orchestration-coordinator.js +1 -2
- package/dist/orchestrator/orchestrator.js +1 -2
- package/dist/processors/implementations/agents-md-validation-processor.d.ts +0 -1
- package/dist/processors/implementations/agents-md-validation-processor.js +0 -11
- package/dist/processors/implementations/console-log-guard-processor.js +39 -50
- package/dist/processors/implementations/performance-budget-processor.d.ts +0 -5
- package/dist/processors/implementations/performance-budget-processor.js +29 -38
- package/dist/processors/implementations/storytelling-trigger-processor.d.ts +5 -0
- package/dist/processors/implementations/storytelling-trigger-processor.js +69 -16
- package/dist/processors/implementations/test-auto-creation-processor.js +39 -36
- package/dist/reporting/framework-reporting-system.d.ts +3 -0
- package/dist/reporting/framework-reporting-system.js +61 -8
- package/dist/reporting/log-parser.js +188 -105
- package/dist/reporting/metrics.js +42 -18
- package/dist/security/prompt-security-validator.js +8 -7
- package/dist/security/security-auditor.d.ts +4 -0
- package/dist/security/security-auditor.js +132 -57
- package/dist/security/security-hardener.d.ts +2 -0
- package/dist/security/security-hardener.js +31 -6
- package/dist/security/security-headers.d.ts +2 -0
- package/dist/security/security-headers.js +23 -21
- package/dist/security/security-scanner.js +18 -4
- package/dist/session/session-cleanup-manager.d.ts +2 -0
- package/dist/session/session-cleanup-manager.js +29 -8
- package/dist/session/session-monitor.d.ts +14 -4
- package/dist/session/session-monitor.js +208 -106
- package/dist/session/session-state-manager.d.ts +11 -0
- package/dist/session/session-state-manager.js +90 -8
- package/docs-site/docs/plates/activity-log.md +38 -0
- package/docs-site/docs/plates/cursor-compact.md +35 -0
- package/docs-site/docs/plates/grok-compact.md +35 -0
- package/docs-site/docs/plates/index.md +25 -3
- package/docs-site/docs/plates/inference-files.md +35 -0
- package/docs-site/docs/plates/kept-line.md +35 -0
- package/docs-site/docs/plates/lens-gate.md +35 -0
- package/docs-site/docs/plates/lens-page.md +35 -0
- package/docs-site/docs/plates/notes-page.md +35 -0
- package/docs-site/docs/plates/package-face.md +35 -0
- package/docs-site/docs/plates/payload-heat.md +35 -0
- package/docs-site/docs/plates/pickup-stamp.md +35 -0
- package/docs-site/docs/plates/pre-tool.md +35 -0
- package/docs-site/docs/plates/record-map.md +41 -0
- package/docs-site/docs/plates/reflection-page.md +35 -0
- package/docs-site/docs/plates/review.md +57 -0
- package/docs-site/docs/plates/session-capture.md +38 -0
- package/docs-site/docs/plates/site-manual.md +35 -0
- package/docs-site/docs/plates/stamp-plate.md +35 -0
- package/docs-site/docs/plates/station-card.md +35 -0
- package/docs-site/docs/plates/station-heat.md +35 -0
- package/docs-site/docs/plates/suit-organs.md +38 -0
- package/docs-site/docs/plates/suit-settings.md +35 -0
- package/docs-site/docs/plates/suit-wear.md +37 -0
- package/docs-site/docs/plates/trail-state.md +35 -0
- package/docs-site/docs/plates/work-fresh.md +35 -0
- package/docs-site/docs/plates/write-home.md +38 -0
- package/package.json +3 -2
- package/scripts/node/install-bridges.cjs +302 -78
- package/scripts/node/postinstall.cjs +14 -3
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/architect-tools.server.ts +25 -20
- package/src/mcps/orchestrator/server.ts +33 -19
- package/xray/features.json +1 -1
- package/vendor/@0xray/repertoire/dist/RepertoireService.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/RepertoireService.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/governance/ontological-trap-enforcer.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/governance/ontological-trap-enforcer.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/index.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/index.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/groover-log-ingester.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/groover-log-parser.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/groover-log-parser.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/orchestrator-feedback-ingester.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/orchestrator-feedback-ingester.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/ingestion/xray-session-ingester.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/mcp/server.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/mcp/server.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/RepertoireOrchestratorBridge.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/capability-enhancer.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/capability-enhancer.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/confidence-gate.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-agent-affinity.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-agent-affinity.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/orchestrator-bridge/signal-injector.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/paths.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/paths.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/provider/memory-routing-provider.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/CuratedSignalsManager.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/InferenceStateManager.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/confidence-decay.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/confidence-decay.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/signal-prune.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/registry/signal-prune.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/concrete-sections.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/concrete-sections.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/meta-inference-engine.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/primitive-confidence-aggregator.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/primitive-confidence-aggregator.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/synthesis-prompt-builder.d.ts.map +0 -1
- package/vendor/@0xray/repertoire/dist/synthesis/synthesis-prompt-builder.js.map +0 -1
- package/vendor/@0xray/repertoire/dist/types.d.ts.map +0 -1
- 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 =
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
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
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
46
|
-
const
|
|
47
|
-
?
|
|
48
|
-
:
|
|
49
|
-
|
|
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:
|
|
114
|
+
return { path: sourcePath, data: this.getBuiltinFallback(), isFallback: true };
|
|
56
115
|
}
|
|
57
116
|
try {
|
|
58
|
-
const
|
|
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:
|
|
136
|
+
source: sourcePath,
|
|
63
137
|
version: data?.version || 'unknown',
|
|
64
138
|
termCount,
|
|
65
139
|
lastUpdated: data?.lastUpdated,
|
|
66
140
|
});
|
|
67
|
-
return
|
|
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:
|
|
147
|
+
source: sourcePath,
|
|
73
148
|
error: msg,
|
|
74
149
|
});
|
|
75
150
|
// Safe fallback (never break callers)
|
|
76
|
-
return { path:
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|