@cairn-tool/cairn 3.4.0 → 3.6.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 (105) hide show
  1. package/README.md +6 -4
  2. package/dist/agent/manifest.d.ts +17 -0
  3. package/dist/agent/manifest.js +59 -1
  4. package/dist/agent/manifest.js.map +1 -1
  5. package/dist/agent/parser.js +209 -24
  6. package/dist/agent/parser.js.map +1 -1
  7. package/dist/agent/render.js +64 -3
  8. package/dist/agent/render.js.map +1 -1
  9. package/dist/cli.js +46 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/qa-list.d.ts +7 -0
  12. package/dist/commands/qa-list.js +69 -0
  13. package/dist/commands/qa-list.js.map +1 -0
  14. package/dist/commands/qa-run.d.ts +3 -0
  15. package/dist/commands/qa-run.js +106 -0
  16. package/dist/commands/qa-run.js.map +1 -0
  17. package/dist/commands/qa-summary.d.ts +6 -0
  18. package/dist/commands/qa-summary.js +38 -0
  19. package/dist/commands/qa-summary.js.map +1 -0
  20. package/dist/config-schema.d.ts +2 -0
  21. package/dist/config-schema.js +4 -0
  22. package/dist/config-schema.js.map +1 -1
  23. package/dist/config.d.ts +12 -2
  24. package/dist/config.js +36 -7
  25. package/dist/config.js.map +1 -1
  26. package/dist/contract/registry.js +43 -0
  27. package/dist/contract/registry.js.map +1 -1
  28. package/dist/contract/schemas/index.js +2 -0
  29. package/dist/contract/schemas/index.js.map +1 -1
  30. package/dist/contract/schemas/qa.d.ts +2 -0
  31. package/dist/contract/schemas/qa.js +111 -0
  32. package/dist/contract/schemas/qa.js.map +1 -0
  33. package/dist/qa/agent.d.ts +25 -0
  34. package/dist/qa/agent.js +142 -0
  35. package/dist/qa/agent.js.map +1 -0
  36. package/dist/qa/agents/claude-code.d.ts +2 -0
  37. package/dist/qa/agents/claude-code.js +121 -0
  38. package/dist/qa/agents/claude-code.js.map +1 -0
  39. package/dist/qa/agents/cursor.d.ts +5 -0
  40. package/dist/qa/agents/cursor.js +143 -0
  41. package/dist/qa/agents/cursor.js.map +1 -0
  42. package/dist/qa/agents/index.d.ts +32 -0
  43. package/dist/qa/agents/index.js +83 -0
  44. package/dist/qa/agents/index.js.map +1 -0
  45. package/dist/qa/agents/types.d.ts +61 -0
  46. package/dist/qa/agents/types.js +18 -0
  47. package/dist/qa/agents/types.js.map +1 -0
  48. package/dist/qa/async.d.ts +27 -0
  49. package/dist/qa/async.js +91 -0
  50. package/dist/qa/async.js.map +1 -0
  51. package/dist/qa/casefile.d.ts +35 -0
  52. package/dist/qa/casefile.js +181 -0
  53. package/dist/qa/casefile.js.map +1 -0
  54. package/dist/qa/cases.d.ts +51 -0
  55. package/dist/qa/cases.js +144 -0
  56. package/dist/qa/cases.js.map +1 -0
  57. package/dist/qa/config.d.ts +50 -0
  58. package/dist/qa/config.js +105 -0
  59. package/dist/qa/config.js.map +1 -0
  60. package/dist/qa/demo.d.ts +10 -0
  61. package/dist/qa/demo.js +194 -0
  62. package/dist/qa/demo.js.map +1 -0
  63. package/dist/qa/format.d.ts +38 -0
  64. package/dist/qa/format.js +208 -0
  65. package/dist/qa/format.js.map +1 -0
  66. package/dist/qa/harness.d.ts +68 -0
  67. package/dist/qa/harness.js +528 -0
  68. package/dist/qa/harness.js.map +1 -0
  69. package/dist/qa/keys.d.ts +12 -0
  70. package/dist/qa/keys.js +96 -0
  71. package/dist/qa/keys.js.map +1 -0
  72. package/dist/qa/layout.d.ts +102 -0
  73. package/dist/qa/layout.js +278 -0
  74. package/dist/qa/layout.js.map +1 -0
  75. package/dist/qa/lock.d.ts +5 -0
  76. package/dist/qa/lock.js +114 -0
  77. package/dist/qa/lock.js.map +1 -0
  78. package/dist/qa/options.d.ts +38 -0
  79. package/dist/qa/options.js +165 -0
  80. package/dist/qa/options.js.map +1 -0
  81. package/dist/qa/outcome.d.ts +30 -0
  82. package/dist/qa/outcome.js +162 -0
  83. package/dist/qa/outcome.js.map +1 -0
  84. package/dist/qa/reports.d.ts +51 -0
  85. package/dist/qa/reports.js +121 -0
  86. package/dist/qa/reports.js.map +1 -0
  87. package/dist/qa/schedule.d.ts +55 -0
  88. package/dist/qa/schedule.js +145 -0
  89. package/dist/qa/schedule.js.map +1 -0
  90. package/dist/qa/screen.d.ts +31 -0
  91. package/dist/qa/screen.js +146 -0
  92. package/dist/qa/screen.js.map +1 -0
  93. package/dist/qa/slot.d.ts +28 -0
  94. package/dist/qa/slot.js +126 -0
  95. package/dist/qa/slot.js.map +1 -0
  96. package/dist/qa/tracker.d.ts +36 -0
  97. package/dist/qa/tracker.js +357 -0
  98. package/dist/qa/tracker.js.map +1 -0
  99. package/dist/query/execute.js +0 -0
  100. package/dist/query/execute.js.map +1 -1
  101. package/dist/scripts/resolve.js +1 -4
  102. package/dist/scripts/resolve.js.map +1 -1
  103. package/dist/usage/providers/cursor.js +1 -1
  104. package/dist/usage/providers/cursor.js.map +1 -1
  105. package/package.json +3 -2
@@ -0,0 +1,143 @@
1
+ import { flatten } from "../format.js";
2
+ import { isRecord, num, sessionIdOf } from "./types.js";
3
+ const TOOL_ARG_FIELDS = [
4
+ "command",
5
+ "path",
6
+ "globPattern",
7
+ "pattern",
8
+ "query",
9
+ "target_notebook",
10
+ "url",
11
+ ];
12
+ /** Return [display name, short args] from a Cursor `tool_call` object. */
13
+ export function toolLabel(toolCall) {
14
+ for (const [key, payload] of Object.entries(toolCall)) {
15
+ if (!(key.endsWith("ToolCall") && isRecord(payload)))
16
+ continue;
17
+ const raw = key.slice(0, -"ToolCall".length);
18
+ const name = raw ? raw[0].toUpperCase() + raw.slice(1) : key;
19
+ const args = isRecord(payload["args"]) ? payload["args"] : {};
20
+ let summary = "";
21
+ for (const fieldName of TOOL_ARG_FIELDS) {
22
+ const value = args[fieldName];
23
+ if (value !== undefined && value !== null && value !== "" && value !== 0 && value !== false) {
24
+ summary = flatten(String(value));
25
+ break;
26
+ }
27
+ }
28
+ return [name, summary];
29
+ }
30
+ return ["tool", ""];
31
+ }
32
+ export function toolFailed(toolCall) {
33
+ for (const [key, payload] of Object.entries(toolCall)) {
34
+ if (key.endsWith("ToolCall") && isRecord(payload)) {
35
+ const result = payload["result"];
36
+ return isRecord(result) && "error" in result;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ function cursorUsage(raw) {
42
+ if (!isRecord(raw))
43
+ return null;
44
+ return {
45
+ inputTokens: num(raw["inputTokens"]),
46
+ outputTokens: num(raw["outputTokens"]),
47
+ cacheReadTokens: num(raw["cacheReadTokens"]),
48
+ cacheWriteTokens: num(raw["cacheWriteTokens"]),
49
+ };
50
+ }
51
+ export const cursorProfile = {
52
+ name: "cursor",
53
+ binaries: ["cursor-agent", "agent"],
54
+ defaultModel: "cursor-grok-4.6-high",
55
+ buildArgv(input, binary) {
56
+ return [
57
+ binary,
58
+ "-p",
59
+ "--output-format",
60
+ "stream-json",
61
+ "--force",
62
+ "--trust",
63
+ "--workspace",
64
+ input.repo,
65
+ "--model",
66
+ input.model,
67
+ input.prompt,
68
+ ];
69
+ },
70
+ normalize(raw) {
71
+ const kind = raw["type"];
72
+ const subtype = raw["subtype"];
73
+ const sessionId = sessionIdOf(raw);
74
+ if (kind === "thinking") {
75
+ if (subtype === "delta") {
76
+ const text = raw["text"];
77
+ return {
78
+ kind: "thinking",
79
+ phase: "delta",
80
+ text: typeof text === "string" ? text : "",
81
+ sessionId,
82
+ };
83
+ }
84
+ if (subtype === "completed")
85
+ return { kind: "thinking", phase: "completed", sessionId };
86
+ return null;
87
+ }
88
+ if (kind === "assistant") {
89
+ const message = isRecord(raw["message"]) ? raw["message"] : {};
90
+ const rawContent = message["content"];
91
+ const content = typeof rawContent === "string"
92
+ ? [{ type: "text", text: rawContent }]
93
+ : Array.isArray(rawContent)
94
+ ? rawContent
95
+ : [];
96
+ const events = [];
97
+ for (const part of content) {
98
+ if (isRecord(part) && part["type"] === "text" && part["text"]) {
99
+ events.push({ kind: "text", text: String(part["text"]), sessionId });
100
+ }
101
+ }
102
+ return events.length === 0 ? null : events;
103
+ }
104
+ if (kind === "tool_call") {
105
+ const tool = isRecord(raw["tool_call"]) ? raw["tool_call"] : {};
106
+ const [name, summary] = toolLabel(tool);
107
+ if (subtype === "started") {
108
+ return { kind: "tool", phase: "started", name, summary, sessionId };
109
+ }
110
+ if (subtype === "completed") {
111
+ return {
112
+ kind: "tool",
113
+ phase: "completed",
114
+ name,
115
+ summary,
116
+ failed: toolFailed(tool),
117
+ sessionId,
118
+ };
119
+ }
120
+ return null;
121
+ }
122
+ if (kind === "result") {
123
+ const usage = cursorUsage(raw["usage"]);
124
+ const error = raw["is_error"]
125
+ ? String(raw["result"] ? raw["result"] : "agent reported an error")
126
+ : undefined;
127
+ if (!usage && !error)
128
+ return null;
129
+ return {
130
+ kind: "usage",
131
+ usage: usage ?? {},
132
+ ...(error ? { error } : {}),
133
+ sessionId,
134
+ };
135
+ }
136
+ if (kind === "system" && subtype === "init") {
137
+ const model = raw["model"];
138
+ return { kind: "init", model: model ? String(model) : undefined, sessionId };
139
+ }
140
+ return null;
141
+ },
142
+ };
143
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../../src/qa/agents/cursor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { QaAgentProfile } from "./types.js";
2
+ /**
3
+ * Every backend `qa run` can spawn.
4
+ *
5
+ * Registering another assistant is a new module plus a line here. Nothing
6
+ * outside this directory may branch on an agent's name: argv, event parsing,
7
+ * and the default model are read from the profile.
8
+ */
9
+ export declare const AGENTS: readonly QaAgentProfile[];
10
+ export declare const SUPPORTED_AGENTS: readonly string[];
11
+ export declare function resolveAgent(name: string): QaAgentProfile;
12
+ export declare function defaultModels(): Record<string, string>;
13
+ /**
14
+ * Resolve the executable for one profile. An explicit `--agent` path overrides
15
+ * every backend — that is the fake-agent test hook. PATH lookup is lazy and
16
+ * cached per profile so a cursor-only queue does not fail because `claude` is
17
+ * absent.
18
+ */
19
+ export declare function resolveBinary(name: string, explicit: string | null): string;
20
+ /**
21
+ * The executable to *display* for a profile, for --dry-run only.
22
+ *
23
+ * Falls back to the profile's first candidate name when nothing is on PATH: previewing a
24
+ * queue launches nothing, so requiring the backend to be installed would make `qa run
25
+ * --dry-run` fail on exactly the machine you would preview a queue on — and would make the
26
+ * e2e suite depend on cursor-agent and claude being present on the CI runner. An explicit
27
+ * --agent is still validated, because a bad path there is a real invocation error.
28
+ */
29
+ export declare function displayBinary(name: string, explicit: string | null): string;
30
+ export declare function agentCaps(parallel: number): ReadonlyMap<string, number>;
31
+ export type { AgentArgvInput, NormalizedEvents, QaAgentProfile, QaEvent } from "./types.js";
32
+ export { asEventList } from "./types.js";
@@ -0,0 +1,83 @@
1
+ import { UserError, which } from "../config.js";
2
+ import { findExplicitAgent } from "../config.js";
3
+ import { claudeCodeProfile } from "./claude-code.js";
4
+ import { cursorProfile } from "./cursor.js";
5
+ /**
6
+ * Every backend `qa run` can spawn.
7
+ *
8
+ * Registering another assistant is a new module plus a line here. Nothing
9
+ * outside this directory may branch on an agent's name: argv, event parsing,
10
+ * and the default model are read from the profile.
11
+ */
12
+ export const AGENTS = [cursorProfile, claudeCodeProfile];
13
+ for (const profile of AGENTS) {
14
+ if (profile.maxParallel !== undefined &&
15
+ (!Number.isInteger(profile.maxParallel) || profile.maxParallel < 1)) {
16
+ throw new Error(`qa agent profile ${profile.name} maxParallel must be >= 1`);
17
+ }
18
+ }
19
+ export const SUPPORTED_AGENTS = AGENTS.map((profile) => profile.name);
20
+ const BY_NAME = new Map(AGENTS.map((profile) => [profile.name, profile]));
21
+ const binaryCache = new Map();
22
+ export function resolveAgent(name) {
23
+ const profile = BY_NAME.get(name);
24
+ if (!profile) {
25
+ throw new UserError(`Unknown agent: ${name} (known: ${SUPPORTED_AGENTS.join(", ")})`);
26
+ }
27
+ return profile;
28
+ }
29
+ export function defaultModels() {
30
+ return Object.fromEntries(AGENTS.map((profile) => [profile.name, profile.defaultModel]));
31
+ }
32
+ /**
33
+ * Resolve the executable for one profile. An explicit `--agent` path overrides
34
+ * every backend — that is the fake-agent test hook. PATH lookup is lazy and
35
+ * cached per profile so a cursor-only queue does not fail because `claude` is
36
+ * absent.
37
+ */
38
+ export function resolveBinary(name, explicit) {
39
+ if (explicit)
40
+ return findExplicitAgent(explicit);
41
+ const hit = binaryCache.get(name);
42
+ if (hit)
43
+ return hit;
44
+ const profile = resolveAgent(name);
45
+ for (const candidate of profile.binaries) {
46
+ const found = which(candidate);
47
+ if (found) {
48
+ binaryCache.set(name, found);
49
+ return found;
50
+ }
51
+ }
52
+ throw new UserError(`${profile.binaries[0]} not on PATH; pass --agent /path/to/${profile.binaries[0]}`);
53
+ }
54
+ /**
55
+ * The executable to *display* for a profile, for --dry-run only.
56
+ *
57
+ * Falls back to the profile's first candidate name when nothing is on PATH: previewing a
58
+ * queue launches nothing, so requiring the backend to be installed would make `qa run
59
+ * --dry-run` fail on exactly the machine you would preview a queue on — and would make the
60
+ * e2e suite depend on cursor-agent and claude being present on the CI runner. An explicit
61
+ * --agent is still validated, because a bad path there is a real invocation error.
62
+ */
63
+ export function displayBinary(name, explicit) {
64
+ if (explicit)
65
+ return findExplicitAgent(explicit);
66
+ try {
67
+ return resolveBinary(name, null);
68
+ }
69
+ catch {
70
+ return resolveAgent(name).binaries[0];
71
+ }
72
+ }
73
+ export function agentCaps(parallel) {
74
+ const caps = new Map();
75
+ for (const profile of AGENTS) {
76
+ // An absent cap means --parallel alone decides, so a queue of one backend can use
77
+ // every slot the user asked for.
78
+ caps.set(profile.name, Math.min(profile.maxParallel ?? parallel, parallel));
79
+ }
80
+ return caps;
81
+ }
82
+ export { asEventList } from "./types.js";
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/qa/agents/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type { Usage } from "../outcome.js";
2
+ /**
3
+ * Normalized harness event. Slot.applyEvent consumes only this; vendor JSON never
4
+ * reaches the TUI, reports, or tracker.
5
+ */
6
+ export type QaEvent = {
7
+ kind: "text";
8
+ text: string;
9
+ sessionId?: string;
10
+ } | {
11
+ kind: "thinking";
12
+ phase: "delta" | "completed";
13
+ text?: string;
14
+ sessionId?: string;
15
+ } | {
16
+ kind: "tool";
17
+ phase: "started" | "completed";
18
+ name: string;
19
+ summary: string;
20
+ failed?: boolean;
21
+ sessionId?: string;
22
+ } | {
23
+ kind: "usage";
24
+ usage: Usage;
25
+ error?: string;
26
+ sessionId?: string;
27
+ } | {
28
+ kind: "init";
29
+ model?: string;
30
+ sessionId?: string;
31
+ };
32
+ export type NormalizedEvents = QaEvent | QaEvent[] | null;
33
+ /** Enough of a case for argv construction without importing Case. */
34
+ export interface AgentArgvInput {
35
+ repo: string;
36
+ model: string;
37
+ prompt: string;
38
+ }
39
+ /**
40
+ * Per-backend profile. Registering another assistant is a new module plus a
41
+ * line in index.ts. Nothing outside src/qa/agents/ may branch on an agent's name.
42
+ */
43
+ export interface QaAgentProfile {
44
+ readonly name: string;
45
+ /** PATH candidates, first match wins. */
46
+ readonly binaries: readonly string[];
47
+ readonly defaultModel: string;
48
+ /**
49
+ * A real concurrency limit for this backend, or absent when the vendor imposes none we
50
+ * know of. Combined with --parallel by min(), so an absent cap means --parallel alone
51
+ * decides. Never set this to the *default* of --parallel: that silently caps the flag.
52
+ * Must be >= 1 when present.
53
+ */
54
+ readonly maxParallel?: number;
55
+ buildArgv(input: AgentArgvInput, binary: string): string[];
56
+ normalize(raw: Record<string, unknown>): NormalizedEvents;
57
+ }
58
+ export declare function asEventList(events: NormalizedEvents): QaEvent[];
59
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
60
+ export declare function sessionIdOf(raw: Record<string, unknown>): string | undefined;
61
+ export declare function num(value: unknown): number | undefined;
@@ -0,0 +1,18 @@
1
+ export function asEventList(events) {
2
+ if (events === null)
3
+ return [];
4
+ if (Array.isArray(events))
5
+ return [...events];
6
+ return [events];
7
+ }
8
+ export function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ export function sessionIdOf(raw) {
12
+ const value = raw["session_id"];
13
+ return value ? String(value) : undefined;
14
+ }
15
+ export function num(value) {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
17
+ }
18
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/qa/agents/types.ts"],"names":[],"mappings":"AAmEA,MAAM,UAAU,WAAW,CAAC,MAAwB;IAClD,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC;IAC9C,OAAO,CAAC,MAAM,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAA4B;IACtD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IAChC,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAc;IAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACjF,CAAC"}
@@ -0,0 +1,27 @@
1
+ /** A latch workers await while dispatch is paused. Opening it wakes everyone at once. */
2
+ export declare class Gate {
3
+ private opened;
4
+ private waiters;
5
+ constructor(opened?: boolean);
6
+ get isOpen(): boolean;
7
+ set(opened: boolean): void;
8
+ wait(): Promise<void>;
9
+ }
10
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
11
+ export declare function signalNumber(sig: NodeJS.Signals): number;
12
+ /**
13
+ * Actions that must run exactly once, on every exit path. LIFO, idempotent, synchronous only —
14
+ * a `process.on('exit')` handler cannot await. The terminal restore and the lock release both
15
+ * register here rather than installing competing handlers that fight over process.exit.
16
+ *
17
+ * Signals are deliberately NOT handled here: the harness maps them to requestStop() so a run
18
+ * unwinds gracefully, and the resulting normal exit fires this via 'exit'.
19
+ */
20
+ export declare class Cleanup {
21
+ private actions;
22
+ private ran;
23
+ private installed;
24
+ add(action: () => void): void;
25
+ run(): void;
26
+ install(onFatal: (err: unknown) => void): void;
27
+ }
@@ -0,0 +1,91 @@
1
+ import { constants as osConstants } from "node:os";
2
+ /** A latch workers await while dispatch is paused. Opening it wakes everyone at once. */
3
+ export class Gate {
4
+ opened;
5
+ waiters = [];
6
+ constructor(opened = true) {
7
+ this.opened = opened;
8
+ }
9
+ get isOpen() {
10
+ return this.opened;
11
+ }
12
+ set(opened) {
13
+ this.opened = opened;
14
+ if (!opened)
15
+ return;
16
+ const pending = this.waiters;
17
+ this.waiters = [];
18
+ for (const wake of pending)
19
+ wake();
20
+ }
21
+ wait() {
22
+ if (this.opened)
23
+ return Promise.resolve();
24
+ return new Promise((resolve) => {
25
+ this.waiters.push(resolve);
26
+ });
27
+ }
28
+ }
29
+ export function sleep(ms, signal) {
30
+ return new Promise((resolve) => {
31
+ if (signal?.aborted) {
32
+ resolve();
33
+ return;
34
+ }
35
+ const onAbort = () => {
36
+ clearTimeout(timer);
37
+ resolve();
38
+ };
39
+ const timer = setTimeout(() => {
40
+ signal?.removeEventListener("abort", onAbort);
41
+ resolve();
42
+ }, ms);
43
+ signal?.addEventListener("abort", onAbort, { once: true });
44
+ });
45
+ }
46
+ export function signalNumber(sig) {
47
+ return osConstants.signals[sig] ?? 0;
48
+ }
49
+ /**
50
+ * Actions that must run exactly once, on every exit path. LIFO, idempotent, synchronous only —
51
+ * a `process.on('exit')` handler cannot await. The terminal restore and the lock release both
52
+ * register here rather than installing competing handlers that fight over process.exit.
53
+ *
54
+ * Signals are deliberately NOT handled here: the harness maps them to requestStop() so a run
55
+ * unwinds gracefully, and the resulting normal exit fires this via 'exit'.
56
+ */
57
+ export class Cleanup {
58
+ actions = [];
59
+ ran = false;
60
+ installed = false;
61
+ add(action) {
62
+ this.actions.push(action);
63
+ }
64
+ run() {
65
+ if (this.ran)
66
+ return;
67
+ this.ran = true;
68
+ for (const action of [...this.actions].reverse()) {
69
+ try {
70
+ action();
71
+ }
72
+ catch {
73
+ /* a failing cleanup must not block the others */
74
+ }
75
+ }
76
+ }
77
+ install(onFatal) {
78
+ if (this.installed)
79
+ return;
80
+ this.installed = true;
81
+ process.on("exit", () => this.run());
82
+ const fatal = (err) => {
83
+ this.run();
84
+ onFatal(err);
85
+ process.exit(1);
86
+ };
87
+ process.on("uncaughtException", fatal);
88
+ process.on("unhandledRejection", fatal);
89
+ }
90
+ }
91
+ //# sourceMappingURL=async.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async.js","sourceRoot":"","sources":["../../src/qa/async.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /** One `_plans/tc-<N>.yaml`, validated. Everything the harness needs about a case. */
2
+ export interface CaseFile {
3
+ /** `tc-24` — the identity, derived from the filename and cross-checked against `id`. */
4
+ name: string;
5
+ number: number;
6
+ /** Absolute path to the YAML itself. */
7
+ file: string;
8
+ /** YAML `id`, as written (`TC-24`). */
9
+ id: string;
10
+ /** YAML `name` — the human title. */
11
+ title: string;
12
+ agent: string;
13
+ /** YAML `model`, or null when the case defers to `--model`. */
14
+ model: string | null;
15
+ parallel: boolean;
16
+ tags: string[];
17
+ /** The plan body, verbatim. */
18
+ plan: string;
19
+ }
20
+ /** `tc-24.yaml` -> `tc-24`, or null when the name is not a case file at all. */
21
+ export declare function caseNameOf(entry: string): string | null;
22
+ /**
23
+ * Validate one parsed YAML document against the schema. Every problem is reported, not just the
24
+ * first, so a single run tells the author everything wrong with the file.
25
+ */
26
+ export declare function validateCaseFile(doc: unknown, name: string, file: string, fail: (message: string) => void): CaseFile | null;
27
+ export interface LoadResult {
28
+ cases: CaseFile[];
29
+ errors: string[];
30
+ }
31
+ /**
32
+ * Every `tc-<N>.yaml` under `_plans/`, parsed and validated. Errors accumulate across files so one
33
+ * invocation reports every broken case rather than stopping at the first.
34
+ */
35
+ export declare function loadCaseFiles(plansDir: string): LoadResult;