@manyoars/oars 0.2.0 → 0.4.0
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/README.md +147 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +10 -4
- package/dist/cli.js +440 -91
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +47 -0
- package/dist/gate/hook-process.d.ts +24 -0
- package/dist/gate/hook-process.js +112 -0
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +48 -0
- package/dist/gate/hook.js +259 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +254 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +50 -0
- package/dist/issuer.d.ts +10 -6
- package/dist/issuer.js +19 -24
- package/dist/main.d.ts +1 -0
- package/dist/main.js +67 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +429 -0
- package/dist/sessions/allow.d.ts +23 -0
- package/dist/sessions/allow.js +71 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +190 -0
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +47 -0
- package/dist/sessions/cell-increments.js +243 -0
- package/dist/sessions/cells.d.ts +109 -0
- package/dist/sessions/cells.js +261 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +745 -0
- package/dist/sessions/command.d.ts +127 -0
- package/dist/sessions/command.js +419 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +386 -0
- package/dist/sessions/exclusion.d.ts +87 -0
- package/dist/sessions/exclusion.js +361 -0
- package/dist/sessions/extract.d.ts +181 -0
- package/dist/sessions/extract.js +1806 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +66 -0
- package/dist/sessions/hook-index.js +164 -0
- package/dist/sessions/hooks.d.ts +17 -0
- package/dist/sessions/hooks.js +281 -0
- package/dist/sessions/identity.d.ts +27 -0
- package/dist/sessions/identity.js +131 -0
- package/dist/sessions/install.d.ts +70 -0
- package/dist/sessions/install.js +246 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +7 -0
- package/dist/sessions/mcp.js +157 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +44 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +32 -0
- package/dist/sessions/package-resolve.js +185 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +115 -0
- package/dist/sessions/pool-command.js +669 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +76 -0
- package/dist/sessions/pool-outbox.js +313 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +435 -0
- package/dist/sessions/pool-wire.js +1100 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +72 -0
- package/dist/sessions/public-vocabulary.js +295 -0
- package/dist/sessions/query.d.ts +123 -0
- package/dist/sessions/query.js +268 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +48 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +86 -0
- package/dist/sessions/read.js +87 -0
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +139 -0
- package/dist/sessions/recall-object.js +78 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +47 -0
- package/dist/sessions/recall-reads.d.ts +124 -0
- package/dist/sessions/recall-reads.js +417 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +17 -0
- package/dist/sessions/redact.js +113 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +694 -0
- package/dist/sessions/schema.js +167 -0
- package/dist/sessions/session-shape.d.ts +76 -0
- package/dist/sessions/session-shape.js +355 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +233 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +309 -0
- package/dist/sessions/store.js +1498 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +27 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +494 -0
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export declare const RETENTION_FLOOR = 365;
|
|
2
|
+
export declare const DEFAULT_RETENTION = 30;
|
|
3
|
+
export declare const TASK_NAME = "OarsSessionsNightly";
|
|
4
|
+
/** What the install needs from the machine, so a test can stand in for each. */
|
|
5
|
+
export interface InstallRuntime {
|
|
6
|
+
/** `process.platform`: only `win32` has the nightly task. */
|
|
7
|
+
platform: NodeJS.Platform;
|
|
8
|
+
nodePath: string;
|
|
9
|
+
/** Absolute path of the built `hook-bin.js`. */
|
|
10
|
+
hookBin: string;
|
|
11
|
+
/** `<claude config dir>/settings.json`. */
|
|
12
|
+
userSettings: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
/** Runs `schtasks` with these arguments. */
|
|
15
|
+
schtasks: (args: string[]) => Promise<{
|
|
16
|
+
code: number;
|
|
17
|
+
output: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export type Scope = 'user' | 'project' | 'file';
|
|
21
|
+
/** A settings file is open-ended JSON the harness owns; only the keys named here are read or written. */
|
|
22
|
+
interface Json {
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The post-tool command reads the event first in the hook shell (Git Bash on Windows, where the harness runs hooks). For a shell tool it starts node only
|
|
27
|
+
* when the failure text carries `Exit code` (a refusal or an interrupt has none, and a fresh node costs 250 ms to seconds on Windows, the shell builtins a few
|
|
28
|
+
* milliseconds); any other tool's failure goes straight to node, because its text has no such line and its recovery read does not need one.
|
|
29
|
+
*/
|
|
30
|
+
export declare const commandOf: (runtime: InstallRuntime, arg: string) => string;
|
|
31
|
+
export declare const settingsPath: (runtime: InstallRuntime, scope: Scope, file: string | undefined) => string;
|
|
32
|
+
export declare const readSettings: (path: string) => Promise<Json>;
|
|
33
|
+
/** The retention the file sets: its `cleanupPeriodDays` when a number, else the harness default. */
|
|
34
|
+
export declare const effectiveRetention: (settings: Json) => number;
|
|
35
|
+
export interface InstallResult {
|
|
36
|
+
file: string;
|
|
37
|
+
installed: string[];
|
|
38
|
+
retention: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const installHooks: (runtime: InstallRuntime, path: string, keepRetention: boolean) => Promise<InstallResult>;
|
|
41
|
+
export declare const uninstallHooks: (path: string) => Promise<{
|
|
42
|
+
file: string;
|
|
43
|
+
removed: string[];
|
|
44
|
+
}>;
|
|
45
|
+
export interface FileStatus {
|
|
46
|
+
file: string;
|
|
47
|
+
scope: Scope;
|
|
48
|
+
installed: string[];
|
|
49
|
+
retentionDays: number;
|
|
50
|
+
}
|
|
51
|
+
export declare const hookStatus: (runtime: InstallRuntime, extra: string | undefined) => Promise<FileStatus[]>;
|
|
52
|
+
/** The `schtasks` arguments for the nightly task: the same node and hook-bin the hooks use, at a fixed hour. */
|
|
53
|
+
export declare const scheduleArgs: (runtime: InstallRuntime, hour: number) => string[];
|
|
54
|
+
export declare const SCHEDULE_HOUR = 3;
|
|
55
|
+
export declare const setSchedule: (runtime: InstallRuntime, mode: "nightly" | "off") => Promise<string>;
|
|
56
|
+
/** True once the operator ran `--schedule off` and has not run `--schedule nightly` since. A record that cannot be read or parsed counts as no opt-out. */
|
|
57
|
+
export declare const scheduleOptedOut: (storeDir: string) => Promise<boolean>;
|
|
58
|
+
/** Records the choice of `--schedule`: `off` writes the opt-out; `nightly` clears it. Without a store the choice has nowhere to live and is not recorded. */
|
|
59
|
+
export declare const recordSchedule: (storeDir: string | undefined, mode: "nightly" | "off") => Promise<void>;
|
|
60
|
+
/** What a plain `hook install` says when the operator turned the nightly task off: it is left alone. */
|
|
61
|
+
export declare const SCHEDULE_LEFT_OFF = "Scheduled task OarsSessionsNightly: left off, as you chose with --schedule off. Turn it back on with: oars sessions hook install --schedule nightly";
|
|
62
|
+
/**
|
|
63
|
+
* What a plain `hook install` does about the nightly task on Windows: register it unless it is already there. Registration is
|
|
64
|
+
* best effort: the hooks are installed by then, so a machine that refuses the task (policy, no Task Scheduler) is told how to retry
|
|
65
|
+
* and is not failed. `install --schedule off` removes the task and records the opt-out (`recordSchedule`), which the caller checks
|
|
66
|
+
* (`scheduleOptedOut`) before calling this; `--schedule nightly` clears it.
|
|
67
|
+
*/
|
|
68
|
+
export declare const ensureSchedule: (runtime: InstallRuntime) => Promise<string>;
|
|
69
|
+
export declare const scheduleStatus: (runtime: InstallRuntime) => Promise<string>;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// `oars sessions hook install|uninstall|status`: writes the four Claude Code hook entries into one settings file, merging with what is
|
|
2
|
+
// there. An entry is ours when its command ends with `hook-bin.js" <session-start|pre-tool|post-tool|stop>`, so uninstall removes those entries and nothing else. Also the retention floor
|
|
3
|
+
// (the harness deletes raw transcripts after `cleanupPeriodDays`, 30 when unset) and the Windows nightly task.
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
7
|
+
import { ACTION_TABLES } from './actions.js';
|
|
8
|
+
export const RETENTION_FLOOR = 365;
|
|
9
|
+
export const DEFAULT_RETENTION = 30;
|
|
10
|
+
export const TASK_NAME = 'OarsSessionsNightly';
|
|
11
|
+
// The tail of a command this module wrote; anything else in a settings file is the operator's, whatever it is called.
|
|
12
|
+
const OWN_COMMAND = /hook-bin\.js" (?:session-start|prompt|pre-tool|post-tool|stop)$/;
|
|
13
|
+
const SPECS = [
|
|
14
|
+
{ event: 'SessionStart', arg: 'session-start', timeout: 5 },
|
|
15
|
+
// The split-positions table again, only when it changed since this session's last one (`hooks.ts`); a prompt with nothing new costs one node start and prints nothing.
|
|
16
|
+
{ event: 'UserPromptSubmit', arg: 'prompt', timeout: 5 },
|
|
17
|
+
// Every tool (no matcher): the push is gated by the index's fit floors and lift, not by the tool's name, so a sharp read on any tool can fire. The cost is one node start per call.
|
|
18
|
+
{ event: 'PreToolUse', arg: 'pre-tool', timeout: 5 },
|
|
19
|
+
// A failed call arrives as PostToolUseFailure (PostToolUse itself fires on success only); a shell tool's `error` starts "Exit code N". Every tool the action table maps can fail into a recovery read, so the matcher is the table's names and the MCP pattern, not the two shells.
|
|
20
|
+
{ event: 'PostToolUseFailure', arg: 'post-tool', timeout: 5, matcher: `${Object.keys(ACTION_TABLES['claude-code'].rows).filter((name) => !name.startsWith('mcp__')).join('|')}|mcp__.*` },
|
|
21
|
+
// SessionEnd fires once per session; Stop fires after every assistant turn and would re-extract the growing transcript each time.
|
|
22
|
+
{ event: 'SessionEnd', arg: 'stop', timeout: 30 },
|
|
23
|
+
];
|
|
24
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
/**
|
|
26
|
+
* The post-tool command reads the event first in the hook shell (Git Bash on Windows, where the harness runs hooks). For a shell tool it starts node only
|
|
27
|
+
* when the failure text carries `Exit code` (a refusal or an interrupt has none, and a fresh node costs 250 ms to seconds on Windows, the shell builtins a few
|
|
28
|
+
* milliseconds); any other tool's failure goes straight to node, because its text has no such line and its recovery read does not need one.
|
|
29
|
+
*/
|
|
30
|
+
export const commandOf = (runtime, arg) => {
|
|
31
|
+
const launch = `"${runtime.nodePath}" "${runtime.hookBin}" ${arg}`;
|
|
32
|
+
// Fails open: a settings file outlives the build it points at (a cloud worker, a clone with no build), and a hook that cannot start must not fail the session.
|
|
33
|
+
const present = `[ -f "${runtime.hookBin}" ] || exit 0; `;
|
|
34
|
+
if (arg !== 'post-tool')
|
|
35
|
+
return `${present}${launch}`;
|
|
36
|
+
return `${present}IFS= read -r event; case "$event" in '') exit 0;; *'"tool_name":"Bash"'*|*'"tool_name":"PowerShell"'*) case "$event" in *'Exit code'*) ;; *) exit 0;; esac;; esac; { printf '%s\\n' "$event"; cat; } | ${launch}`;
|
|
37
|
+
};
|
|
38
|
+
const isOwnHook = (hook) => isObject(hook) && typeof hook.command === 'string' && OWN_COMMAND.test(hook.command);
|
|
39
|
+
/** The groups with our entries taken out; a group that held only ours goes, one that held others keeps them. */
|
|
40
|
+
const withoutOwn = (groups) => groups.flatMap((group) => {
|
|
41
|
+
if (!isObject(group) || !Array.isArray(group.hooks) || !group.hooks.some(isOwnHook))
|
|
42
|
+
return [group];
|
|
43
|
+
const rest = group.hooks.filter((hook) => !isOwnHook(hook));
|
|
44
|
+
return rest.length === 0 ? [] : [{ ...group, hooks: rest }];
|
|
45
|
+
});
|
|
46
|
+
const hasOwn = (groups) => groups.some((group) => isObject(group) && Array.isArray(group.hooks) && group.hooks.some(isOwnHook));
|
|
47
|
+
export const settingsPath = (runtime, scope, file) => {
|
|
48
|
+
if (scope === 'user')
|
|
49
|
+
return runtime.userSettings;
|
|
50
|
+
if (scope === 'project')
|
|
51
|
+
return join(runtime.cwd, '.claude', 'settings.json');
|
|
52
|
+
if (file === undefined) {
|
|
53
|
+
throw new CliError('--scope file needs --file <settings path>; nothing is written to a settings file you have not named.', ExitCode.USAGE, 'usage');
|
|
54
|
+
}
|
|
55
|
+
return resolve(runtime.cwd, file);
|
|
56
|
+
};
|
|
57
|
+
export const readSettings = async (path) => {
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = await readFile(path, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof Error && Reflect.get(error, 'code') === 'ENOENT')
|
|
64
|
+
return {};
|
|
65
|
+
throw new CliError(`Could not read ${path} (${error instanceof Error ? error.message : String(error)}); nothing was changed.`, ExitCode.USAGE, 'usage');
|
|
66
|
+
}
|
|
67
|
+
if (raw.trim() === '')
|
|
68
|
+
return {};
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new CliError(`${path} is not valid JSON; fix or remove it, nothing was changed.`, ExitCode.USAGE, 'usage');
|
|
75
|
+
}
|
|
76
|
+
if (!isObject(parsed))
|
|
77
|
+
throw new CliError(`${path} is not a JSON object; nothing was changed.`, ExitCode.USAGE, 'usage');
|
|
78
|
+
return parsed;
|
|
79
|
+
};
|
|
80
|
+
const writeSettings = async (path, settings) => {
|
|
81
|
+
await mkdir(dirname(path), { recursive: true });
|
|
82
|
+
await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`);
|
|
83
|
+
};
|
|
84
|
+
const hooksOf = (settings) => (isObject(settings.hooks) ? settings.hooks : {});
|
|
85
|
+
const groupsOf = (hooks, event) => {
|
|
86
|
+
const groups = hooks[event];
|
|
87
|
+
return Array.isArray(groups) ? groups : [];
|
|
88
|
+
};
|
|
89
|
+
/** The retention the file sets: its `cleanupPeriodDays` when a number, else the harness default. */
|
|
90
|
+
export const effectiveRetention = (settings) => (typeof settings.cleanupPeriodDays === 'number' ? settings.cleanupPeriodDays : DEFAULT_RETENTION);
|
|
91
|
+
/** Events an earlier build registered our entries under; install and uninstall clear them so a stale entry cannot keep running. */
|
|
92
|
+
const LEGACY_EVENTS = ['Stop'];
|
|
93
|
+
/** Takes our entries out of `event`, dropping the key when that empties it. */
|
|
94
|
+
const clearLegacy = (hooks, event) => {
|
|
95
|
+
const groups = groupsOf(hooks, event);
|
|
96
|
+
if (!hasOwn(groups))
|
|
97
|
+
return false;
|
|
98
|
+
const kept = withoutOwn(groups);
|
|
99
|
+
if (kept.length === 0)
|
|
100
|
+
Reflect.deleteProperty(hooks, event);
|
|
101
|
+
else
|
|
102
|
+
hooks[event] = kept;
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
export const installHooks = async (runtime, path, keepRetention) => {
|
|
106
|
+
const settings = await readSettings(path);
|
|
107
|
+
const hooks = hooksOf(settings);
|
|
108
|
+
for (const event of LEGACY_EVENTS)
|
|
109
|
+
clearLegacy(hooks, event);
|
|
110
|
+
for (const spec of SPECS) {
|
|
111
|
+
const others = withoutOwn(groupsOf(hooks, spec.event));
|
|
112
|
+
const entry = { type: 'command', command: commandOf(runtime, spec.arg), timeout: spec.timeout };
|
|
113
|
+
hooks[spec.event] = [...others, spec.matcher === undefined ? { hooks: [entry] } : { matcher: spec.matcher, hooks: [entry] }];
|
|
114
|
+
}
|
|
115
|
+
settings.hooks = hooks;
|
|
116
|
+
// A project or file scope overrides the user's `cleanupPeriodDays`, so a file with no number of its own takes the user's, lower or higher; that is the value reported.
|
|
117
|
+
const own = effectiveRetention(settings);
|
|
118
|
+
// Only a value the user file sets counts: its unset default must not outrank a lower value written on purpose here.
|
|
119
|
+
const userDays = path === runtime.userSettings ? undefined : (await readSettings(runtime.userSettings)).cleanupPeriodDays;
|
|
120
|
+
const userValue = typeof userDays === 'number' ? userDays : undefined;
|
|
121
|
+
// A number this file sets overrides the user's, so the user's counts only when the file sets none.
|
|
122
|
+
const inherited = typeof settings.cleanupPeriodDays === 'number' ? undefined : userValue;
|
|
123
|
+
// With no number of its own the file takes the user's, lower or higher: that is what the harness deletes by.
|
|
124
|
+
const before = inherited ?? own;
|
|
125
|
+
const shown = inherited !== undefined ? `${String(before)} (from ${runtime.userSettings})` : typeof settings.cleanupPeriodDays === 'number' ? String(before) : `${String(before)} (unset, the default)`;
|
|
126
|
+
let retention;
|
|
127
|
+
if (keepRetention) {
|
|
128
|
+
retention = `Left cleanupPeriodDays alone at ${shown} in ${path} (--keep-retention); the harness deletes raw transcripts older than that.`;
|
|
129
|
+
}
|
|
130
|
+
else if (before >= RETENTION_FLOOR) {
|
|
131
|
+
retention = `cleanupPeriodDays is ${shown} in ${path}, at or above the ${String(RETENTION_FLOOR)} floor; left as is.`;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// The floor is per target file: a user-scope value never lands in a project file, which can be committed. Without a number of its own the
|
|
135
|
+
// file reaches this branch only when the user value is below the floor too, so the floor is right there as well.
|
|
136
|
+
settings.cleanupPeriodDays = RETENTION_FLOOR;
|
|
137
|
+
retention = `Raised cleanupPeriodDays from ${shown} to ${String(RETENTION_FLOOR)} in ${path}.`;
|
|
138
|
+
}
|
|
139
|
+
await writeSettings(path, settings);
|
|
140
|
+
return { file: path, installed: SPECS.map((spec) => spec.event), retention };
|
|
141
|
+
};
|
|
142
|
+
export const uninstallHooks = async (path) => {
|
|
143
|
+
const settings = await readSettings(path);
|
|
144
|
+
const hooks = hooksOf(settings);
|
|
145
|
+
const removed = [];
|
|
146
|
+
const cleared = LEGACY_EVENTS.map((event) => clearLegacy(hooks, event)).includes(true);
|
|
147
|
+
for (const spec of SPECS) {
|
|
148
|
+
const groups = groupsOf(hooks, spec.event);
|
|
149
|
+
if (!hasOwn(groups))
|
|
150
|
+
continue;
|
|
151
|
+
const kept = withoutOwn(groups);
|
|
152
|
+
removed.push(spec.event);
|
|
153
|
+
hooks[spec.event] = kept;
|
|
154
|
+
}
|
|
155
|
+
if (removed.length > 0 || cleared) {
|
|
156
|
+
// An event this call emptied goes; an event that was already an empty list in the file is the operator's and stays.
|
|
157
|
+
const remaining = Object.entries(hooks).filter(([event, groups]) => !(removed.includes(event) && Array.isArray(groups) && groups.length === 0));
|
|
158
|
+
// `undefined` is left out of the JSON, so an emptied `hooks` key goes.
|
|
159
|
+
settings.hooks = remaining.length === 0 ? undefined : Object.fromEntries(remaining);
|
|
160
|
+
await writeSettings(path, settings);
|
|
161
|
+
}
|
|
162
|
+
return { file: path, removed };
|
|
163
|
+
};
|
|
164
|
+
export const hookStatus = async (runtime, extra) => {
|
|
165
|
+
const targets = [
|
|
166
|
+
{ file: runtime.userSettings, scope: 'user' },
|
|
167
|
+
{ file: join(runtime.cwd, '.claude', 'settings.json'), scope: 'project' },
|
|
168
|
+
...(extra === undefined ? [] : [{ file: resolve(runtime.cwd, extra), scope: 'file' }]),
|
|
169
|
+
];
|
|
170
|
+
const statuses = [];
|
|
171
|
+
for (const target of targets) {
|
|
172
|
+
const settings = await readSettings(target.file);
|
|
173
|
+
const hooks = hooksOf(settings);
|
|
174
|
+
statuses.push({ ...target, installed: SPECS.filter((spec) => hasOwn(groupsOf(hooks, spec.event))).map((spec) => spec.event), retentionDays: effectiveRetention(settings) });
|
|
175
|
+
}
|
|
176
|
+
return statuses;
|
|
177
|
+
};
|
|
178
|
+
/** The `schtasks` arguments for the nightly task: the same node and hook-bin the hooks use, at a fixed hour. */
|
|
179
|
+
export const scheduleArgs = (runtime, hour) => [
|
|
180
|
+
'/Create',
|
|
181
|
+
'/TN',
|
|
182
|
+
TASK_NAME,
|
|
183
|
+
'/TR',
|
|
184
|
+
`"${runtime.nodePath}" "${runtime.hookBin}" nightly`,
|
|
185
|
+
'/SC',
|
|
186
|
+
'DAILY',
|
|
187
|
+
'/ST',
|
|
188
|
+
`${String(hour).padStart(2, '0')}:00`,
|
|
189
|
+
'/F',
|
|
190
|
+
];
|
|
191
|
+
export const SCHEDULE_HOUR = 3;
|
|
192
|
+
const registered = () => `Scheduled task ${TASK_NAME}: the full deposit and the recurrence refresh run daily at ${String(SCHEDULE_HOUR).padStart(2, '0')}:00.`;
|
|
193
|
+
export const setSchedule = async (runtime, mode) => {
|
|
194
|
+
if (mode === 'nightly') {
|
|
195
|
+
const created = await runtime.schtasks(scheduleArgs(runtime, SCHEDULE_HOUR));
|
|
196
|
+
if (created.code !== 0)
|
|
197
|
+
throw new CliError(`Could not register the scheduled task: ${created.output.trim()}`, ExitCode.UNEXPECTED, 'unexpected');
|
|
198
|
+
return registered();
|
|
199
|
+
}
|
|
200
|
+
const deleted = await runtime.schtasks(['/Delete', '/TN', TASK_NAME, '/F']);
|
|
201
|
+
return deleted.code === 0 ? `Removed the scheduled task ${TASK_NAME}.` : `No scheduled task ${TASK_NAME} to remove.`;
|
|
202
|
+
};
|
|
203
|
+
/** Where `--schedule off` is remembered: a file of its own in the sessions store, because `state.json` is rewritten whole by every deposit. */
|
|
204
|
+
const scheduleRecord = (storeDir) => join(storeDir, 'schedule.json');
|
|
205
|
+
/** True once the operator ran `--schedule off` and has not run `--schedule nightly` since. A record that cannot be read or parsed counts as no opt-out. */
|
|
206
|
+
export const scheduleOptedOut = async (storeDir) => {
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(await readFile(scheduleRecord(storeDir), 'utf8'));
|
|
209
|
+
return typeof parsed === 'object' && parsed !== null && 'schedule' in parsed && parsed.schedule === 'off';
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/** Records the choice of `--schedule`: `off` writes the opt-out; `nightly` clears it. Without a store the choice has nowhere to live and is not recorded. */
|
|
216
|
+
export const recordSchedule = async (storeDir, mode) => {
|
|
217
|
+
if (storeDir === undefined)
|
|
218
|
+
return;
|
|
219
|
+
if (mode === 'nightly') {
|
|
220
|
+
await rm(scheduleRecord(storeDir), { force: true });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await mkdir(storeDir, { recursive: true });
|
|
224
|
+
await writeFile(scheduleRecord(storeDir), `${JSON.stringify({ schedule: 'off' })}\n`, { mode: 0o600 });
|
|
225
|
+
};
|
|
226
|
+
/** What a plain `hook install` says when the operator turned the nightly task off: it is left alone. */
|
|
227
|
+
export const SCHEDULE_LEFT_OFF = `Scheduled task ${TASK_NAME}: left off, as you chose with --schedule off. Turn it back on with: oars sessions hook install --schedule nightly`;
|
|
228
|
+
/**
|
|
229
|
+
* What a plain `hook install` does about the nightly task on Windows: register it unless it is already there. Registration is
|
|
230
|
+
* best effort: the hooks are installed by then, so a machine that refuses the task (policy, no Task Scheduler) is told how to retry
|
|
231
|
+
* and is not failed. `install --schedule off` removes the task and records the opt-out (`recordSchedule`), which the caller checks
|
|
232
|
+
* (`scheduleOptedOut`) before calling this; `--schedule nightly` clears it.
|
|
233
|
+
*/
|
|
234
|
+
export const ensureSchedule = async (runtime) => {
|
|
235
|
+
const queried = await runtime.schtasks(['/Query', '/TN', TASK_NAME]);
|
|
236
|
+
if (queried.code === 0)
|
|
237
|
+
return `Scheduled task ${TASK_NAME}: already registered.`;
|
|
238
|
+
const created = await runtime.schtasks(scheduleArgs(runtime, SCHEDULE_HOUR));
|
|
239
|
+
if (created.code !== 0)
|
|
240
|
+
return `Could not register the scheduled task ${TASK_NAME} (${created.output.trim()}); the hooks are installed. Retry with: oars sessions hook install --schedule nightly`;
|
|
241
|
+
return registered();
|
|
242
|
+
};
|
|
243
|
+
export const scheduleStatus = async (runtime) => {
|
|
244
|
+
const queried = await runtime.schtasks(['/Query', '/TN', TASK_NAME]);
|
|
245
|
+
return queried.code === 0 ? `Scheduled task ${TASK_NAME}: registered.` : `Scheduled task ${TASK_NAME}: not registered.`;
|
|
246
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Cell } from './cells.js';
|
|
2
|
+
import type { Read } from './read.js';
|
|
3
|
+
import { type Recall } from './recall-object.js';
|
|
4
|
+
export interface LineageSources {
|
|
5
|
+
recalls: readonly Recall[];
|
|
6
|
+
reads: readonly Read[];
|
|
7
|
+
cells: readonly Cell[];
|
|
8
|
+
}
|
|
9
|
+
export interface LineageHop {
|
|
10
|
+
depth: number;
|
|
11
|
+
kind: 'recall' | 'read' | 'cell' | 'evidence' | 'missing';
|
|
12
|
+
ref: string;
|
|
13
|
+
frames: string[];
|
|
14
|
+
day: string | null;
|
|
15
|
+
detail: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const walkLineage: (sources: LineageSources, start: string) => LineageHop[];
|
|
18
|
+
export declare const formatLineage: (hops: readonly LineageHop[]) => string;
|
|
19
|
+
export declare const loadLineage: (storeDir: string) => Promise<LineageSources>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// `oars sessions lineage` (recall spec Section 3): walks a recall or read back through its evidence, one line per hop with its frame range and day. Evidence must come from
|
|
2
|
+
// before the recall that cites it, which is what keeps the commons acyclic; a store whose records say otherwise is refused here, not printed as if it held. The walk also keeps
|
|
3
|
+
// a visited set, so a hand-edited file cannot loop it.
|
|
4
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
7
|
+
import { cellIdOf, UNKNOWN } from './cells.js';
|
|
8
|
+
import { poolFiles } from './pool.js';
|
|
9
|
+
import { evidencePredates, readRecalls } from './recall-object.js';
|
|
10
|
+
import { count } from './recall-render.js';
|
|
11
|
+
const FRAMES_SHOWN = 3;
|
|
12
|
+
const dayOf = (at) => at.slice(0, 10);
|
|
13
|
+
const refuse = (ref, day, limit) => new CliError(`Refused: ${ref} is from ${String(day)}, newer than the recall citing it (${limit}).`, ExitCode.INVALID, 'lineage');
|
|
14
|
+
const framesOfRecall = (recall) => (recall.frame === null ? [] : [[recall.frame.harness, recall.frame.harnessVersion ?? UNKNOWN, recall.frame.model ?? UNKNOWN].join(' ')]);
|
|
15
|
+
const framesOfEvidence = (evidence) => [...evidence.frameRange.harnesses, ...evidence.frameRange.harnessVersions, ...evidence.frameRange.models];
|
|
16
|
+
const framesOfRead = (read, cells) => {
|
|
17
|
+
const buckets = [...new Set(read.inputs.flatMap((input) => cells.get(input)?.frameBucket ?? []))].sort();
|
|
18
|
+
return buckets.length <= FRAMES_SHOWN ? buckets : [...buckets.slice(0, FRAMES_SHOWN), `+${String(buckets.length - FRAMES_SHOWN)}`];
|
|
19
|
+
};
|
|
20
|
+
const tighter = (limit, day) => (limit === null || day < limit ? day : limit);
|
|
21
|
+
export const walkLineage = (sources, start) => {
|
|
22
|
+
const recalls = new Map(sources.recalls.map((recall) => [recall.id, recall]));
|
|
23
|
+
const reads = new Map(sources.reads.map((read) => [read.readId, read]));
|
|
24
|
+
const cells = new Map(sources.cells.map((cell) => [cellIdOf(cell), cell]));
|
|
25
|
+
const known = (ref) => recalls.has(ref) || reads.has(ref) || cells.has(ref);
|
|
26
|
+
if (!known(start))
|
|
27
|
+
throw new CliError(`No recall, read, or cell in this store has the ID ${start}.`, ExitCode.INVALID, 'lineage');
|
|
28
|
+
const hops = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const walkRecall = (recall, depth, limit) => {
|
|
31
|
+
const day = dayOf(recall.at);
|
|
32
|
+
if (limit !== null && day > limit)
|
|
33
|
+
throw refuse(recall.id, day, limit);
|
|
34
|
+
if (!evidencePredates(recall))
|
|
35
|
+
throw refuse(`the evidence of recall ${recall.id}`, recall.evidence.flatMap((item) => item.lastDay ?? []).sort().at(-1) ?? recall.poolVersion, day);
|
|
36
|
+
hops.push({ depth, kind: 'recall', ref: recall.id, frames: framesOfRecall(recall), day, detail: `${recall.mode} ${recall.arm} ${recall.answer.kind}` });
|
|
37
|
+
for (const evidence of recall.evidence) {
|
|
38
|
+
if (known(evidence.ref)) {
|
|
39
|
+
walk(evidence.ref, depth + 1, tighter(limit, day));
|
|
40
|
+
}
|
|
41
|
+
else if (!seen.has(evidence.ref)) {
|
|
42
|
+
// A stand-in: the shape or signature hash a recall cites where no read was stored. The recall holds all there is to say of it.
|
|
43
|
+
seen.add(evidence.ref);
|
|
44
|
+
hops.push({ depth: depth + 1, kind: 'evidence', ref: evidence.ref, frames: framesOfEvidence(evidence), day: evidence.lastDay, detail: `${count(evidence.n, 'session')}, ${count(evidence.clusters, 'cluster')}, from ${String(evidence.firstDay)}` });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const walk = (ref, depth, limit) => {
|
|
49
|
+
if (seen.has(ref))
|
|
50
|
+
return;
|
|
51
|
+
seen.add(ref);
|
|
52
|
+
const recall = recalls.get(ref);
|
|
53
|
+
if (recall !== undefined) {
|
|
54
|
+
walkRecall(recall, depth, limit);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const read = reads.get(ref);
|
|
58
|
+
if (read !== undefined) {
|
|
59
|
+
if (limit !== null && read.asOf.day !== null && read.asOf.day > limit)
|
|
60
|
+
throw refuse(ref, read.asOf.day, limit);
|
|
61
|
+
hops.push({ depth, kind: 'read', ref, frames: framesOfRead(read, cells), day: read.asOf.day, detail: `${read.family}, order ${String(read.order)}, n ${String(read.n)}, ${count(read.clusters, 'cluster')}, basis ${read.basis}` });
|
|
62
|
+
for (const input of read.inputs)
|
|
63
|
+
walk(input, depth + 1, limit);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const cell = cells.get(ref);
|
|
67
|
+
if (cell !== undefined) {
|
|
68
|
+
// A week cell is named by its Monday, so only a week that opens after the recall is newer than it; a week that straddles the recall is checked by the read's own day, above.
|
|
69
|
+
if (limit !== null && cell.day !== UNKNOWN && cell.day > limit)
|
|
70
|
+
throw refuse(ref, cell.day, limit);
|
|
71
|
+
hops.push({ depth, kind: 'cell', ref, frames: [cell.frameBucket], day: cell.day, detail: `${String(cell.sessions)} sessions, ${String(cell.ok)} ok, ${String(cell.failed)} failed${cell.exposed ? ', exposed' : ''}` });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// `cells.jsonl` holds the current pool only, so a read kept from an older pool can cite a cell since rewritten.
|
|
75
|
+
hops.push({ depth, kind: 'missing', ref, frames: [], day: null, detail: 'not in the current pool' });
|
|
76
|
+
};
|
|
77
|
+
walk(start, 0, null);
|
|
78
|
+
return hops;
|
|
79
|
+
};
|
|
80
|
+
export const formatLineage = (hops) => hops.map((hop) => `${' '.repeat(hop.depth)}${hop.kind} ${hop.ref} | ${hop.frames.join(', ') || 'frames unknown'} | ${hop.day ?? 'day unknown'} | ${hop.detail}`).join('\n');
|
|
81
|
+
const rowsOf = async (file, isRow) => {
|
|
82
|
+
let text = '';
|
|
83
|
+
try {
|
|
84
|
+
text = await readFile(file, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
return text.split('\n').flatMap((line) => {
|
|
91
|
+
if (line === '')
|
|
92
|
+
return [];
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(line);
|
|
95
|
+
return isRow(parsed) ? [parsed] : [];
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const isRead = (value) => typeof value === 'object' && value !== null && 'readId' in value && 'inputs' in value;
|
|
103
|
+
const isCell = (value) => typeof value === 'object' && value !== null && 'motifId' in value && 'frameBucket' in value && 'grain' in value;
|
|
104
|
+
const readFiles = async (dir) => {
|
|
105
|
+
try {
|
|
106
|
+
return (await readdir(dir)).flatMap((name) => (name.endsWith('.jsonl') ? [join(dir, name)] : []));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
110
|
+
return [];
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
export const loadLineage = async (storeDir) => {
|
|
115
|
+
const files = poolFiles(storeDir);
|
|
116
|
+
const lists = await Promise.all((await readFiles(dirname(files.reads('any')))).map((file) => rowsOf(file, isRead)));
|
|
117
|
+
return { recalls: await readRecalls(storeDir), reads: lists.flat(), cells: await rowsOf(files.cells, isCell) };
|
|
118
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Emit } from '../telemetry/emit.js';
|
|
2
|
+
import { type SessionsRuntime } from './store.js';
|
|
3
|
+
export declare const TOOL_NAME = "oars_recall";
|
|
4
|
+
/** Answers one JSON-RPC message; a notification (no `id`) gets no answer. */
|
|
5
|
+
export declare const handleMessage: (runtime: SessionsRuntime, now: () => number, message: unknown, emit?: Emit) => Promise<object | undefined>;
|
|
6
|
+
/** Reads requests from `runtime.stdin` until it ends, writing one response line per request to `runtime.stdout`. */
|
|
7
|
+
export declare const serve: (runtime: SessionsRuntime, now: () => number, emit?: Emit) => Promise<void>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// `oars sessions recall --mcp`: a Model Context Protocol server over stdio (newline-delimited JSON-RPC 2.0) exposing the same
|
|
2
|
+
// query as one tool, so a running agent can look a failure up before re-solving it. Hand-rolled because the surface is three
|
|
3
|
+
// methods and the package takes no new dependency for it. Every answered call is appended to `mcp-calls.jsonl`, which is what
|
|
4
|
+
// `oars sessions measure` reads for recall calls per session.
|
|
5
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { servedPool } from './pool.js';
|
|
8
|
+
import { recallPool } from './recall-bundle.js';
|
|
9
|
+
import { noEmit } from '../telemetry/emit.js';
|
|
10
|
+
import { recallFields } from '../telemetry/fields.js';
|
|
11
|
+
import { appendRecall } from './recall-object.js';
|
|
12
|
+
import { formsRefusal, pull, pullRefusal, RECALL_FAMILIES } from './recall-reads.js';
|
|
13
|
+
import { MISS_TEXT } from './recall-render.js';
|
|
14
|
+
import { makeRedactor } from './redact.js';
|
|
15
|
+
import { tryLog } from './steps.js';
|
|
16
|
+
import { loadStore, paths, readRedactDomains } from './store.js';
|
|
17
|
+
const PROTOCOL = '2025-06-18';
|
|
18
|
+
export const TOOL_NAME = 'oars_recall';
|
|
19
|
+
const TOOL = {
|
|
20
|
+
name: TOOL_NAME,
|
|
21
|
+
description: "Look up a tool error, a file, or a prompt class in this operator's own past agent sessions before solving it again. Returns counts per read: sessions, days, harness versions, how far the evidence sits from you, and the actions that followed. Local; each call is recorded as a recall.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
error: { type: 'string', description: 'The error text, as printed.' },
|
|
26
|
+
path: { type: 'string', description: 'A file path or file name an earlier session touched.' },
|
|
27
|
+
class: { type: 'string', description: 'A prompt class: edit-verify, edit-only, shell-only, research, orchestration, explore-only, mixed, no-tools. Recovery and fit questions and the error, path, and class inputs only; any other form refuses it.' },
|
|
28
|
+
sinceDays: { type: 'integer', minimum: 1, description: 'Only errors from the last N days. Recovery and fit questions and the error, path, and class inputs only; any other form refuses it.' },
|
|
29
|
+
prefix: { type: 'array', items: { type: 'string' }, description: 'Read IDs for your steps so far: what happened from here?' },
|
|
30
|
+
entity: { type: 'string', description: 'A public name: a package, a tool, an MCP server, an action, or a model.' },
|
|
31
|
+
family: { type: 'string', description: `A read family to answer: ${RECALL_FAMILIES.join(', ')}. Only recovery and fit can be narrowed by class or sinceDays; any other family, and the prefix, entity, and text forms, count all time and refuse both. Name one form per call.` },
|
|
32
|
+
text: { type: 'string', description: 'Text to look for in the local record.' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
+
/** The forms an MCP call names, by the same tests `toQuery` reads them with. */
|
|
38
|
+
const namedForms = (args) => {
|
|
39
|
+
const source = isObj(args) ? args : {};
|
|
40
|
+
const prefix = Array.isArray(source.prefix) ? source.prefix.filter((id) => typeof id === 'string') : [];
|
|
41
|
+
return [
|
|
42
|
+
...(prefix.length > 0 ? ['prefix'] : []),
|
|
43
|
+
...(typeof source.entity === 'string' ? ['entity'] : []),
|
|
44
|
+
...(typeof source.family === 'string' ? ['family'] : []),
|
|
45
|
+
...(typeof source.text === 'string' ? ['text'] : []),
|
|
46
|
+
...(typeof source.error === 'string' || typeof source.path === 'string' ? ['error or path'] : []),
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
/** A `sinceDays` or `class` that is present but outside the schema is refused, not dropped: dropping it answers all time under a count that looks narrowed. */
|
|
50
|
+
const narrowingRefusal = (args) => {
|
|
51
|
+
const source = isObj(args) ? args : {};
|
|
52
|
+
if (Object.hasOwn(source, 'class') && typeof source.class !== 'string')
|
|
53
|
+
return 'class must be a string.';
|
|
54
|
+
const days = source.sinceDays;
|
|
55
|
+
if (Object.hasOwn(source, 'sinceDays') && !(typeof days === 'number' && Number.isInteger(days) && days > 0))
|
|
56
|
+
return 'sinceDays must be a positive integer.';
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
const toQuery = (args) => {
|
|
60
|
+
const source = isObj(args) ? args : {};
|
|
61
|
+
const narrowing = {};
|
|
62
|
+
if (typeof source.class === 'string')
|
|
63
|
+
narrowing.cls = source.class;
|
|
64
|
+
if (typeof source.sinceDays === 'number' && Number.isInteger(source.sinceDays) && source.sinceDays > 0)
|
|
65
|
+
narrowing.sinceDays = source.sinceDays;
|
|
66
|
+
const prefix = Array.isArray(source.prefix) ? source.prefix.filter((id) => typeof id === 'string') : [];
|
|
67
|
+
if (prefix.length > 0)
|
|
68
|
+
return { form: 'pattern', prefix, slice: narrowing };
|
|
69
|
+
if (typeof source.entity === 'string')
|
|
70
|
+
return { form: 'entity', entity: source.entity, slice: narrowing };
|
|
71
|
+
if (typeof source.family === 'string')
|
|
72
|
+
return { form: 'question', family: source.family, slice: narrowing };
|
|
73
|
+
if (typeof source.text === 'string')
|
|
74
|
+
return { form: 'text', text: source.text, slice: narrowing };
|
|
75
|
+
const query = {};
|
|
76
|
+
if (typeof source.error === 'string')
|
|
77
|
+
query.error = source.error;
|
|
78
|
+
if (typeof source.path === 'string')
|
|
79
|
+
query.path = source.path;
|
|
80
|
+
return { ...query, ...narrowing };
|
|
81
|
+
};
|
|
82
|
+
const reply = (id, result) => ({ jsonrpc: '2.0', id, result });
|
|
83
|
+
const failure = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
84
|
+
/** Answers one JSON-RPC message; a notification (no `id`) gets no answer. */
|
|
85
|
+
export const handleMessage = async (runtime, now, message, emit = noEmit) => {
|
|
86
|
+
if (!isObj(message) || typeof message.method !== 'string')
|
|
87
|
+
return failure(isObj(message) ? message.id : null, -32600, 'Invalid request');
|
|
88
|
+
const { id, method } = message;
|
|
89
|
+
if (id === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (method === 'initialize') {
|
|
92
|
+
return reply(id, { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'oars-sessions', version: '1' } });
|
|
93
|
+
}
|
|
94
|
+
if (method === 'ping')
|
|
95
|
+
return reply(id, {});
|
|
96
|
+
if (method === 'tools/list')
|
|
97
|
+
return reply(id, { tools: [TOOL] });
|
|
98
|
+
if (method === 'tools/call') {
|
|
99
|
+
const params = isObj(message.params) ? message.params : {};
|
|
100
|
+
if (params.name !== TOOL_NAME)
|
|
101
|
+
return failure(id, -32602, `Unknown tool: ${typeof params.name === 'string' ? params.name : ''}`);
|
|
102
|
+
const invalid = narrowingRefusal(params.arguments);
|
|
103
|
+
if (invalid !== undefined)
|
|
104
|
+
return failure(id, -32602, invalid);
|
|
105
|
+
const conflict = formsRefusal(namedForms(params.arguments));
|
|
106
|
+
if (conflict !== undefined)
|
|
107
|
+
return failure(id, -32602, conflict);
|
|
108
|
+
const query = toQuery(params.arguments);
|
|
109
|
+
const refusal = pullRefusal(query);
|
|
110
|
+
if (refusal !== undefined)
|
|
111
|
+
return failure(id, -32602, refusal);
|
|
112
|
+
try {
|
|
113
|
+
// The server is not told which session it serves, so `sid` is null: naming it is what a consequence for an MCP pull still needs.
|
|
114
|
+
const store = await loadStore(runtime.storeDir);
|
|
115
|
+
const redactDomains = await readRedactDomains(runtime.storeDir);
|
|
116
|
+
const result = pull(store, query, { sid: null, now: now(), frame: null, redactDomains, current: await recallPool(runtime.storeDir, store, servedPool) });
|
|
117
|
+
// What an asker typed is class A: the call log masks each string with the redactor `pull` applies to the recall row, before it is serialized, because JSON escaping of a quote or newline hides a secret from the rules.
|
|
118
|
+
const redact = makeRedactor(redactDomains);
|
|
119
|
+
const masked = JSON.stringify(query, (_key, value) => (typeof value === 'string' ? redact(value) : value));
|
|
120
|
+
const call = { at: new Date(now()).toISOString(), query: masked, matches: result.legacy?.matches.length ?? result.reads.length, cwd: process.cwd() };
|
|
121
|
+
// A pull always answers: a log or recall row that cannot be written costs the record, never the agent's answer.
|
|
122
|
+
await tryLog(async () => {
|
|
123
|
+
await mkdir(runtime.storeDir, { recursive: true });
|
|
124
|
+
await appendFile(paths(runtime.storeDir).calls, `${JSON.stringify(call)}\n`, { mode: 0o600 });
|
|
125
|
+
});
|
|
126
|
+
await tryLog(() => appendRecall(runtime.storeDir, result.recall));
|
|
127
|
+
emit('recall', recallFields(result.recall, result.reads.map((read) => read.family), result.legacy !== undefined));
|
|
128
|
+
// An agent reads this, so it gets the rendered counts and the structured recall, and no signature text.
|
|
129
|
+
const { answer, id: recallId, evidence } = result.recall;
|
|
130
|
+
return reply(id, { content: [{ type: 'text', text: answer.rendered === '' ? MISS_TEXT : answer.rendered }], structuredContent: { recallId, kind: answer.kind, reads: answer.reads, evidence } });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// A store read failure must not end the server mid-session: the agent gets an error for this call and can carry on.
|
|
134
|
+
return failure(id, -32603, `Internal error: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return failure(id, -32601, `Method not found: ${method}`);
|
|
138
|
+
};
|
|
139
|
+
/** Reads requests from `runtime.stdin` until it ends, writing one response line per request to `runtime.stdout`. */
|
|
140
|
+
export const serve = async (runtime, now, emit = noEmit) => {
|
|
141
|
+
const lines = createInterface({ input: runtime.stdin, crlfDelay: Infinity });
|
|
142
|
+
for await (const line of lines) {
|
|
143
|
+
if (line.trim() === '')
|
|
144
|
+
continue;
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = JSON.parse(line);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
runtime.stdout.write(`${JSON.stringify(failure(null, -32700, 'Parse error'))}\n`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const answer = await handleMessage(runtime, now, parsed, emit);
|
|
154
|
+
if (answer !== undefined)
|
|
155
|
+
runtime.stdout.write(`${JSON.stringify(answer)}\n`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const sha256: (text: string) => string;
|
|
2
|
+
/** A root over fewer lines than this joins no cluster and counts toward no `n` (spec 2.5, open question 5.4 item 3). */
|
|
3
|
+
export declare const MIN_ROOT_LEAVES = 10;
|
|
4
|
+
/** The lines capture hashes: split on any line ending, and a final empty piece (the trailing newline) is no line. */
|
|
5
|
+
export declare const rawLinesOf: (text: string) => string[];
|
|
6
|
+
/** SHA-256 over the SHA-256 of every line, an odd node paired with itself; no lines is `sha256('')`. */
|
|
7
|
+
export declare const merkleRootOf: (lines: readonly string[]) => string;
|