@synaplink/orqlaude 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.mcp.json.template +8 -0
  2. package/README.md +239 -0
  3. package/dist/__tests__/hallucination.test.d.ts +1 -0
  4. package/dist/__tests__/hallucination.test.js +65 -0
  5. package/dist/__tests__/hallucination.test.js.map +1 -0
  6. package/dist/__tests__/state.test.d.ts +1 -0
  7. package/dist/__tests__/state.test.js +124 -0
  8. package/dist/__tests__/state.test.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +322 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/lib/audit.d.ts +38 -0
  13. package/dist/lib/audit.js +108 -0
  14. package/dist/lib/audit.js.map +1 -0
  15. package/dist/lib/budgeting.d.ts +37 -0
  16. package/dist/lib/budgeting.js +67 -0
  17. package/dist/lib/budgeting.js.map +1 -0
  18. package/dist/lib/hallucination.d.ts +46 -0
  19. package/dist/lib/hallucination.js +154 -0
  20. package/dist/lib/hallucination.js.map +1 -0
  21. package/dist/lib/jsonl_tail.d.ts +40 -0
  22. package/dist/lib/jsonl_tail.js +126 -0
  23. package/dist/lib/jsonl_tail.js.map +1 -0
  24. package/dist/lib/pricing.d.ts +24 -0
  25. package/dist/lib/pricing.js +43 -0
  26. package/dist/lib/pricing.js.map +1 -0
  27. package/dist/lib/state.d.ts +118 -0
  28. package/dist/lib/state.js +138 -0
  29. package/dist/lib/state.js.map +1 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.js +34 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/telegram/api.d.ts +45 -0
  34. package/dist/telegram/api.js +59 -0
  35. package/dist/telegram/api.js.map +1 -0
  36. package/dist/telegram/bot.d.ts +11 -0
  37. package/dist/telegram/bot.js +73 -0
  38. package/dist/telegram/bot.js.map +1 -0
  39. package/dist/telegram/commands.d.ts +22 -0
  40. package/dist/telegram/commands.js +217 -0
  41. package/dist/telegram/commands.js.map +1 -0
  42. package/dist/telegram/config.d.ts +30 -0
  43. package/dist/telegram/config.js +37 -0
  44. package/dist/telegram/config.js.map +1 -0
  45. package/dist/telegram/notifier.d.ts +14 -0
  46. package/dist/telegram/notifier.js +136 -0
  47. package/dist/telegram/notifier.js.map +1 -0
  48. package/dist/tools/broker.d.ts +15 -0
  49. package/dist/tools/broker.js +245 -0
  50. package/dist/tools/broker.js.map +1 -0
  51. package/dist/tools/dispatch.d.ts +14 -0
  52. package/dist/tools/dispatch.js +231 -0
  53. package/dist/tools/dispatch.js.map +1 -0
  54. package/dist/tools/lifecycle.d.ts +18 -0
  55. package/dist/tools/lifecycle.js +124 -0
  56. package/dist/tools/lifecycle.js.map +1 -0
  57. package/dist/tools/ping.d.ts +2 -0
  58. package/dist/tools/ping.js +26 -0
  59. package/dist/tools/ping.js.map +1 -0
  60. package/dist/tools/planning.d.ts +4 -0
  61. package/dist/tools/planning.js +160 -0
  62. package/dist/tools/planning.js.map +1 -0
  63. package/dist/tools/review.d.ts +18 -0
  64. package/dist/tools/review.js +93 -0
  65. package/dist/tools/review.js.map +1 -0
  66. package/package.json +56 -0
@@ -0,0 +1,154 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
4
+ const READ_TOOLS = new Set(["Read", "Grep", "Glob", "NotebookRead"]);
5
+ /**
6
+ * Score a session by inspecting its full set of tool_use events (extracted by
7
+ * the caller from the JSONL). cwd is the worktree root to check paths against.
8
+ */
9
+ export async function detectHallucination(toolUses, cwd) {
10
+ const concerns = [];
11
+ let score = 0;
12
+ // ---- Check 1: path-existence ---------------------------------------------
13
+ const referencedPaths = new Set();
14
+ for (const tu of toolUses) {
15
+ const p = extractPathFromToolUse(tu);
16
+ if (p)
17
+ referencedPaths.add(p);
18
+ }
19
+ const missing = [];
20
+ for (const p of referencedPaths) {
21
+ if (!path.isAbsolute(p))
22
+ continue; // skip relative; we'd need worktree context
23
+ if (!p.startsWith(cwd) && !p.startsWith("/tmp"))
24
+ continue; // outside worktree, skip
25
+ try {
26
+ await fs.access(p);
27
+ }
28
+ catch {
29
+ missing.push(p);
30
+ }
31
+ }
32
+ if (missing.length > 0) {
33
+ const ratio = missing.length / Math.max(1, referencedPaths.size);
34
+ if (ratio > 0.3 || missing.length >= 3) {
35
+ score += 0.4;
36
+ concerns.push(`Referenced ${missing.length} path(s) that don't exist in the worktree: ${missing
37
+ .slice(0, 3)
38
+ .join(", ")}${missing.length > 3 ? ` (+${missing.length - 3} more)` : ""}`);
39
+ }
40
+ else if (missing.length > 0) {
41
+ score += 0.1;
42
+ concerns.push(`Minor: ${missing.length} referenced path(s) missing — could be in-flight files.`);
43
+ }
44
+ }
45
+ // ---- Check 2a: edit-without-prior-read -----------------------------------
46
+ const editedWithoutRead = [];
47
+ const readPaths = new Set();
48
+ for (const tu of toolUses) {
49
+ if (READ_TOOLS.has(tu.name)) {
50
+ const p = extractPathFromToolUse(tu);
51
+ if (p)
52
+ readPaths.add(p);
53
+ }
54
+ else if (EDIT_TOOLS.has(tu.name)) {
55
+ const p = extractPathFromToolUse(tu);
56
+ if (p && !readPaths.has(p) && tu.name !== "Write") {
57
+ // Write is OK without read (new file). Edit/MultiEdit needs prior read.
58
+ editedWithoutRead.push(p);
59
+ }
60
+ }
61
+ }
62
+ if (editedWithoutRead.length > 0) {
63
+ score += 0.15;
64
+ concerns.push(`Edited ${editedWithoutRead.length} file(s) without first reading them: ${editedWithoutRead
65
+ .slice(0, 2)
66
+ .join(", ")}. Agent may be guessing at file contents.`);
67
+ }
68
+ // ---- Check 2b: repeated identical tool calls (tight loop) ---------------
69
+ const seen = new Map();
70
+ for (const tu of toolUses) {
71
+ const key = JSON.stringify({ n: tu.name, i: tu.input });
72
+ seen.set(key, (seen.get(key) ?? 0) + 1);
73
+ }
74
+ const maxRepeat = Math.max(0, ...seen.values());
75
+ if (maxRepeat >= 4) {
76
+ score += 0.2;
77
+ concerns.push(`Repeated the same tool call ${maxRepeat} times — agent may be looping.`);
78
+ }
79
+ else if (maxRepeat === 3) {
80
+ score += 0.05;
81
+ concerns.push(`Repeated the same tool call 3 times — possible loop.`);
82
+ }
83
+ // ---- Check 2c: commit without testing ------------------------------------
84
+ // Heuristic: look for `git commit` in Bash tool calls without any prior Bash
85
+ // call that includes test/check/lint.
86
+ const bashCmds = toolUses
87
+ .filter((tu) => tu.name === "Bash")
88
+ .map((tu) => String(tu.input?.command ?? ""));
89
+ const committed = bashCmds.some((c) => /\bgit\s+commit\b/.test(c));
90
+ const tested = bashCmds.some((c) => /\b(npm\s+test|pytest|cargo\s+test|tsc|lint|check|jest|vitest|mocha)\b/.test(c));
91
+ if (committed && !tested) {
92
+ score += 0.1;
93
+ concerns.push(`Committed without running tests/typecheck. Worth verifying before merge.`);
94
+ }
95
+ return {
96
+ score: Math.min(1, score),
97
+ level: levelFromScore(score),
98
+ concerns,
99
+ };
100
+ }
101
+ function levelFromScore(s) {
102
+ if (s >= 0.6)
103
+ return "severe";
104
+ if (s >= 0.3)
105
+ return "moderate";
106
+ if (s >= 0.1)
107
+ return "minor";
108
+ return "clean";
109
+ }
110
+ function extractPathFromToolUse(tu) {
111
+ const i = tu.input;
112
+ if (!i || typeof i !== "object")
113
+ return null;
114
+ return ((typeof i.file_path === "string" && i.file_path) ||
115
+ (typeof i.path === "string" && i.path) ||
116
+ (typeof i.notebook_path === "string" && i.notebook_path) ||
117
+ null);
118
+ }
119
+ /**
120
+ * Extract all tool_use events from a JSONL-derived snapshot's raw lines.
121
+ * The snapshot module currently only keeps the *last* tool_use; for
122
+ * hallucination detection we need the full history. This re-reads the JSONL.
123
+ */
124
+ export async function extractToolUses(jsonlPath) {
125
+ try {
126
+ const raw = await fs.readFile(jsonlPath, "utf8");
127
+ const out = [];
128
+ for (const line of raw.split("\n")) {
129
+ if (!line.trim())
130
+ continue;
131
+ let evt;
132
+ try {
133
+ evt = JSON.parse(line);
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ if (evt.type === "assistant" && Array.isArray(evt.message?.content)) {
139
+ for (const block of evt.message.content) {
140
+ if (block.type === "tool_use") {
141
+ out.push({ name: block.name, input: block.input });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ catch (err) {
149
+ if (err.code === "ENOENT")
150
+ return [];
151
+ throw err;
152
+ }
153
+ }
154
+ //# sourceMappingURL=hallucination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hallucination.js","sourceRoot":"","sources":["../../src/lib/hallucination.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Read the tail of a Claude Code session JSONL and derive a status snapshot.
3
+ *
4
+ * Session files live at:
5
+ * ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
6
+ *
7
+ * v0.2.0 optimization: per-session byte-offset cache. After the first full
8
+ * read we remember where we stopped and only parse new bytes on subsequent
9
+ * snapshots. This matters for long-running fleets where status() is polled
10
+ * frequently against multi-MB JSONLs.
11
+ *
12
+ * The cache lives in-process; on MCP server restart it's rebuilt by re-reading
13
+ * the file once. Cache hit reduces a `status()` call from O(filesize) to
14
+ * O(new-bytes-only).
15
+ */
16
+ export interface SessionSnapshot {
17
+ exists: boolean;
18
+ sessionId: string;
19
+ jsonlPath: string;
20
+ totalCostUsd: number;
21
+ inputTokens: number;
22
+ outputTokens: number;
23
+ cacheReadTokens: number;
24
+ cacheCreationTokens: number;
25
+ totalEffectiveTokens: number;
26
+ lastEventType: string | null;
27
+ lastActivityAt: number | null;
28
+ lastAssistantText: string | null;
29
+ lastToolUse: {
30
+ name: string;
31
+ input: unknown;
32
+ } | null;
33
+ terminated: boolean;
34
+ terminationReason: string | null;
35
+ }
36
+ export declare function encodeCwdForProjects(cwd: string): string;
37
+ export declare function jsonlPathFor(cwd: string, sessionId: string): string;
38
+ export declare function snapshotSession(cwd: string, sessionId: string): Promise<SessionSnapshot>;
39
+ /** For tests / forced refresh. */
40
+ export declare function clearTailCache(): void;
@@ -0,0 +1,126 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ const HOME = process.env.HOME ?? process.env.USERPROFILE ?? "";
4
+ export function encodeCwdForProjects(cwd) {
5
+ return cwd.replace(/[\/]/g, "-");
6
+ }
7
+ export function jsonlPathFor(cwd, sessionId) {
8
+ return path.join(HOME, ".claude", "projects", encodeCwdForProjects(cwd), `${sessionId}.jsonl`);
9
+ }
10
+ const cache = new Map(); // keyed by jsonlPath
11
+ function emptySnap(sessionId, jsonlPath) {
12
+ return {
13
+ exists: false,
14
+ sessionId,
15
+ jsonlPath,
16
+ totalCostUsd: 0,
17
+ inputTokens: 0,
18
+ outputTokens: 0,
19
+ cacheReadTokens: 0,
20
+ cacheCreationTokens: 0,
21
+ totalEffectiveTokens: 0,
22
+ lastEventType: null,
23
+ lastActivityAt: null,
24
+ lastAssistantText: null,
25
+ lastToolUse: null,
26
+ terminated: false,
27
+ terminationReason: null,
28
+ };
29
+ }
30
+ export async function snapshotSession(cwd, sessionId) {
31
+ const jsonlPath = jsonlPathFor(cwd, sessionId);
32
+ let stat;
33
+ try {
34
+ stat = await fs.stat(jsonlPath);
35
+ }
36
+ catch (err) {
37
+ if (err.code === "ENOENT")
38
+ return emptySnap(sessionId, jsonlPath);
39
+ throw err;
40
+ }
41
+ let entry = cache.get(jsonlPath);
42
+ // If the file shrank or rotated, reset.
43
+ if (entry && entry.byteOffset > stat.size) {
44
+ cache.delete(jsonlPath);
45
+ entry = undefined;
46
+ }
47
+ if (entry && entry.byteOffset === stat.size) {
48
+ return entry.snap; // no new data
49
+ }
50
+ // Read only the new bytes since last visit.
51
+ const startOffset = entry?.byteOffset ?? 0;
52
+ const carry = entry?.carry ?? "";
53
+ const snap = entry ? { ...entry.snap, exists: true } : { ...emptySnap(sessionId, jsonlPath), exists: true };
54
+ const fh = await fs.open(jsonlPath, "r");
55
+ try {
56
+ const buf = Buffer.alloc(stat.size - startOffset);
57
+ if (buf.byteLength > 0) {
58
+ await fh.read({ buffer: buf, position: startOffset });
59
+ }
60
+ const text = carry + buf.toString("utf8");
61
+ const lines = text.split("\n");
62
+ // Last element may be a partial line.
63
+ const newCarry = lines.pop() ?? "";
64
+ for (const line of lines) {
65
+ if (!line.trim())
66
+ continue;
67
+ let evt;
68
+ try {
69
+ evt = JSON.parse(line);
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ applyEvent(snap, evt);
75
+ }
76
+ cache.set(jsonlPath, { byteOffset: stat.size, carry: newCarry, snap });
77
+ }
78
+ finally {
79
+ await fh.close();
80
+ }
81
+ return snap;
82
+ }
83
+ function applyEvent(snap, evt) {
84
+ snap.lastEventType = evt.type ?? snap.lastEventType;
85
+ const ts = typeof evt.timestamp === "string" ? Date.parse(evt.timestamp) : undefined;
86
+ if (ts && !Number.isNaN(ts))
87
+ snap.lastActivityAt = ts;
88
+ const usage = evt.message?.usage ?? evt.usage;
89
+ if (usage) {
90
+ if (typeof usage.input_tokens === "number")
91
+ snap.inputTokens += usage.input_tokens;
92
+ if (typeof usage.output_tokens === "number")
93
+ snap.outputTokens += usage.output_tokens;
94
+ if (typeof usage.cache_read_input_tokens === "number")
95
+ snap.cacheReadTokens += usage.cache_read_input_tokens;
96
+ if (typeof usage.cache_creation_input_tokens === "number")
97
+ snap.cacheCreationTokens += usage.cache_creation_input_tokens;
98
+ }
99
+ snap.totalEffectiveTokens =
100
+ snap.inputTokens + snap.outputTokens + snap.cacheReadTokens + snap.cacheCreationTokens;
101
+ if (typeof evt.total_cost_usd === "number") {
102
+ snap.totalCostUsd = evt.total_cost_usd; // running total from result rows
103
+ }
104
+ else if (typeof evt.cost_usd === "number") {
105
+ snap.totalCostUsd += evt.cost_usd;
106
+ }
107
+ if (evt.type === "assistant" && Array.isArray(evt.message?.content)) {
108
+ for (const block of evt.message.content) {
109
+ if (block.type === "text" && typeof block.text === "string") {
110
+ snap.lastAssistantText = block.text.slice(0, 500);
111
+ }
112
+ else if (block.type === "tool_use") {
113
+ snap.lastToolUse = { name: block.name, input: block.input };
114
+ }
115
+ }
116
+ }
117
+ if (evt.type === "result") {
118
+ snap.terminated = true;
119
+ snap.terminationReason = evt.subtype ?? evt.terminal_reason ?? "completed";
120
+ }
121
+ }
122
+ /** For tests / forced refresh. */
123
+ export function clearTailCache() {
124
+ cache.clear();
125
+ }
126
+ //# sourceMappingURL=jsonl_tail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl_tail.js","sourceRoot":"","sources":["../../src/lib/jsonl_tail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-model pricing as of model release.
3
+ * Numbers are USD per 1M tokens. Update when Anthropic publishes new pricing.
4
+ *
5
+ * `estimateAgentCost` is intentionally rough — it assumes a single agent burns
6
+ * roughly `baselineInputTokens` of system prompt + small per-turn user/tool
7
+ * input and produces `baselineOutputTokens` of output across the run. Refine
8
+ * by observing real runs and tuning the baselines.
9
+ */
10
+ export interface Pricing {
11
+ inputPer1M: number;
12
+ outputPer1M: number;
13
+ cacheWritePer1M: number;
14
+ cacheReadPer1M: number;
15
+ }
16
+ export declare const MODEL_PRICING: Record<string, Pricing>;
17
+ /**
18
+ * Rough cost estimate for one agent running a sub-task.
19
+ *
20
+ * Defaults assume a moderate task (~10 turns of reading/editing). Tune via the
21
+ * `effortMultiplier` argument: 0.5 for trivial, 1.0 for moderate, 2.0+ for
22
+ * heavy refactors. This estimate is a budgeting safety net, not a forecast.
23
+ */
24
+ export declare function estimateAgentCost(model: string, effortMultiplier?: number): number;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Per-model pricing as of model release.
3
+ * Numbers are USD per 1M tokens. Update when Anthropic publishes new pricing.
4
+ *
5
+ * `estimateAgentCost` is intentionally rough — it assumes a single agent burns
6
+ * roughly `baselineInputTokens` of system prompt + small per-turn user/tool
7
+ * input and produces `baselineOutputTokens` of output across the run. Refine
8
+ * by observing real runs and tuning the baselines.
9
+ */
10
+ export const MODEL_PRICING = {
11
+ // Defaults; conservative for estimation.
12
+ "claude-opus-4-7": { inputPer1M: 15, outputPer1M: 75, cacheWritePer1M: 18.75, cacheReadPer1M: 1.5 },
13
+ "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheWritePer1M: 3.75, cacheReadPer1M: 0.3 },
14
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheWritePer1M: 1.25, cacheReadPer1M: 0.1 },
15
+ // Fallback for unrecognized models.
16
+ default: { inputPer1M: 3, outputPer1M: 15, cacheWritePer1M: 3.75, cacheReadPer1M: 0.3 },
17
+ };
18
+ function pricingFor(model) {
19
+ // Strip date suffix (e.g. "-20251001") and any [1m] context-window suffix.
20
+ const stripped = model.replace(/-\d{8}.*$/, "").replace(/\[[^\]]+\]$/, "");
21
+ return MODEL_PRICING[stripped] ?? MODEL_PRICING[model] ?? MODEL_PRICING.default;
22
+ }
23
+ /**
24
+ * Rough cost estimate for one agent running a sub-task.
25
+ *
26
+ * Defaults assume a moderate task (~10 turns of reading/editing). Tune via the
27
+ * `effortMultiplier` argument: 0.5 for trivial, 1.0 for moderate, 2.0+ for
28
+ * heavy refactors. This estimate is a budgeting safety net, not a forecast.
29
+ */
30
+ export function estimateAgentCost(model, effortMultiplier = 1) {
31
+ const p = pricingFor(model);
32
+ // Baselines tuned to match an empirical haiku run we observed: ~30k cache-creation tokens, ~10 input, ~45 output → $0.038.
33
+ // Scale by multiplier and adjust output baseline upward for real work.
34
+ const cacheCreate = 30_000 * effortMultiplier;
35
+ const input = 1_000 * effortMultiplier;
36
+ const cacheRead = 50_000 * effortMultiplier; // most input tokens are cached after first turn
37
+ const output = 4_000 * effortMultiplier;
38
+ return ((cacheCreate * p.cacheWritePer1M) / 1_000_000 +
39
+ (input * p.inputPer1M) / 1_000_000 +
40
+ (cacheRead * p.cacheReadPer1M) / 1_000_000 +
41
+ (output * p.outputPer1M) / 1_000_000);
42
+ }
43
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/lib/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,MAAM,CAAC,MAAM,aAAa,GAA4B;IACpD,yCAAyC;IACzC,iBAAiB,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE;IACnG,mBAAmB,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE;IACnG,kBAAkB,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE;IACjG,oCAAoC;IACpC,OAAO,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE;CACxF,CAAC;AAEF,SAAS,UAAU,CAAC,KAAa;IAC/B,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAC3E,OAAO,aAAa,CAAC,QAAQ,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,gBAAgB,GAAG,CAAC;IACnE,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC5B,2HAA2H;IAC3H,uEAAuE;IACvE,MAAM,WAAW,GAAG,MAAM,GAAG,gBAAgB,CAAC;IAC9C,MAAM,KAAK,GAAG,KAAK,GAAG,gBAAgB,CAAC;IACvC,MAAM,SAAS,GAAG,MAAM,GAAG,gBAAgB,CAAC,CAAC,gDAAgD;IAC7F,MAAM,MAAM,GAAG,KAAK,GAAG,gBAAgB,CAAC;IACxC,OAAO,CACL,CAAC,WAAW,GAAG,CAAC,CAAC,eAAe,CAAC,GAAG,SAAS;QAC7C,CAAC,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,SAAS;QAClC,CAAC,SAAS,GAAG,CAAC,CAAC,cAAc,CAAC,GAAG,SAAS;QAC1C,CAAC,MAAM,GAAG,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,CACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * orqlaude state store — single JSON file per project, atomically written.
3
+ *
4
+ * Why JSON not SQLite: state is small (a fleet rarely has more than ~10 agents,
5
+ * a handful of notes/messages), and atomic JSON writes via tmp+rename are
6
+ * sufficient for the concurrency we expect (handful of MCP calls per second).
7
+ * If contention becomes a problem, swap to node:sqlite without changing the
8
+ * external API of this module.
9
+ *
10
+ * Schema version 2 (v0.2.0): tokens-first budget, file claims, audit
11
+ * resumability hooks.
12
+ */
13
+ export type TaskStatus = "pending" | "dispatched" | "running" | "done" | "failed" | "cancelled";
14
+ export type PlanStatus = "draft" | "estimating" | "awaiting_approval" | "approved" | "dispatching" | "running" | "collected" | "cancelled" | "cancelled_overbudget";
15
+ export interface Task {
16
+ id: string;
17
+ title: string;
18
+ prompt: string;
19
+ tldr: string;
20
+ scope?: string[];
21
+ branchHint?: string;
22
+ status: TaskStatus;
23
+ spawnedSessionId?: string;
24
+ costUsd?: number;
25
+ tokensUsed?: number;
26
+ startedAt?: number;
27
+ finishedAt?: number;
28
+ prUrl?: string;
29
+ summary?: string;
30
+ exitReason?: string;
31
+ stopRequested?: {
32
+ reason: string;
33
+ requestedAt: number;
34
+ };
35
+ }
36
+ export interface Note {
37
+ id: string;
38
+ fromSessionId: string;
39
+ taskId: string;
40
+ text: string;
41
+ blocking: boolean;
42
+ postedAt: number;
43
+ acked: boolean;
44
+ prUrl?: string;
45
+ }
46
+ export interface Message {
47
+ id: string;
48
+ toSessionId: string;
49
+ fromTaskId?: string;
50
+ text: string;
51
+ queuedAt: number;
52
+ delivered: boolean;
53
+ deliveredAt?: number;
54
+ kind?: "directed" | "stop";
55
+ }
56
+ export interface FileClaim {
57
+ path: string;
58
+ claimedBy: string;
59
+ taskId: string;
60
+ reason?: string;
61
+ claimedAt: number;
62
+ }
63
+ export interface Plan {
64
+ id: string;
65
+ createdAt: number;
66
+ rootTask: string;
67
+ budgetCapTokens: number;
68
+ perAgentCapTokens: number;
69
+ estimatedTokens?: number;
70
+ budgetCapUsd?: number;
71
+ perAgentCapUsd?: number;
72
+ estimatedCostUsd?: number;
73
+ estimatedDurationSec?: number;
74
+ modelForEstimate?: string;
75
+ effortMultiplier?: number;
76
+ status: PlanStatus;
77
+ approvalToken?: string;
78
+ approvedAt?: number;
79
+ tasks: Task[];
80
+ notes: Note[];
81
+ messages: Message[];
82
+ claims: FileClaim[];
83
+ reviewPlanId?: string;
84
+ }
85
+ export interface State {
86
+ schemaVersion: 2;
87
+ plans: Record<string, Plan>;
88
+ }
89
+ export declare class StateStore {
90
+ private filePath;
91
+ private cache;
92
+ private writeLock;
93
+ constructor(stateDir: string);
94
+ private load;
95
+ update<T>(mutator: (state: State) => T | Promise<T>): Promise<T>;
96
+ read<T>(reader: (state: State) => T): Promise<T>;
97
+ private persist;
98
+ }
99
+ export declare function newPlan(rootTask: string, budgetCapTokens: number, tasksInput: Array<Omit<Task, "id" | "status">>): Plan;
100
+ export declare function findPlan(state: State, planId: string): Plan;
101
+ export declare function findTask(plan: Plan, taskId: string): Task;
102
+ export declare function findTaskBySession(plan: Plan, sessionId: string): Task | undefined;
103
+ /** Locate the plan+task a given child session belongs to. */
104
+ export declare function planForSession(state: State, sessionId: string): {
105
+ plan: Plan;
106
+ task: Task;
107
+ } | undefined;
108
+ /**
109
+ * Find a dispatched-but-unclaimed task by task_id. Used for self-registration:
110
+ * when a freshly-spawned child agent calls `checkin` with its task_id (which we
111
+ * embed in the spawn prompt), we can adopt it.
112
+ */
113
+ export declare function unclaimedTaskById(state: State, taskId: string): {
114
+ plan: Plan;
115
+ task: Task;
116
+ } | undefined;
117
+ /** Normalize a path for claim comparison (handle . , .. , trailing slash, case). */
118
+ export declare function normalizeClaimPath(p: string, cwd: string): string;
@@ -0,0 +1,138 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ const EMPTY_STATE = { schemaVersion: 2, plans: {} };
5
+ export class StateStore {
6
+ filePath;
7
+ cache = null;
8
+ writeLock = Promise.resolve();
9
+ constructor(stateDir) {
10
+ this.filePath = path.join(stateDir, "orqlaude-state.json");
11
+ }
12
+ async load() {
13
+ if (this.cache)
14
+ return this.cache;
15
+ try {
16
+ const raw = await fs.readFile(this.filePath, "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ this.cache = migrate(parsed);
19
+ }
20
+ catch (err) {
21
+ if (err.code === "ENOENT") {
22
+ this.cache = structuredClone(EMPTY_STATE);
23
+ }
24
+ else {
25
+ throw err;
26
+ }
27
+ }
28
+ return this.cache;
29
+ }
30
+ async update(mutator) {
31
+ let release = () => { };
32
+ const next = new Promise((res) => (release = res));
33
+ const prev = this.writeLock;
34
+ this.writeLock = prev.then(() => next);
35
+ await prev;
36
+ try {
37
+ const state = await this.load();
38
+ const result = await mutator(state);
39
+ await this.persist(state);
40
+ return result;
41
+ }
42
+ finally {
43
+ release();
44
+ }
45
+ }
46
+ async read(reader) {
47
+ const state = await this.load();
48
+ return reader(state);
49
+ }
50
+ async persist(state) {
51
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
52
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
53
+ await fs.writeFile(tmp, JSON.stringify(state, null, 2));
54
+ await fs.rename(tmp, this.filePath);
55
+ this.cache = state;
56
+ }
57
+ }
58
+ /** Forward-compatible migration from earlier schemas. */
59
+ function migrate(input) {
60
+ const v = input.schemaVersion ?? 1;
61
+ if (v === 2 && input.plans)
62
+ return input;
63
+ // v1 → v2: synthesize token fields from USD if missing.
64
+ const out = { schemaVersion: 2, plans: {} };
65
+ for (const [id, plan] of Object.entries(input.plans ?? {})) {
66
+ const p = plan;
67
+ out.plans[id] = {
68
+ ...p,
69
+ budgetCapTokens: p.budgetCapTokens ?? Math.round((p.budgetCapUsd ?? 5) * 25_000), // rough $0.04/k
70
+ perAgentCapTokens: p.perAgentCapTokens ?? Math.round((p.perAgentCapUsd ?? 1) * 25_000),
71
+ claims: p.claims ?? [],
72
+ };
73
+ }
74
+ return out;
75
+ }
76
+ // ---- Plan helpers (pure functions; mutator-friendly) ----
77
+ export function newPlan(rootTask, budgetCapTokens, tasksInput) {
78
+ const tasks = tasksInput.map((t) => ({
79
+ ...t,
80
+ id: randomUUID(),
81
+ status: "pending",
82
+ }));
83
+ return {
84
+ id: randomUUID(),
85
+ createdAt: Date.now(),
86
+ rootTask,
87
+ budgetCapTokens,
88
+ perAgentCapTokens: tasks.length > 0 ? Math.floor(budgetCapTokens / tasks.length) : budgetCapTokens,
89
+ status: "draft",
90
+ tasks,
91
+ notes: [],
92
+ messages: [],
93
+ claims: [],
94
+ };
95
+ }
96
+ export function findPlan(state, planId) {
97
+ const plan = state.plans[planId];
98
+ if (!plan)
99
+ throw new Error(`Plan not found: ${planId}`);
100
+ return plan;
101
+ }
102
+ export function findTask(plan, taskId) {
103
+ const task = plan.tasks.find((t) => t.id === taskId);
104
+ if (!task)
105
+ throw new Error(`Task not found in plan ${plan.id}: ${taskId}`);
106
+ return task;
107
+ }
108
+ export function findTaskBySession(plan, sessionId) {
109
+ return plan.tasks.find((t) => t.spawnedSessionId === sessionId);
110
+ }
111
+ /** Locate the plan+task a given child session belongs to. */
112
+ export function planForSession(state, sessionId) {
113
+ for (const plan of Object.values(state.plans)) {
114
+ const task = findTaskBySession(plan, sessionId);
115
+ if (task)
116
+ return { plan, task };
117
+ }
118
+ return undefined;
119
+ }
120
+ /**
121
+ * Find a dispatched-but-unclaimed task by task_id. Used for self-registration:
122
+ * when a freshly-spawned child agent calls `checkin` with its task_id (which we
123
+ * embed in the spawn prompt), we can adopt it.
124
+ */
125
+ export function unclaimedTaskById(state, taskId) {
126
+ for (const plan of Object.values(state.plans)) {
127
+ const task = plan.tasks.find((t) => t.id === taskId && !t.spawnedSessionId);
128
+ if (task)
129
+ return { plan, task };
130
+ }
131
+ return undefined;
132
+ }
133
+ /** Normalize a path for claim comparison (handle . , .. , trailing slash, case). */
134
+ export function normalizeClaimPath(p, cwd) {
135
+ const abs = path.isAbsolute(p) ? p : path.resolve(cwd, p);
136
+ return path.normalize(abs);
137
+ }
138
+ //# sourceMappingURL=state.js.map