@manyoars/oars 0.3.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 +44 -29
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +102 -32
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/gate/hook-main.js +13 -1
- package/dist/gate/hook-process.d.ts +11 -1
- package/dist/gate/hook-process.js +50 -2
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +2 -0
- package/dist/gate/hook.js +3 -0
- package/dist/gate/replay.js +2 -5
- package/dist/gate/report.js +1 -1
- package/dist/gate/transcripts.js +1 -1
- package/dist/hook-bin.js +27 -2
- package/dist/issuer.d.ts +7 -9
- package/dist/issuer.js +10 -32
- package/dist/main.js +3 -0
- package/dist/sessions/actions.js +78 -0
- package/dist/sessions/allow.d.ts +10 -1
- package/dist/sessions/allow.js +29 -9
- package/dist/sessions/baseline.d.ts +1 -1
- package/dist/sessions/baseline.js +6 -4
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/cell-increments.d.ts +5 -1
- package/dist/sessions/cell-increments.js +84 -50
- package/dist/sessions/cells.d.ts +33 -3
- package/dist/sessions/cells.js +89 -9
- package/dist/sessions/codex.js +93 -27
- package/dist/sessions/command.d.ts +11 -1
- package/dist/sessions/command.js +64 -18
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.js +2 -2
- package/dist/sessions/derive.js +72 -6
- package/dist/sessions/exclusion.d.ts +6 -0
- package/dist/sessions/exclusion.js +8 -0
- package/dist/sessions/extract.d.ts +4 -2
- package/dist/sessions/extract.js +58 -26
- package/dist/sessions/hook-calls.js +1 -1
- package/dist/sessions/hook-index.d.ts +14 -1
- package/dist/sessions/hook-index.js +10 -3
- package/dist/sessions/hooks.d.ts +5 -1
- package/dist/sessions/hooks.js +133 -22
- package/dist/sessions/identity.d.ts +8 -3
- package/dist/sessions/identity.js +45 -15
- package/dist/sessions/install.d.ts +18 -2
- package/dist/sessions/install.js +53 -8
- package/dist/sessions/mcp.d.ts +3 -2
- package/dist/sessions/mcp.js +8 -4
- package/dist/sessions/motif.d.ts +2 -1
- package/dist/sessions/motif.js +2 -2
- 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 +2 -0
- package/dist/sessions/package-resolve.js +3 -1
- 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 +76 -5
- package/dist/sessions/pool-command.js +263 -136
- 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 +15 -2
- package/dist/sessions/pool-outbox.js +79 -26
- package/dist/sessions/pool-print.d.ts +1 -1
- 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 +240 -30
- package/dist/sessions/pool-wire.js +695 -215
- package/dist/sessions/public-vocabulary.d.ts +2 -3
- package/dist/sessions/public-vocabulary.js +2 -9
- package/dist/sessions/query.d.ts +16 -4
- package/dist/sessions/query.js +32 -10
- package/dist/sessions/read-cost.js +14 -3
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read.d.ts +8 -1
- package/dist/sessions/read.js +6 -3
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -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 +2 -0
- package/dist/sessions/recall-object.js +1 -0
- package/dist/sessions/recall-push.d.ts +2 -2
- package/dist/sessions/recall-push.js +3 -51
- package/dist/sessions/recall-reads.d.ts +7 -0
- package/dist/sessions/recall-reads.js +7 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +11 -0
- package/dist/sessions/redact.js +59 -0
- package/dist/sessions/schema.d.ts +59 -9
- package/dist/sessions/schema.js +22 -3
- package/dist/sessions/session-shape.d.ts +6 -3
- package/dist/sessions/session-shape.js +19 -6
- package/dist/sessions/sources.js +75 -1
- package/dist/sessions/store.d.ts +13 -1
- package/dist/sessions/store.js +36 -16
- package/dist/sessions/survival.d.ts +2 -0
- package/dist/sessions/survival.js +2 -2
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/vscode-copilot.js +24 -7
- 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 +2 -2
|
@@ -8,6 +8,17 @@ import { parseRows } from './record.js';
|
|
|
8
8
|
const unprinted = (text) => parseRows(text)
|
|
9
9
|
.map((row) => `${JSON.stringify(row.kind === 'crossing' ? { ...row, arm: 'none', lineShown: 'none', lineHeld: 'none' } : row)}\n`)
|
|
10
10
|
.join('');
|
|
11
|
+
/** The transcript path the harness put in the hook input, when there is one; anything else, including input that is not JSON, names none. */
|
|
12
|
+
const transcriptOf = (input) => {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(input);
|
|
15
|
+
const path = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'transcript_path') : undefined;
|
|
16
|
+
return typeof path === 'string' && path !== '' ? path : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
11
22
|
const sizeOf = (file) => {
|
|
12
23
|
try {
|
|
13
24
|
return statSync(file).size;
|
|
@@ -26,7 +37,33 @@ const landed = (file, text, size) => {
|
|
|
26
37
|
}
|
|
27
38
|
};
|
|
28
39
|
/** Resolves once `exit(0)` has been called, or never when the deadline timer is what ended the process. */
|
|
29
|
-
export const runHookProcess = async ({ stdin, deps, exit, write, deadlineMs }) => {
|
|
40
|
+
export const runHookProcess = async ({ stdin, deps, exit, write, deadlineMs, record, clock = Date.now }) => {
|
|
41
|
+
const started = clock();
|
|
42
|
+
// `held` and `error` are what the output cannot say; the callback reaches the hook through `deps`.
|
|
43
|
+
const how = { ended: null };
|
|
44
|
+
let fired;
|
|
45
|
+
let transcript;
|
|
46
|
+
// Telemetry hears of a fire once: the deadline timer records the fire it cuts short (the slowest ones), and the main path skips a fire the timer has already told.
|
|
47
|
+
let recorded = false;
|
|
48
|
+
// Set once the main path's `record` has resolved. While it is false after `recorded`, the main path is still reading the transcript, and a stalled read would otherwise lose the fire at the deadline.
|
|
49
|
+
let settled = false;
|
|
50
|
+
let told;
|
|
51
|
+
const send = async (result, transcript) => {
|
|
52
|
+
try {
|
|
53
|
+
await record?.({ latencyMs: Math.max(0, Math.round(clock() - started)), ...result }, transcript);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Telemetry never changes what the hook did.
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const tell = async (result, transcript) => {
|
|
60
|
+
if (recorded)
|
|
61
|
+
return;
|
|
62
|
+
recorded = true;
|
|
63
|
+
told = result;
|
|
64
|
+
await send(result, transcript);
|
|
65
|
+
settled = true;
|
|
66
|
+
};
|
|
30
67
|
// `size` is the file's length when the record was staged: an append that wrote and then stalled has put the record's own bytes past it, and the deadline must not write it again.
|
|
31
68
|
let staged = null;
|
|
32
69
|
// The last resort when the process is stuck past its grace: keep the staged record, then go. A write that fails here has nowhere to report, so it is dropped.
|
|
@@ -39,6 +76,12 @@ export const runHookProcess = async ({ stdin, deps, exit, write, deadlineMs }) =
|
|
|
39
76
|
catch {
|
|
40
77
|
// Fail-open.
|
|
41
78
|
}
|
|
79
|
+
// The fire the deadline cut short is still a fire: recorded as an error with nothing printed, after the row and before the stop so a slow stop cannot lose it.
|
|
80
|
+
// When the main path had told it first and its transcript read is still going, the fire it settled on goes now, with no transcript, so the read cannot cost the event.
|
|
81
|
+
if (!recorded)
|
|
82
|
+
void tell({ outcome: 'error', bytes: 0 });
|
|
83
|
+
else if (!settled && told !== undefined)
|
|
84
|
+
void send(told);
|
|
42
85
|
// A registry ask still running is then stopped, so the manager the hook started does not outlive it; a stop that throws must not cost the exit.
|
|
43
86
|
try {
|
|
44
87
|
staged?.stop?.();
|
|
@@ -52,13 +95,18 @@ export const runHookProcess = async ({ stdin, deps, exit, write, deadlineMs }) =
|
|
|
52
95
|
const built = deps((pending) => {
|
|
53
96
|
staged = pending === null ? null : { ...pending, size: sizeOf(pending.file) };
|
|
54
97
|
});
|
|
55
|
-
const
|
|
98
|
+
const input = await stdin();
|
|
99
|
+
transcript = transcriptOf(input);
|
|
100
|
+
const output = await gateHook(input, { ...built, ended: (result) => { how.ended = result; built.ended?.(result); } });
|
|
56
101
|
if (output !== '')
|
|
57
102
|
await write?.(output);
|
|
103
|
+
fired = { outcome: how.ended ?? (output === '' ? 'miss' : 'hit'), bytes: Buffer.byteLength(output) };
|
|
58
104
|
}
|
|
59
105
|
catch {
|
|
60
106
|
// Fail-open: a throw anywhere above is a record not written, never an exit code.
|
|
107
|
+
fired = { outcome: 'error', bytes: 0 };
|
|
61
108
|
}
|
|
109
|
+
await tell(fired, transcript);
|
|
62
110
|
// A manager the registry ask started can outlive the ask; leaving by `exit` rather than waiting for it is what keeps the call under a second.
|
|
63
111
|
exit(0);
|
|
64
112
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// What `oars gate hook` tells telemetry about one fire: the fire itself, plus the harness version and model its transcript states. Apart from `hook-main.ts` so a test drives the same line the process runs.
|
|
2
|
+
import { statedAt } from '../sessions/transcript-tail.js';
|
|
3
|
+
import { frameWords, HOOK_HARNESS } from '../telemetry/fields.js';
|
|
4
|
+
export const fireRecorder = (emit) => async (fire, transcript) => {
|
|
5
|
+
// No await before this on the deadline path (no transcript), so that fire is written before the timer's `exit`.
|
|
6
|
+
const stated = transcript === undefined ? undefined : await statedAt(transcript);
|
|
7
|
+
emit('hook', { name: 'gate', ...fire, ...frameWords(HOOK_HARNESS, stated?.harnessVersion, stated?.model) });
|
|
8
|
+
};
|
package/dist/gate/hook.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface GateHookDeps {
|
|
|
36
36
|
text: string;
|
|
37
37
|
stop?: () => void;
|
|
38
38
|
} | null) => void;
|
|
39
|
+
/** Told how a call ended when its output alone does not say: `held` when a note was kept from the agent (the unexposed arm), `error` when the hook failed open. For telemetry; it changes nothing. */
|
|
40
|
+
ended?: (how: 'held' | 'error') => void;
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
43
|
* Records the crossings of one tool call. Resolves with the JSON to print: a note as context, or a deny labeled with the fact; never an allow or a changed call.
|
package/dist/gate/hook.js
CHANGED
|
@@ -239,6 +239,8 @@ export const runGateHook = async (stdin, deps) => {
|
|
|
239
239
|
finally {
|
|
240
240
|
deps.stage?.(null);
|
|
241
241
|
}
|
|
242
|
+
if (blocks.length === 0 && notes.length === 0 && written.some((row) => row.arm === 'held'))
|
|
243
|
+
deps.ended?.('held');
|
|
242
244
|
await keepIndex(deps.storeDir, index, [...lines]).catch(() => undefined);
|
|
243
245
|
if (answered.length > 0)
|
|
244
246
|
await append(registryCachePath(deps.storeDir), answered.map((row) => `${JSON.stringify(row)}\n`).join('')).catch(() => undefined);
|
|
@@ -251,6 +253,7 @@ export const gateHook = async (stdin, deps) => {
|
|
|
251
253
|
}
|
|
252
254
|
catch {
|
|
253
255
|
// Fail-open: a recorder that cannot record must not cost a session its tool call.
|
|
256
|
+
deps.ended?.('error');
|
|
254
257
|
}
|
|
255
258
|
return '';
|
|
256
259
|
};
|
package/dist/gate/replay.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// It never calls `localSalt` or the step log, both of which write, and it holds no store lock, since a replay touches nothing a deposit reads.
|
|
3
3
|
import { readFile, stat } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { compileAllow } from '../sessions/allow.js';
|
|
5
|
+
import { compileAllow, runAllow } from '../sessions/allow.js';
|
|
7
6
|
import { makeRedactor } from '../sessions/redact.js';
|
|
8
7
|
import { paths, readState, writeAtomic } from '../sessions/store.js';
|
|
9
8
|
import { GATE_VERSION, liveCrossings, mergeCrossings, parseRows } from './record.js';
|
|
@@ -209,9 +208,7 @@ const readAll = async (runtime, entries, domains) => {
|
|
|
209
208
|
};
|
|
210
209
|
export const replay = async (runtime, options) => {
|
|
211
210
|
const state = await readState(runtime.storeDir);
|
|
212
|
-
const entries =
|
|
213
|
-
if (entries.length === 0)
|
|
214
|
-
throw new CliError('Name a folder with --allow: nothing is read from a folder you have not named, and no deposit has saved an allowlist.', ExitCode.USAGE, 'usage');
|
|
211
|
+
const entries = runAllow(state.allow, options.allow);
|
|
215
212
|
const { reads, transcripts, unreadable } = await readAll(runtime, entries, state.redactDomains);
|
|
216
213
|
const held = [];
|
|
217
214
|
for (const { transcript, read } of reads) {
|
package/dist/gate/report.js
CHANGED
|
@@ -9,7 +9,7 @@ export const LIMITS = [
|
|
|
9
9
|
'what a person saw or approved on screen; the permission mode is what the log kept',
|
|
10
10
|
'an MCP server that was started and never called',
|
|
11
11
|
'folders off the allowlist, and logs the harness has already cleaned up',
|
|
12
|
-
'a session in Codex, Cursor, Copilot, Gemini CLI, Grok Build,
|
|
12
|
+
'a session in Codex, Cursor, Copilot, Gemini CLI, Grok Build, OpenCode, Pi, VS Code Copilot Chat, or an OpenTelemetry GenAI trace, which replay does not read (`--allow codex`, `--allow cursor`, `--allow copilot`, `--allow gemini`, `--allow grok`, `--allow opencode`, `--allow pi`, `--allow vscode-copilot`, and `--allow otel` add nothing to it): they keep sessions in their own formats, not as transcript files',
|
|
13
13
|
'pool reads, sign-in, and deposit prompts; this release reads none of them',
|
|
14
14
|
];
|
|
15
15
|
const tally = (vocabulary, rows, pick) => vocabulary.map((value) => ({ value, count: rows.filter((row) => pick(row) === value).length }));
|
package/dist/gate/transcripts.js
CHANGED
|
@@ -28,7 +28,7 @@ export const discover = async (roots, allow) => {
|
|
|
28
28
|
const files = [];
|
|
29
29
|
let unreadable = 0;
|
|
30
30
|
for (const root of roots) {
|
|
31
|
-
// Replay reads only the harness's transcript format: a source-table folder (Cursor, Codex, Copilot, Gemini CLI, Grok Build, VS Code Copilot Chat) is not that format, and a project folder is never named for one.
|
|
31
|
+
// Replay reads only the harness's transcript format: a source-table folder (Cursor, Codex, Copilot, Gemini CLI, Grok Build, OpenCode, Pi, VS Code Copilot Chat) is not that format, and a project folder is never named for one.
|
|
32
32
|
const projects = (await listDir(root)).filter((entry) => entry.isDirectory() && !SOURCES.some((source) => source.name === entry.name) && allow.allows(entry.name));
|
|
33
33
|
for (const project of projects) {
|
|
34
34
|
try {
|
package/dist/hook-bin.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// The one entry file the installed hooks and the nightly task run: `node hook-bin.js <session-start|pre-tool|post-tool|stop|nightly>`, with the
|
|
2
2
|
// hook event JSON on stdin. It imports only the store modules (not the CLI), so a call costs little more than the node start, and it
|
|
3
3
|
// always exits 0 with no output on any failure: a hook must never fail or block a session.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
|
-
import {
|
|
6
|
+
import { hookEmitter } from './telemetry/hook-emit.js';
|
|
7
|
+
import { sendNightly } from './telemetry/send.js';
|
|
8
|
+
import { HOOK_EVENTS, hookRuntime, nightlyTelemetry, runHook } from './sessions/hooks.js';
|
|
6
9
|
const readStdin = async () => {
|
|
7
10
|
process.stdin.setEncoding('utf8');
|
|
8
11
|
let text = '';
|
|
@@ -10,13 +13,35 @@ const readStdin = async () => {
|
|
|
10
13
|
text += String(chunk);
|
|
11
14
|
return text;
|
|
12
15
|
};
|
|
16
|
+
/** The version in this package's package.json (one folder up from dist/), or `0.0.0` when it cannot be read. */
|
|
17
|
+
const versionOf = () => {
|
|
18
|
+
try {
|
|
19
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
20
|
+
const version = typeof manifest === 'object' && manifest !== null ? Reflect.get(manifest, 'version') : undefined;
|
|
21
|
+
return typeof version === 'string' ? version : '0.0.0';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** After the covenant is accepted, the nightly task sends what the pool outbox holds; with no server, sign-in, or acceptance it sends nothing. Loaded only here so the per-tool hooks never pay for it. It never throws. */
|
|
28
|
+
const nightlyPool = async (event) => {
|
|
29
|
+
const { nightlyPoolDeps } = await import('./sessions/pool-nightly-deps.js');
|
|
30
|
+
const { nightlyPoolSend } = await import('./sessions/pool-nightly.js');
|
|
31
|
+
await nightlyPoolSend(nightlyPoolDeps(process.env, hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), versionOf(), Date.now));
|
|
32
|
+
};
|
|
13
33
|
const eventOf = (name) => HOOK_EVENTS.find((event) => event === name);
|
|
14
34
|
try {
|
|
15
35
|
const event = eventOf(process.argv[2]);
|
|
16
36
|
if (event !== undefined) {
|
|
17
|
-
const output = await runHook(event, event === 'nightly' ? '' : await readStdin(), hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), Date.now);
|
|
37
|
+
const output = await runHook(event, event === 'nightly' ? '' : await readStdin(), hookRuntime(event, process.env, process.platform, homedir(), { stdin: process.stdin, stdout: process.stdout }), Date.now, nightlyTelemetry(event, process.env, process.platform, homedir(), versionOf, Date.now), hookEmitter(process.env, process.platform, homedir(), new URL('../package.json', import.meta.url), Date.now));
|
|
18
38
|
if (output !== undefined)
|
|
19
39
|
process.stdout.write(output);
|
|
40
|
+
// The nightly task is where telemetry is sent from; it never throws, and it sends nothing unless telemetry is on and the notice was shown (the server is OARS_POOL_URL, else the Many Oars pool).
|
|
41
|
+
if (event === 'nightly') {
|
|
42
|
+
await sendNightly(process.env, process.platform, homedir(), Date.now);
|
|
43
|
+
await nightlyPool(event);
|
|
44
|
+
}
|
|
20
45
|
}
|
|
21
46
|
}
|
|
22
47
|
catch {
|
package/dist/issuer.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import type { RunDeps } from './cli.js';
|
|
2
2
|
/**
|
|
3
|
-
* The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it
|
|
4
|
-
*
|
|
3
|
+
* The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it,
|
|
4
|
+
* and is the default for `oars login`: no site or file can send a login anywhere else.
|
|
5
5
|
*/
|
|
6
6
|
export declare const PRODUCTION_ISSUER_URL = "https://login.manyoars.com/realms/aor";
|
|
7
|
-
/** The marketing site serves `{"launched": true}` here from launch day. */
|
|
8
|
-
export declare const LAUNCH_FLAG_URL = "https://manyoars.com/.well-known/oars.json";
|
|
9
7
|
/**
|
|
10
|
-
* The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If that issuer does not answer
|
|
9
|
+
* discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not up yet, so say so (exit 2) and
|
|
10
|
+
* name the pool. An issuer the developer named keeps its own error.
|
|
13
11
|
*/
|
|
14
12
|
export declare const discoverIssuer: <T>(deps: RunDeps, flag: string | undefined, discover: () => Promise<T>) => Promise<T>;
|
|
15
|
-
/** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer
|
|
16
|
-
export declare const resolveIssuerUrl: (deps: RunDeps, flag?: string) =>
|
|
13
|
+
/** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer. */
|
|
14
|
+
export declare const resolveIssuerUrl: (deps: RunDeps, flag?: string) => string;
|
package/dist/issuer.js
CHANGED
|
@@ -1,34 +1,14 @@
|
|
|
1
1
|
import { CliError, ExitCode } from './errors.js';
|
|
2
2
|
/**
|
|
3
|
-
* The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it
|
|
4
|
-
*
|
|
3
|
+
* The production Keycloak realm (the platform's k8s/configmap.yaml: KEYCLOAK_ISSUER_URI). It ships in the CLI, reviewed with it,
|
|
4
|
+
* and is the default for `oars login`: no site or file can send a login anywhere else.
|
|
5
5
|
*/
|
|
6
6
|
export const PRODUCTION_ISSUER_URL = 'https://login.manyoars.com/realms/aor';
|
|
7
|
-
|
|
8
|
-
export const LAUNCH_FLAG_URL = 'https://manyoars.com/.well-known/oars.json';
|
|
9
|
-
const FLAG_TIMEOUT_MS = 5000;
|
|
10
|
-
const NOT_LAUNCHED = 'Many Oars has not launched yet: pass --issuer-url or set OARS_ISSUER_URL.';
|
|
11
|
-
/** True only for a 200 answer whose JSON is an object with `launched: true`; every other answer, or none, means not launched. */
|
|
12
|
-
const hasLaunched = async (deps) => {
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch(LAUNCH_FLAG_URL, {
|
|
15
|
-
headers: { 'User-Agent': `oars/${deps.version}`, Accept: 'application/json' },
|
|
16
|
-
redirect: 'error',
|
|
17
|
-
signal: AbortSignal.timeout(FLAG_TIMEOUT_MS),
|
|
18
|
-
});
|
|
19
|
-
if (!response.ok)
|
|
20
|
-
return false;
|
|
21
|
-
const body = await response.json();
|
|
22
|
-
return typeof body === 'object' && body !== null && Reflect.get(body, 'launched') === true;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
7
|
+
const UNAVAILABLE = 'Cannot reach Many Oars sign-in at login.manyoars.com (Many Oars and its pool at pool.manyoars.com may not be deployed yet). Pass --issuer-url or set OARS_ISSUER_URL to use another issuer.';
|
|
28
8
|
/**
|
|
29
|
-
* The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If
|
|
30
|
-
*
|
|
31
|
-
*
|
|
9
|
+
* The built-in issuer is in use only when neither `--issuer-url` nor `OARS_ISSUER_URL` named one. If that issuer does not answer
|
|
10
|
+
* discovery (no answer, an error status, or a body that is not discovery JSON), the platform is not up yet, so say so (exit 2) and
|
|
11
|
+
* name the pool. An issuer the developer named keeps its own error.
|
|
32
12
|
*/
|
|
33
13
|
export const discoverIssuer = async (deps, flag, discover) => {
|
|
34
14
|
try {
|
|
@@ -38,18 +18,16 @@ export const discoverIssuer = async (deps, flag, discover) => {
|
|
|
38
18
|
const named = flag !== undefined || (deps.env.OARS_ISSUER_URL !== undefined && deps.env.OARS_ISSUER_URL !== '');
|
|
39
19
|
const unanswered = error instanceof CliError && (error.exitCode === ExitCode.UNREACHABLE || error.exitCode === ExitCode.UNEXPECTED);
|
|
40
20
|
if (!named && unanswered)
|
|
41
|
-
throw new CliError(
|
|
21
|
+
throw new CliError(UNAVAILABLE, ExitCode.USAGE, 'usage');
|
|
42
22
|
throw error;
|
|
43
23
|
}
|
|
44
24
|
};
|
|
45
|
-
/** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer
|
|
46
|
-
export const resolveIssuerUrl =
|
|
25
|
+
/** `--issuer-url`, else `OARS_ISSUER_URL`, else the built-in production issuer. */
|
|
26
|
+
export const resolveIssuerUrl = (deps, flag) => {
|
|
47
27
|
if (flag !== undefined)
|
|
48
28
|
return flag;
|
|
49
29
|
const fromEnv = deps.env.OARS_ISSUER_URL;
|
|
50
30
|
if (fromEnv !== undefined && fromEnv !== '')
|
|
51
31
|
return fromEnv;
|
|
52
|
-
|
|
53
|
-
return PRODUCTION_ISSUER_URL;
|
|
54
|
-
throw new CliError(NOT_LAUNCHED, ExitCode.USAGE, 'usage');
|
|
32
|
+
return PRODUCTION_ISSUER_URL;
|
|
55
33
|
};
|
package/dist/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { defaultConfigDir, fileCredentialStore } from './credentials.js';
|
|
|
10
10
|
import { cursorRoot } from './sessions/cursor.js';
|
|
11
11
|
import { claudeEvaluator, runClaude } from './sessions/gold-arms.js';
|
|
12
12
|
import { SOURCES } from './sessions/sources.js';
|
|
13
|
+
import { spawnDetachedSend } from './telemetry/send.js';
|
|
13
14
|
/** The version in this package's package.json (one folder up from dist/). */
|
|
14
15
|
const readVersion = () => {
|
|
15
16
|
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -25,12 +26,14 @@ process.exitCode = await run(process.argv.slice(2), {
|
|
|
25
26
|
stderr: (text) => process.stderr.write(text),
|
|
26
27
|
},
|
|
27
28
|
store: fileCredentialStore(configDir),
|
|
29
|
+
telemetry: { dir: join(configDir, 'oars', 'telemetry'), platform: process.platform, spawnSend: spawnDetachedSend },
|
|
28
30
|
version: readVersion(),
|
|
29
31
|
sleep: (ms) => sleep(ms),
|
|
30
32
|
random: Math.random,
|
|
31
33
|
now: Date.now,
|
|
32
34
|
openBrowser,
|
|
33
35
|
install: {
|
|
36
|
+
platform: process.platform,
|
|
34
37
|
nodePath: process.execPath,
|
|
35
38
|
hookBin: fileURLToPath(new URL('./hook-bin.js', import.meta.url)),
|
|
36
39
|
userSettings: join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'settings.json'),
|
package/dist/sessions/actions.js
CHANGED
|
@@ -328,6 +328,84 @@ export const ACTION_TABLES = {
|
|
|
328
328
|
toolDelimited: true,
|
|
329
329
|
shells: { bash: 'bash', run_terminal_cmd: 'bash', run_terminal_command: 'bash' },
|
|
330
330
|
},
|
|
331
|
+
// OpenCode's own names (sst/opencode @ 21bf6b4, packages/core/src/tool/plugin/*.ts, each file's `export const name`): a tool part's `name`. The reader keeps that name in
|
|
332
|
+
// `native`, which is what this table looks up, and writes `Bash`, `Edit`, `Write`, `Read`, `Grep`, and `Glob` for the extractor. An MCP tool is `<server>_<tool>`
|
|
333
|
+
// (tool/runtime.ts:290-293), the same spelling as a built-in's, so it cannot be told apart and reads as an unknown tool with its name withheld.
|
|
334
|
+
opencode: {
|
|
335
|
+
rows: {
|
|
336
|
+
shell: 'shell.exec',
|
|
337
|
+
edit: 'file.edit',
|
|
338
|
+
patch: 'file.edit',
|
|
339
|
+
write: 'file.write',
|
|
340
|
+
read: 'file.read',
|
|
341
|
+
grep: 'file.search',
|
|
342
|
+
glob: 'file.search',
|
|
343
|
+
websearch: 'web.search',
|
|
344
|
+
webfetch: 'web.fetch',
|
|
345
|
+
question: 'ask.user',
|
|
346
|
+
subagent: 'agent.spawn',
|
|
347
|
+
skill: 'skill.run',
|
|
348
|
+
todowrite: 'plan.update',
|
|
349
|
+
// Known public names with no canonical action.
|
|
350
|
+
experimental_image_generate: 'other',
|
|
351
|
+
artifact_create: 'other',
|
|
352
|
+
artifact_read: 'other',
|
|
353
|
+
artifact_update: 'other',
|
|
354
|
+
artifact_delete: 'other',
|
|
355
|
+
schedule_create: 'other',
|
|
356
|
+
schedule_list: 'other',
|
|
357
|
+
schedule_pause: 'other',
|
|
358
|
+
schedule_resume: 'other',
|
|
359
|
+
schedule_delete: 'other',
|
|
360
|
+
},
|
|
361
|
+
mcp: null,
|
|
362
|
+
toolDelimited: false,
|
|
363
|
+
shells: { shell: 'bash' },
|
|
364
|
+
},
|
|
365
|
+
// Pi's own names (badlogic/pi-mono @ 085c84e, packages/coding-agent, docs/cli.md:144-158): `read`, `bash`, `powershell` (Windows), `edit`, `write`, `grep`, `find`, `ls`,
|
|
366
|
+
// `codemode`, and `tool_search`. A shell call and an edit reach the transcript as `Bash` and `Edit` with the original name in `native`, which is what this table looks up.
|
|
367
|
+
// Pi registers an MCP tool as `mcp__<server>__<tool>` (docs/mcp.md:77). It has no sub-agent, skill, plan, or ask tool of its own (README.md:19).
|
|
368
|
+
pi: {
|
|
369
|
+
rows: {
|
|
370
|
+
read: 'file.read',
|
|
371
|
+
bash: 'shell.exec',
|
|
372
|
+
powershell: 'shell.exec',
|
|
373
|
+
edit: 'file.edit',
|
|
374
|
+
write: 'file.write',
|
|
375
|
+
grep: 'file.search',
|
|
376
|
+
find: 'file.search',
|
|
377
|
+
ls: 'file.search',
|
|
378
|
+
codemode: 'other',
|
|
379
|
+
tool_search: 'other',
|
|
380
|
+
},
|
|
381
|
+
mcp: /^mcp__(?<server>.+?)__(?<tool>.+)$/,
|
|
382
|
+
toolDelimited: true,
|
|
383
|
+
shells: { bash: 'bash', powershell: 'powershell' },
|
|
384
|
+
},
|
|
385
|
+
// OpenTelemetry GenAI traces (open-telemetry/semantic-conventions-genai @ e6658f1, docs/gen-ai/execute-tool.md): `gen_ai.tool.name` is whatever the instrumented agent calls its
|
|
386
|
+
// tool, so no name is guaranteed. These are the names the conventions use in their own examples and refinements (the command, skill, and agent-as-tool sections); a span the
|
|
387
|
+
// reader recognizes by attributes alone (a nonzero `process.exit.code`, a `gen_ai.skill.name`) is renamed before this table is read. An MCP call is spelled `mcp__<server>__<tool>`.
|
|
388
|
+
otel: {
|
|
389
|
+
rows: {
|
|
390
|
+
exec_command: 'shell.exec',
|
|
391
|
+
execute: 'shell.exec',
|
|
392
|
+
shell: 'shell.exec',
|
|
393
|
+
bash: 'shell.exec',
|
|
394
|
+
command: 'shell.exec',
|
|
395
|
+
run_skill_script: 'shell.exec',
|
|
396
|
+
load_skill: 'skill.run',
|
|
397
|
+
skills: 'skill.run',
|
|
398
|
+
get_skill_instructions: 'skill.run',
|
|
399
|
+
load_skill_resource: 'file.read',
|
|
400
|
+
read_skill_resource: 'file.read',
|
|
401
|
+
get_skill_reference: 'file.read',
|
|
402
|
+
transfer_to_agent: 'agent.spawn',
|
|
403
|
+
invoke_agent: 'agent.spawn',
|
|
404
|
+
},
|
|
405
|
+
mcp: /^mcp__(?<server>.+?)__(?<tool>.+)$/,
|
|
406
|
+
toolDelimited: true,
|
|
407
|
+
shells: { bash: 'bash', shell: 'bash', exec_command: 'bash' },
|
|
408
|
+
},
|
|
331
409
|
};
|
|
332
410
|
/** The MCP call a native name spells, in full, as the transcript wrote it; undefined for any other name. */
|
|
333
411
|
export const mcpOf = (table, native) => {
|
package/dist/sessions/allow.d.ts
CHANGED
|
@@ -8,7 +8,16 @@ export interface Allow {
|
|
|
8
8
|
/** The entries that matched none of `names`. */
|
|
9
9
|
unmatched: (names: readonly string[]) => string[];
|
|
10
10
|
}
|
|
11
|
+
/** The prefix `--deny` saves an entry under. */
|
|
12
|
+
export declare const DENY = "!";
|
|
13
|
+
export declare const denialOf: (entry: string) => string;
|
|
11
14
|
export declare const compileAllow: (entries: readonly string[]) => Allow;
|
|
15
|
+
/**
|
|
16
|
+
* The list a deposit saves: the saved entries and the given ones, where naming a folder again with `--allow` lifts the denial of exactly
|
|
17
|
+
* that entry. A denial is kept until then, so a later `--allow` of a wider prefix cannot read a denied folder.
|
|
18
|
+
*/
|
|
19
|
+
export declare const mergeAllow: (saved: readonly string[], given: readonly string[]) => string[];
|
|
20
|
+
/** What one deposit reads: the folders it names, else the saved list (empty is every folder), less every denial saved or given. */
|
|
21
|
+
export declare const runAllow: (saved: readonly string[], given: readonly string[]) => string[];
|
|
12
22
|
/** One entry per line; blank lines and lines starting with `#` are skipped. */
|
|
13
23
|
export declare const parseAllowList: (text: string) => string[];
|
|
14
|
-
export declare const requireEntries: (entries: readonly string[]) => void;
|
package/dist/sessions/allow.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// a folder prefix `~/work/` (trailing slash): that folder and everything under it
|
|
4
4
|
// a glob `*-work-*`, `C:\work\app-*`: `*` any run of characters, `?` one character
|
|
5
5
|
// A path is matched through its slug, because Claude Code names a project folder after its working directory with every
|
|
6
|
-
// non-alphanumeric character as `-`.
|
|
6
|
+
// non-alphanumeric character as `-`. An entry that starts with `!` (what `--deny` saves) keeps the folders it names out, whatever
|
|
7
|
+
// else names them. With no entry that is not a denial, every folder is on the list: the default is every folder.
|
|
7
8
|
import { homedir } from 'node:os';
|
|
8
9
|
import { resolve } from 'node:path';
|
|
9
|
-
import { CliError, ExitCode } from '../errors.js';
|
|
10
10
|
/** A leading `~` is the home folder, expanded here because a quoted entry or one read from a list file reaches us unexpanded by any shell. */
|
|
11
11
|
export const expandHome = (entry) => (/^~(?:[\\/]|$)/.test(entry) ? homedir() + entry.slice(1) : entry);
|
|
12
12
|
/** Claude Code names a project folder after its working directory with every non-alphanumeric character as `-`. */
|
|
@@ -32,20 +32,40 @@ const matcherOf = (entry) => {
|
|
|
32
32
|
const slug = slugOf(entry);
|
|
33
33
|
return (name) => name === entry || name === slug;
|
|
34
34
|
};
|
|
35
|
+
/** The prefix `--deny` saves an entry under. */
|
|
36
|
+
export const DENY = '!';
|
|
37
|
+
const isDenial = (entry) => entry.startsWith(DENY);
|
|
38
|
+
export const denialOf = (entry) => `${DENY}${entry}`;
|
|
35
39
|
export const compileAllow = (entries) => {
|
|
36
|
-
const compiled = entries.map((entry) => ({ entry, matches: matcherOf(expandHome(entry)) }));
|
|
40
|
+
const compiled = entries.filter((entry) => !isDenial(entry)).map((entry) => ({ entry, matches: matcherOf(expandHome(entry)) }));
|
|
41
|
+
const denied = entries.filter(isDenial).map((entry) => matcherOf(expandHome(entry.slice(DENY.length))));
|
|
37
42
|
return {
|
|
38
|
-
allows: (name) => compiled.some((item) => item.matches(name)),
|
|
43
|
+
allows: (name) => (compiled.length === 0 || compiled.some((item) => item.matches(name))) && !denied.some((matches) => matches(name)),
|
|
39
44
|
unmatched: (names) => compiled.filter((item) => !names.some((name) => item.matches(name))).map((item) => item.entry),
|
|
40
45
|
};
|
|
41
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Whether the named entries only lift saved denials: each is denied in `saved`, and `saved` names no folder. On every-folder scope such
|
|
49
|
+
* an `--allow` drops the denial and adds nothing, because adding the entry would narrow every later run to it.
|
|
50
|
+
*/
|
|
51
|
+
const liftsOnly = (saved, named) => named.length > 0 && saved.every(isDenial) && named.every((entry) => saved.includes(denialOf(entry)));
|
|
52
|
+
/**
|
|
53
|
+
* The list a deposit saves: the saved entries and the given ones, where naming a folder again with `--allow` lifts the denial of exactly
|
|
54
|
+
* that entry. A denial is kept until then, so a later `--allow` of a wider prefix cannot read a denied folder.
|
|
55
|
+
*/
|
|
56
|
+
export const mergeAllow = (saved, given) => {
|
|
57
|
+
const kept = saved.filter((entry) => !(isDenial(entry) && given.includes(entry.slice(DENY.length))));
|
|
58
|
+
const added = liftsOnly(saved, given.filter((entry) => !isDenial(entry))) ? given.filter(isDenial) : given;
|
|
59
|
+
return [...new Set([...kept, ...added])];
|
|
60
|
+
};
|
|
61
|
+
/** What one deposit reads: the folders it names, else the saved list (empty is every folder), less every denial saved or given. */
|
|
62
|
+
export const runAllow = (saved, given) => {
|
|
63
|
+
const merged = mergeAllow(saved, given);
|
|
64
|
+
const named = given.filter((entry) => !isDenial(entry));
|
|
65
|
+
return named.length === 0 || liftsOnly(saved, named) ? merged : [...named, ...merged.filter(isDenial)];
|
|
66
|
+
};
|
|
42
67
|
/** One entry per line; blank lines and lines starting with `#` are skipped. */
|
|
43
68
|
export const parseAllowList = (text) => text
|
|
44
69
|
.split(/\r?\n/)
|
|
45
70
|
.map((line) => line.trim())
|
|
46
71
|
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
47
|
-
export const requireEntries = (entries) => {
|
|
48
|
-
if (entries.length === 0) {
|
|
49
|
-
throw new CliError('Nothing to read: pass at least one --allow <folder> or --allow-file <list>. Nothing is read from a folder you have not named.', ExitCode.USAGE, 'usage');
|
|
50
|
-
}
|
|
51
|
-
};
|
|
@@ -19,7 +19,7 @@ export type Dimension = (typeof DIMENSIONS)[number];
|
|
|
19
19
|
/** The fields each record needs, so a caller passes the store's records and a test passes small ones. */
|
|
20
20
|
export type BaselineSession = Pick<SessionObservation, 'session' | 'tsFirst' | 'tools' | 'harness'>;
|
|
21
21
|
export type BaselineTool = Pick<ToolObservation, 'session' | 'status' | 'durationMs'>;
|
|
22
|
-
export type BaselinePrompt = Pick<PromptObservation, 'session' | 'wallSec' | 'cls' | 'turns' | 'usage'>;
|
|
22
|
+
export type BaselinePrompt = Pick<PromptObservation, 'session' | 'wallSec' | 'cls' | 'turns' | 'usage' | 'usageObserved'>;
|
|
23
23
|
export interface DimensionDistance {
|
|
24
24
|
dimension: Dimension;
|
|
25
25
|
distance: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Layer 2's self-baseline (spec 2.2): a session's week against the operator's own other weeks on the same harness. Local only: no pooled field,
|
|
2
2
|
// no network, no account step. The peer baseline waits for T2.
|
|
3
|
-
import { harnessOf } from './schema.js';
|
|
3
|
+
import { harnessOf, observesUsage } from './schema.js';
|
|
4
4
|
/** A week needs this many tool calls to be a unit or a baseline week (spec 5.4 item 1). */
|
|
5
5
|
export const BASELINE_MIN_CALLS = 1000;
|
|
6
6
|
/** A baseline needs this many other qualifying weeks (spec 5.4 item 1). */
|
|
@@ -137,10 +137,12 @@ export const selfBaseline = (sessions, tools, prompts, asOf) => {
|
|
|
137
137
|
add(histogramOf(group.histograms, 'prompt class'), prompt.cls);
|
|
138
138
|
if (prompt.wallSec !== null)
|
|
139
139
|
add(histogramOf(group.histograms, 'wallSec'), binOf(prompt.wallSec, WALL_EDGES_SEC));
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// A category the source never recorded is a placeholder zero, not a measurement: the share needs input and cacheRead, and counts only the input-side categories recorded;
|
|
141
|
+
// output per turn needs output. A prompt missing what a dimension needs adds no count to it, so its denominator holds only the prompts that recorded the category.
|
|
142
|
+
const input = ['input', 'cacheWrite', 'cacheRead'].reduce((sum, field) => sum + (observesUsage(prompt, field) ? prompt.usage[field] : 0), 0);
|
|
143
|
+
if (observesUsage(prompt, 'input') && observesUsage(prompt, 'cacheRead') && input > 0)
|
|
142
144
|
add(histogramOf(group.histograms, 'cache-read share'), binOf(prompt.usage.cacheRead * 10, CACHE_SHARE_TENTHS, input));
|
|
143
|
-
if (prompt.turns > 0)
|
|
145
|
+
if (observesUsage(prompt, 'output') && prompt.turns > 0)
|
|
144
146
|
add(histogramOf(group.histograms, 'output per turn'), binOf(prompt.usage.output, OUTPUT_PER_TURN_EDGES, prompt.turns));
|
|
145
147
|
}
|
|
146
148
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TargetBase } from './schema.js';
|
|
2
|
+
/** Where a path target sits in the project, as a closed list. `none` is a call with no path target (a shell call, a search, a fetch). */
|
|
3
|
+
export declare const TARGET_CLASSES: readonly ["source", "test", "config", "doc", "lockfile", "generated", "data", "none", "other"];
|
|
4
|
+
export type TargetClass = (typeof TARGET_CLASSES)[number];
|
|
5
|
+
/** What an argument holds, as a closed list: a call's arguments are the sorted set of these. `none` is a call with no arguments. */
|
|
6
|
+
export declare const ARG_CLASSES: readonly ["path", "url", "pattern", "command", "number", "flag", "text", "none"];
|
|
7
|
+
export type ArgClass = (typeof ARG_CLASSES)[number];
|
|
8
|
+
/**
|
|
9
|
+
* The class of a path target, from the base that hashed it and the path's own suffix. A call with no path target (`base` null) is `none`; the order below is the order a path is asked,
|
|
10
|
+
* so a lockfile is never `config` and a `tests/data.json` is `test`. The mapping is pinned in `call-shape.test.ts`. `path` is the key the target is hashed on (relative to the repository or the cwd, never the absolute path), so the folders above a repository do not decide the class; it is read here and goes no further.
|
|
11
|
+
*/
|
|
12
|
+
export declare const targetClassOf: (path: string | undefined, base: TargetBase | null) => TargetClass;
|
|
13
|
+
/**
|
|
14
|
+
* The classes of a call's arguments, as a sorted set: which of path, URL, pattern, command, number, flag, and text its input holds, or just `none` when it holds no value. A key name
|
|
15
|
+
* gives the class of a string (`file_path` is a path, `pattern` a pattern, `command` a command), a number or a boolean gives its own, and any other value is text. The values are read here and go no further.
|
|
16
|
+
*/
|
|
17
|
+
export declare const argClassOf: (input: {
|
|
18
|
+
readonly [key: string]: unknown;
|
|
19
|
+
}) => ArgClass[];
|