@coldtea/abide 0.0.1
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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/abide-hook.d.ts +2 -0
- package/dist/abide-hook.d.ts.map +1 -0
- package/dist/abide-hook.js +23 -0
- package/dist/abide-hook.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +64 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +79 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/bench.d.ts +3 -0
- package/dist/commands/bench.d.ts.map +1 -0
- package/dist/commands/bench.js +206 -0
- package/dist/commands/bench.js.map +1 -0
- package/dist/commands/calibrate.d.ts +3 -0
- package/dist/commands/calibrate.d.ts.map +1 -0
- package/dist/commands/calibrate.js +159 -0
- package/dist/commands/calibrate.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +106 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +116 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +58 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/replay.d.ts +4 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +118 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/report.d.ts +5 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +78 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/rubric.d.ts +3 -0
- package/dist/commands/rubric.d.ts.map +1 -0
- package/dist/commands/rubric.js +40 -0
- package/dist/commands/rubric.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +31 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/hooks/postToolUse.d.ts +3 -0
- package/dist/hooks/postToolUse.d.ts.map +1 -0
- package/dist/hooks/postToolUse.js +145 -0
- package/dist/hooks/postToolUse.js.map +1 -0
- package/dist/hooks/sessionStart.d.ts +11 -0
- package/dist/hooks/sessionStart.d.ts.map +1 -0
- package/dist/hooks/sessionStart.js +83 -0
- package/dist/hooks/sessionStart.js.map +1 -0
- package/dist/hooks/stop.d.ts +26 -0
- package/dist/hooks/stop.d.ts.map +1 -0
- package/dist/hooks/stop.js +227 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/hooks/turnStart.d.ts +8 -0
- package/dist/hooks/turnStart.d.ts.map +1 -0
- package/dist/hooks/turnStart.js +40 -0
- package/dist/hooks/turnStart.js.map +1 -0
- package/dist/lib/applyPatch.d.ts +22 -0
- package/dist/lib/applyPatch.d.ts.map +1 -0
- package/dist/lib/applyPatch.js +63 -0
- package/dist/lib/applyPatch.js.map +1 -0
- package/dist/lib/audit.d.ts +40 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +215 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/band.d.ts +20 -0
- package/dist/lib/band.d.ts.map +1 -0
- package/dist/lib/band.js +40 -0
- package/dist/lib/band.js.map +1 -0
- package/dist/lib/calibration.d.ts +13 -0
- package/dist/lib/calibration.d.ts.map +1 -0
- package/dist/lib/calibration.js +25 -0
- package/dist/lib/calibration.js.map +1 -0
- package/dist/lib/checkRunner.d.ts +34 -0
- package/dist/lib/checkRunner.d.ts.map +1 -0
- package/dist/lib/checkRunner.js +76 -0
- package/dist/lib/checkRunner.js.map +1 -0
- package/dist/lib/claude.d.ts +4 -0
- package/dist/lib/claude.d.ts.map +1 -0
- package/dist/lib/claude.js +28 -0
- package/dist/lib/claude.js.map +1 -0
- package/dist/lib/compilePrompt.d.ts +22 -0
- package/dist/lib/compilePrompt.d.ts.map +1 -0
- package/dist/lib/compilePrompt.js +33 -0
- package/dist/lib/compilePrompt.js.map +1 -0
- package/dist/lib/constants.d.ts +33 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +33 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/coverage.d.ts +7 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +17 -0
- package/dist/lib/coverage.js.map +1 -0
- package/dist/lib/credentials.d.ts +27 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +89 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/diff.d.ts +30 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +139 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/events.d.ts +5 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/events.js +38 -0
- package/dist/lib/events.js.map +1 -0
- package/dist/lib/git.d.ts +37 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +163 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/hookRunner.d.ts +10 -0
- package/dist/lib/hookRunner.d.ts.map +1 -0
- package/dist/lib/hookRunner.js +52 -0
- package/dist/lib/hookRunner.js.map +1 -0
- package/dist/lib/hosts.d.ts +20 -0
- package/dist/lib/hosts.d.ts.map +1 -0
- package/dist/lib/hosts.js +101 -0
- package/dist/lib/hosts.js.map +1 -0
- package/dist/lib/jev.d.ts +24 -0
- package/dist/lib/jev.d.ts.map +1 -0
- package/dist/lib/jev.js +169 -0
- package/dist/lib/jev.js.map +1 -0
- package/dist/lib/lintConfig.d.ts +3 -0
- package/dist/lib/lintConfig.d.ts.map +1 -0
- package/dist/lib/lintConfig.js +55 -0
- package/dist/lib/lintConfig.js.map +1 -0
- package/dist/lib/loadRules.d.ts +12 -0
- package/dist/lib/loadRules.d.ts.map +1 -0
- package/dist/lib/loadRules.js +31 -0
- package/dist/lib/loadRules.js.map +1 -0
- package/dist/lib/opencodePlugin.d.ts +10 -0
- package/dist/lib/opencodePlugin.d.ts.map +1 -0
- package/dist/lib/opencodePlugin.js +36 -0
- package/dist/lib/opencodePlugin.js.map +1 -0
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +35 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/packageRoot.d.ts +12 -0
- package/dist/lib/packageRoot.d.ts.map +1 -0
- package/dist/lib/packageRoot.js +50 -0
- package/dist/lib/packageRoot.js.map +1 -0
- package/dist/lib/paths.d.ts +24 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +75 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/reason.d.ts +11 -0
- package/dist/lib/reason.d.ts.map +1 -0
- package/dist/lib/reason.js +31 -0
- package/dist/lib/reason.js.map +1 -0
- package/dist/lib/regularFile.d.ts +26 -0
- package/dist/lib/regularFile.d.ts.map +1 -0
- package/dist/lib/regularFile.js +77 -0
- package/dist/lib/regularFile.js.map +1 -0
- package/dist/lib/replay.d.ts +74 -0
- package/dist/lib/replay.d.ts.map +1 -0
- package/dist/lib/replay.js +250 -0
- package/dist/lib/replay.js.map +1 -0
- package/dist/lib/replayCodex.d.ts +6 -0
- package/dist/lib/replayCodex.d.ts.map +1 -0
- package/dist/lib/replayCodex.js +132 -0
- package/dist/lib/replayCodex.js.map +1 -0
- package/dist/lib/replayOpencode.d.ts +15 -0
- package/dist/lib/replayOpencode.d.ts.map +1 -0
- package/dist/lib/replayOpencode.js +154 -0
- package/dist/lib/replayOpencode.js.map +1 -0
- package/dist/lib/rubricFile.d.ts +31 -0
- package/dist/lib/rubricFile.d.ts.map +1 -0
- package/dist/lib/rubricFile.js +75 -0
- package/dist/lib/rubricFile.js.map +1 -0
- package/dist/lib/scope.d.ts +4 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +14 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/session.d.ts +48 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +182 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/settings.d.ts +26 -0
- package/dist/lib/settings.d.ts.map +1 -0
- package/dist/lib/settings.js +104 -0
- package/dist/lib/settings.js.map +1 -0
- package/dist/lib/sources.d.ts +28 -0
- package/dist/lib/sources.d.ts.map +1 -0
- package/dist/lib/sources.js +137 -0
- package/dist/lib/sources.js.map +1 -0
- package/dist/lib/stdin.d.ts +3 -0
- package/dist/lib/stdin.d.ts.map +1 -0
- package/dist/lib/stdin.js +24 -0
- package/dist/lib/stdin.js.map +1 -0
- package/dist/lib/transcript.d.ts +3 -0
- package/dist/lib/transcript.d.ts.map +1 -0
- package/dist/lib/transcript.js +57 -0
- package/dist/lib/transcript.js.map +1 -0
- package/dist/lib/ui.d.ts +8 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +34 -0
- package/dist/lib/ui.js.map +1 -0
- package/dist/ui/components/Buckets.d.ts +6 -0
- package/dist/ui/components/Buckets.d.ts.map +1 -0
- package/dist/ui/components/Buckets.js +11 -0
- package/dist/ui/components/Buckets.js.map +1 -0
- package/dist/ui/components/Callout.d.ts +11 -0
- package/dist/ui/components/Callout.d.ts.map +1 -0
- package/dist/ui/components/Callout.js +10 -0
- package/dist/ui/components/Callout.js.map +1 -0
- package/dist/ui/components/Checklist.d.ts +10 -0
- package/dist/ui/components/Checklist.d.ts.map +1 -0
- package/dist/ui/components/Checklist.js +8 -0
- package/dist/ui/components/Checklist.js.map +1 -0
- package/dist/ui/components/Columns.d.ts +28 -0
- package/dist/ui/components/Columns.d.ts.map +1 -0
- package/dist/ui/components/Columns.js +20 -0
- package/dist/ui/components/Columns.js.map +1 -0
- package/dist/ui/components/Header.d.ts +9 -0
- package/dist/ui/components/Header.d.ts.map +1 -0
- package/dist/ui/components/Header.js +7 -0
- package/dist/ui/components/Header.js.map +1 -0
- package/dist/ui/components/Live.d.ts +16 -0
- package/dist/ui/components/Live.d.ts.map +1 -0
- package/dist/ui/components/Live.js +40 -0
- package/dist/ui/components/Live.js.map +1 -0
- package/dist/ui/components/RuleTable.d.ts +15 -0
- package/dist/ui/components/RuleTable.d.ts.map +1 -0
- package/dist/ui/components/RuleTable.js +81 -0
- package/dist/ui/components/RuleTable.js.map +1 -0
- package/dist/ui/components/Section.d.ts +10 -0
- package/dist/ui/components/Section.d.ts.map +1 -0
- package/dist/ui/components/Section.js +8 -0
- package/dist/ui/components/Section.js.map +1 -0
- package/dist/ui/components/Verdicts.d.ts +9 -0
- package/dist/ui/components/Verdicts.d.ts.map +1 -0
- package/dist/ui/components/Verdicts.js +12 -0
- package/dist/ui/components/Verdicts.js.map +1 -0
- package/dist/ui/render.d.ts +20 -0
- package/dist/ui/render.d.ts.map +1 -0
- package/dist/ui/render.js +32 -0
- package/dist/ui/render.js.map +1 -0
- package/dist/ui/theme.d.ts +37 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +63 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/views/AuditView.d.ts +18 -0
- package/dist/ui/views/AuditView.d.ts.map +1 -0
- package/dist/ui/views/AuditView.js +48 -0
- package/dist/ui/views/AuditView.js.map +1 -0
- package/dist/ui/views/BenchView.d.ts +33 -0
- package/dist/ui/views/BenchView.d.ts.map +1 -0
- package/dist/ui/views/BenchView.js +19 -0
- package/dist/ui/views/BenchView.js.map +1 -0
- package/dist/ui/views/CalibrateView.d.ts +26 -0
- package/dist/ui/views/CalibrateView.d.ts.map +1 -0
- package/dist/ui/views/CalibrateView.js +42 -0
- package/dist/ui/views/CalibrateView.js.map +1 -0
- package/dist/ui/views/CheckView.d.ts +19 -0
- package/dist/ui/views/CheckView.d.ts.map +1 -0
- package/dist/ui/views/CheckView.js +15 -0
- package/dist/ui/views/CheckView.js.map +1 -0
- package/dist/ui/views/ErrorView.d.ts +4 -0
- package/dist/ui/views/ErrorView.d.ts.map +1 -0
- package/dist/ui/views/ErrorView.js +26 -0
- package/dist/ui/views/ErrorView.js.map +1 -0
- package/dist/ui/views/HelpView.d.ts +2 -0
- package/dist/ui/views/HelpView.d.ts.map +1 -0
- package/dist/ui/views/HelpView.js +25 -0
- package/dist/ui/views/HelpView.js.map +1 -0
- package/dist/ui/views/InitView.d.ts +27 -0
- package/dist/ui/views/InitView.d.ts.map +1 -0
- package/dist/ui/views/InitView.js +24 -0
- package/dist/ui/views/InitView.js.map +1 -0
- package/dist/ui/views/ReplayView.d.ts +16 -0
- package/dist/ui/views/ReplayView.d.ts.map +1 -0
- package/dist/ui/views/ReplayView.js +44 -0
- package/dist/ui/views/ReplayView.js.map +1 -0
- package/dist/ui/views/ReportView.d.ts +14 -0
- package/dist/ui/views/ReportView.d.ts.map +1 -0
- package/dist/ui/views/ReportView.js +86 -0
- package/dist/ui/views/ReportView.js.map +1 -0
- package/dist/ui/views/RubricView.d.ts +18 -0
- package/dist/ui/views/RubricView.d.ts.map +1 -0
- package/dist/ui/views/RubricView.js +18 -0
- package/dist/ui/views/RubricView.js.map +1 -0
- package/opencode/abide.mjs +246 -0
- package/package.json +50 -0
- package/skills/abide-compile/SKILL.md +173 -0
- package/src/abide-hook.ts +23 -0
- package/src/bin.ts +64 -0
- package/src/commands/audit.ts +97 -0
- package/src/commands/bench.ts +226 -0
- package/src/commands/calibrate.ts +165 -0
- package/src/commands/check.ts +114 -0
- package/src/commands/compile.ts +127 -0
- package/src/commands/init.ts +109 -0
- package/src/commands/login.ts +61 -0
- package/src/commands/replay.ts +148 -0
- package/src/commands/report.ts +83 -0
- package/src/commands/rubric.ts +45 -0
- package/src/commands/uninstall.ts +33 -0
- package/src/hooks/postToolUse.ts +171 -0
- package/src/hooks/sessionStart.ts +100 -0
- package/src/hooks/stop.ts +275 -0
- package/src/hooks/turnStart.ts +36 -0
- package/src/lib/applyPatch.ts +75 -0
- package/src/lib/audit.ts +265 -0
- package/src/lib/band.ts +49 -0
- package/src/lib/calibration.ts +35 -0
- package/src/lib/checkRunner.ts +124 -0
- package/src/lib/claude.ts +35 -0
- package/src/lib/compilePrompt.ts +67 -0
- package/src/lib/constants.ts +39 -0
- package/src/lib/coverage.ts +21 -0
- package/src/lib/credentials.ts +104 -0
- package/src/lib/diff.ts +209 -0
- package/src/lib/events.ts +34 -0
- package/src/lib/git.ts +206 -0
- package/src/lib/hookRunner.ts +55 -0
- package/src/lib/hosts.ts +112 -0
- package/src/lib/jev.ts +223 -0
- package/src/lib/lintConfig.ts +53 -0
- package/src/lib/loadRules.ts +39 -0
- package/src/lib/opencodePlugin.ts +38 -0
- package/src/lib/output.ts +44 -0
- package/src/lib/packageRoot.ts +48 -0
- package/src/lib/paths.ts +96 -0
- package/src/lib/reason.ts +46 -0
- package/src/lib/regularFile.ts +96 -0
- package/src/lib/replay.ts +314 -0
- package/src/lib/replayCodex.ts +130 -0
- package/src/lib/replayOpencode.ts +186 -0
- package/src/lib/rubricFile.ts +99 -0
- package/src/lib/scope.ts +18 -0
- package/src/lib/session.ts +223 -0
- package/src/lib/settings.ts +112 -0
- package/src/lib/sources.ts +154 -0
- package/src/lib/stdin.ts +23 -0
- package/src/lib/transcript.ts +49 -0
- package/src/lib/ui.ts +38 -0
- package/src/ui/components/Buckets.tsx +30 -0
- package/src/ui/components/Callout.tsx +26 -0
- package/src/ui/components/Checklist.tsx +29 -0
- package/src/ui/components/Columns.tsx +62 -0
- package/src/ui/components/Header.tsx +35 -0
- package/src/ui/components/Live.tsx +73 -0
- package/src/ui/components/RuleTable.tsx +94 -0
- package/src/ui/components/Section.tsx +24 -0
- package/src/ui/components/Verdicts.tsx +40 -0
- package/src/ui/render.tsx +51 -0
- package/src/ui/theme.ts +65 -0
- package/src/ui/views/AuditView.tsx +140 -0
- package/src/ui/views/BenchView.tsx +87 -0
- package/src/ui/views/CalibrateView.tsx +108 -0
- package/src/ui/views/CheckView.tsx +56 -0
- package/src/ui/views/ErrorView.tsx +32 -0
- package/src/ui/views/HelpView.tsx +51 -0
- package/src/ui/views/InitView.tsx +103 -0
- package/src/ui/views/ReplayView.tsx +100 -0
- package/src/ui/views/ReportView.tsx +225 -0
- package/src/ui/views/RubricView.tsx +82 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { postToolUseInputSchema } from "@coldtea/abide-schema";
|
|
5
|
+
import { MAX_TASK_CHARS } from "./constants.js";
|
|
6
|
+
import type { ReplaySession, ReplayTurn } from "./replay.js";
|
|
7
|
+
|
|
8
|
+
/** One rollout file per session, one JSON object per line. An `apply_patch` call carries the same patch text the live hook receives. */
|
|
9
|
+
export const codexSessionsDir = (): string => path.join(homedir(), ".codex", "sessions");
|
|
10
|
+
|
|
11
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined => {
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
13
|
+
const record: Record<string, unknown> = { ...value };
|
|
14
|
+
return record;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const asList = (value: unknown): unknown[] => (Array.isArray(value) ? value : []);
|
|
18
|
+
|
|
19
|
+
/** Codex's own additions to a user turn, not prompts. */
|
|
20
|
+
const INJECTED = [
|
|
21
|
+
"# AGENTS.md instructions",
|
|
22
|
+
"<environment_context>",
|
|
23
|
+
"<permissions instructions>",
|
|
24
|
+
"<turn_aborted>",
|
|
25
|
+
"\n# Files mentioned by the user",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const promptOf = (payload: Record<string, unknown>): string | undefined => {
|
|
29
|
+
const text = asList(payload.content)
|
|
30
|
+
.flatMap((c) => {
|
|
31
|
+
const part = asRecord(c);
|
|
32
|
+
return part?.type === "input_text" && typeof part.text === "string" ? [part.text] : [];
|
|
33
|
+
})
|
|
34
|
+
.join("\n");
|
|
35
|
+
if (text.trim() === "" || INJECTED.some((prefix) => text.startsWith(prefix))) return undefined;
|
|
36
|
+
return text.slice(0, MAX_TASK_CHARS);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FAILED_OUTPUT = /^apply_patch (verification )?failed|^error/i;
|
|
40
|
+
|
|
41
|
+
export const parseCodexRollout = (file: string): ReplaySession => {
|
|
42
|
+
const turns: ReplayTurn[] = [];
|
|
43
|
+
const pending = new Map<string, { turn: ReplayTurn; command: string }>();
|
|
44
|
+
let cwd: string | undefined;
|
|
45
|
+
let turnIndex = 0;
|
|
46
|
+
const current = (): ReplayTurn => {
|
|
47
|
+
const last = turns.at(-1);
|
|
48
|
+
if (last !== undefined) return last;
|
|
49
|
+
const first: ReplayTurn = { index: 0, prompt: undefined, edits: [] };
|
|
50
|
+
turns.push(first);
|
|
51
|
+
return first;
|
|
52
|
+
};
|
|
53
|
+
const sessionId = path.basename(file, ".jsonl");
|
|
54
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
55
|
+
if (line.trim() === "") continue;
|
|
56
|
+
let entry: Record<string, unknown> | undefined;
|
|
57
|
+
try {
|
|
58
|
+
entry = asRecord(JSON.parse(line));
|
|
59
|
+
} catch {
|
|
60
|
+
// a torn line, still being written
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const payload = asRecord(entry?.payload);
|
|
64
|
+
if (entry === undefined || payload === undefined) continue;
|
|
65
|
+
if (entry.type === "session_meta" && typeof payload.cwd === "string") cwd ??= payload.cwd;
|
|
66
|
+
if (entry.type !== "response_item") continue;
|
|
67
|
+
if (payload.type === "message" && payload.role === "user") {
|
|
68
|
+
const prompt = promptOf(payload);
|
|
69
|
+
if (prompt === undefined) continue;
|
|
70
|
+
turnIndex += 1;
|
|
71
|
+
turns.push({ index: turnIndex, prompt, edits: [] });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
payload.type === "custom_tool_call" &&
|
|
76
|
+
payload.name === "apply_patch" &&
|
|
77
|
+
typeof payload.call_id === "string" &&
|
|
78
|
+
typeof payload.input === "string"
|
|
79
|
+
) {
|
|
80
|
+
pending.set(payload.call_id, { turn: current(), command: payload.input });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (payload.type === "custom_tool_call_output" && typeof payload.call_id === "string") {
|
|
84
|
+
const call = pending.get(payload.call_id);
|
|
85
|
+
pending.delete(payload.call_id);
|
|
86
|
+
if (call === undefined) continue;
|
|
87
|
+
// a patch that did not apply changed nothing
|
|
88
|
+
if (typeof payload.output === "string" && FAILED_OUTPUT.test(payload.output)) continue;
|
|
89
|
+
const parsed = postToolUseInputSchema.safeParse({
|
|
90
|
+
session_id: sessionId,
|
|
91
|
+
cwd: cwd ?? process.cwd(),
|
|
92
|
+
hook_event_name: "PostToolUse",
|
|
93
|
+
tool_name: "apply_patch",
|
|
94
|
+
tool_use_id: payload.call_id,
|
|
95
|
+
tool_input: { command: call.command },
|
|
96
|
+
});
|
|
97
|
+
if (parsed.success) call.turn.edits.push({ turn: call.turn.index, input: parsed.data });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { file, cwd: cwd ?? process.cwd(), turns: turns.filter((t) => t.edits.length > 0) };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const rolloutFiles = (dir: string): string[] => {
|
|
104
|
+
let names: string[];
|
|
105
|
+
try {
|
|
106
|
+
names = readdirSync(dir);
|
|
107
|
+
} catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return names.flatMap((name) => {
|
|
111
|
+
const full = path.join(dir, name);
|
|
112
|
+
try {
|
|
113
|
+
if (statSync(full).isDirectory()) return rolloutFiles(full);
|
|
114
|
+
} catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return name.startsWith("rollout-") && name.endsWith(".jsonl") ? [full] : [];
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const inside = (root: string, dir: string): boolean => {
|
|
122
|
+
const rel = path.relative(root, dir);
|
|
123
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const codexSessionsFor = (root: string, dir = codexSessionsDir()): ReplaySession[] =>
|
|
127
|
+
rolloutFiles(dir)
|
|
128
|
+
.sort()
|
|
129
|
+
.map(parseCodexRollout)
|
|
130
|
+
.filter((s) => inside(root, s.cwd) && s.turns.length > 0);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { AbideError, postToolUseInputSchema } from "@coldtea/abide-schema";
|
|
6
|
+
import { MAX_TASK_CHARS } from "./constants.js";
|
|
7
|
+
import type { ReplaySession, ReplayTurn } from "./replay.js";
|
|
8
|
+
|
|
9
|
+
/** Sessions live in SQLite. The `sqlite3` binary reads it so the CLI needs no driver. */
|
|
10
|
+
export const opencodeDbPath = (): string =>
|
|
11
|
+
path.join(homedir(), ".local", "share", "opencode", "opencode.db");
|
|
12
|
+
|
|
13
|
+
export type OpencodeRow = {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
directory: string;
|
|
16
|
+
messageId: string;
|
|
17
|
+
role: string;
|
|
18
|
+
created: number;
|
|
19
|
+
part: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined => {
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
24
|
+
const record: Record<string, unknown> = { ...value };
|
|
25
|
+
return record;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const query = (db: string, sql: string): unknown[] => {
|
|
29
|
+
const result = spawnSync("sqlite3", ["-json", "-readonly", db, sql], {
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
32
|
+
});
|
|
33
|
+
if (result.error !== undefined)
|
|
34
|
+
throw new AbideError(
|
|
35
|
+
"GIT_UNAVAILABLE",
|
|
36
|
+
"sqlite3 is not on PATH, and OpenCode sessions live in a SQLite database",
|
|
37
|
+
{
|
|
38
|
+
cause: result.error,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
if (result.status !== 0)
|
|
42
|
+
throw new AbideError(
|
|
43
|
+
"GIT_UNAVAILABLE",
|
|
44
|
+
`sqlite3 could not read ${db}: ${result.stderr.trim()}`,
|
|
45
|
+
);
|
|
46
|
+
if (result.stdout.trim() === "") return [];
|
|
47
|
+
const parsed: unknown = JSON.parse(result.stdout);
|
|
48
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const readOpencodeRows = (db: string): OpencodeRow[] =>
|
|
52
|
+
query(
|
|
53
|
+
db,
|
|
54
|
+
`select s.id as sessionId, s.directory as directory, m.id as messageId,
|
|
55
|
+
json_extract(m.data, '$.role') as role, m.time_created as created, p.data as part
|
|
56
|
+
from part p
|
|
57
|
+
join message m on m.id = p.message_id
|
|
58
|
+
join session s on s.id = m.session_id
|
|
59
|
+
order by s.id, m.time_created, m.id, p.id`,
|
|
60
|
+
).flatMap((row) => {
|
|
61
|
+
const r = asRecord(row);
|
|
62
|
+
if (
|
|
63
|
+
r === undefined ||
|
|
64
|
+
typeof r.sessionId !== "string" ||
|
|
65
|
+
typeof r.directory !== "string" ||
|
|
66
|
+
typeof r.messageId !== "string" ||
|
|
67
|
+
typeof r.role !== "string" ||
|
|
68
|
+
typeof r.created !== "number" ||
|
|
69
|
+
typeof r.part !== "string"
|
|
70
|
+
)
|
|
71
|
+
return [];
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
sessionId: r.sessionId,
|
|
75
|
+
directory: r.directory,
|
|
76
|
+
messageId: r.messageId,
|
|
77
|
+
role: r.role,
|
|
78
|
+
created: r.created,
|
|
79
|
+
part: r.part,
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** The same payload the live plugin builds. */
|
|
85
|
+
const editPayload = (
|
|
86
|
+
sessionId: string,
|
|
87
|
+
directory: string,
|
|
88
|
+
callId: string,
|
|
89
|
+
tool: string,
|
|
90
|
+
input: Record<string, unknown>,
|
|
91
|
+
): unknown => {
|
|
92
|
+
const file = typeof input.filePath === "string" ? input.filePath : "";
|
|
93
|
+
if (file === "") return undefined;
|
|
94
|
+
const file_path = path.isAbsolute(file) ? file : path.join(directory, file);
|
|
95
|
+
const base = {
|
|
96
|
+
session_id: sessionId,
|
|
97
|
+
cwd: directory,
|
|
98
|
+
hook_event_name: "PostToolUse",
|
|
99
|
+
tool_use_id: callId,
|
|
100
|
+
};
|
|
101
|
+
switch (tool) {
|
|
102
|
+
case "edit":
|
|
103
|
+
return {
|
|
104
|
+
...base,
|
|
105
|
+
tool_name: "Edit",
|
|
106
|
+
tool_input: {
|
|
107
|
+
file_path,
|
|
108
|
+
old_string: String(input.oldString ?? ""),
|
|
109
|
+
new_string: String(input.newString ?? ""),
|
|
110
|
+
replace_all: Boolean(input.replaceAll),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
case "write":
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
tool_name: "Write",
|
|
117
|
+
tool_input: { file_path, content: String(input.content ?? "") },
|
|
118
|
+
tool_response: { filePath: file_path, originalFile: null },
|
|
119
|
+
};
|
|
120
|
+
default:
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const opencodeSessionsFromRows = (
|
|
126
|
+
root: string,
|
|
127
|
+
rows: readonly OpencodeRow[],
|
|
128
|
+
): ReplaySession[] => {
|
|
129
|
+
const sessions = new Map<string, ReplaySession & { turnIndex: number; lastMessage?: string }>();
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
const rel = path.relative(root, row.directory);
|
|
132
|
+
if (rel !== "" && (rel.startsWith("..") || path.isAbsolute(rel))) continue;
|
|
133
|
+
let session = sessions.get(row.sessionId);
|
|
134
|
+
if (session === undefined) {
|
|
135
|
+
session = { file: row.sessionId, cwd: row.directory, turns: [], turnIndex: 0 };
|
|
136
|
+
sessions.set(row.sessionId, session);
|
|
137
|
+
}
|
|
138
|
+
let part: Record<string, unknown> | undefined;
|
|
139
|
+
try {
|
|
140
|
+
part = asRecord(JSON.parse(row.part));
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (part === undefined) continue;
|
|
145
|
+
if (row.role === "user") {
|
|
146
|
+
if (session.lastMessage !== row.messageId) {
|
|
147
|
+
session.lastMessage = row.messageId;
|
|
148
|
+
session.turnIndex += 1;
|
|
149
|
+
session.turns.push({ index: session.turnIndex, prompt: undefined, edits: [] });
|
|
150
|
+
}
|
|
151
|
+
const turn = session.turns.at(-1);
|
|
152
|
+
if (turn !== undefined && part.type === "text" && typeof part.text === "string") {
|
|
153
|
+
const text = part.text.trim();
|
|
154
|
+
if (text !== "" && !text.startsWith("Abide:"))
|
|
155
|
+
turn.prompt = `${turn.prompt === undefined ? "" : `${turn.prompt}\n`}${text}`.slice(
|
|
156
|
+
0,
|
|
157
|
+
MAX_TASK_CHARS,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (part.type !== "tool" || typeof part.tool !== "string" || typeof part.callID !== "string")
|
|
163
|
+
continue;
|
|
164
|
+
const state = asRecord(part.state);
|
|
165
|
+
const input = asRecord(state?.input);
|
|
166
|
+
if (state?.status !== "completed" || input === undefined) continue;
|
|
167
|
+
const payload = editPayload(row.sessionId, row.directory, part.callID, part.tool, input);
|
|
168
|
+
if (payload === undefined) continue;
|
|
169
|
+
const parsed = postToolUseInputSchema.safeParse(payload);
|
|
170
|
+
if (!parsed.success) continue;
|
|
171
|
+
let turn: ReplayTurn | undefined = session.turns.at(-1);
|
|
172
|
+
if (turn === undefined) {
|
|
173
|
+
turn = { index: 0, prompt: undefined, edits: [] };
|
|
174
|
+
session.turns.push(turn);
|
|
175
|
+
}
|
|
176
|
+
turn.edits.push({ turn: turn.index, input: parsed.data });
|
|
177
|
+
}
|
|
178
|
+
return [...sessions.values()]
|
|
179
|
+
.map(({ file, cwd, turns }) => ({ file, cwd, turns: turns.filter((t) => t.edits.length > 0) }))
|
|
180
|
+
.filter((s) => s.turns.length > 0);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const opencodeSessionsFor = (root: string, db = opencodeDbPath()): ReplaySession[] => {
|
|
184
|
+
if (!existsSync(db)) throw new AbideError("RUBRIC_MISSING", `no OpenCode database at ${db}`);
|
|
185
|
+
return opencodeSessionsFromRows(root, readOpencodeRows(db));
|
|
186
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
rubricSchema,
|
|
5
|
+
type CheckType,
|
|
6
|
+
type Rubric,
|
|
7
|
+
type Rule,
|
|
8
|
+
type RuleStatus,
|
|
9
|
+
} from "@coldtea/abide-schema";
|
|
10
|
+
import { hashFile } from "./sources.js";
|
|
11
|
+
import { canonicalSourcePath, resolveSourcePath } from "./paths.js";
|
|
12
|
+
import { readRegularText } from "./regularFile.js";
|
|
13
|
+
|
|
14
|
+
export type RubricRead =
|
|
15
|
+
| { kind: "missing"; path: string }
|
|
16
|
+
| { kind: "invalid"; path: string; issues: string[] }
|
|
17
|
+
| { kind: "ok"; path: string; rubric: Rubric };
|
|
18
|
+
|
|
19
|
+
export const readRubric = (file: string): RubricRead => {
|
|
20
|
+
const raw = readRegularText(file);
|
|
21
|
+
if (raw === undefined) return { kind: "missing", path: file };
|
|
22
|
+
let json: unknown;
|
|
23
|
+
try {
|
|
24
|
+
json = JSON.parse(raw);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : "not JSON";
|
|
27
|
+
return { kind: "invalid", path: file, issues: [message] };
|
|
28
|
+
}
|
|
29
|
+
const parsed = rubricSchema.safeParse(json);
|
|
30
|
+
if (!parsed.success) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "invalid",
|
|
33
|
+
path: file,
|
|
34
|
+
issues: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return { kind: "ok", path: file, rubric: parsed.data };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const writeRubric = (file: string, rubric: Rubric): void => {
|
|
41
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
42
|
+
writeFileSync(file, `${JSON.stringify(rubric, null, 2)}\n`);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Hashes every source and settles on one spelling for each path, in sources and in rules. */
|
|
46
|
+
export const fillSourceShas = (
|
|
47
|
+
rubric: Rubric,
|
|
48
|
+
root: string,
|
|
49
|
+
): { rubric: Rubric; missing: string[] } => {
|
|
50
|
+
const missing: string[] = [];
|
|
51
|
+
const sources = rubric.sources.map((source) => {
|
|
52
|
+
const canonical = canonicalSourcePath(root, source.path);
|
|
53
|
+
const sha = hashFile(resolveSourcePath(root, source.path));
|
|
54
|
+
if (sha === undefined) {
|
|
55
|
+
missing.push(source.path);
|
|
56
|
+
return { ...source, path: canonical };
|
|
57
|
+
}
|
|
58
|
+
return { ...source, path: canonical, sha };
|
|
59
|
+
});
|
|
60
|
+
const rules = rubric.rules.map((rule) => ({
|
|
61
|
+
...rule,
|
|
62
|
+
source: { ...rule.source, path: canonicalSourcePath(root, rule.source.path) },
|
|
63
|
+
}));
|
|
64
|
+
return { rubric: { ...rubric, sources, rules }, missing };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type MergedRule = Rule & { origin: "project" | "global" };
|
|
68
|
+
|
|
69
|
+
/** Project rules win on an id clash. */
|
|
70
|
+
export const mergeRules = (
|
|
71
|
+
project: Rubric | undefined,
|
|
72
|
+
global: Rubric | undefined,
|
|
73
|
+
): MergedRule[] => {
|
|
74
|
+
const byId = new Map<string, MergedRule>();
|
|
75
|
+
for (const rule of global?.rules ?? []) byId.set(rule.id, { ...rule, origin: "global" });
|
|
76
|
+
for (const rule of project?.rules ?? []) byId.set(rule.id, { ...rule, origin: "project" });
|
|
77
|
+
return [...byId.values()];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type BucketCounts = Record<CheckType, number> & {
|
|
81
|
+
total: number;
|
|
82
|
+
byStatus: Record<RuleStatus, number>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const bucketCounts = (rules: readonly Rule[]): BucketCounts => {
|
|
86
|
+
const counts: BucketCounts = {
|
|
87
|
+
lint: 0,
|
|
88
|
+
model: 0,
|
|
89
|
+
deferred: 0,
|
|
90
|
+
unenforceable: 0,
|
|
91
|
+
total: rules.length,
|
|
92
|
+
byStatus: { active: 0, weak: 0, noisy: 0, disabled: 0 },
|
|
93
|
+
};
|
|
94
|
+
for (const rule of rules) {
|
|
95
|
+
counts[rule.check.type] += 1;
|
|
96
|
+
counts.byStatus[rule.status] += 1;
|
|
97
|
+
}
|
|
98
|
+
return counts;
|
|
99
|
+
};
|
package/src/lib/scope.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import type { Rule } from "@coldtea/abide-schema";
|
|
3
|
+
|
|
4
|
+
const matchers = new Map<string, (p: string) => boolean>();
|
|
5
|
+
|
|
6
|
+
const matcherFor = (globs: readonly string[]): ((p: string) => boolean) => {
|
|
7
|
+
const key = globs.join("\n");
|
|
8
|
+
let m = matchers.get(key);
|
|
9
|
+
if (m === undefined) {
|
|
10
|
+
m = picomatch([...globs], { dot: true });
|
|
11
|
+
matchers.set(key, m);
|
|
12
|
+
}
|
|
13
|
+
return m;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Repo-relative posix path against the rule's globs. No scope means every file. */
|
|
17
|
+
export const ruleAppliesTo = (rule: Pick<Rule, "scope">, relativePath: string): boolean =>
|
|
18
|
+
rule.scope === undefined || matcherFor(rule.scope)(relativePath);
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { MAX_TASK_CHARS, SESSION_STATE_MAX_AGE_MS } from "./constants.js";
|
|
6
|
+
import { sessionsDir } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Turn state on disk, one directory per session and prompt, written as
|
|
10
|
+
* first-write-wins files. Hooks for parallel tool calls run at the same time,
|
|
11
|
+
* so nothing here is read, changed and written back: a file's start-of-turn
|
|
12
|
+
* content is created once with `wx`, and a counter is the number of files
|
|
13
|
+
* with its prefix, each created with `wx`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const safe = (part: string): string => part.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
17
|
+
const shortHash = (value: string): string =>
|
|
18
|
+
createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
19
|
+
|
|
20
|
+
export const NO_PROMPT_TURN = "turn";
|
|
21
|
+
|
|
22
|
+
export const turnDir = (sessionId: string, promptId: string | undefined): string =>
|
|
23
|
+
path.join(sessionsDir(), safe(sessionId), safe(promptId ?? NO_PROMPT_TURN));
|
|
24
|
+
|
|
25
|
+
const fileStartSchema = z.object({ path: z.string(), original: z.string().nullable() });
|
|
26
|
+
export type FileStart = z.infer<typeof fileStartSchema>;
|
|
27
|
+
|
|
28
|
+
// Owner-only: a start-of-turn snapshot holds whatever the agent edited.
|
|
29
|
+
const writePrivate = (file: string, contents: string, flag: "w" | "wx" = "w"): void => {
|
|
30
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
31
|
+
writeFileSync(file, contents, { flag, mode: 0o600 });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createOnce = (file: string, contents: string): boolean => {
|
|
35
|
+
try {
|
|
36
|
+
writePrivate(file, contents, "wx");
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const countWithPrefix = (dir: string, prefix: string): number => {
|
|
44
|
+
try {
|
|
45
|
+
return readdirSync(dir).filter((name) => name.startsWith(prefix)).length;
|
|
46
|
+
} catch {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Bumps a counter by creating the next numbered file. Two hooks racing both land: each retries past the other's number. */
|
|
52
|
+
const increment = (dir: string, prefix: string): number => {
|
|
53
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
54
|
+
const next = countWithPrefix(dir, prefix) + 1;
|
|
55
|
+
if (createOnce(path.join(dir, `${prefix}${next}`), "")) return next;
|
|
56
|
+
}
|
|
57
|
+
return countWithPrefix(dir, prefix);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** The content a file had when this turn first touched it. Only the first record counts. */
|
|
61
|
+
export const recordFileStart = (
|
|
62
|
+
dir: string,
|
|
63
|
+
absolutePath: string,
|
|
64
|
+
original: string | null,
|
|
65
|
+
): void => {
|
|
66
|
+
const record: FileStart = { path: absolutePath, original };
|
|
67
|
+
createOnce(path.join(dir, "files", `${shortHash(absolutePath)}.json`), JSON.stringify(record));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const readRecords = <T>(dir: string, schema: z.ZodType<T>): T[] => {
|
|
71
|
+
let names: string[];
|
|
72
|
+
try {
|
|
73
|
+
names = readdirSync(dir);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const records: T[] = [];
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = schema.safeParse(JSON.parse(readFileSync(path.join(dir, name), "utf8")));
|
|
81
|
+
if (parsed.success) records.push(parsed.data);
|
|
82
|
+
} catch {
|
|
83
|
+
// torn
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return records;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const readFileStarts = (dir: string): FileStart[] =>
|
|
90
|
+
readRecords(path.join(dir, "files"), fileStartSchema);
|
|
91
|
+
|
|
92
|
+
/** Stop judges these again: a block the agent ignored must not end the turn quietly. */
|
|
93
|
+
export const recordBlockedFile = (dir: string, relativePath: string): void => {
|
|
94
|
+
createOnce(path.join(dir, "blocked", shortHash(relativePath)), relativePath);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const readBlockedFiles = (dir: string): Set<string> => {
|
|
98
|
+
const files = new Set<string>();
|
|
99
|
+
try {
|
|
100
|
+
for (const name of readdirSync(path.join(dir, "blocked"))) {
|
|
101
|
+
try {
|
|
102
|
+
files.add(readFileSync(path.join(dir, "blocked", name), "utf8"));
|
|
103
|
+
} catch {
|
|
104
|
+
// being written by the other hook
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// no blocks this turn
|
|
109
|
+
}
|
|
110
|
+
return files;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const checkedSchema = z.object({
|
|
114
|
+
path: z.string(),
|
|
115
|
+
/** Null when the file did not exist. */
|
|
116
|
+
before: z.string().nullable(),
|
|
117
|
+
after: z.string(),
|
|
118
|
+
});
|
|
119
|
+
/** An edit a check judged, as blob ids on either side of it. */
|
|
120
|
+
export type CheckedEdit = z.infer<typeof checkedSchema>;
|
|
121
|
+
|
|
122
|
+
/** All kept: Stop walks them as a chain from turn start to the file as it stands. */
|
|
123
|
+
export const recordChecked = (dir: string, record: CheckedEdit): void => {
|
|
124
|
+
const contents = JSON.stringify(record);
|
|
125
|
+
createOnce(path.join(dir, "checked", `${shortHash(contents)}.json`), contents);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const readChecked = (dir: string): CheckedEdit[] =>
|
|
129
|
+
readRecords(path.join(dir, "checked"), checkedSchema);
|
|
130
|
+
|
|
131
|
+
const blockPrefix = (key: string): string => `${shortHash(key)}.`;
|
|
132
|
+
|
|
133
|
+
export const blockCount = (dir: string, key: string): number =>
|
|
134
|
+
countWithPrefix(path.join(dir, "blocks"), blockPrefix(key));
|
|
135
|
+
|
|
136
|
+
export const incrementBlock = (dir: string, key: string): number =>
|
|
137
|
+
increment(path.join(dir, "blocks"), blockPrefix(key));
|
|
138
|
+
|
|
139
|
+
export const stopCheckCount = (dir: string): number =>
|
|
140
|
+
countWithPrefix(path.join(dir, "stops"), "stop.");
|
|
141
|
+
|
|
142
|
+
export const incrementStopChecks = (dir: string): number =>
|
|
143
|
+
increment(path.join(dir, "stops"), "stop.");
|
|
144
|
+
|
|
145
|
+
/** The prompt that started the turn, for hosts that send it instead of a transcript. */
|
|
146
|
+
export const writePrompt = (dir: string, prompt: string): void => {
|
|
147
|
+
createOnce(path.join(dir, "prompt"), prompt);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const readPrompt = (dir: string): string | undefined => {
|
|
151
|
+
try {
|
|
152
|
+
const text = readFileSync(path.join(dir, "prompt"), "utf8").trim();
|
|
153
|
+
return text === "" ? undefined : text.slice(0, MAX_TASK_CHARS);
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** The git tree the working tree was at when the turn began, when a turn-start hook recorded one. */
|
|
160
|
+
export const writeBaseline = (dir: string, tree: string): void => {
|
|
161
|
+
createOnce(path.join(dir, "baseline"), tree);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const readBaseline = (dir: string): string | undefined => {
|
|
165
|
+
try {
|
|
166
|
+
const tree = readFileSync(path.join(dir, "baseline"), "utf8").trim();
|
|
167
|
+
return /^[0-9a-f]{40,64}$/.test(tree) ? tree : undefined;
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* What became of the turn-start snapshot. Absent on a repository without git,
|
|
175
|
+
* where the per-file fallback is the documented mode. `pending` means the
|
|
176
|
+
* hook died before it could say, which for the Stop check is the same as
|
|
177
|
+
* failed: the turn cannot be read back whole.
|
|
178
|
+
*/
|
|
179
|
+
export type BaselineStatus = "pending" | "ok" | "failed";
|
|
180
|
+
|
|
181
|
+
const statusFile = (dir: string): string => path.join(dir, "baseline-status");
|
|
182
|
+
|
|
183
|
+
export const markBaseline = (dir: string, status: BaselineStatus): void => {
|
|
184
|
+
try {
|
|
185
|
+
writePrivate(statusFile(dir), status);
|
|
186
|
+
} catch {
|
|
187
|
+
// nothing to record on; Stop will see no status and take the fallback
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const readBaselineStatus = (dir: string): BaselineStatus | undefined => {
|
|
192
|
+
try {
|
|
193
|
+
const raw = readFileSync(statusFile(dir), "utf8").trim();
|
|
194
|
+
return raw === "ok" || raw === "pending" || raw === "failed" ? raw : undefined;
|
|
195
|
+
} catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const hasTurnState = (dir: string): boolean =>
|
|
201
|
+
readFileStarts(dir).length > 0 ||
|
|
202
|
+
readBaseline(dir) !== undefined ||
|
|
203
|
+
readBaselineStatus(dir) !== undefined;
|
|
204
|
+
|
|
205
|
+
export const clearTurn = (dir: string): void => {
|
|
206
|
+
try {
|
|
207
|
+
rmSync(dir, { recursive: true, force: true });
|
|
208
|
+
} catch {
|
|
209
|
+
// already gone
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const pruneOldTurns = (now = Date.now()): void => {
|
|
214
|
+
try {
|
|
215
|
+
for (const name of readdirSync(sessionsDir())) {
|
|
216
|
+
const file = path.join(sessionsDir(), name);
|
|
217
|
+
if (now - statSync(file).mtimeMs > SESSION_STATE_MAX_AGE_MS)
|
|
218
|
+
rmSync(file, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// no sessions dir yet
|
|
222
|
+
}
|
|
223
|
+
};
|