@manyoars/oars 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -28
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +10 -4
- package/dist/cli.js +440 -91
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -3
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +47 -0
- package/dist/gate/hook-process.d.ts +24 -0
- package/dist/gate/hook-process.js +112 -0
- package/dist/gate/hook-record.d.ts +3 -0
- package/dist/gate/hook-record.js +8 -0
- package/dist/gate/hook.d.ts +48 -0
- package/dist/gate/hook.js +259 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +254 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +50 -0
- package/dist/issuer.d.ts +10 -6
- package/dist/issuer.js +19 -24
- package/dist/main.d.ts +1 -0
- package/dist/main.js +67 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +429 -0
- package/dist/sessions/allow.d.ts +23 -0
- package/dist/sessions/allow.js +71 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +190 -0
- package/dist/sessions/call-shape.d.ts +19 -0
- package/dist/sessions/call-shape.js +85 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +47 -0
- package/dist/sessions/cell-increments.js +243 -0
- package/dist/sessions/cells.d.ts +109 -0
- package/dist/sessions/cells.js +261 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +745 -0
- package/dist/sessions/command.d.ts +127 -0
- package/dist/sessions/command.js +419 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/coverage.d.ts +19 -0
- package/dist/sessions/coverage.js +56 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +386 -0
- package/dist/sessions/exclusion.d.ts +87 -0
- package/dist/sessions/exclusion.js +361 -0
- package/dist/sessions/extract.d.ts +181 -0
- package/dist/sessions/extract.js +1806 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +66 -0
- package/dist/sessions/hook-index.js +164 -0
- package/dist/sessions/hooks.d.ts +17 -0
- package/dist/sessions/hooks.js +281 -0
- package/dist/sessions/identity.d.ts +27 -0
- package/dist/sessions/identity.js +131 -0
- package/dist/sessions/install.d.ts +70 -0
- package/dist/sessions/install.js +246 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +7 -0
- package/dist/sessions/mcp.js +157 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +44 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/opencode.d.ts +33 -0
- package/dist/sessions/opencode.js +319 -0
- package/dist/sessions/otel.d.ts +60 -0
- package/dist/sessions/otel.js +494 -0
- package/dist/sessions/package-books.d.ts +48 -0
- package/dist/sessions/package-books.js +120 -0
- package/dist/sessions/package-resolve.d.ts +32 -0
- package/dist/sessions/package-resolve.js +185 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pi.d.ts +36 -0
- package/dist/sessions/pi.js +353 -0
- package/dist/sessions/pool-books.d.ts +21 -0
- package/dist/sessions/pool-books.js +75 -0
- package/dist/sessions/pool-choices.d.ts +5 -0
- package/dist/sessions/pool-choices.js +11 -0
- package/dist/sessions/pool-command.d.ts +115 -0
- package/dist/sessions/pool-command.js +669 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-nightly-deps.d.ts +3 -0
- package/dist/sessions/pool-nightly-deps.js +11 -0
- package/dist/sessions/pool-nightly.d.ts +10 -0
- package/dist/sessions/pool-nightly.js +85 -0
- package/dist/sessions/pool-outbox.d.ts +76 -0
- package/dist/sessions/pool-outbox.js +313 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-raw.d.ts +27 -0
- package/dist/sessions/pool-raw.js +68 -0
- package/dist/sessions/pool-replica.d.ts +35 -0
- package/dist/sessions/pool-replica.js +91 -0
- package/dist/sessions/pool-send-lock.d.ts +20 -0
- package/dist/sessions/pool-send-lock.js +87 -0
- package/dist/sessions/pool-sent.d.ts +4 -0
- package/dist/sessions/pool-sent.js +26 -0
- package/dist/sessions/pool-totals.d.ts +10 -0
- package/dist/sessions/pool-totals.js +21 -0
- package/dist/sessions/pool-wire.d.ts +435 -0
- package/dist/sessions/pool-wire.js +1100 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +72 -0
- package/dist/sessions/public-vocabulary.js +295 -0
- package/dist/sessions/query.d.ts +123 -0
- package/dist/sessions/query.js +268 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +48 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery-edge.d.ts +2 -0
- package/dist/sessions/read-recovery-edge.js +26 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +86 -0
- package/dist/sessions/read.js +87 -0
- package/dist/sessions/recall-bundle.d.ts +12 -0
- package/dist/sessions/recall-bundle.js +33 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-frames.d.ts +18 -0
- package/dist/sessions/recall-frames.js +44 -0
- package/dist/sessions/recall-next.d.ts +145 -0
- package/dist/sessions/recall-next.js +241 -0
- package/dist/sessions/recall-object.d.ts +139 -0
- package/dist/sessions/recall-object.js +78 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +47 -0
- package/dist/sessions/recall-reads.d.ts +124 -0
- package/dist/sessions/recall-reads.js +417 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/recovery-calls.d.ts +21 -0
- package/dist/sessions/recovery-calls.js +25 -0
- package/dist/sessions/redact.d.ts +17 -0
- package/dist/sessions/redact.js +113 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +694 -0
- package/dist/sessions/schema.js +167 -0
- package/dist/sessions/session-shape.d.ts +76 -0
- package/dist/sessions/session-shape.js +355 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +233 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +309 -0
- package/dist/sessions/store.js +1498 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +27 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/transcript-tail.d.ts +14 -0
- package/dist/sessions/transcript-tail.js +64 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +494 -0
- package/dist/telemetry/command.d.ts +8 -0
- package/dist/telemetry/command.js +85 -0
- package/dist/telemetry/consent.d.ts +17 -0
- package/dist/telemetry/consent.js +63 -0
- package/dist/telemetry/crash.d.ts +8 -0
- package/dist/telemetry/crash.js +52 -0
- package/dist/telemetry/emit.d.ts +31 -0
- package/dist/telemetry/emit.js +42 -0
- package/dist/telemetry/fields.d.ts +16 -0
- package/dist/telemetry/fields.js +26 -0
- package/dist/telemetry/health.d.ts +21 -0
- package/dist/telemetry/health.js +108 -0
- package/dist/telemetry/hook-emit.d.ts +4 -0
- package/dist/telemetry/hook-emit.js +18 -0
- package/dist/telemetry/measures.d.ts +32 -0
- package/dist/telemetry/measures.js +128 -0
- package/dist/telemetry/observe.d.ts +13 -0
- package/dist/telemetry/observe.js +51 -0
- package/dist/telemetry/read-events.d.ts +9 -0
- package/dist/telemetry/read-events.js +17 -0
- package/dist/telemetry/record.d.ts +17 -0
- package/dist/telemetry/record.js +26 -0
- package/dist/telemetry/report.d.ts +77 -0
- package/dist/telemetry/report.js +188 -0
- package/dist/telemetry/schema.d.ts +40 -0
- package/dist/telemetry/schema.js +187 -0
- package/dist/telemetry/send-main.d.ts +1 -0
- package/dist/telemetry/send-main.js +11 -0
- package/dist/telemetry/send.d.ts +36 -0
- package/dist/telemetry/send.js +172 -0
- package/dist/telemetry/spool.d.ts +17 -0
- package/dist/telemetry/spool.js +70 -0
- package/dist/telemetry/wire.d.ts +25 -0
- package/dist/telemetry/wire.js +76 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -38
- package/dist/commands/deploy-input.js +0 -150
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -124
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/templates.d.ts +0 -11
- package/dist/commands/templates.js +0 -32
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The `oars gate` command group's bodies: each returns the `--json` document and the text a human sees.
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { CliError, ExitCode } from '../errors.js';
|
|
4
|
+
import { crossingsPath } from './hook.js';
|
|
5
|
+
import { COMMAND, EVENT, installGateHook, MATCHER, uninstallGateHook } from './install.js';
|
|
6
|
+
import { loadIndex } from './novelty.js';
|
|
7
|
+
import { formatPolicy, loadPolicy, policyPath } from './policy.js';
|
|
8
|
+
import { parseRows } from './record.js';
|
|
9
|
+
import { formatReport, reportOf } from './report.js';
|
|
10
|
+
import { crossingsFile, replay } from './replay.js';
|
|
11
|
+
const runtimeOf = (deps) => {
|
|
12
|
+
if (deps.sessions === undefined)
|
|
13
|
+
throw new CliError('The session store is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
14
|
+
return deps.sessions;
|
|
15
|
+
};
|
|
16
|
+
/** Reads local logs only; a `registry` reader is passed solely by `--registry-state`. */
|
|
17
|
+
export const gateReplay = async (deps, options) => {
|
|
18
|
+
const done = await replay(runtimeOf(deps), { allow: options.allow, now: deps.now, ...(options.registry === undefined ? {} : { registry: options.registry }) });
|
|
19
|
+
return { document: { ok: true, run: done.run, file: done.file }, text: done.text };
|
|
20
|
+
};
|
|
21
|
+
export const gateReport = async (deps) => {
|
|
22
|
+
const text = await readFile(crossingsFile(runtimeOf(deps).storeDir), 'utf8').catch(() => '');
|
|
23
|
+
const rows = parseRows(text);
|
|
24
|
+
if (!rows.some((row) => row.kind === 'run' || row.kind === 'crossing')) {
|
|
25
|
+
throw new CliError('No crossings have been recorded yet: run `oars gate install` to record them as they happen, or `oars gate replay --allow <folder>` to find the ones already in your logs.', ExitCode.USAGE, 'usage');
|
|
26
|
+
}
|
|
27
|
+
const report = reportOf(rows);
|
|
28
|
+
return { document: report, text: formatReport(report) };
|
|
29
|
+
};
|
|
30
|
+
const installOf = (deps) => {
|
|
31
|
+
if (deps.install === undefined)
|
|
32
|
+
throw new CliError('Hook install is not configured in this run.', ExitCode.UNEXPECTED, 'unexpected');
|
|
33
|
+
return deps.install;
|
|
34
|
+
};
|
|
35
|
+
/** User scope only: the one place the harness reads for every project, and the one place this command ever writes. */
|
|
36
|
+
export const gateInstall = async (deps) => {
|
|
37
|
+
const file = installOf(deps).userSettings;
|
|
38
|
+
const result = await installGateHook(file);
|
|
39
|
+
const lines = [
|
|
40
|
+
result.changed
|
|
41
|
+
? `Installed the gate hook in ${file}: ${EVENT} for ${MATCHER}, running \`${COMMAND}\`. It records each crossing in crossings.jsonl, adds a note about a young or missing name by default, and denies a call only if you write \`block\` in gate-policy.json; it never allows or changes a call.`
|
|
42
|
+
: `The gate hook is already installed in ${file}; nothing was changed.`,
|
|
43
|
+
];
|
|
44
|
+
// A record with no kept index cannot tell what is new without a scan the hook must not do, so it reads `not-read` until a replay seeds the index.
|
|
45
|
+
const storeDir = deps.sessions?.storeDir;
|
|
46
|
+
if (storeDir !== undefined && (await loadIndex(storeDir, crossingsPath(storeDir))).state === 'unseeded') {
|
|
47
|
+
lines.push('Run `oars gate replay --allow <folder>` once to seed the novelty index; until then the hook records novelty as not-read and shows no note.');
|
|
48
|
+
}
|
|
49
|
+
return { document: { ok: true, file, changed: result.changed, event: EVENT, matcher: MATCHER, command: COMMAND }, text: lines.join('\n') };
|
|
50
|
+
};
|
|
51
|
+
/** The policy in force for this store: the file's settings, or the defaults and why. Reads one file, writes none. */
|
|
52
|
+
export const gatePolicy = async (deps) => {
|
|
53
|
+
const storeDir = runtimeOf(deps).storeDir;
|
|
54
|
+
const read = await loadPolicy(storeDir);
|
|
55
|
+
return { document: { ok: true, file: policyPath(storeDir), source: read.source, policy: read.policy }, text: formatPolicy(read, policyPath(storeDir)) };
|
|
56
|
+
};
|
|
57
|
+
export const gateUninstall = async (deps) => {
|
|
58
|
+
const result = await uninstallGateHook(installOf(deps).userSettings);
|
|
59
|
+
const text = result.removed ? `Removed the gate hook from ${result.file}; other settings and hooks are untouched, and crossings.jsonl stays.` : `No gate hook in ${result.file}.`;
|
|
60
|
+
return { document: { ok: true, ...result }, text };
|
|
61
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The entry the hook starts, detached, after a cut ask: `node hook-fill-main.js <storeDir> <class> <name> <cwd>`. It prints nothing, answers nothing, and exits 0 whatever
|
|
2
|
+
// happens; nobody waits on it.
|
|
3
|
+
import { CROSSING_CLASSES } from './checks.js';
|
|
4
|
+
import { FILL_TIMEOUT_MS, runFill } from './hook-fill.js';
|
|
5
|
+
import { managerReader } from './registry.js';
|
|
6
|
+
const [storeDir, cls, name, cwd] = process.argv.slice(2);
|
|
7
|
+
const known = CROSSING_CLASSES.find((one) => one === cls);
|
|
8
|
+
try {
|
|
9
|
+
if (storeDir !== undefined && known !== undefined && name !== undefined && cwd !== undefined) {
|
|
10
|
+
await runFill(storeDir, { class: known, name, cwd }, { registry: managerReader(undefined, FILL_TIMEOUT_MS), now: Date.now });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Fail-open: an answer that cannot be kept is asked about again.
|
|
15
|
+
}
|
|
16
|
+
process.exit(0);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CrossingClass } from './checks.js';
|
|
2
|
+
import { type RegistryReader } from './registry.js';
|
|
3
|
+
export interface FillAsk {
|
|
4
|
+
class: CrossingClass;
|
|
5
|
+
name: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
}
|
|
8
|
+
/** The helper gives the manager this long. A lock younger than five minutes keeps a second helper from starting, whether the first is still asking or has failed. */
|
|
9
|
+
export declare const FILL_TIMEOUT_MS = 15000;
|
|
10
|
+
export declare const FILL_LOCK_MS = 300000;
|
|
11
|
+
/** Whether a helper holds the name now: the lock is there and younger than `FILL_LOCK_MS`. The hook asks nothing of the manager itself while it is, so one name has one ask. */
|
|
12
|
+
export declare const isLocked: (storeDir: string, ask: Pick<FillAsk, "class" | "name">, now: number) => Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Takes the lock back: a helper that never started will never release it, and the name would go unasked until `FILL_LOCK_MS` passed. Synchronous, like `create`, so a deadline's
|
|
15
|
+
* `exit(0)` cannot end the process between the failed start and the removal.
|
|
16
|
+
*/
|
|
17
|
+
export declare const releaseLock: (storeDir: string, ask: Pick<FillAsk, "class" | "name">) => void;
|
|
18
|
+
/** The part of `child_process.spawn` the helper start uses, so a test can hand in a child that fails to start. */
|
|
19
|
+
export type HelperSpawn = (command: string, args: string[], options: {
|
|
20
|
+
detached: true;
|
|
21
|
+
stdio: 'ignore';
|
|
22
|
+
windowsHide: true;
|
|
23
|
+
}) => {
|
|
24
|
+
on: (event: 'error', listener: () => void) => unknown;
|
|
25
|
+
unref: () => void;
|
|
26
|
+
};
|
|
27
|
+
/** The helper outlives the hook: detached, no stdio, never waited on, so the harness's call does not wait for it either. One that cannot start never releases its lock, so the lock goes on its `error`. */
|
|
28
|
+
export declare const startHelper: (spawn: HelperSpawn, storeDir: string, ask: FillAsk) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Called by the hook after a cut ask: takes the lock and starts the helper. Nothing here throws into the hook: a store that cannot be written, or a helper that cannot
|
|
31
|
+
* start, is an answer that does not come. The lock goes when the start throws, so the next sight asks about the name again.
|
|
32
|
+
*/
|
|
33
|
+
export declare const startFill: (storeDir: string, ask: FillAsk, now: number, start: (storeDir: string, ask: FillAsk) => void) => Promise<void>;
|
|
34
|
+
/** The helper's work: ask, and keep a `found` or `not-found` answer in the cache. The lock goes once an answer is kept; after a failure it stays, so a down registry is asked about at most every five minutes. */
|
|
35
|
+
export declare const runFill: (storeDir: string, ask: FillAsk, deps: {
|
|
36
|
+
registry: RegistryReader;
|
|
37
|
+
now: () => number;
|
|
38
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The fill: a registry ask the hook had to cut is finished by a helper process that outlives the hook and writes the answer to the hook's cache, so the next sight of the
|
|
2
|
+
// name reads it inside the budget. The helper calls the installed manager as the hook does (`managerReader`), contacts nothing the manager would not, and a failed or
|
|
3
|
+
// unreachable ask writes nothing. One ask per name at a time: a lock file in the store, dated by its mtime, is taken before the helper starts.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { appendFile, mkdir, rm, stat } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { GATE_VERSION, folderKey } from './record.js';
|
|
10
|
+
import { askable } from './registry.js';
|
|
11
|
+
/** The helper gives the manager this long. A lock younger than five minutes keeps a second helper from starting, whether the first is still asking or has failed. */
|
|
12
|
+
export const FILL_TIMEOUT_MS = 15_000;
|
|
13
|
+
export const FILL_LOCK_MS = 300_000;
|
|
14
|
+
const lockPath = (storeDir, ask) => join(storeDir, `gate-ask-${createHash('sha256').update(`${ask.class}\0${ask.name}`).digest('hex').slice(0, 16)}.lock`);
|
|
15
|
+
/**
|
|
16
|
+
* Creates the lock, failing if it exists: of two hooks racing for the name, one wins. It is synchronous so the lock never exists across a turn of the event loop before the
|
|
17
|
+
* helper starts: a deadline timer firing in such a turn would end the hook with a lock held and nothing behind it.
|
|
18
|
+
*/
|
|
19
|
+
const create = (path) => {
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(path, '', { mode: 0o600, flag: 'wx' });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Takes the name's lock. False when a younger one is held; an older one is a helper that died, and is taken over. */
|
|
29
|
+
const takeLock = async (path, now) => {
|
|
30
|
+
if (create(path))
|
|
31
|
+
return true;
|
|
32
|
+
const held = await stat(path).then((found) => found.mtimeMs, () => now);
|
|
33
|
+
if (now - held < FILL_LOCK_MS)
|
|
34
|
+
return false;
|
|
35
|
+
await rm(path, { force: true });
|
|
36
|
+
return create(path);
|
|
37
|
+
};
|
|
38
|
+
/** Whether a helper holds the name now: the lock is there and younger than `FILL_LOCK_MS`. The hook asks nothing of the manager itself while it is, so one name has one ask. */
|
|
39
|
+
export const isLocked = async (storeDir, ask, now) => {
|
|
40
|
+
const held = await stat(lockPath(storeDir, ask)).then((found) => found.mtimeMs, () => undefined);
|
|
41
|
+
return held !== undefined && now - held < FILL_LOCK_MS;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Takes the lock back: a helper that never started will never release it, and the name would go unasked until `FILL_LOCK_MS` passed. Synchronous, like `create`, so a deadline's
|
|
45
|
+
* `exit(0)` cannot end the process between the failed start and the removal.
|
|
46
|
+
*/
|
|
47
|
+
export const releaseLock = (storeDir, ask) => {
|
|
48
|
+
try {
|
|
49
|
+
rmSync(lockPath(storeDir, ask), { force: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Fail-open: a lock that stays is only a name left unasked until it ages out.
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** The helper outlives the hook: detached, no stdio, never waited on, so the harness's call does not wait for it either. One that cannot start never releases its lock, so the lock goes on its `error`. */
|
|
56
|
+
export const startHelper = (spawn, storeDir, ask) => {
|
|
57
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL('./hook-fill-main.js', import.meta.url)), storeDir, ask.class, ask.name, ask.cwd], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
58
|
+
child.on('error', () => {
|
|
59
|
+
releaseLock(storeDir, ask);
|
|
60
|
+
});
|
|
61
|
+
child.unref();
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Called by the hook after a cut ask: takes the lock and starts the helper. Nothing here throws into the hook: a store that cannot be written, or a helper that cannot
|
|
65
|
+
* start, is an answer that does not come. The lock goes when the start throws, so the next sight asks about the name again.
|
|
66
|
+
*/
|
|
67
|
+
export const startFill = async (storeDir, ask, now, start) => {
|
|
68
|
+
try {
|
|
69
|
+
await mkdir(storeDir, { recursive: true });
|
|
70
|
+
if (!(await takeLock(lockPath(storeDir, ask), now)))
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
start(storeDir, ask);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
releaseLock(storeDir, ask);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Fail-open.
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** The helper's work: ask, and keep a `found` or `not-found` answer in the cache. The lock goes once an answer is kept; after a failure it stays, so a down registry is asked about at most every five minutes. */
|
|
84
|
+
export const runFill = async (storeDir, ask, deps) => {
|
|
85
|
+
if (!askable(ask.class, ask.name))
|
|
86
|
+
return;
|
|
87
|
+
const answer = await deps.registry(ask);
|
|
88
|
+
if (answer.state === 'unreachable')
|
|
89
|
+
return;
|
|
90
|
+
const at = new Date(deps.now()).toISOString();
|
|
91
|
+
const row = { kind: 'registry', v: GATE_VERSION, day: at.slice(0, 10), ts: at, folder: folderKey(ask.cwd), class: ask.class, state: answer.state, created: answer.created, name: ask.name, latest: answer.latest };
|
|
92
|
+
await appendFile(join(storeDir, 'gate-registry.jsonl'), `${JSON.stringify(row)}\n`, { mode: 0o600 });
|
|
93
|
+
await rm(lockPath(storeDir, ask), { force: true });
|
|
94
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The entry for `oars gate hook`: it imports the hook and the little it needs, never the CLI. Everything it does is in `hook-process.ts`, which a test can drive; this file
|
|
2
|
+
// only wires the real process into it, and every line of the wiring sits inside the guard that exits 0.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import { defaultConfigDir } from '../credentials.js';
|
|
8
|
+
import { DEADLINE_MS } from './hook.js';
|
|
9
|
+
import { isLocked, startFill, startHelper } from './hook-fill.js';
|
|
10
|
+
import { fireRecorder } from './hook-record.js';
|
|
11
|
+
import { runHookProcess } from './hook-process.js';
|
|
12
|
+
import { managerReader } from './registry.js';
|
|
13
|
+
import { hookEmitter } from '../telemetry/hook-emit.js';
|
|
14
|
+
/** The process itself ends this long after `DEADLINE_MS`. */
|
|
15
|
+
const GRACE_MS = 100;
|
|
16
|
+
const readStdin = async () => {
|
|
17
|
+
process.stdin.setEncoding('utf8');
|
|
18
|
+
let text = '';
|
|
19
|
+
for await (const chunk of process.stdin)
|
|
20
|
+
text += String(chunk);
|
|
21
|
+
return text;
|
|
22
|
+
};
|
|
23
|
+
const depsFor = (stage) => {
|
|
24
|
+
const storeDir = join(defaultConfigDir(process.env, process.platform, homedir()), 'oars', 'sessions');
|
|
25
|
+
return {
|
|
26
|
+
storeDir,
|
|
27
|
+
now: Date.now,
|
|
28
|
+
// `performance.now()` counts from the start of the process, so node's own start is inside the budget.
|
|
29
|
+
clock: () => performance.now(),
|
|
30
|
+
deadlineMs: DEADLINE_MS,
|
|
31
|
+
registry: managerReader(undefined, DEADLINE_MS),
|
|
32
|
+
fill: (ask) => startFill(storeDir, ask, Date.now(), (dir, one) => { startHelper(spawn, dir, one); }),
|
|
33
|
+
locked: (ask) => isLocked(storeDir, ask, Date.now()),
|
|
34
|
+
stage,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
// One event per fire, recorded after the answer is written (`runHookProcess`), so it is never in front of the tool call. The version is read from `dist/gate/../../package.json`.
|
|
38
|
+
const emit = hookEmitter(process.env, process.platform, homedir(), new URL('../../package.json', import.meta.url), Date.now);
|
|
39
|
+
await runHookProcess({
|
|
40
|
+
stdin: readStdin,
|
|
41
|
+
deps: depsFor,
|
|
42
|
+
exit: (code) => process.exit(code),
|
|
43
|
+
write: (text) => new Promise((done) => process.stdout.write(text, () => { done(); })),
|
|
44
|
+
deadlineMs: DEADLINE_MS + GRACE_MS - performance.now(),
|
|
45
|
+
clock: () => performance.now(),
|
|
46
|
+
record: fireRecorder(emit),
|
|
47
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type GateHookDeps } from './hook.js';
|
|
2
|
+
/** What telemetry is told of one fire of `oars gate hook`: how long the handler took, how it ended, and the bytes it printed. */
|
|
3
|
+
export interface HookFire {
|
|
4
|
+
latencyMs: number;
|
|
5
|
+
outcome: 'hit' | 'miss' | 'held' | 'error';
|
|
6
|
+
bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface HookProcess {
|
|
9
|
+
/** Reads the whole of stdin. Called inside the guard, so a stream that cannot be set up is a failed read, not a crash. */
|
|
10
|
+
stdin: () => Promise<string>;
|
|
11
|
+
/** Builds the hook's dependencies, handed the function that records what is about to be written. Called inside the guard. */
|
|
12
|
+
deps: (stage: NonNullable<GateHookDeps['stage']>) => GateHookDeps;
|
|
13
|
+
exit: (code: number) => void;
|
|
14
|
+
/** Writes the hook's answer to stdout and resolves once it is flushed. Absent: nothing is written. */
|
|
15
|
+
write?: (text: string) => Promise<void>;
|
|
16
|
+
/** Milliseconds from now until the process ends itself. */
|
|
17
|
+
deadlineMs: number;
|
|
18
|
+
/** Told once how the fire went, with the transcript path the harness named (to read the frame from, never to record), after its output is written so recording is never in front of the answer. A throw here is swallowed: the exit code and stdout do not depend on it. */
|
|
19
|
+
record?: (fire: HookFire, transcript: string | undefined) => void | Promise<void>;
|
|
20
|
+
/** Milliseconds, only compared with itself: the latency in a fire is the difference across the handler. Absent: `Date.now`. */
|
|
21
|
+
clock?: () => number;
|
|
22
|
+
}
|
|
23
|
+
/** Resolves once `exit(0)` has been called, or never when the deadline timer is what ended the process. */
|
|
24
|
+
export declare const runHookProcess: ({ stdin, deps, exit, write, deadlineMs, record, clock }: HookProcess) => Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// The process behind `oars gate hook`, with every side effect handed in so a test can drive it: stdin in, at most one JSON document out, `exit(0)` whatever happens. A hard deadline ends
|
|
2
|
+
// the process before the harness's own, writing the record it has staged if the write had not finished. Nothing here may throw past `runHookProcess`: exit 1 from a
|
|
3
|
+
// PreToolUse hook is a visible error and exit 2 blocks the tool call, and the hook must never cost a session either.
|
|
4
|
+
import { appendFileSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { gateHook } from './hook.js';
|
|
6
|
+
import { parseRows } from './record.js';
|
|
7
|
+
/** The staged record is written before anything prints, so when the deadline ends the process with it still staged no note or deny was shown: its crossing rows read as no exposure, since a row cannot be backfilled. A block row has `arm: none` already but names its line, so every crossing row is reset, not only those with an arm. */
|
|
8
|
+
const unprinted = (text) => parseRows(text)
|
|
9
|
+
.map((row) => `${JSON.stringify(row.kind === 'crossing' ? { ...row, arm: 'none', lineShown: 'none', lineHeld: 'none' } : row)}\n`)
|
|
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
|
+
};
|
|
22
|
+
const sizeOf = (file) => {
|
|
23
|
+
try {
|
|
24
|
+
return statSync(file).size;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Whether the record's own bytes are in the file past the length it had when the record was staged. Another hook's row may have grown the file too, so growth alone proves nothing. */
|
|
31
|
+
const landed = (file, text, size) => {
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(file).subarray(size).includes(text);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Resolves once `exit(0)` has been called, or never when the deadline timer is what ended the process. */
|
|
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
|
+
};
|
|
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.
|
|
68
|
+
let staged = null;
|
|
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.
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
// The row first: it is the record this timer exists to keep, and the stop can be slow (on Windows it is a `taskkill` spawn at a moment already past the deadline).
|
|
72
|
+
try {
|
|
73
|
+
if (staged !== null && !landed(staged.file, staged.text, staged.size))
|
|
74
|
+
appendFileSync(staged.file, unprinted(staged.text), { mode: 0o600 });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Fail-open.
|
|
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);
|
|
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.
|
|
86
|
+
try {
|
|
87
|
+
staged?.stop?.();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Fail-open.
|
|
91
|
+
}
|
|
92
|
+
exit(0);
|
|
93
|
+
}, Math.max(deadlineMs, 0)).unref();
|
|
94
|
+
try {
|
|
95
|
+
const built = deps((pending) => {
|
|
96
|
+
staged = pending === null ? null : { ...pending, size: sizeOf(pending.file) };
|
|
97
|
+
});
|
|
98
|
+
const input = await stdin();
|
|
99
|
+
transcript = transcriptOf(input);
|
|
100
|
+
const output = await gateHook(input, { ...built, ended: (result) => { how.ended = result; built.ended?.(result); } });
|
|
101
|
+
if (output !== '')
|
|
102
|
+
await write?.(output);
|
|
103
|
+
fired = { outcome: how.ended ?? (output === '' ? 'miss' : 'hit'), bytes: Buffer.byteLength(output) };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Fail-open: a throw anywhere above is a record not written, never an exit code.
|
|
107
|
+
fired = { outcome: 'error', bytes: 0 };
|
|
108
|
+
}
|
|
109
|
+
await tell(fired, transcript);
|
|
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.
|
|
111
|
+
exit(0);
|
|
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
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FillAsk } from './hook-fill.js';
|
|
2
|
+
import { type RegistryReader } from './registry.js';
|
|
3
|
+
/** Where the crossings go. Named here, not imported from `replay.ts`, which reads transcripts and would be loaded for nothing. */
|
|
4
|
+
export declare const crossingsPath: (storeDir: string) => string;
|
|
5
|
+
/** The hook's kept registry answers, small and its own: reading the record for them would scan it. */
|
|
6
|
+
export declare const registryCachePath: (storeDir: string) => string;
|
|
7
|
+
/** The registry ask is cut at this many ms on the process clock, less a write reserve; `hook-main.ts` ends the process a short grace after it, so the whole call is under a second. */
|
|
8
|
+
export declare const DEADLINE_MS = 800;
|
|
9
|
+
/** A registry answer the hook kept is used for this many days. A `latest` is read "as of the crossing", so an older one is asked again. */
|
|
10
|
+
export declare const HOOK_REGISTRY_DAYS = 1;
|
|
11
|
+
/** A not-found answer is trusted this long: a name can be published any minute, and a block rests on the answer. */
|
|
12
|
+
export declare const NOT_FOUND_MS = 600000;
|
|
13
|
+
export interface GateHookDeps {
|
|
14
|
+
storeDir: string;
|
|
15
|
+
now: () => number;
|
|
16
|
+
/** The process clock: ms since the process started in the entry, so the deadline counts node's own start. */
|
|
17
|
+
clock: () => number;
|
|
18
|
+
/** The whole hook ends by this many ms on `clock`. */
|
|
19
|
+
deadlineMs: number;
|
|
20
|
+
/** The draw for the holdback, in [0, 1). Absent: `Math.random`. */
|
|
21
|
+
random?: () => number;
|
|
22
|
+
/** Asks the package manager. Absent: no question is ever asked. */
|
|
23
|
+
registry?: RegistryReader;
|
|
24
|
+
/** Appends text to a file. Replaceable so a full disk can be tried. */
|
|
25
|
+
append?: (file: string, text: string) => Promise<void>;
|
|
26
|
+
/** Called with the name whose ask was cut or had no time to start, so a helper can finish it and fill the cache for the next sight. Absent: nothing is finished. */
|
|
27
|
+
fill?: (ask: FillAsk) => Promise<void>;
|
|
28
|
+
/** Whether a helper already holds the name, which keeps the hook from asking the manager a second time. Absent: never locked. */
|
|
29
|
+
locked?: (ask: FillAsk) => Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Told what is about to be written (and `null` once it is), so a process that must stop at a hard deadline can write the record it has. While a registry ask is live the record
|
|
32
|
+
* is the rows so far plus a not-read row for the crossing asked about, with `stop` ending that ask: a process cut off mid-ask stops it and leaves one row, not a manager running unseen.
|
|
33
|
+
*/
|
|
34
|
+
stage?: (pending: {
|
|
35
|
+
file: string;
|
|
36
|
+
text: string;
|
|
37
|
+
stop?: () => void;
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
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.
|
|
44
|
+
* Nothing is written, and no network is touched, for a call that crosses nothing.
|
|
45
|
+
*/
|
|
46
|
+
export declare const runGateHook: (stdin: string, deps: GateHookDeps) => Promise<string>;
|
|
47
|
+
/** What the hook writes to stdout: the JSON of a note or a deny, or '' when silent and on any failure, so the call runs as asked. */
|
|
48
|
+
export declare const gateHook: (stdin: string, deps: GateHookDeps) => Promise<string>;
|