@manyoars/oars 0.2.0 → 0.3.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 +132 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +367 -88
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.js +2 -2
- 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 +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -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 +257 -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 +25 -0
- package/dist/issuer.d.ts +7 -1
- package/dist/issuer.js +18 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -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 +188 -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 +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -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 +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -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 +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -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 +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -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 +37 -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.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 +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -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 +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -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 +477 -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,117 @@
|
|
|
1
|
+
import type { Outcome } from '../cli.js';
|
|
2
|
+
import { type BooksBy } from './query.js';
|
|
3
|
+
import { type PullInput } from './recall-reads.js';
|
|
4
|
+
import { type ContextOptions } from './hook-index.js';
|
|
5
|
+
import { type InstallRuntime, type Scope } from './install.js';
|
|
6
|
+
import { type SessionsRuntime, type SkippedScope } from './store.js';
|
|
7
|
+
import { type StuckStep } from './steps.js';
|
|
8
|
+
import { type SelfBaselineResult } from './baseline.js';
|
|
9
|
+
import { type SessionAudit, type T0Counts } from './audit.js';
|
|
10
|
+
import { type InvariantResult, type SessionVerification } from './verify.js';
|
|
11
|
+
export interface SessionsDeps {
|
|
12
|
+
sessions: SessionsRuntime | undefined;
|
|
13
|
+
install: InstallRuntime | undefined;
|
|
14
|
+
now: () => number;
|
|
15
|
+
}
|
|
16
|
+
export declare const sessionsDeposit: (deps: SessionsDeps, options: {
|
|
17
|
+
allow: string[];
|
|
18
|
+
allowFiles: string[];
|
|
19
|
+
redactDomains: string[];
|
|
20
|
+
}) => Promise<Outcome>;
|
|
21
|
+
/**
|
|
22
|
+
* One pull: it writes its own recall record, a hit or a miss, because no harness writes one for it. A record that cannot be written costs the asker nothing, and
|
|
23
|
+
* this command answers an operator, so the `--error`, `--path`, and `--class` forms keep the signature text they have always printed; the MCP tool, which answers an agent, does not.
|
|
24
|
+
*/
|
|
25
|
+
export declare const sessionsRecall: (deps: SessionsDeps, query: PullInput, sid?: string | null) => Promise<Outcome>;
|
|
26
|
+
/** `recall --counts`: push and pull, shown and held, as counts only. */
|
|
27
|
+
export declare const sessionsRecallCounts: (deps: SessionsDeps, day: string | undefined) => Promise<Outcome>;
|
|
28
|
+
/** `lineage`: a walk over files the derive already wrote. It computes nothing, so it logs no step. */
|
|
29
|
+
export declare const sessionsLineage: (deps: SessionsDeps, ref: string) => Promise<Outcome>;
|
|
30
|
+
/**
|
|
31
|
+
* `read`: rows from the current pool's reads. The step log holds it under its own step, because the `recall` counters answer how often a recall was asked for. Every row states
|
|
32
|
+
* its clusters and order. No floor applies at order 1, because every cell here is the operator's own; above it a read below its floor does not exist, so this also says what the
|
|
33
|
+
* store cannot read, which tells that apart from a read that found nothing. A recall states a read as counts; this verb is the one way out for a read's whole value and for an experimental read.
|
|
34
|
+
*/
|
|
35
|
+
export declare const sessionsRead: (deps: SessionsDeps, options: {
|
|
36
|
+
family: string | undefined;
|
|
37
|
+
slices: readonly string[];
|
|
38
|
+
}) => Promise<Outcome>;
|
|
39
|
+
/** `recall --context`: the injection block for a repository, from the hook index (built from the store when no deposit has written one). */
|
|
40
|
+
export declare const sessionsContext: (deps: SessionsDeps, options: ContextOptions) => Promise<Outcome>;
|
|
41
|
+
export declare const sessionsMcp: (deps: SessionsDeps) => Promise<void>;
|
|
42
|
+
export declare const sessionsBooks: (deps: SessionsDeps, options: {
|
|
43
|
+
by: BooksBy;
|
|
44
|
+
sinceDays: number | undefined;
|
|
45
|
+
repo?: string;
|
|
46
|
+
}) => Promise<Outcome>;
|
|
47
|
+
export declare const sessionsMeasure: (deps: SessionsDeps, options: {
|
|
48
|
+
last: number;
|
|
49
|
+
since: string | undefined;
|
|
50
|
+
repo?: string;
|
|
51
|
+
}) => Promise<Outcome>;
|
|
52
|
+
export interface VerifyReport {
|
|
53
|
+
sessions: number;
|
|
54
|
+
/** Skeleton files that could not be parsed: not an invariant, so counted apart from them. */
|
|
55
|
+
unreadable: number;
|
|
56
|
+
invariants: Array<InvariantResult & {
|
|
57
|
+
pass: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
/** Steps whose key failed on three runs in a row (spec 4.4), with the first error line of the newest failure. */
|
|
60
|
+
stuck: StuckStep[];
|
|
61
|
+
/** Sessions behind the current capture version whose transcript is gone from the harness and `raw/` (or, for a Cursor session, whose composer is gone from Cursor): counted apart from `cv-behind`, never a strict failure. */
|
|
62
|
+
cvFrozen: number;
|
|
63
|
+
/** Folders under the projects root that hold transcripts but are not on the allowlist, so are never read: scope, not a failure. */
|
|
64
|
+
scope: SkippedScope;
|
|
65
|
+
/** Every session's rows 1 to 10 with a status each (spec 5.4 item 5): `pass`, `fail`, or `not run` when the row had nothing to check. */
|
|
66
|
+
sessionResults: SessionVerification[];
|
|
67
|
+
/** Layer 4 at T0, local (spec 2.4): each session's raw-file match, recomputed root, and count bindings, with the count of each row of the result table. */
|
|
68
|
+
audit: {
|
|
69
|
+
counts: T0Counts;
|
|
70
|
+
sessions: SessionAudit[];
|
|
71
|
+
};
|
|
72
|
+
/** Layer 2 at T0, local (spec 2.2): each session's week against the operator's own other weeks on its harness; `not run` below 1,000 calls or 4 baseline weeks. */
|
|
73
|
+
selfBaseline: SelfBaselineResult[];
|
|
74
|
+
/** Batches in any server's outbox built under an older vocabulary, field list, or skeleton format: never printed, never sent, and removed by the next `pool deposit`. Counted from local folders; nothing is sent. */
|
|
75
|
+
staleBatches: number;
|
|
76
|
+
}
|
|
77
|
+
/** With `strict`, any violated invariant, stuck step, `merkle: root mismatch`, or `merkle: counts do not bind` also fails the command (exit 4) after the full report is printed; the report is built first so callers can emit it. */
|
|
78
|
+
export declare const sessionsVerify: (deps: SessionsDeps, options: {
|
|
79
|
+
session: string | undefined;
|
|
80
|
+
}) => Promise<Outcome & {
|
|
81
|
+
failed: string[];
|
|
82
|
+
report: VerifyReport;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* The sidecar, then the gold file, each created with `wx`: the existence check in `gold sample` came before a long draw, so this is what holds the
|
|
86
|
+
* promise never to overwrite a file that may hold judgments if another run wrote in between.
|
|
87
|
+
*/
|
|
88
|
+
export declare const writeGoldFiles: (file: string, arms: string, sidecar: string, rendered: string) => Promise<void>;
|
|
89
|
+
/** `gold sample`: writes the blind gold file and its hidden sidecar. It never overwrites one: the file may hold judgments. */
|
|
90
|
+
export declare const sessionsGoldSample: (deps: SessionsDeps, options: {
|
|
91
|
+
size: number;
|
|
92
|
+
batch: number;
|
|
93
|
+
seed: string | undefined;
|
|
94
|
+
termsFile: string;
|
|
95
|
+
excludeTerms: string[];
|
|
96
|
+
outDir: string | undefined;
|
|
97
|
+
}) => Promise<Outcome>;
|
|
98
|
+
/** `gold score`: reads whatever is judged so far, so it runs after any batch. */
|
|
99
|
+
export declare const sessionsGoldScore: (options: {
|
|
100
|
+
file: string;
|
|
101
|
+
}) => Promise<Outcome>;
|
|
102
|
+
/** `gold arms`: the text arms and the fresh readers, into the sidecar only. It prints coverage and no reading: a read waits for judgments. */
|
|
103
|
+
export declare const sessionsGoldArms: (deps: SessionsDeps, options: {
|
|
104
|
+
file: string;
|
|
105
|
+
}) => Promise<Outcome>;
|
|
106
|
+
export interface HookTarget {
|
|
107
|
+
scope: Scope | undefined;
|
|
108
|
+
file: string | undefined;
|
|
109
|
+
}
|
|
110
|
+
export declare const sessionsHookInstall: (deps: SessionsDeps, options: HookTarget & {
|
|
111
|
+
keepRetention: boolean;
|
|
112
|
+
schedule: "nightly" | "off" | undefined;
|
|
113
|
+
}) => Promise<Outcome>;
|
|
114
|
+
export declare const sessionsHookUninstall: (deps: SessionsDeps, options: HookTarget) => Promise<Outcome>;
|
|
115
|
+
export declare const sessionsHookStatus: (deps: SessionsDeps, options: {
|
|
116
|
+
file: string | undefined;
|
|
117
|
+
}) => Promise<Outcome>;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// The `oars sessions` command group's bodies: each returns the `--json` document and the text a human sees.
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { parseAllowList } from './allow.js';
|
|
5
|
+
import { SOURCES, sourceLabel } from './sources.js';
|
|
6
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
7
|
+
import { loadTerms } from './exclusion.js';
|
|
8
|
+
import { EVALUATOR_MODEL, fillArms, sidecarSaver } from './gold-arms.js';
|
|
9
|
+
import { armsPath, coverageLines, formatScore, isGoldDocument, parseGold, plural, renderGold, sampleGold, scoreGold } from './gold.js';
|
|
10
|
+
import { makeRedactor } from './redact.js';
|
|
11
|
+
import { books, BOOKS_CAVEAT, formatBooks, formatMeasure, formatRecall, measure } from './query.js';
|
|
12
|
+
import { formatLineage, loadLineage, walkLineage } from './lineage.js';
|
|
13
|
+
import { appendRecall, readRecalls } from './recall-object.js';
|
|
14
|
+
import { deriveConsequences, formatRecallCounts, recallCounts } from './recall-consequence.js';
|
|
15
|
+
import { pull, pullRefusal } from './recall-reads.js';
|
|
16
|
+
import { MISS_TEXT } from './recall-render.js';
|
|
17
|
+
import { readHookCalls } from './hook-calls.js';
|
|
18
|
+
import { buildIndex, contextBlock, inRepository, readIndex } from './hook-index.js';
|
|
19
|
+
import { hookStatus, installHooks, scheduleStatus, setSchedule, settingsPath, uninstallHooks } from './install.js';
|
|
20
|
+
import { serve } from './mcp.js';
|
|
21
|
+
import { refreshPool, servedPool } from './pool.js';
|
|
22
|
+
import { FAMILIES } from './read.js';
|
|
23
|
+
import { formatNotReadable, notReadable } from './read-readable.js';
|
|
24
|
+
import { formatReads, isFamily, parseSlice, selectReads } from './read-rows.js';
|
|
25
|
+
import { CAPTURE_VERSION, OLDEST_READABLE_CAPTURE } from './schema.js';
|
|
26
|
+
import { deposit, frozenSessions, loadStore, readCalls, readRedactDomains, readSkeletons, skippedFolders, slugOf } from './store.js';
|
|
27
|
+
import { formatStepCounters, inputKey, newContext, readSteps, stampKey, runDeposit, runStep, stepCounters, stuckSteps, tryLog } from './steps.js';
|
|
28
|
+
import { formatSelfBaseline, selfBaseline as selfBaselineOf } from './baseline.js';
|
|
29
|
+
import { staleBatchesAnywhere } from './pool-outbox.js';
|
|
30
|
+
import { auditCounts, auditStore, formatAudit } from './audit.js';
|
|
31
|
+
import { readSurvival, recordSurvival, SURVIVAL_DAYS } from './survival.js';
|
|
32
|
+
import { CAPTURE_INVARIANTS, formatSessionResults, formatVerify, verifyAll, verifyCaptured, verifySessions } from './verify.js';
|
|
33
|
+
const runtimeOf = (deps) => {
|
|
34
|
+
if (deps.sessions === undefined)
|
|
35
|
+
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
36
|
+
return deps.sessions;
|
|
37
|
+
};
|
|
38
|
+
const readAllowFile = async (file) => {
|
|
39
|
+
try {
|
|
40
|
+
return parseAllowList(await readFile(file, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new CliError(`Cannot read the --allow-file ${file}.`, ExitCode.USAGE, 'usage');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const sessionsDeposit = async (deps, options) => {
|
|
47
|
+
const fromFiles = await Promise.all(options.allowFiles.map((file) => readAllowFile(file)));
|
|
48
|
+
const runtime = runtimeOf(deps);
|
|
49
|
+
const table = runtime.sources ?? SOURCES;
|
|
50
|
+
const allow = [...options.allow, ...fromFiles.flat()];
|
|
51
|
+
const result = await runDeposit(newContext(runtime.storeDir, deps.now, 'manual'), true, inputKey('manual', allow, options.redactDomains), async (trace) => {
|
|
52
|
+
const done = await deposit(runtime, { allow, redactDomains: options.redactDomains, now: deps.now }, trace);
|
|
53
|
+
return { value: done, in: { transcript: done.sessions + done.subagents + done.unchanged } };
|
|
54
|
+
});
|
|
55
|
+
const survival = await recordSurvival(runtime, allow, deps.now);
|
|
56
|
+
const lines = [
|
|
57
|
+
`Deposited ${String(result.sessions)} sessions and ${String(result.subagents)} subagent transcripts (${String(result.lines)} lines, ${String(result.badLines)} unparseable) as ${String(result.observations)} observations; ${String(result.unchanged)} files unchanged.`,
|
|
58
|
+
`Read ${String(result.folders.length)} folders; skipped ${String(result.skippedFolders)} not on the allowlist.`,
|
|
59
|
+
...Object.entries(result.sources).flatMap(([name, counts]) => [
|
|
60
|
+
...(counts.read === 0 ? [] : [`Read ${String(counts.read)} ${sourceLabel(name, table)} sessions.`]),
|
|
61
|
+
...counts.unreadable.map((reason) => `${sourceLabel(name, table)} store not read: ${reason}`),
|
|
62
|
+
...(counts.rejected === 0 ? [] : [`Skipped ${String(counts.rejected)} ${sourceLabel(name, table)} sessions whose id is not a safe file name.`]),
|
|
63
|
+
]),
|
|
64
|
+
...(result.missing.length === 0 ? [] : [`No project folder matched: ${result.missing.join(', ')}.`]),
|
|
65
|
+
survival === null
|
|
66
|
+
? 'Commit survival not read this run.'
|
|
67
|
+
: `Commits older than ${String(SURVIVAL_DAYS)} days: ${String(survival.survived)} survived, ${String(survival.reverted)} reverted, ${String(survival.unreachable)} unreachable, ${String(survival.unknown)} unknown.`,
|
|
68
|
+
];
|
|
69
|
+
return { document: { ok: true, ...result, survival }, text: lines.join('\n') };
|
|
70
|
+
};
|
|
71
|
+
const isAnswerable = (query) => ('form' in query ? true : query.error !== undefined || query.path !== undefined || query.cls !== undefined);
|
|
72
|
+
/**
|
|
73
|
+
* One pull: it writes its own recall record, a hit or a miss, because no harness writes one for it. A record that cannot be written costs the asker nothing, and
|
|
74
|
+
* this command answers an operator, so the `--error`, `--path`, and `--class` forms keep the signature text they have always printed; the MCP tool, which answers an agent, does not.
|
|
75
|
+
*/
|
|
76
|
+
export const sessionsRecall = async (deps, query, sid = null) => {
|
|
77
|
+
if (!isAnswerable(query)) {
|
|
78
|
+
throw new CliError('Pass --error "<text>", --path <file>, --class <name>, --pattern <id>, --entity <name>, --family <name>, or --text "<text>".', ExitCode.USAGE, 'usage');
|
|
79
|
+
}
|
|
80
|
+
const refusal = pullRefusal(query);
|
|
81
|
+
if (refusal !== undefined)
|
|
82
|
+
throw new CliError(refusal, ExitCode.USAGE, 'usage');
|
|
83
|
+
const { storeDir } = runtimeOf(deps);
|
|
84
|
+
// The key is the query and `ev`, known before the work, so a recall that keeps failing is the same key each time.
|
|
85
|
+
const key = stampKey(JSON.stringify(query));
|
|
86
|
+
const result = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
87
|
+
const store = await loadStore(storeDir);
|
|
88
|
+
const found = pull(store, query, { sid, now: deps.now(), frame: null, redactDomains: await readRedactDomains(storeDir), current: await servedPool(storeDir, store) });
|
|
89
|
+
return { value: found, report: { in: { query: 1 }, out: { matches: found.legacy?.matches.length ?? found.reads.length }, key } };
|
|
90
|
+
}, key);
|
|
91
|
+
await tryLog(() => appendRecall(storeDir, result.recall));
|
|
92
|
+
if (result.legacy !== undefined)
|
|
93
|
+
return { document: { ok: true, ...result.legacy, recall: result.recall }, text: formatRecall(result.legacy) };
|
|
94
|
+
// The document states the recall and not the raw reads: a read's tool, keys, and versions are store strings that can carry text, and `recall.evidence` already states them closed.
|
|
95
|
+
return { document: { ok: true, recall: result.recall }, text: result.recall.answer.rendered === '' ? MISS_TEXT : result.recall.answer.rendered };
|
|
96
|
+
};
|
|
97
|
+
/** `recall --counts`: push and pull, shown and held, as counts only. */
|
|
98
|
+
export const sessionsRecallCounts = async (deps, day) => {
|
|
99
|
+
const { storeDir } = runtimeOf(deps);
|
|
100
|
+
const rows = recallCounts(deriveConsequences(await loadStore(storeDir), await readRecalls(storeDir)), day);
|
|
101
|
+
return { document: { ok: true, day: day ?? null, rows }, text: formatRecallCounts(rows) };
|
|
102
|
+
};
|
|
103
|
+
/** `lineage`: a walk over files the derive already wrote. It computes nothing, so it logs no step. */
|
|
104
|
+
export const sessionsLineage = async (deps, ref) => {
|
|
105
|
+
const hops = walkLineage(await loadLineage(runtimeOf(deps).storeDir), ref);
|
|
106
|
+
return { document: { ok: true, hops }, text: formatLineage(hops) };
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* `read`: rows from the current pool's reads. The step log holds it under its own step, because the `recall` counters answer how often a recall was asked for. Every row states
|
|
110
|
+
* its clusters and order. No floor applies at order 1, because every cell here is the operator's own; above it a read below its floor does not exist, so this also says what the
|
|
111
|
+
* store cannot read, which tells that apart from a read that found nothing. A recall states a read as counts; this verb is the one way out for a read's whole value and for an experimental read.
|
|
112
|
+
*/
|
|
113
|
+
export const sessionsRead = async (deps, options) => {
|
|
114
|
+
const { family } = options;
|
|
115
|
+
if (family !== undefined && !isFamily(family))
|
|
116
|
+
throw new CliError(`Pass --family as one of ${FAMILIES.join(', ')}.`, ExitCode.USAGE, 'usage');
|
|
117
|
+
const slice = parseSlice(options.slices);
|
|
118
|
+
if (slice === undefined)
|
|
119
|
+
throw new CliError('Pass --slice as <axis>=<value>.', ExitCode.USAGE, 'usage');
|
|
120
|
+
const { storeDir } = runtimeOf(deps);
|
|
121
|
+
const key = stampKey(family ?? '', ...options.slices);
|
|
122
|
+
const { pool, reads, unreadable, newer } = await runStep(newContext(storeDir, deps.now, 'manual'), 'read', async () => {
|
|
123
|
+
const store = await loadStore(storeDir);
|
|
124
|
+
const refreshed = await refreshPool(storeDir, store);
|
|
125
|
+
const selected = selectReads(refreshed.reads, { family, slice });
|
|
126
|
+
const missing = notReadable(refreshed.reads).filter((row) => family === undefined || row.family === family);
|
|
127
|
+
return { value: { pool: refreshed.pool.version, reads: selected, unreadable: missing, newer: store }, report: { in: { reads: refreshed.reads.length }, out: { rows: selected.length }, key } };
|
|
128
|
+
}, key);
|
|
129
|
+
return { document: { ok: true, pool, reads, notReadable: unreadable, newerCaptures: newer.newerCaptures }, text: [formatReads(reads), ...formatNotReadable(unreadable), ...newerNotice(newer)].join('\n') };
|
|
130
|
+
};
|
|
131
|
+
/** `recall --context`: the injection block for a repository, from the hook index (built from the store when no deposit has written one). */
|
|
132
|
+
export const sessionsContext = async (deps, options) => {
|
|
133
|
+
const { storeDir } = runtimeOf(deps);
|
|
134
|
+
// The manual recall verb (spec 4.1): its key is what it looks up, the repository's cwd and the harness, known before the work.
|
|
135
|
+
const key = stampKey('context', options.cwd, options.harness ?? '');
|
|
136
|
+
const block = await runStep(newContext(storeDir, deps.now, 'manual'), 'recall', async () => {
|
|
137
|
+
const index = (await readIndex(storeDir)) ?? buildIndex(await loadStore(storeDir), await readRedactDomains(storeDir), new Date(deps.now()).toISOString());
|
|
138
|
+
const found = contextBlock(index, options);
|
|
139
|
+
return { value: found, report: { in: { query: 1 }, out: { matches: found === undefined ? 0 : 1 }, key } };
|
|
140
|
+
}, key);
|
|
141
|
+
return { document: { ok: true, context: block ?? null }, text: block ?? 'Nothing recurs in this repository yet.' };
|
|
142
|
+
};
|
|
143
|
+
export const sessionsMcp = async (deps) => {
|
|
144
|
+
await serve(runtimeOf(deps), deps.now);
|
|
145
|
+
};
|
|
146
|
+
/** Names shown in the `verify` and `books` text; each `--json` document carries them all. */
|
|
147
|
+
const SKIPPED_NAMES_SHOWN = 10;
|
|
148
|
+
/** One line for the folders the allowlist leaves out: the store holds nothing from them, and it says so rather than looking complete. */
|
|
149
|
+
const skippedNotice = (scope) => {
|
|
150
|
+
if (scope.folders === 0)
|
|
151
|
+
return [];
|
|
152
|
+
const shown = scope.names.slice(0, SKIPPED_NAMES_SHOWN).join(', ');
|
|
153
|
+
const more = scope.names.length > SKIPPED_NAMES_SHOWN ? `, and ${String(scope.names.length - SKIPPED_NAMES_SHOWN)} more` : '';
|
|
154
|
+
const one = scope.folders === 1;
|
|
155
|
+
const folders = one ? '1 project folder holds' : `${String(scope.folders)} project folders hold`;
|
|
156
|
+
// Main transcripts only: a folder with none holds subagent transcripts only, or could not be listed.
|
|
157
|
+
const transcripts = scope.transcripts === 0 ? 'no main transcript' : scope.transcripts === 1 ? '1 main transcript' : `${String(scope.transcripts)} main transcripts`;
|
|
158
|
+
return [`${folders} ${transcripts}; ${one ? 'it is' : 'they are'} never read: not on the allowlist. Not captured: ${shown}${more}.`];
|
|
159
|
+
};
|
|
160
|
+
/** Scope is an extra, like the step log: a projects root this process cannot list reports an empty scope and fails neither `verify` nor `books`. */
|
|
161
|
+
const scopeOf = (runtime) => skippedFolders(runtime).catch(() => ({ folders: 0, transcripts: 0, names: [] }));
|
|
162
|
+
/** One line for a store a newer oars wrote into (spec state-on-open row for a newer `cv`: read known fields, warn). */
|
|
163
|
+
const newerNotice = (store) => (store.newerCaptures > 0 ? [`captured by a newer oars; upgrade oars (${String(store.newerCaptures)} sessions)`] : []);
|
|
164
|
+
/**
|
|
165
|
+
* The store restricted to one repository (the test `measure --repo` uses). `books` reads its rows from `prompts`, and looks up a
|
|
166
|
+
* subagent, a sha, or a pull by the kept prompt it belongs to, so the kept sessions and prompts are enough; the kept subagents only keep
|
|
167
|
+
* the newer-capture count to this repository.
|
|
168
|
+
*/
|
|
169
|
+
const inRepositoryStore = (store, repo) => {
|
|
170
|
+
const sessions = store.sessions.filter((session) => inRepository(session.project, repo));
|
|
171
|
+
const mains = new Set(sessions.map((session) => session.session));
|
|
172
|
+
// A subagent is kept by its own project, like a session: its parent's session record may be absent from the store.
|
|
173
|
+
const subagents = store.subagents.filter((agent) => inRepository(agent.project, repo));
|
|
174
|
+
const newer = (cv) => Number(cv) > Number(CAPTURE_VERSION);
|
|
175
|
+
return {
|
|
176
|
+
...store,
|
|
177
|
+
sessions,
|
|
178
|
+
subagents,
|
|
179
|
+
prompts: store.prompts.filter((prompt) => mains.has(prompt.session)),
|
|
180
|
+
newerCaptures: [...sessions, ...subagents].filter((record) => newer(record.cv)).length,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
export const sessionsBooks = async (deps, options) => {
|
|
184
|
+
const runtime = runtimeOf(deps);
|
|
185
|
+
const { storeDir } = runtime;
|
|
186
|
+
const loaded = await loadStore(storeDir);
|
|
187
|
+
const store = options.repo === undefined ? loaded : inRepositoryStore(loaded, options.repo);
|
|
188
|
+
const rows = books(store, options.by, deps.now(), options.sinceDays, await readSurvival(storeDir));
|
|
189
|
+
// A mistyped root, or a --since window nothing falls in, over a populated store must not read as an empty store.
|
|
190
|
+
const emptyWindow = rows.length === 0 && options.sinceDays !== undefined && store.prompts.length > 0;
|
|
191
|
+
const table = store.prompts.length === 0 && options.repo !== undefined
|
|
192
|
+
? `No prompts for ${options.repo} in the store (${String(loaded.prompts.length)} prompts across all repositories).`
|
|
193
|
+
: emptyWindow
|
|
194
|
+
? `No ${options.by === 'pull' ? 'pulls' : 'prompts'} in the --since window.`
|
|
195
|
+
: formatBooks(rows, options.by);
|
|
196
|
+
const scope = await scopeOf(runtime);
|
|
197
|
+
return { document: { ok: true, by: options.by, rows, newerCaptures: store.newerCaptures, scope }, text: [table, ...skippedNotice(scope), ...newerNotice(store), ...(rows.length === 0 ? [] : [BOOKS_CAVEAT])].join('\n') };
|
|
198
|
+
};
|
|
199
|
+
export const sessionsMeasure = async (deps, options) => {
|
|
200
|
+
const runtime = runtimeOf(deps);
|
|
201
|
+
const loaded = await loadStore(runtime.storeDir);
|
|
202
|
+
const { repo } = options;
|
|
203
|
+
// The same repository test recall uses, so worktrees and subfolders count; other repositories' errors drop out with their sessions.
|
|
204
|
+
const store = repo === undefined ? loaded : { ...loaded, sessions: loaded.sessions.filter((session) => inRepository(session.project, repo)) };
|
|
205
|
+
// A call with a folder counts only for its own repository; an older call has none and counts for every repository.
|
|
206
|
+
const calls = (await readCalls(runtime.storeDir)).filter((call) => repo === undefined || call.cwd === undefined || inRepository(slugOf(call.cwd), repo));
|
|
207
|
+
const result = measure(store, calls, await readHookCalls(runtime.storeDir), options.last, options.since);
|
|
208
|
+
// The step counters are all-time over the whole log: `--repo`, `--last`, and `--since` filter the G1 table only.
|
|
209
|
+
// A step log that cannot be read leaves the G1 table as it is (the same rule as `verify`'s stuck check).
|
|
210
|
+
const steps = stepCounters(await readSteps(runtime.storeDir).catch(() => []));
|
|
211
|
+
return { document: { ok: true, ...result, steps, newerCaptures: store.newerCaptures }, text: [formatMeasure(result), formatStepCounters(steps), ...newerNotice(store)].join('\n') };
|
|
212
|
+
};
|
|
213
|
+
/** With `strict`, any violated invariant, stuck step, `merkle: root mismatch`, or `merkle: counts do not bind` also fails the command (exit 4) after the full report is printed; the report is built first so callers can emit it. */
|
|
214
|
+
export const sessionsVerify = async (deps, options) => {
|
|
215
|
+
const runtime = runtimeOf(deps);
|
|
216
|
+
const { storeDir } = runtime;
|
|
217
|
+
// When the store cannot be read there is no high-water mark yet, so a failed run's key is the session asked for and `ev`.
|
|
218
|
+
return runStep(newContext(storeDir, deps.now, 'manual'), 'verify', async () => {
|
|
219
|
+
// Every skeleton is read even for one session: row 6 takes its subagent transcripts' lines, which `<id>.json` alone would miss.
|
|
220
|
+
const everySkeleton = await readSkeletons(storeDir, undefined, true);
|
|
221
|
+
const { unreadable } = options.session === undefined ? everySkeleton : await readSkeletons(storeDir, options.session, true);
|
|
222
|
+
const skeletons = everySkeleton.skeletons.filter((skeleton) => options.session === undefined || skeleton.session === options.session);
|
|
223
|
+
const store = await loadStore(storeDir);
|
|
224
|
+
const mine = (records) => records.filter((record) => options.session === undefined || record.session === options.session);
|
|
225
|
+
const captured = mine([...store.sessions, ...store.subagents]);
|
|
226
|
+
// A session behind the current version whose transcript is in neither the harness nor raw/ (or, for a Cursor session, whose composer is gone from Cursor) can never be captured again: counted apart, never a strict failure.
|
|
227
|
+
const frozen = await frozenSessions(runtime, captured.filter((session) => Number(session.cv) >= OLDEST_READABLE_CAPTURE && Number(session.cv) < Number(CAPTURE_VERSION)));
|
|
228
|
+
const results = [...verifyAll(skeletons, everySkeleton.skeletons), ...verifyCaptured(captured, mine(store.tools), frozen)];
|
|
229
|
+
const sessionResults = verifySessions(everySkeleton.skeletons, captured, mine(store.tools), frozen).filter((entry) => options.session === undefined || entry.session === options.session);
|
|
230
|
+
// The baseline reads every week of the store, so --session only picks which results are reported.
|
|
231
|
+
const selfBaseline = selfBaselineOf([...store.sessions, ...store.subagents], store.tools, store.prompts, deps.now()).filter((entry) => options.session === undefined || entry.session === options.session);
|
|
232
|
+
const audits = await auditStore(runtime, captured, skeletons);
|
|
233
|
+
const audit = { counts: auditCounts(audits), sessions: audits };
|
|
234
|
+
// A root that misses a file which matches, or records that disagree with each other, leave `W` at 0 (spec 2.4): they fail `--strict` like a violated invariant.
|
|
235
|
+
const merkleFailed = ['root mismatch', 'counts do not bind'].filter((result) => audit.counts[result] > 0).map((result) => `merkle: ${result}`);
|
|
236
|
+
// Steps whose key failed on three runs in a row (spec 4.4), read before this run's own row is appended; each fails `--strict` like a violated invariant.
|
|
237
|
+
// A step log that cannot be read leaves the invariant check as it is: the stuck check is an extra, never a reason `verify` fails.
|
|
238
|
+
// A verify that got here has succeeded, so a verify streak is the one this run ends and is not listed.
|
|
239
|
+
const stuck = stuckSteps(await readSteps(storeDir).catch(() => [])).filter((entry) => entry.step !== 'verify');
|
|
240
|
+
const scope = await scopeOf(runtime);
|
|
241
|
+
// A local count like the scope: an outbox folder that cannot be read is no reason for `verify` to fail.
|
|
242
|
+
const staleBatches = await staleBatchesAnywhere(storeDir).catch(() => 0);
|
|
243
|
+
const report = { sessions: skeletons.length, unreadable, cvFrozen: frozen.size, scope, invariants: results.map((result) => ({ ...result, pass: result.violations === 0 })), stuck, sessionResults, audit, selfBaseline, staleBatches };
|
|
244
|
+
const failed = [...results.filter((result) => result.violations > 0).map((result) => result.name), ...(unreadable > 0 ? ['unreadable skeletons'] : []), ...merkleFailed, ...stuck.map((entry) => `stuck: ${entry.step}`)];
|
|
245
|
+
const stuckLines = stuck.map((entry) => `FAIL ${entry.step} is stuck: key ${entry.key} failed ${String(entry.runs)} runs in a row: ${entry.error}`);
|
|
246
|
+
const highWater = mine([...store.sessions, ...store.subagents]).map((session) => `${session.session}:${String(session.rev)}:${session.cv}`).sort();
|
|
247
|
+
const resultLines = formatSessionResults(sessionResults);
|
|
248
|
+
const outcome = {
|
|
249
|
+
failed,
|
|
250
|
+
report,
|
|
251
|
+
document: { ok: true, ...report, newerCaptures: store.newerCaptures },
|
|
252
|
+
text: [formatVerify(results, skeletons.length), formatAudit(audits), formatSelfBaseline(selfBaseline), ...(resultLines === '' ? [] : [resultLines]), ...(frozen.size > 0 ? [`${String(frozen.size)} cv-frozen sessions: behind the current capture version, but the transcript is gone from the harness and raw/ (or, for a Cursor session, the composer is gone from Cursor), so they cannot be captured again`] : []), ...skippedNotice(scope), ...(staleBatches > 0 ? [`${String(staleBatches)} outbox batches were built under an older vocabulary, field list, or skeleton format; they will never be sent, and oars pool deposit removes them`] : []), ...newerNotice(store), ...(unreadable > 0 ? [`FAIL ${String(unreadable)} unreadable skeleton files (torn or corrupted); redeposit to rebuild them`] : []), ...stuckLines].join('\n'),
|
|
253
|
+
};
|
|
254
|
+
const cvBehind = results.find((result) => result.name === CAPTURE_INVARIANTS[0])?.violations ?? 0;
|
|
255
|
+
// The key is the sessions checked, each at its rev and cv, and the code's ev (spec 4.2).
|
|
256
|
+
const key = stampKey(...highWater);
|
|
257
|
+
const counts = { checked: results.length, passed: results.filter((result) => result.violations === 0).length, cvBehind, cvFrozen: frozen.size, stuck: stuck.length, merkle: audit.counts['root mismatch'] + audit.counts['counts do not bind'] };
|
|
258
|
+
return { value: outcome, report: { in: { session: mine(store.sessions).length, subagent: mine(store.subagents).length }, out: counts, key } };
|
|
259
|
+
}, stampKey('verify', options.session ?? ''));
|
|
260
|
+
};
|
|
261
|
+
const GOLD_MODE = 0o600;
|
|
262
|
+
/**
|
|
263
|
+
* The sidecar, then the gold file, each created with `wx`: the existence check in `gold sample` came before a long draw, so this is what holds the
|
|
264
|
+
* promise never to overwrite a file that may hold judgments if another run wrote in between.
|
|
265
|
+
*/
|
|
266
|
+
export const writeGoldFiles = async (file, arms, sidecar, rendered) => {
|
|
267
|
+
await writeFile(arms, sidecar, { mode: GOLD_MODE, flag: 'wx' });
|
|
268
|
+
await writeFile(file, rendered, { mode: GOLD_MODE, flag: 'wx' });
|
|
269
|
+
};
|
|
270
|
+
/** `gold sample`: writes the blind gold file and its hidden sidecar. It never overwrites one: the file may hold judgments. */
|
|
271
|
+
export const sessionsGoldSample = async (deps, options) => {
|
|
272
|
+
const runtime = runtimeOf(deps);
|
|
273
|
+
// With no terms every session is excluded, so a draw would be empty: say so before anything is read or written.
|
|
274
|
+
if ((await loadTerms(options.termsFile)).length === 0)
|
|
275
|
+
throw new CliError('The term file is missing or empty, so every session is excluded. Set OARS_EXCLUSION_TERMS to a file of terms, one per line.', ExitCode.INVALID, 'unreadable');
|
|
276
|
+
const date = new Date(deps.now()).toISOString().slice(0, 10);
|
|
277
|
+
const dir = options.outDir ?? join(dirname(runtime.projectsRoot), 'session-commons', 'review');
|
|
278
|
+
const file = join(dir, `outcome-gold-${date}.md`);
|
|
279
|
+
const arms = armsPath(file);
|
|
280
|
+
const taken = async (path) => stat(path).then(() => true, () => false);
|
|
281
|
+
if (await taken(file))
|
|
282
|
+
throw new CliError(`${file} already exists, and it may hold judgments. Move it, or pass another --out-dir.`, ExitCode.USAGE, 'usage');
|
|
283
|
+
if (await taken(arms))
|
|
284
|
+
throw new CliError(`${arms} already exists, and it may hold readings. Move it, or pass another --out-dir.`, ExitCode.USAGE, 'usage');
|
|
285
|
+
const store = await loadStore(runtime.storeDir);
|
|
286
|
+
const redact = makeRedactor(await readRedactDomains(runtime.storeDir));
|
|
287
|
+
const { document, views, unreadable } = await sampleGold(runtime, store, redact, { size: options.size, batch: options.batch, seed: options.seed ?? date, date, termsFile: options.termsFile, extraTerms: options.excludeTerms });
|
|
288
|
+
await mkdir(dir, { recursive: true });
|
|
289
|
+
await writeGoldFiles(file, arms, JSON.stringify(document), renderGold(views, { date, batch: options.batch }));
|
|
290
|
+
const subagents = document.entries.filter((entry) => entry.subagents > 0).length;
|
|
291
|
+
const lines = [
|
|
292
|
+
`Wrote ${plural(document.entries.length, 'prompt')}, drawn from ${String(document.frame)}, to ${file}.`,
|
|
293
|
+
`Excluded ${plural(document.excludedSessions, 'session')} the draw reached under the exclusion rule (a listed term, no raw to scan, or unreadable).`,
|
|
294
|
+
`Passed over ${plural(unreadable, 'prompt')} whose transcript no longer matches the store.`,
|
|
295
|
+
`${plural(subagents, 'sampled prompt')} ran subagents, whose work is not read.`,
|
|
296
|
+
`Score it after any batch: oars sessions gold score ${file}`,
|
|
297
|
+
];
|
|
298
|
+
return { document: { ok: true, file, prompts: document.entries.length, frame: document.frame, excludedSessions: document.excludedSessions, unreadable, subagentPrompts: subagents }, text: lines.join('\n') };
|
|
299
|
+
};
|
|
300
|
+
const readGold = async (file) => {
|
|
301
|
+
const text = await readFile(file, 'utf8').catch(() => {
|
|
302
|
+
throw new CliError(`Cannot read the gold file ${file}.`, ExitCode.USAGE, 'usage');
|
|
303
|
+
});
|
|
304
|
+
const arms = armsPath(file);
|
|
305
|
+
const document = await readFile(arms, 'utf8').then((raw) => JSON.parse(raw)).catch(() => undefined);
|
|
306
|
+
if (!isGoldDocument(document))
|
|
307
|
+
throw new CliError(`Cannot read the arms file ${arms}, which holds the signals for this gold file.`, ExitCode.USAGE, 'usage');
|
|
308
|
+
return { text, document };
|
|
309
|
+
};
|
|
310
|
+
/** `gold score`: reads whatever is judged so far, so it runs after any batch. */
|
|
311
|
+
export const sessionsGoldScore = async (options) => {
|
|
312
|
+
const { text, document } = await readGold(options.file);
|
|
313
|
+
const { judged, unreadable } = parseGold(text);
|
|
314
|
+
const score = scoreGold(document.entries, judged);
|
|
315
|
+
return { document: { ok: true, ...score, unreadable }, text: formatScore(score, unreadable) };
|
|
316
|
+
};
|
|
317
|
+
/** `gold arms`: the text arms and the fresh readers, into the sidecar only. It prints coverage and no reading: a read waits for judgments. */
|
|
318
|
+
export const sessionsGoldArms = async (deps, options) => {
|
|
319
|
+
const runtime = runtimeOf(deps);
|
|
320
|
+
if (runtime.evaluator === undefined)
|
|
321
|
+
throw new CliError('No evaluator is configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
322
|
+
const { document } = await readGold(options.file);
|
|
323
|
+
const redact = makeRedactor(await readRedactDomains(runtime.storeDir));
|
|
324
|
+
const outcome = await fillArms(runtime, document, redact, runtime.evaluator, sidecarSaver(armsPath(options.file), document, GOLD_MODE));
|
|
325
|
+
if (outcome.failure !== undefined)
|
|
326
|
+
throw new CliError(`${outcome.failure.message} ${plural(outcome.left, 'reading')} left, and every answer so far is saved. Rerun this command to go on.`, ExitCode.UNEXPECTED, 'unexpected');
|
|
327
|
+
const score = scoreGold(document.entries, new Map());
|
|
328
|
+
const lines = [
|
|
329
|
+
`Asked ${plural(outcome.asked, 'reading')} on ${EVALUATOR_MODEL}, with ${String(outcome.left)} left. The gold file is unchanged.`,
|
|
330
|
+
outcome.changed === 1
|
|
331
|
+
? '1 prompt has a transcript that is gone or no longer matches what the judge sees, so its text arm is unread.'
|
|
332
|
+
: `${String(outcome.changed)} prompts have a transcript that is gone or no longer matches what the judge sees, so their text arms are unread.`,
|
|
333
|
+
...coverageLines(score),
|
|
334
|
+
];
|
|
335
|
+
return { document: { ok: true, asked: outcome.asked, left: outcome.left, changed: outcome.changed, model: EVALUATOR_MODEL, coverage: score.coverage }, text: lines.join('\n') };
|
|
336
|
+
};
|
|
337
|
+
const installOf = (deps) => {
|
|
338
|
+
if (deps.install === undefined)
|
|
339
|
+
throw new CliError('Hook install is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
340
|
+
return deps.install;
|
|
341
|
+
};
|
|
342
|
+
export const sessionsHookInstall = async (deps, options) => {
|
|
343
|
+
const runtime = installOf(deps);
|
|
344
|
+
const lines = [];
|
|
345
|
+
const document = { ok: true };
|
|
346
|
+
// `--schedule` alone touches no settings file: nothing is written to one the operator has not named.
|
|
347
|
+
if (options.schedule === undefined || options.scope !== undefined || options.file !== undefined) {
|
|
348
|
+
const file = settingsPath(runtime, options.scope ?? 'file', options.file);
|
|
349
|
+
const result = await installHooks(runtime, file, options.keepRetention);
|
|
350
|
+
document.install = result;
|
|
351
|
+
lines.push(`Installed ${result.installed.join(', ')} hooks in ${file}.`, result.retention);
|
|
352
|
+
}
|
|
353
|
+
if (options.schedule !== undefined) {
|
|
354
|
+
const line = await setSchedule(runtime, options.schedule);
|
|
355
|
+
document.schedule = line;
|
|
356
|
+
lines.push(line);
|
|
357
|
+
}
|
|
358
|
+
return { document, text: lines.join('\n') };
|
|
359
|
+
};
|
|
360
|
+
export const sessionsHookUninstall = async (deps, options) => {
|
|
361
|
+
const runtime = installOf(deps);
|
|
362
|
+
const file = settingsPath(runtime, options.scope ?? 'file', options.file);
|
|
363
|
+
const result = await uninstallHooks(file);
|
|
364
|
+
const text = result.removed.length === 0 ? `No Many Oars hooks in ${file}.` : `Removed ${result.removed.join(', ')} hooks from ${file}; other settings and hooks are untouched (cleanupPeriodDays is left as it is).`;
|
|
365
|
+
return { document: { ok: true, ...result }, text };
|
|
366
|
+
};
|
|
367
|
+
export const sessionsHookStatus = async (deps, options) => {
|
|
368
|
+
const runtime = installOf(deps);
|
|
369
|
+
const files = await hookStatus(runtime, options.file);
|
|
370
|
+
const schedule = await scheduleStatus(runtime);
|
|
371
|
+
const lines = files.map((item) => `${item.scope}: ${item.file}: ${item.installed.length === 0 ? 'no hooks' : item.installed.join(', ')} (cleanupPeriodDays ${String(item.retentionDays)})`);
|
|
372
|
+
return { document: { ok: true, files, schedule }, text: [...lines, schedule].join('\n') };
|
|
373
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The Copilot home for an environment; a test passes its own so it never reads the real one. */
|
|
2
|
+
export declare const copilotRoot: (env: {
|
|
3
|
+
[name: string]: string | undefined;
|
|
4
|
+
}, home?: string) => string;
|
|
5
|
+
type Obj = {
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** A session found in the store, before its events are read. */
|
|
9
|
+
export interface CopilotSession {
|
|
10
|
+
/** The session folder's name, which is the session id (docs/features/session-persistence.md) and names the skeleton. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** `session.start` `context.cwd`; null when the head names none. */
|
|
13
|
+
folder: string | null;
|
|
14
|
+
/** The live `events.jsonl`. */
|
|
15
|
+
file: string;
|
|
16
|
+
/** Rotated archives `events.N.jsonl`, oldest first (by N); the session is these, then `file`. */
|
|
17
|
+
archives: string[];
|
|
18
|
+
/** The bytes of every file of the session, so a new archive or a longer live file changes the cursor. */
|
|
19
|
+
size: number;
|
|
20
|
+
/** The newest modification time among the session's files. */
|
|
21
|
+
mtimeMs: number;
|
|
22
|
+
}
|
|
23
|
+
export interface CopilotStore {
|
|
24
|
+
sessions: CopilotSession[];
|
|
25
|
+
/** The session as transcript text for `captureSession`; throws when the file cannot be read or holds no parseable line. */
|
|
26
|
+
transcript: (session: CopilotSession) => Promise<string>;
|
|
27
|
+
/** Folders skipped because their name is not a safe file name (it names a skeleton), counted so the skip is never silent. */
|
|
28
|
+
rejected: number;
|
|
29
|
+
/** Sessions that could not be inspected, reported once each and never fatal; the reason names no path. */
|
|
30
|
+
unreadable: {
|
|
31
|
+
file: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
/** The non-blank lines that are not JSON (a torn write), kept so `captureSession` counts them and the deposit line reports them. */
|
|
36
|
+
export declare const eventBadLines: (text: string) => string[];
|
|
37
|
+
/** One session as the lines `captureSession` reads. */
|
|
38
|
+
export declare const eventLines: (text: string, fallbackId: string) => Obj[];
|
|
39
|
+
export declare const openCopilotStore: (root: string) => Promise<CopilotStore | undefined>;
|
|
40
|
+
export {};
|