pi-harness-runtime 0.9.15 → 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 (101) hide show
  1. package/package.json +1 -1
  2. package/packages/autonomous-runtime/README.md +48 -0
  3. package/packages/autonomous-runtime/dist/inbox.d.ts +75 -0
  4. package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -0
  5. package/packages/autonomous-runtime/dist/inbox.js +212 -0
  6. package/packages/autonomous-runtime/dist/inbox.js.map +1 -0
  7. package/packages/autonomous-runtime/dist/index.d.ts +17 -0
  8. package/packages/autonomous-runtime/dist/index.d.ts.map +1 -0
  9. package/packages/autonomous-runtime/dist/index.js +17 -0
  10. package/packages/autonomous-runtime/dist/index.js.map +1 -0
  11. package/packages/autonomous-runtime/dist/lease.d.ts +78 -0
  12. package/packages/autonomous-runtime/dist/lease.d.ts.map +1 -0
  13. package/packages/autonomous-runtime/dist/lease.js +273 -0
  14. package/packages/autonomous-runtime/dist/lease.js.map +1 -0
  15. package/packages/autonomous-runtime/dist/types.d.ts +160 -0
  16. package/packages/autonomous-runtime/dist/types.d.ts.map +1 -0
  17. package/packages/autonomous-runtime/dist/types.js +31 -0
  18. package/packages/autonomous-runtime/dist/types.js.map +1 -0
  19. package/packages/autonomous-runtime/dist/worker.d.ts +63 -0
  20. package/packages/autonomous-runtime/dist/worker.d.ts.map +1 -0
  21. package/packages/autonomous-runtime/dist/worker.js +133 -0
  22. package/packages/autonomous-runtime/dist/worker.js.map +1 -0
  23. package/packages/autonomous-runtime/package.json +34 -0
  24. package/packages/autonomous-runtime/src/inbox.ts +266 -0
  25. package/packages/autonomous-runtime/src/index.ts +22 -0
  26. package/packages/autonomous-runtime/src/lease.ts +318 -0
  27. package/packages/autonomous-runtime/src/types.ts +257 -0
  28. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +194 -0
  29. package/packages/autonomous-runtime/test/inbox.test.mjs +266 -0
  30. package/packages/autonomous-runtime/test/inbox.test.ts +80 -0
  31. package/packages/autonomous-runtime/test/types.test.ts +68 -0
  32. package/packages/autonomous-runtime/tsconfig.json +22 -0
  33. package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -0
  34. package/packages/privilege-broker/README.md +45 -0
  35. package/packages/privilege-broker/config/privileges.yaml.example +172 -0
  36. package/packages/privilege-broker/dist/approval.d.ts +23 -0
  37. package/packages/privilege-broker/dist/approval.d.ts.map +1 -0
  38. package/packages/privilege-broker/dist/approval.js +45 -0
  39. package/packages/privilege-broker/dist/approval.js.map +1 -0
  40. package/packages/privilege-broker/dist/audit.d.ts +38 -0
  41. package/packages/privilege-broker/dist/audit.d.ts.map +1 -0
  42. package/packages/privilege-broker/dist/audit.js +125 -0
  43. package/packages/privilege-broker/dist/audit.js.map +1 -0
  44. package/packages/privilege-broker/dist/executor.d.ts +25 -0
  45. package/packages/privilege-broker/dist/executor.d.ts.map +1 -0
  46. package/packages/privilege-broker/dist/executor.js +83 -0
  47. package/packages/privilege-broker/dist/executor.js.map +1 -0
  48. package/packages/privilege-broker/dist/index.d.ts +30 -0
  49. package/packages/privilege-broker/dist/index.d.ts.map +1 -0
  50. package/packages/privilege-broker/dist/index.js +5 -0
  51. package/packages/privilege-broker/dist/index.js.map +1 -0
  52. package/packages/privilege-broker/dist/registry.d.ts +15 -0
  53. package/packages/privilege-broker/dist/registry.d.ts.map +1 -0
  54. package/packages/privilege-broker/dist/registry.js +76 -0
  55. package/packages/privilege-broker/dist/registry.js.map +1 -0
  56. package/packages/privilege-broker/dist/types.d.ts +77 -0
  57. package/packages/privilege-broker/dist/types.d.ts.map +1 -0
  58. package/packages/privilege-broker/dist/types.js +11 -0
  59. package/packages/privilege-broker/dist/types.js.map +1 -0
  60. package/packages/privilege-broker/package.json +35 -0
  61. package/packages/privilege-broker/src/approval.ts +70 -0
  62. package/packages/privilege-broker/src/audit.ts +142 -0
  63. package/packages/privilege-broker/src/executor.ts +110 -0
  64. package/packages/privilege-broker/src/index.ts +46 -0
  65. package/packages/privilege-broker/src/registry.ts +98 -0
  66. package/packages/privilege-broker/src/types.ts +105 -0
  67. package/packages/privilege-broker/tsconfig.json +22 -0
  68. package/packages/privilege-broker/tsconfig.tsbuildinfo +1 -0
  69. package/packages/scheduler-adapter/README.md +36 -0
  70. package/packages/scheduler-adapter/dist/cron.d.ts +21 -0
  71. package/packages/scheduler-adapter/dist/cron.d.ts.map +1 -0
  72. package/packages/scheduler-adapter/dist/cron.js +116 -0
  73. package/packages/scheduler-adapter/dist/cron.js.map +1 -0
  74. package/packages/scheduler-adapter/dist/index.d.ts +9 -0
  75. package/packages/scheduler-adapter/dist/index.d.ts.map +1 -0
  76. package/packages/scheduler-adapter/dist/index.js +8 -0
  77. package/packages/scheduler-adapter/dist/index.js.map +1 -0
  78. package/packages/scheduler-adapter/dist/interface.d.ts +61 -0
  79. package/packages/scheduler-adapter/dist/interface.d.ts.map +1 -0
  80. package/packages/scheduler-adapter/dist/interface.js +2 -0
  81. package/packages/scheduler-adapter/dist/interface.js.map +1 -0
  82. package/packages/scheduler-adapter/dist/internal.d.ts +30 -0
  83. package/packages/scheduler-adapter/dist/internal.d.ts.map +1 -0
  84. package/packages/scheduler-adapter/dist/internal.js +82 -0
  85. package/packages/scheduler-adapter/dist/internal.js.map +1 -0
  86. package/packages/scheduler-adapter/dist/systemd.d.ts +31 -0
  87. package/packages/scheduler-adapter/dist/systemd.d.ts.map +1 -0
  88. package/packages/scheduler-adapter/dist/systemd.js +203 -0
  89. package/packages/scheduler-adapter/dist/systemd.js.map +1 -0
  90. package/packages/scheduler-adapter/package.json +32 -0
  91. package/packages/scheduler-adapter/src/cron.ts +123 -0
  92. package/packages/scheduler-adapter/src/index.ts +9 -0
  93. package/packages/scheduler-adapter/src/interface.ts +61 -0
  94. package/packages/scheduler-adapter/src/internal.ts +99 -0
  95. package/packages/scheduler-adapter/src/systemd.ts +221 -0
  96. package/packages/scheduler-adapter/tsconfig.json +22 -0
  97. package/packages/scheduler-adapter/tsconfig.tsbuildinfo +1 -0
  98. package/packages/types/dist/ai-providers.d.ts +0 -36
  99. package/packages/types/dist/ai-providers.d.ts.map +0 -1
  100. package/packages/types/dist/ai-providers.js +0 -66
  101. package/packages/types/dist/ai-providers.js.map +0 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Worker Stub — RFC-0101 §4
3
+ *
4
+ * A worker is a pi session that owns tasks from the inbox.
5
+ * This is the lightweight worker-side of the lease protocol.
6
+ *
7
+ * The worker picks up work via the inbox's `claim()` method
8
+ * and reports results via `complete()`.
9
+ */
10
+ /**
11
+ * Worker — picks up tasks from the inbox and processes them.
12
+ *
13
+ * Lifecycle:
14
+ * 1. Construct with inbox + workerId
15
+ * 2. `start()` — begins polling/claiming tasks
16
+ * 3. `stop()` — stops claiming, expires held leases
17
+ *
18
+ * The worker does NOT run tasks itself — it delegates to the harness
19
+ * loop runtime. It just manages the lease lifecycle.
20
+ */
21
+ export class Worker {
22
+ config;
23
+ events = {};
24
+ running = false;
25
+ heartbeatTimer = null;
26
+ constructor(config) {
27
+ this.config = {
28
+ heartbeatIntervalMs: 30_000,
29
+ ...config,
30
+ };
31
+ }
32
+ on(event, handler) {
33
+ this.events[event] = handler;
34
+ }
35
+ /**
36
+ * Start the worker — begins polling for available tasks.
37
+ */
38
+ async start() {
39
+ if (this.running)
40
+ return;
41
+ this.running = true;
42
+ // Start heartbeat
43
+ this.heartbeatTimer = setInterval(() => this._heartbeat(), this.config.heartbeatIntervalMs);
44
+ // Claim any immediately available tasks
45
+ await this._poll();
46
+ }
47
+ /**
48
+ * Stop the worker — stops polling and expires any held leases.
49
+ */
50
+ async stop() {
51
+ if (!this.running)
52
+ return;
53
+ this.running = false;
54
+ if (this.heartbeatTimer) {
55
+ clearInterval(this.heartbeatTimer);
56
+ this.heartbeatTimer = null;
57
+ }
58
+ // Expire all leases held by this worker
59
+ // (inbox.expireLease is idempotent — safe to call repeatedly)
60
+ }
61
+ /**
62
+ * Status of this worker.
63
+ */
64
+ status() {
65
+ return {
66
+ workerId: this.config.workerId,
67
+ running: this.running,
68
+ heartbeatIntervalMs: this.config.heartbeatIntervalMs ?? 30_000,
69
+ };
70
+ }
71
+ async _poll() {
72
+ if (!this.running)
73
+ return;
74
+ try {
75
+ // Try to claim one task
76
+ const lease = await this.config.inbox.claim({
77
+ workerId: this.config.workerId,
78
+ heartbeatIntervalMs: this.config.heartbeatIntervalMs ?? 30_000,
79
+ });
80
+ if (lease) {
81
+ this.events.onLease?.(lease);
82
+ await this._processTask(lease);
83
+ }
84
+ }
85
+ catch (err) {
86
+ this.config.onError?.("__poll__", err);
87
+ }
88
+ }
89
+ async _processTask(lease) {
90
+ const task = lease.task;
91
+ try {
92
+ // Delegate to harness loop runtime (stub — will be wired in Phase 2)
93
+ const result = await this._runTask(task);
94
+ // Report completion
95
+ await this.config.inbox.complete(lease.taskId, result);
96
+ this.events.onComplete?.(lease.taskId, result);
97
+ }
98
+ catch (err) {
99
+ this.events.onError?.(lease.taskId, err);
100
+ await this.config.inbox.complete(lease.taskId, {
101
+ ok: false,
102
+ error: String(err),
103
+ completedAt: new Date().toISOString(),
104
+ });
105
+ }
106
+ // Continue polling for more work
107
+ if (this.running)
108
+ void this._poll();
109
+ }
110
+ /**
111
+ * Run a single task. Stub — replaces with actual harness loop integration.
112
+ */
113
+ async _runTask(task) {
114
+ // TODO(Phase 2): Wire to harness loop runtime
115
+ // For now, return a stub result so the lease protocol works end-to-end.
116
+ return {
117
+ ok: true,
118
+ summary: `[stub] Processed ${task.id}`,
119
+ completedAt: new Date().toISOString(),
120
+ };
121
+ }
122
+ async _heartbeat() {
123
+ // Refresh leases held by this worker
124
+ // The inbox tracks which leases this worker holds and refreshes them.
125
+ try {
126
+ await this.config.inbox.refreshLeases(this.config.workerId);
127
+ }
128
+ catch {
129
+ // Non-fatal — lease will expire naturally
130
+ }
131
+ }
132
+ }
133
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH;;;;;;;;;;GAUG;AACH,MAAM,OAAO,MAAM;IACD,MAAM,CAAe;IACrB,MAAM,GAA0B,EAAE,CAAC;IAC5C,OAAO,GAAG,KAAK,CAAC;IAChB,cAAc,GAA0C,IAAI,CAAC;IAErE,YAAY,MAAoB;QAC/B,IAAI,CAAC,MAAM,GAAG;YACb,mBAAmB,EAAE,MAAM;YAC3B,GAAG,MAAM;SACT,CAAC;IACH,CAAC;IAED,EAAE,CAA+B,KAAQ,EAAE,OAAwB;QACjE,IAAI,CAAC,MAAkC,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC;IAC3D,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK;QACV,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAEpB,kBAAkB;QAClB,IAAI,CAAC,cAAc,GAAG,WAAW,CAChC,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,EACvB,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAC/B,CAAC;QAEF,wCAAwC;QACxC,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI;QACT,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QAErB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC5B,CAAC;QAED,wCAAwC;QACxC,8DAA8D;IAC/D,CAAC;IAED;;OAEG;IACH,MAAM;QACL,OAAO;YACN,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC9B,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,mBAAmB,EAAE,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,MAAM;SAC9D,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,KAAK;QAClB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,IAAI,CAAC;YACJ,wBAAwB;YACxB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;gBAC3C,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,mBAAmB,EAAE,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,MAAM;aAC9D,CAAC,CAAC;YAEH,IAAI,KAAK,EAAE,CAAC;gBACX,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC7B,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC;QACF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,KAAgB;QAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QAExB,IAAI,CAAC;YACJ,qEAAqE;YACrE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAEzC,oBAAoB;YACpB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACzC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE;gBAC9C,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC;gBAClB,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACrC,CAAC,CAAC;QACJ,CAAC;QAED,iCAAiC;QACjC,IAAI,IAAI,CAAC,OAAO;YAAE,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;IACrC,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,QAAQ,CAAC,IAAc;QACpC,8CAA8C;QAC9C,wEAAwE;QACxE,OAAO;YACN,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,oBAAoB,IAAI,CAAC,EAAE,EAAE;YACtC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU;QACvB,qCAAqC;QACrC,sEAAsE;QACtE,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACR,0CAA0C;QAC3C,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@pi/autonomous-runtime",
3
+ "version": "0.9.15",
4
+ "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "bun test",
17
+ "test:watch": "bun test --watch",
18
+ "clean": "rm -rf dist"
19
+ },
20
+ "keywords": [
21
+ "autonomous",
22
+ "runtime",
23
+ "task-queue",
24
+ "lease",
25
+ "worker",
26
+ "rfc-0101"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@types/node": "^20.11.0",
32
+ "typescript": "^7.0.2"
33
+ }
34
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Task Inbox — RFC-0101 §2
3
+ *
4
+ * Append-only durable store for TaskRecords backed by `tasks.jsonl`.
5
+ * One JSONL line per task — easy to tail, grep, and replay.
6
+ */
7
+ import {
8
+ appendFileSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { dirname } from "node:path";
15
+ import type { TaskRecord, TaskStatus, TaskEvent } from "./types.js";
16
+ import { getInboxDir, getTasksPath } from "./types.js";
17
+
18
+ // ─── Errors ───────────────────────────────────────────────────────────────────
19
+
20
+ export class InboxError extends Error {
21
+ constructor(msg: string) {
22
+ super(`[InboxError] ${msg}`);
23
+ this.name = "InboxError";
24
+ }
25
+ }
26
+
27
+ // ─── TaskInbox ────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Manages the append-only task inbox at `~/.pi/harness/inbox/tasks.jsonl`.
31
+ *
32
+ * Storage layout:
33
+ * ```
34
+ * ~/.pi/harness/inbox/
35
+ * ├── tasks.jsonl # append-only log (one JSON line per task)
36
+ * ├── claimed/ # active leases (one JSON file per task)
37
+ * └── BACKLOG.md # human-friendly authoring surface (optional)
38
+ * ```
39
+ *
40
+ * Operations are:
41
+ * - **append** — append a new task to tasks.jsonl
42
+ * - **list** — read all tasks, filterable by status
43
+ * - **transition** — update a task's status in-place (rewrite)
44
+ * - **get** — read one task by id
45
+ * - **reindex** — compact tasks.jsonl by rewriting it from in-memory state
46
+ */
47
+ export class TaskInbox {
48
+ private readonly tasksPath: string;
49
+ private readonly inboxDir: string;
50
+ private _cache: TaskRecord[] | null = null;
51
+
52
+ constructor(options: { tasksPath?: string; inboxDir?: string } = {}) {
53
+ this.inboxDir = options.inboxDir ?? getInboxDir();
54
+ this.tasksPath = options.tasksPath ?? getTasksPath();
55
+ }
56
+
57
+ /** Ensure the inbox directory exists. Call before any write. */
58
+ ensureDir(): void {
59
+ if (!existsSync(this.inboxDir)) {
60
+ mkdirSync(this.inboxDir, { recursive: true });
61
+ }
62
+ }
63
+
64
+ // ─── append ──────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Append a new task to tasks.jsonl.
68
+ * The task is validated before writing. Duplicate ids are rejected.
69
+ *
70
+ * @throws {InboxError} if the task id already exists or validation fails
71
+ */
72
+ append(task: TaskRecord): void {
73
+ this.ensureDir();
74
+
75
+ // Guard against duplicate ids
76
+ const existing = this.list().find((t) => t.id === task.id);
77
+ if (existing) {
78
+ throw new InboxError(
79
+ `Task ${task.id} already exists in inbox (status=${existing.status})`,
80
+ );
81
+ }
82
+
83
+ const line = JSON.stringify(task) + "\n";
84
+ appendFileSync(this.tasksPath, line, "utf8");
85
+ this._cache = null; // invalidate
86
+ }
87
+
88
+ // ─── list ─────────────────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * Read all tasks from tasks.jsonl.
92
+ * Results are sorted by priority ASC then createdAt ASC (oldest first).
93
+ *
94
+ * @param filter Optionally filter by status. If omitted, returns all.
95
+ */
96
+ list(filter?: { status?: TaskStatus | TaskStatus[] }): TaskRecord[] {
97
+ if (!existsSync(this.tasksPath)) {
98
+ return [];
99
+ }
100
+
101
+ const raw = readFileSync(this.tasksPath, "utf8");
102
+ const tasks: TaskRecord[] = [];
103
+
104
+ for (const line of raw.split("\n")) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed) continue;
107
+ try {
108
+ const task = JSON.parse(trimmed) as TaskRecord;
109
+ tasks.push(task);
110
+ } catch {
111
+ // Corrupt line — skip but don't fail the whole list
112
+ console.warn(`[TaskInbox] Skipping corrupt line in ${this.tasksPath}`);
113
+ }
114
+ }
115
+
116
+ // Filter by status
117
+ let filtered = tasks;
118
+ if (filter?.status !== undefined) {
119
+ const statuses = Array.isArray(filter.status)
120
+ ? filter.status
121
+ : [filter.status];
122
+ filtered = tasks.filter((t) => statuses.includes(t.status));
123
+ }
124
+
125
+ // Sort: priority ASC, then createdAt ASC
126
+ filtered.sort((a, b) => {
127
+ if (a.priority !== b.priority) return a.priority - b.priority;
128
+ return a.createdAt.localeCompare(b.createdAt);
129
+ });
130
+
131
+ return filtered;
132
+ }
133
+
134
+ // ─── get ─────────────────────────────────────────────────────────────────
135
+
136
+ /** Retrieve a single task by id, or null if not found. */
137
+ get(id: string): TaskRecord | null {
138
+ const tasks = this.list();
139
+ return tasks.find((t) => t.id === id) ?? null;
140
+ }
141
+
142
+ // ─── transition ──────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Update a task's status in-place inside tasks.jsonl.
146
+ *
147
+ * Uses read→parse→update→rewrite to avoid corrupting the JSONL file.
148
+ * The rewrite is atomic (write-tmp-then-rename).
149
+ *
150
+ * @param taskId The id of the task to transition
151
+ * @param newStatus The target status
152
+ * @param extra Optional additional fields to merge into the task record
153
+ * @param event Optional TaskEvent to append to the history
154
+ * @throws {InboxError} if the task does not exist
155
+ */
156
+ transition(
157
+ taskId: string,
158
+ newStatus: TaskStatus,
159
+ extra?: Partial<TaskRecord>,
160
+ event?: TaskEvent,
161
+ ): TaskRecord {
162
+ if (!existsSync(this.tasksPath)) {
163
+ throw new InboxError(`tasks.jsonl does not exist at ${this.tasksPath}`);
164
+ }
165
+
166
+ const tasks = this.list();
167
+ const idx = tasks.findIndex((t) => t.id === taskId);
168
+ if (idx === -1) {
169
+ throw new InboxError(`Task ${taskId} not found in inbox`);
170
+ }
171
+
172
+ const updated: TaskRecord = {
173
+ ...tasks[idx],
174
+ ...extra,
175
+ status: newStatus,
176
+ updatedAt: new Date().toISOString(),
177
+ history: event
178
+ ? [...tasks[idx].history, event]
179
+ : [
180
+ ...tasks[idx].history,
181
+ {
182
+ ts: new Date().toISOString(),
183
+ kind: "transitioned",
184
+ payload: { from: tasks[idx].status, to: newStatus },
185
+ },
186
+ ],
187
+ };
188
+
189
+ // Rewrite tasks.jsonl atomically
190
+ this._rewriteAll([
191
+ ...tasks.slice(0, idx),
192
+ updated,
193
+ ...tasks.slice(idx + 1),
194
+ ]);
195
+ this._cache = null;
196
+
197
+ return updated;
198
+ }
199
+
200
+ // ─── markDone / markFailed ───────────────────────────────────────────────
201
+
202
+ /** Convenience: mark a task as completed with a result. */
203
+ complete(taskId: string, result: TaskRecord["result"]): TaskRecord {
204
+ return this.transition(
205
+ taskId,
206
+ "completed",
207
+ { result },
208
+ {
209
+ ts: new Date().toISOString(),
210
+ kind: "completed",
211
+ payload: { result },
212
+ },
213
+ );
214
+ }
215
+
216
+ /** Convenience: mark a task as failed with a reason. */
217
+ fail(taskId: string, reason: string): TaskRecord {
218
+ const task = this.get(taskId);
219
+ const attempts = (task?.attempts ?? 0) + 1;
220
+ const newStatus =
221
+ attempts >= (task?.maxAttempts ?? 3) ? "dead_letter" : "retrying";
222
+ return this.transition(
223
+ taskId,
224
+ newStatus,
225
+ { failureReason: reason, attempts },
226
+ {
227
+ ts: new Date().toISOString(),
228
+ kind: newStatus === "dead_letter" ? "dead_lettered" : "failed",
229
+ payload: { reason, attempts },
230
+ },
231
+ );
232
+ }
233
+
234
+ // ─── reindex ─────────────────────────────────────────────────────────────
235
+
236
+ /** Rewrite tasks.jsonl from the given task list. */
237
+ private _rewriteAll(tasks: TaskRecord[]): void {
238
+ const tmp = `${this.tasksPath}.tmp`;
239
+ const content = tasks.map((t) => JSON.stringify(t)).join("\n") + "\n";
240
+ writeFileSync(tmp, content, "utf8");
241
+ // Atomic rename (POSIX guarantees atomicity)
242
+ // Bun/Node will use the native rename which is atomic on POSIX
243
+ const { renameSync } = require("node:fs");
244
+ renameSync(tmp, this.tasksPath);
245
+ }
246
+
247
+ /** Force a full reindex. Call after manual edits to tasks.jsonl. */
248
+ reindex(): void {
249
+ if (!existsSync(this.tasksPath)) return;
250
+ const tasks = this.list(); // reads and validates
251
+ this._rewriteAll(tasks);
252
+ this._cache = null;
253
+ }
254
+
255
+ // ─── count ───────────────────────────────────────────────────────────────
256
+
257
+ /** Return counts of tasks grouped by status. */
258
+ stats(): Record<TaskStatus, number> {
259
+ const tasks = this.list();
260
+ const counts: Record<string, number> = {};
261
+ for (const t of tasks) {
262
+ counts[t.status] = (counts[t.status] ?? 0) + 1;
263
+ }
264
+ return counts as Record<TaskStatus, number>;
265
+ }
266
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Autonomous Operations Runtime — Public API
3
+ *
4
+ * Phase 1 exports (RFC-0101 §1-2):
5
+ * - TaskRecord, TaskLease, TaskEvent, TaskStatus, CapabilityName, ApprovalClass types
6
+ * - TaskInbox: append, list, get, transition, complete, fail
7
+ * - LeaseManager: claim, heartbeat, release, reap, recoverOnStartup
8
+ *
9
+ * Phase 2+ exports (added as implemented):
10
+ * - Worker lifecycle (worker.ts)
11
+ * - Privilege broker (privilege-broker package)
12
+ * - Scheduler adapter (scheduler-adapter package)
13
+ */
14
+ export * from "./types.js";
15
+ export { TaskInbox, InboxError } from "./inbox.js";
16
+ export {
17
+ LeaseManager,
18
+ LeaseError,
19
+ DEFAULT_LEASE_TTL_MS,
20
+ HEARTBEAT_INTERVAL_MS,
21
+ HEARTBEAT_EXTENSION_MS,
22
+ } from "./lease.js";