pi-harness-runtime 0.9.14 → 0.9.16

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 (175) hide show
  1. package/harness/auto-quota-resume.ts +129 -0
  2. package/harness/e2e/minimax-quota-scraper.ts +10 -4
  3. package/harness/index.ts +8 -0
  4. package/harness/loop-runtime.ts +57 -0
  5. package/index.ts +47 -11
  6. package/mirror.ts +40 -18
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/README.md +48 -0
  12. package/packages/autonomous-runtime/dist/inbox.d.ts +75 -0
  13. package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -0
  14. package/packages/autonomous-runtime/dist/inbox.js +212 -0
  15. package/packages/autonomous-runtime/dist/inbox.js.map +1 -0
  16. package/packages/autonomous-runtime/dist/index.d.ts +17 -0
  17. package/packages/autonomous-runtime/dist/index.d.ts.map +1 -0
  18. package/packages/autonomous-runtime/dist/index.js +17 -0
  19. package/packages/autonomous-runtime/dist/index.js.map +1 -0
  20. package/packages/autonomous-runtime/dist/lease.d.ts +78 -0
  21. package/packages/autonomous-runtime/dist/lease.d.ts.map +1 -0
  22. package/packages/autonomous-runtime/dist/lease.js +273 -0
  23. package/packages/autonomous-runtime/dist/lease.js.map +1 -0
  24. package/packages/autonomous-runtime/dist/types.d.ts +160 -0
  25. package/packages/autonomous-runtime/dist/types.d.ts.map +1 -0
  26. package/packages/autonomous-runtime/dist/types.js +31 -0
  27. package/packages/autonomous-runtime/dist/types.js.map +1 -0
  28. package/packages/autonomous-runtime/dist/worker.d.ts +63 -0
  29. package/packages/autonomous-runtime/dist/worker.d.ts.map +1 -0
  30. package/packages/autonomous-runtime/dist/worker.js +133 -0
  31. package/packages/autonomous-runtime/dist/worker.js.map +1 -0
  32. package/packages/autonomous-runtime/package.json +34 -0
  33. package/packages/autonomous-runtime/src/inbox.ts +266 -0
  34. package/packages/autonomous-runtime/src/index.ts +22 -0
  35. package/packages/autonomous-runtime/src/lease.ts +318 -0
  36. package/packages/autonomous-runtime/src/types.ts +257 -0
  37. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +194 -0
  38. package/packages/autonomous-runtime/test/inbox.test.mjs +266 -0
  39. package/packages/autonomous-runtime/test/inbox.test.ts +80 -0
  40. package/packages/autonomous-runtime/test/types.test.ts +68 -0
  41. package/packages/autonomous-runtime/tsconfig.json +22 -0
  42. package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -0
  43. package/packages/cache-strategy/package.json +1 -1
  44. package/packages/capability-registry/package.json +1 -1
  45. package/packages/checkpoint/package.json +1 -1
  46. package/packages/cli-plugin-sdk/package.json +1 -1
  47. package/packages/code-generation/package.json +1 -1
  48. package/packages/code-review/package.json +1 -1
  49. package/packages/codex-adapter/package.json +1 -1
  50. package/packages/context-compiler/package.json +1 -1
  51. package/packages/context-manager/package.json +1 -1
  52. package/packages/cookie-sanitizer/package.json +1 -1
  53. package/packages/cost-optimizer/package.json +1 -1
  54. package/packages/django-plugin/package.json +1 -1
  55. package/packages/doc-generator/package.json +1 -1
  56. package/packages/evaluation-engine/package.json +1 -1
  57. package/packages/evaluation-runner/package.json +1 -1
  58. package/packages/experience-replay/package.json +1 -1
  59. package/packages/feedback-collector/package.json +1 -1
  60. package/packages/framework-detector/package.json +1 -1
  61. package/packages/framework-plugin-sdk/package.json +1 -1
  62. package/packages/frappe-plugin/package.json +1 -1
  63. package/packages/generic-web-plugin/package.json +1 -1
  64. package/packages/health-monitor/package.json +1 -1
  65. package/packages/laravel-plugin/package.json +1 -1
  66. package/packages/learning-engine/package.json +1 -1
  67. package/packages/mcp-adapter/package.json +1 -1
  68. package/packages/memory-engine/package.json +1 -1
  69. package/packages/model-registry/package.json +1 -1
  70. package/packages/nextjs-plugin/package.json +1 -1
  71. package/packages/notification/package.json +1 -1
  72. package/packages/observability/package.json +1 -1
  73. package/packages/performance-optimizer/package.json +1 -1
  74. package/packages/privilege-broker/README.md +45 -0
  75. package/packages/privilege-broker/config/privileges.yaml.example +172 -0
  76. package/packages/privilege-broker/dist/approval.d.ts +23 -0
  77. package/packages/privilege-broker/dist/approval.d.ts.map +1 -0
  78. package/packages/privilege-broker/dist/approval.js +45 -0
  79. package/packages/privilege-broker/dist/approval.js.map +1 -0
  80. package/packages/privilege-broker/dist/audit.d.ts +38 -0
  81. package/packages/privilege-broker/dist/audit.d.ts.map +1 -0
  82. package/packages/privilege-broker/dist/audit.js +125 -0
  83. package/packages/privilege-broker/dist/audit.js.map +1 -0
  84. package/packages/privilege-broker/dist/executor.d.ts +25 -0
  85. package/packages/privilege-broker/dist/executor.d.ts.map +1 -0
  86. package/packages/privilege-broker/dist/executor.js +83 -0
  87. package/packages/privilege-broker/dist/executor.js.map +1 -0
  88. package/packages/privilege-broker/dist/index.d.ts +30 -0
  89. package/packages/privilege-broker/dist/index.d.ts.map +1 -0
  90. package/packages/privilege-broker/dist/index.js +5 -0
  91. package/packages/privilege-broker/dist/index.js.map +1 -0
  92. package/packages/privilege-broker/dist/registry.d.ts +15 -0
  93. package/packages/privilege-broker/dist/registry.d.ts.map +1 -0
  94. package/packages/privilege-broker/dist/registry.js +76 -0
  95. package/packages/privilege-broker/dist/registry.js.map +1 -0
  96. package/packages/privilege-broker/dist/types.d.ts +77 -0
  97. package/packages/privilege-broker/dist/types.d.ts.map +1 -0
  98. package/packages/privilege-broker/dist/types.js +11 -0
  99. package/packages/privilege-broker/dist/types.js.map +1 -0
  100. package/packages/privilege-broker/package.json +35 -0
  101. package/packages/privilege-broker/src/approval.ts +70 -0
  102. package/packages/privilege-broker/src/audit.ts +142 -0
  103. package/packages/privilege-broker/src/executor.ts +110 -0
  104. package/packages/privilege-broker/src/index.ts +46 -0
  105. package/packages/privilege-broker/src/registry.ts +98 -0
  106. package/packages/privilege-broker/src/types.ts +105 -0
  107. package/packages/privilege-broker/tsconfig.json +22 -0
  108. package/packages/privilege-broker/tsconfig.tsbuildinfo +1 -0
  109. package/packages/project-analyzer/package.json +1 -1
  110. package/packages/prompt-compiler/package.json +1 -1
  111. package/packages/prompt-versioning/package.json +1 -1
  112. package/packages/provider-adapter-sdk/package.json +1 -1
  113. package/packages/provider-router/package.json +1 -1
  114. package/packages/provider-selector/package.json +1 -1
  115. package/packages/providers/dist/provider-id.d.ts +2 -0
  116. package/packages/providers/dist/provider-id.d.ts.map +1 -1
  117. package/packages/providers/dist/provider-id.js +9 -0
  118. package/packages/providers/dist/provider-id.js.map +1 -1
  119. package/packages/providers/package.json +1 -1
  120. package/packages/providers/src/provider-id.ts +12 -0
  121. package/packages/quota-manager/package.json +1 -1
  122. package/packages/rate-limiter/package.json +1 -1
  123. package/packages/react-vite-plugin/package.json +1 -1
  124. package/packages/release-manager/package.json +1 -1
  125. package/packages/requirement-compiler/package.json +1 -1
  126. package/packages/runtime/package.json +1 -1
  127. package/packages/scheduler/package.json +1 -1
  128. package/packages/scheduler-adapter/README.md +36 -0
  129. package/packages/scheduler-adapter/dist/cron.d.ts +21 -0
  130. package/packages/scheduler-adapter/dist/cron.d.ts.map +1 -0
  131. package/packages/scheduler-adapter/dist/cron.js +116 -0
  132. package/packages/scheduler-adapter/dist/cron.js.map +1 -0
  133. package/packages/scheduler-adapter/dist/index.d.ts +9 -0
  134. package/packages/scheduler-adapter/dist/index.d.ts.map +1 -0
  135. package/packages/scheduler-adapter/dist/index.js +8 -0
  136. package/packages/scheduler-adapter/dist/index.js.map +1 -0
  137. package/packages/scheduler-adapter/dist/interface.d.ts +61 -0
  138. package/packages/scheduler-adapter/dist/interface.d.ts.map +1 -0
  139. package/packages/scheduler-adapter/dist/interface.js +2 -0
  140. package/packages/scheduler-adapter/dist/interface.js.map +1 -0
  141. package/packages/scheduler-adapter/dist/internal.d.ts +30 -0
  142. package/packages/scheduler-adapter/dist/internal.d.ts.map +1 -0
  143. package/packages/scheduler-adapter/dist/internal.js +82 -0
  144. package/packages/scheduler-adapter/dist/internal.js.map +1 -0
  145. package/packages/scheduler-adapter/dist/systemd.d.ts +31 -0
  146. package/packages/scheduler-adapter/dist/systemd.d.ts.map +1 -0
  147. package/packages/scheduler-adapter/dist/systemd.js +203 -0
  148. package/packages/scheduler-adapter/dist/systemd.js.map +1 -0
  149. package/packages/scheduler-adapter/package.json +32 -0
  150. package/packages/scheduler-adapter/src/cron.ts +123 -0
  151. package/packages/scheduler-adapter/src/index.ts +9 -0
  152. package/packages/scheduler-adapter/src/interface.ts +61 -0
  153. package/packages/scheduler-adapter/src/internal.ts +99 -0
  154. package/packages/scheduler-adapter/src/systemd.ts +221 -0
  155. package/packages/scheduler-adapter/tsconfig.json +22 -0
  156. package/packages/scheduler-adapter/tsconfig.tsbuildinfo +1 -0
  157. package/packages/session/package.json +1 -1
  158. package/packages/shared-context/package.json +1 -1
  159. package/packages/skill-registry/package.json +1 -1
  160. package/packages/sprint-planner/package.json +1 -1
  161. package/packages/task-compiler/package.json +1 -1
  162. package/packages/test-data-generator/package.json +1 -1
  163. package/packages/test-generator/package.json +1 -1
  164. package/packages/token-estimation/package.json +1 -1
  165. package/packages/token-optimizer/package.json +1 -1
  166. package/packages/tui/package.json +1 -1
  167. package/packages/types/dist/runtime-types.d.ts +6 -0
  168. package/packages/types/dist/runtime-types.d.ts.map +1 -1
  169. package/packages/types/dist/runtime-types.js +2 -1
  170. package/packages/types/dist/runtime-types.js.map +1 -1
  171. package/packages/types/package.json +1 -1
  172. package/packages/types/src/ai-providers.ts +73 -0
  173. package/packages/types/src/runtime-types.ts +15 -0
  174. package/packages/types/tsconfig.tsbuildinfo +1 -1
  175. package/packages/worktree/package.json +1 -1
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Autonomous Operations Runtime — TypeScript Contracts
3
+ * Based on RFC-0101 §14
4
+ *
5
+ * Consumers: LoopRuntime, privilege-broker, scheduler-adapter, notification-runtime.
6
+ * These types are the canonical contracts. Do not re-define them in other packages.
7
+ */
8
+ import { env } from "node:process";
9
+
10
+ // ─── Capability model ──────────────────────────────────────────────────────────
11
+
12
+ /** A named capability that the privilege broker may grant. */
13
+ export type CapabilityName =
14
+ | "files.read"
15
+ | "files.write"
16
+ | "files.exec"
17
+ | "git.commit"
18
+ | "git.push"
19
+ | "git.pull"
20
+ | "system.restart"
21
+ | "system.restart-service"
22
+ | "system.logs"
23
+ | "bench.migrate"
24
+ | "bench.build"
25
+ | "bench.restart"
26
+ | "okf.promote-pattern"
27
+ | "okf.promote-lesson"
28
+ | "notify.send"
29
+ | "llm.invoke";
30
+
31
+ /** What approval class a capability requires. */
32
+ export type ApprovalClass =
33
+ | "automatic_read_only" // safe, no side effects
34
+ | "automatic_reversible" // side effects but trivially reversible
35
+ | "human_approval_required" // must ask operator
36
+ | "forbidden"; // never grant, regardless of override
37
+
38
+ // ─── Task lifecycle ───────────────────────────────────────────────────────────
39
+
40
+ export type TaskStatus =
41
+ | "queued"
42
+ | "claimed"
43
+ | "running"
44
+ | "waiting_approval"
45
+ | "waiting_quota"
46
+ | "retrying"
47
+ | "completed"
48
+ | "failed"
49
+ | "dead_letter";
50
+
51
+ /** Immutable log of what happened to a task. */
52
+ export type TaskEvent = {
53
+ ts: string; // ISO-8601
54
+ kind:
55
+ | "claimed"
56
+ | "started"
57
+ | "checkpointed"
58
+ | "progress"
59
+ | "blocked"
60
+ | "approval_requested"
61
+ | "approved"
62
+ | "denied"
63
+ | "failed"
64
+ | "completed"
65
+ | "recovered"
66
+ | "dead_lettered"
67
+ | "transitioned"; // generic status change
68
+ payload?: Record<string, unknown>;
69
+ };
70
+
71
+ // ─── Task record ─────────────────────────────────────────────────────────────
72
+
73
+ /** The primary durable record for a task. */
74
+ export interface TaskRecord {
75
+ id: string; // e.g. "task-2026-07-23-001"
76
+ objective: string; // natural-language goal
77
+ acceptanceCriteria: string[]; // verifiable outcomes
78
+ source:
79
+ | { kind: "chat"; userId: string }
80
+ | { kind: "schedule"; scheduleId: string }
81
+ | { kind: "webhook"; url: string }
82
+ | { kind: "subagent"; parentTaskId: string }
83
+ | { kind: "manual"; createdBy: string };
84
+ priority: 0 | 1 | 2 | 3 | 4; // P0..P4 (RFC-0015 convention)
85
+ capabilities: CapabilityName[]; // capabilities this task may need
86
+ approvalClass: ApprovalClass; // initial class (may escalate)
87
+ status: TaskStatus;
88
+ attempts: number;
89
+ maxAttempts: number; // default 3
90
+ context?: ExecutionContext; // planner-provided inputs
91
+ createdAt: string; // ISO-8601
92
+ updatedAt: string; // ISO-8601
93
+ leaseId?: string;
94
+ result?: TaskResult;
95
+ failureReason?: string;
96
+ history: TaskEvent[];
97
+ }
98
+
99
+ // ─── Leasing ──────────────────────────────────────────────────────────────────
100
+
101
+ /** A lease held by a worker on a specific task. */
102
+ export interface TaskLease {
103
+ taskId: string;
104
+ workerId: string;
105
+ acquiredAt: string; // ISO-8601
106
+ expiresAt: string; // ISO-8601
107
+ heartbeatAt: string; // ISO-8601
108
+ attempt: number; // which attempt this lease covers
109
+ }
110
+
111
+ // ─── Worker registry ──────────────────────────────────────────────────────────
112
+
113
+ /** Heartbeat emitted by a worker every 5 s to worker-registry.json. */
114
+ export interface WorkerHeartbeat {
115
+ workerId: string;
116
+ startedAt: string;
117
+ lastBeatAt: string;
118
+ capacity: number; // max concurrent tasks
119
+ inflightTaskIds: string[];
120
+ }
121
+
122
+ // ─── Privilege broker ─────────────────────────────────────────────────────────
123
+
124
+ /** A resolved grant from the privilege broker. */
125
+ export interface CapabilityGrant {
126
+ name: CapabilityName;
127
+ argv: string[]; // exact, no shell
128
+ envWhitelist?: string[]; // env vars that may be passed
129
+ user: string; // service account to run as
130
+ cwd?: string; // working directory
131
+ timeoutMs: number;
132
+ }
133
+
134
+ // ─── Approval ─────────────────────────────────────────────────────────────────
135
+
136
+ /** An in-flight human approval request. */
137
+ export interface ApprovalRequest {
138
+ id: string;
139
+ taskId: string;
140
+ capability: CapabilityName;
141
+ rationale: string; // human-readable description
142
+ signedBy?: { keyFingerprint: string; ts: string };
143
+ decision?: "approved" | "denied";
144
+ decidedAt?: string;
145
+ expiresAt: string; // ISO-8601 — timeout threshold
146
+ }
147
+
148
+ // ─── Scheduler ───────────────────────────────────────────────────────────────
149
+
150
+ /** A scheduled recurring task. */
151
+ export interface ScheduledTask {
152
+ id: string;
153
+ taskTemplate: Omit<
154
+ TaskRecord,
155
+ "id" | "status" | "attempts" | "createdAt" | "updatedAt" | "history"
156
+ >;
157
+ schedule: ScheduleSpec;
158
+ enabled: boolean;
159
+ lastFiredAt?: string;
160
+ nextFireAt?: string;
161
+ }
162
+
163
+ export type ScheduleSpec =
164
+ | { kind: "cron"; expression: string }
165
+ | { kind: "interval"; intervalMs: number }
166
+ | { kind: "once"; at: string };
167
+
168
+ // ─── Notification ─────────────────────────────────────────────────────────────
169
+
170
+ export type RuntimeNotificationEvent =
171
+ | "task.claimed"
172
+ | "task.started"
173
+ | "task.completed"
174
+ | "task.failed"
175
+ | "task.waiting_approval"
176
+ | "task.dead_lettered"
177
+ | "lease.expired"
178
+ | "worker.started"
179
+ | "worker.stopped"
180
+ | "approval.approved"
181
+ | "approval.denied"
182
+ | "approval.expired";
183
+
184
+ export interface NotificationEvent {
185
+ kind: RuntimeNotificationEvent;
186
+ ts: string;
187
+ taskId?: string;
188
+ workerId?: string;
189
+ payload: Record<string, unknown>;
190
+ }
191
+
192
+ // ─── Checkpoint ───────────────────────────────────────────────────────────────
193
+
194
+ export interface Checkpoint {
195
+ taskId: string;
196
+ attempt: number;
197
+ state: Record<string, unknown>;
198
+ savedAt: string; // ISO-8601
199
+ }
200
+
201
+ // ─── Execution context ────────────────────────────────────────────────────────
202
+
203
+ /** The context given to the loop runtime when executing a task. */
204
+ export interface ExecutionContext {
205
+ taskId: string;
206
+ workerId: string;
207
+ inputs: Record<string, unknown>;
208
+ capabilitiesGranted: CapabilityName[];
209
+ approvalClass: ApprovalClass;
210
+ lease: TaskLease;
211
+ checkpoint?: Checkpoint;
212
+ okfBundles: string[]; // content hashes available to planner
213
+ }
214
+
215
+ // ─── Task result ─────────────────────────────────────────────────────────────
216
+
217
+ export interface TaskResult {
218
+ taskId: string;
219
+ status: "completed" | "failed" | "dead_letter";
220
+ deliverables?: { path: string; mime: string }[];
221
+ acceptanceCriteriaMet: {
222
+ criterion: string;
223
+ passed: boolean;
224
+ evidence?: string;
225
+ }[];
226
+ durationMs: number;
227
+ modelUsage?: { inputTokens: number; outputTokens: number; provider: string };
228
+ }
229
+
230
+ // ─── Storage paths ────────────────────────────────────────────────────────────
231
+
232
+ const DEFAULT_ROOT = `${env["HOME"] ?? "."}/.pi/harness`;
233
+
234
+ /** Returns the standard storage root for the runtime. */
235
+ export function getRuntimeRoot(): string {
236
+ return env["PI_HARNESS_RUNTIME_ROOT"] ?? DEFAULT_ROOT;
237
+ }
238
+
239
+ /** Returns the inbox directory path. */
240
+ export function getInboxDir(): string {
241
+ return `${getRuntimeRoot()}/inbox`;
242
+ }
243
+
244
+ /** Returns the claimed leases directory path. */
245
+ export function getLeasesDir(): string {
246
+ return `${getInboxDir()}/claimed`;
247
+ }
248
+
249
+ /** Returns the task JSONL file path. */
250
+ export function getTasksPath(): string {
251
+ return `${getInboxDir()}/tasks.jsonl`;
252
+ }
253
+
254
+ /** Returns the worker registry path. */
255
+ export function getWorkerRegistryPath(): string {
256
+ return `${getRuntimeRoot()}/worker-registry.json`;
257
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tests for autonomous-runtime (RFC-0101 Phase 1)
3
+ *
4
+ * Covers: TaskRecord, TaskStatus, TaskLease, TaskInbox, LeaseManager
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
8
+ import { mkdtempSync, rmSync, existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+
12
+ import type { TaskRecord, TaskLease } from "../src/types.js";
13
+ import { TaskInbox, LeaseManager } from "../src/index.js";
14
+
15
+ const TEST_DIR = mkdtempSync(join(tmpdir(), "pi-autonomous-"));
16
+ const INBOX_DIR = join(TEST_DIR, "inbox");
17
+ const LEASE_DIR = join(TEST_DIR, "leases");
18
+
19
+ afterEach(() => {
20
+ try {
21
+ rmSync(TEST_DIR, { recursive: true, force: true });
22
+ } catch {
23
+ // ignore
24
+ }
25
+ });
26
+
27
+ describe("TaskRecord types", () => {
28
+ it("accepts valid task shape", () => {
29
+ const task: TaskRecord = {
30
+ id: "task-1",
31
+ status: "pending",
32
+ title: "Test task",
33
+ created_at: new Date().toISOString(),
34
+ updated_at: new Date().toISOString(),
35
+ attempts: 0,
36
+ };
37
+ expect(task.id).toBe("task-1");
38
+ expect(task.status).toBe("pending");
39
+ });
40
+ });
41
+
42
+ describe("LeaseManager", () => {
43
+ it("grants exclusive lease to first worker", async () => {
44
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
45
+ const lease = await mgr.acquire("task-1", "worker-1");
46
+ expect(lease).not.toBeNull();
47
+ expect(lease!.worker_id).toBe("worker-1");
48
+ expect(lease!.task_id).toBe("task-1");
49
+ expect(lease!.acquired_at).toBeTruthy();
50
+ });
51
+
52
+ it("denies lease to second worker", async () => {
53
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
54
+ await mgr.acquire("task-1", "worker-1");
55
+ const second = await mgr.acquire("task-1", "worker-2");
56
+ expect(second).toBeNull();
57
+ });
58
+
59
+ it("releases and allows re-acquisition", async () => {
60
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
61
+ await mgr.acquire("task-1", "worker-1");
62
+ await mgr.release("task-1", "worker-1");
63
+ const next = await mgr.acquire("task-1", "worker-2");
64
+ expect(next).not.toBeNull();
65
+ expect(next!.worker_id).toBe("worker-2");
66
+ });
67
+
68
+ it("only owner can release", async () => {
69
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
70
+ await mgr.acquire("task-1", "worker-1");
71
+ const released = await mgr.release("task-1", "worker-2");
72
+ expect(released).toBe(false);
73
+ });
74
+
75
+ it("renewLease extends expiry if owner", async () => {
76
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
77
+ await mgr.acquire("task-1", "worker-1");
78
+ const renewed = await mgr.renewLease("task-1", "worker-1");
79
+ expect(renewed).toBe(true);
80
+ });
81
+
82
+ it("renewLease fails for non-owner", async () => {
83
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
84
+ await mgr.acquire("task-1", "worker-1");
85
+ const renewed = await mgr.renewLease("task-1", "worker-2");
86
+ expect(renewed).toBe(false);
87
+ });
88
+
89
+ it("getLease returns current lease", async () => {
90
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR });
91
+ const lease = await mgr.acquire("task-1", "worker-1");
92
+ const found = await mgr.getLease("task-1");
93
+ expect(found).toEqual(lease);
94
+ });
95
+
96
+ it("expired lease allows re-acquisition", async () => {
97
+ const mgr = new LeaseManager({ leases_dir: LEASE_DIR, default_ttl_ms: 1 });
98
+ await mgr.acquire("task-1", "worker-1");
99
+ // Wait for expiry
100
+ await new Promise((r) => setTimeout(r, 10));
101
+ const next = await mgr.acquire("task-1", "worker-2");
102
+ expect(next).not.toBeNull();
103
+ expect(next!.worker_id).toBe("worker-2");
104
+ });
105
+ });
106
+
107
+ describe("TaskInbox", () => {
108
+ it("accepts and returns pending tasks", async () => {
109
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
110
+ await inbox.submit({
111
+ title: "Do the thing",
112
+ description: "A test task",
113
+ capability: "read",
114
+ });
115
+ const tasks = await inbox.getPending();
116
+ expect(tasks.length).toBe(1);
117
+ expect(tasks[0].status).toBe("pending");
118
+ });
119
+
120
+ it("assigns sequential integer IDs", async () => {
121
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
122
+ await inbox.submit({ title: "Task 1", capability: "read" });
123
+ await inbox.submit({ title: "Task 2", capability: "read" });
124
+ const tasks = await inbox.getPending();
125
+ expect(tasks.length).toBe(2);
126
+ expect(tasks[0].id).toMatch(/^\d+$/);
127
+ expect(tasks[1].id).toMatch(/^\d+$/);
128
+ expect(Number(tasks[0].id)).toBeLessThan(Number(tasks[1].id));
129
+ });
130
+
131
+ it("transitions pending → running → completed", async () => {
132
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
133
+ await inbox.submit({ title: "Test", capability: "read" });
134
+
135
+ const task = (await inbox.getPending())[0];
136
+ const started = await inbox.start(task.id);
137
+ expect(started.status).toBe("running");
138
+
139
+ const completed = await inbox.complete(task.id, { success: true });
140
+ expect(completed.status).toBe("completed");
141
+ });
142
+
143
+ it("fails a running task", async () => {
144
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
145
+ await inbox.submit({ title: "Fail task", capability: "read" });
146
+ const task = (await inbox.getPending())[0];
147
+ await inbox.start(task.id);
148
+ const failed = await inbox.fail(task.id, "Intentional test failure");
149
+ expect(failed.status).toBe("failed");
150
+ expect(failed.error).toBe("Intentional test failure");
151
+ });
152
+
153
+ it("retries a failed task if max_attempts not exceeded", async () => {
154
+ const inbox = new TaskInbox({
155
+ inbox_dir: INBOX_DIR,
156
+ options: { max_attempts: 2 },
157
+ });
158
+ await inbox.submit({ title: "Retry task", capability: "read" });
159
+ const task = (await inbox.getPending())[0];
160
+ await inbox.start(task.id);
161
+ await inbox.fail(task.id, "First attempt failed");
162
+ const retried = (await inbox.getPending()).find((t) => t.id === task.id);
163
+ expect(retried).not.toBeUndefined();
164
+ expect(retried!.attempts).toBe(1);
165
+ });
166
+
167
+ it("does not retry if max_attempts exceeded", async () => {
168
+ const inbox = new TaskInbox({
169
+ inbox_dir: INBOX_DIR,
170
+ options: { max_attempts: 1 },
171
+ });
172
+ await inbox.submit({ title: "No retry task", capability: "read" });
173
+ const task = (await inbox.getPending())[0];
174
+ await inbox.start(task.id);
175
+ await inbox.fail(task.id, "Final failure");
176
+ const retried = await inbox.getTask(task.id);
177
+ expect(retried?.status).toBe("failed");
178
+ expect(retried?.attempts).toBe(1);
179
+ });
180
+
181
+ it("getTask returns specific task by id", async () => {
182
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
183
+ await inbox.submit({ title: "Specific task", capability: "read" });
184
+ const [task] = await inbox.getPending();
185
+ const found = await inbox.getTask(task.id);
186
+ expect(found?.id).toBe(task.id);
187
+ });
188
+
189
+ it("returns null for non-existent task", async () => {
190
+ const inbox = new TaskInbox({ inbox_dir: INBOX_DIR });
191
+ const found = await inbox.getTask("nonexistent");
192
+ expect(found).toBeNull();
193
+ });
194
+ });