forgeos 0.1.0-alpha.63 → 0.1.0-alpha.64

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 (63) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/docs/agent-fabric.md +252 -0
  4. package/package.json +7 -4
  5. package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
  6. package/scripts/field-test-forgeos.mjs +11 -2
  7. package/src/forge/_generated/releaseManifest.json +1 -1
  8. package/src/forge/_generated/releaseManifest.ts +3 -3
  9. package/src/forge/agent-adapters/index.ts +18 -14
  10. package/src/forge/agent-adapters/types.ts +1 -1
  11. package/src/forge/agent-fabric/adapter.ts +131 -0
  12. package/src/forge/agent-fabric/authority.ts +298 -0
  13. package/src/forge/agent-fabric/canonical.ts +135 -0
  14. package/src/forge/agent-fabric/conductor.ts +754 -0
  15. package/src/forge/agent-fabric/dictionary.ts +12 -0
  16. package/src/forge/agent-fabric/errors.ts +32 -0
  17. package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
  18. package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
  19. package/src/forge/agent-fabric/index.ts +18 -0
  20. package/src/forge/agent-fabric/journal.ts +116 -0
  21. package/src/forge/agent-fabric/local-approval-window.ts +169 -0
  22. package/src/forge/agent-fabric/local-coding-worker.ts +190 -0
  23. package/src/forge/agent-fabric/local-control-store.ts +239 -0
  24. package/src/forge/agent-fabric/local-paths.ts +18 -0
  25. package/src/forge/agent-fabric/local-task-contract.ts +142 -0
  26. package/src/forge/agent-fabric/local-task-inbox.ts +199 -0
  27. package/src/forge/agent-fabric/local-task-server.ts +207 -0
  28. package/src/forge/agent-fabric/local-task-service.ts +494 -0
  29. package/src/forge/agent-fabric/p0a.ts +104 -0
  30. package/src/forge/agent-fabric/p0b-model-adapter.ts +404 -0
  31. package/src/forge/agent-fabric/planning.ts +155 -0
  32. package/src/forge/agent-fabric/reducer.ts +863 -0
  33. package/src/forge/agent-fabric/resource-ledger.ts +266 -0
  34. package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
  35. package/src/forge/agent-fabric/types.ts +424 -0
  36. package/src/forge/agent-fabric/validation.ts +500 -0
  37. package/src/forge/agent-memory/bridge.ts +82 -52
  38. package/src/forge/agent-memory/mcp.ts +59 -9
  39. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +12 -113
  40. package/src/forge/cli/commands.ts +4 -0
  41. package/src/forge/cli/fabric.ts +83 -0
  42. package/src/forge/cli/main.ts +7 -0
  43. package/src/forge/cli/new.ts +16 -4
  44. package/src/forge/cli/parse.ts +21 -0
  45. package/src/forge/cli/verify.ts +4 -1
  46. package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
  47. package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
  48. package/src/forge/impact/index.ts +18 -21
  49. package/src/forge/runtime/ai/providers.ts +19 -4
  50. package/src/forge/version.ts +1 -1
  51. package/templates/agent-workroom/package.json +1 -0
  52. package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
  53. package/templates/b2b-support-web/package.json +1 -0
  54. package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
  55. package/templates/b2b-support-web/web/package.json +1 -1
  56. package/templates/minimal-web/package.json +1 -0
  57. package/templates/minimal-web/pnpm-workspace.yaml +2 -0
  58. package/templates/nuxt-web/.yarnrc.yml +1 -0
  59. package/templates/nuxt-web/package.json +1 -0
  60. package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
  61. package/templates/nuxt-web/web/package.json +1 -0
  62. package/templates/vendor-access/package.json +1 -0
  63. package/templates/vendor-access/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,199 @@
1
+ import type { DbAdapter } from "../runtime/db/adapter.ts";
2
+ import { AgentFabricError } from "./errors.ts";
3
+ import { stableStringify } from "./canonical.ts";
4
+ import type { LocalPatchEvidence } from "./local-coding-worker.ts";
5
+ import {
6
+ validateLocalCodingTaskProposal,
7
+ type ValidatedLocalCodingTaskProposal,
8
+ } from "./local-task-contract.ts";
9
+ import type { Digest } from "./types.ts";
10
+
11
+ const CREATE_TASKS_TABLE = `
12
+ CREATE TABLE IF NOT EXISTS _forge_agent_fabric_local_tasks (
13
+ task_id TEXT PRIMARY KEY,
14
+ repository_root TEXT NOT NULL,
15
+ proposal_digest TEXT NOT NULL UNIQUE,
16
+ proposal_json TEXT NOT NULL,
17
+ state TEXT NOT NULL CHECK (state IN ('proposed', 'rejected')),
18
+ created_at BIGINT NOT NULL,
19
+ decided_at BIGINT
20
+ )`;
21
+
22
+ const CREATE_PATCHES_TABLE = `
23
+ CREATE TABLE IF NOT EXISTS _forge_agent_fabric_local_patches (
24
+ task_id TEXT PRIMARY KEY,
25
+ evidence_json TEXT NOT NULL
26
+ )`;
27
+
28
+ const CREATE_DECISIONS_TABLE = `
29
+ CREATE TABLE IF NOT EXISTS _forge_agent_fabric_local_patch_decisions (
30
+ task_id TEXT PRIMARY KEY,
31
+ diff_digest TEXT NOT NULL,
32
+ decision TEXT NOT NULL CHECK (decision IN ('approved', 'rejected')),
33
+ decided_at BIGINT NOT NULL
34
+ )`;
35
+
36
+ export interface LocalTaskRecord extends ValidatedLocalCodingTaskProposal {
37
+ taskId: string;
38
+ repositoryRoot: string;
39
+ state: "proposed" | "rejected";
40
+ createdAt: number;
41
+ decidedAt: number | null;
42
+ }
43
+
44
+ function taskIdFor(digest: Digest): string {
45
+ return `task:${digest.slice("sha256:".length)}`;
46
+ }
47
+
48
+ export class LocalTaskInbox {
49
+ private schemaReady?: Promise<unknown>;
50
+ private patchSchemaReady?: Promise<unknown>;
51
+ private decisionSchemaReady?: Promise<unknown>;
52
+
53
+ constructor(private readonly adapter: DbAdapter) {
54
+ if (adapter.kind !== "pglite") {
55
+ throw new AgentFabricError("AF_INVALID_STATE", "Local task inbox requires PGlite");
56
+ }
57
+ }
58
+
59
+ private async ready(): Promise<void> {
60
+ this.schemaReady ??= this.adapter.query(CREATE_TASKS_TABLE);
61
+ await this.schemaReady;
62
+ }
63
+
64
+ async propose(
65
+ input: unknown,
66
+ repositoryRoot: string,
67
+ now = Date.now(),
68
+ ): Promise<LocalTaskRecord> {
69
+ const validated = validateLocalCodingTaskProposal(input);
70
+ if (validated.proposal.limits.expiresAt <= now) {
71
+ throw new AgentFabricError("AF_INVALID_STATE", "Local coding task has expired");
72
+ }
73
+ const taskId = taskIdFor(validated.proposalDigest);
74
+ await this.ready();
75
+ await this.adapter.query(
76
+ `INSERT INTO _forge_agent_fabric_local_tasks
77
+ (task_id, repository_root, proposal_digest, proposal_json, state, created_at)
78
+ VALUES ($1, $2, $3, $4, 'proposed', $5)
79
+ ON CONFLICT (task_id) DO NOTHING`,
80
+ [taskId, repositoryRoot, validated.proposalDigest,
81
+ JSON.stringify(validated.proposal), now],
82
+ );
83
+ const record = await this.get(taskId);
84
+ if (!record || record.repositoryRoot !== repositoryRoot ||
85
+ record.proposalDigest !== validated.proposalDigest) {
86
+ throw new AgentFabricError("AF_CONFLICT", "Task identity is already bound to another repository or revision");
87
+ }
88
+ return record;
89
+ }
90
+
91
+ async get(taskId: string): Promise<LocalTaskRecord | null> {
92
+ if (!/^task:[0-9a-f]{64}$/u.test(taskId)) {
93
+ throw new AgentFabricError("AF_INVALID_STATE", "Invalid local task identity");
94
+ }
95
+ await this.ready();
96
+ const result = await this.adapter.query(
97
+ `SELECT task_id, repository_root, proposal_digest, proposal_json, state, created_at, decided_at
98
+ FROM _forge_agent_fabric_local_tasks WHERE task_id = $1`,
99
+ [taskId],
100
+ );
101
+ const row = result.rows[0];
102
+ if (!row) return null;
103
+ let parsed: unknown;
104
+ try {
105
+ parsed = JSON.parse(String(row.proposal_json));
106
+ } catch {
107
+ throw new AgentFabricError("AF_INVALID_STATE", "Stored task proposal is not JSON");
108
+ }
109
+ const validated = validateLocalCodingTaskProposal(parsed);
110
+ if (row.task_id !== taskId || row.proposal_digest !== validated.proposalDigest ||
111
+ taskIdFor(validated.proposalDigest) !== taskId ||
112
+ (row.state !== "proposed" && row.state !== "rejected") ||
113
+ typeof row.repository_root !== "string" || !row.repository_root) {
114
+ throw new AgentFabricError("AF_INVALID_STATE", "Stored task proposal is inconsistent");
115
+ }
116
+ return {
117
+ taskId,
118
+ repositoryRoot: row.repository_root,
119
+ ...validated,
120
+ state: row.state,
121
+ createdAt: Number(row.created_at),
122
+ decidedAt: row.decided_at === null ? null : Number(row.decided_at),
123
+ };
124
+ }
125
+
126
+ async reject(taskId: string, expectedDigest: Digest, now = Date.now()): Promise<void> {
127
+ await this.ready();
128
+ const result = await this.adapter.query(
129
+ `UPDATE _forge_agent_fabric_local_tasks SET state = 'rejected', decided_at = $3
130
+ WHERE task_id = $1 AND proposal_digest = $2 AND state = 'proposed'`,
131
+ [taskId, expectedDigest, now],
132
+ );
133
+ if (result.rowCount !== 1) {
134
+ throw new AgentFabricError("AF_CONFLICT", "Task is stale or already reviewed");
135
+ }
136
+ }
137
+
138
+ async recordPatch(taskId: string, evidence: LocalPatchEvidence): Promise<void> {
139
+ await this.ready();
140
+ this.patchSchemaReady ??= this.adapter.query(CREATE_PATCHES_TABLE);
141
+ await this.patchSchemaReady;
142
+ const encoded = stableStringify(evidence);
143
+ await this.adapter.query(
144
+ `INSERT INTO _forge_agent_fabric_local_patches (task_id, evidence_json)
145
+ VALUES ($1, $2) ON CONFLICT (task_id) DO NOTHING`,
146
+ [taskId, encoded],
147
+ );
148
+ const stored = await this.getPatch(taskId);
149
+ if (!stored || stableStringify(stored) !== encoded) {
150
+ throw new AgentFabricError("AF_CONFLICT", "Task already has different patch evidence");
151
+ }
152
+ }
153
+
154
+ async getPatch(taskId: string): Promise<LocalPatchEvidence | null> {
155
+ this.patchSchemaReady ??= this.adapter.query(CREATE_PATCHES_TABLE);
156
+ await this.patchSchemaReady;
157
+ const result = await this.adapter.query(
158
+ `SELECT evidence_json FROM _forge_agent_fabric_local_patches WHERE task_id = $1`,
159
+ [taskId],
160
+ );
161
+ const encoded = result.rows[0]?.evidence_json;
162
+ if (typeof encoded !== "string") return null;
163
+ try {
164
+ return JSON.parse(encoded) as LocalPatchEvidence;
165
+ } catch {
166
+ throw new AgentFabricError("AF_INVALID_STATE", "Stored patch evidence is not JSON");
167
+ }
168
+ }
169
+
170
+ async recordPatchDecision(taskId: string, diffDigest: Digest, decision: "approved" | "rejected", now = Date.now()): Promise<void> {
171
+ const patch = await this.getPatch(taskId);
172
+ if (!patch || patch.diffDigest !== diffDigest) {
173
+ throw new AgentFabricError("AF_CONFLICT", "Patch review does not match the recorded diff");
174
+ }
175
+ this.decisionSchemaReady ??= this.adapter.query(CREATE_DECISIONS_TABLE);
176
+ await this.decisionSchemaReady;
177
+ const result = await this.adapter.query(
178
+ `INSERT INTO _forge_agent_fabric_local_patch_decisions (task_id, diff_digest, decision, decided_at)
179
+ VALUES ($1, $2, $3, $4) ON CONFLICT (task_id) DO NOTHING`,
180
+ [taskId, diffDigest, decision, now],
181
+ );
182
+ if (result.rowCount !== 1) {
183
+ throw new AgentFabricError("AF_CONFLICT", "Patch already has an owner decision");
184
+ }
185
+ }
186
+
187
+ async getPatchDecision(taskId: string): Promise<"approved" | "rejected" | null> {
188
+ this.decisionSchemaReady ??= this.adapter.query(CREATE_DECISIONS_TABLE);
189
+ await this.decisionSchemaReady;
190
+ const result = await this.adapter.query(
191
+ `SELECT decision FROM _forge_agent_fabric_local_patch_decisions WHERE task_id = $1`,
192
+ [taskId],
193
+ );
194
+ const decision = result.rows[0]?.decision;
195
+ if (decision === "approved" || decision === "rejected") return decision;
196
+ if (decision === undefined) return null;
197
+ throw new AgentFabricError("AF_INVALID_STATE", "Stored patch decision is invalid");
198
+ }
199
+ }
@@ -0,0 +1,207 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
4
+ import type { AddressInfo } from "node:net";
5
+ import { dirname } from "node:path";
6
+ import { AgentFabricError, isAgentFabricError } from "./errors.ts";
7
+ import { LocalTaskService, type LocalTaskStatus } from "./local-task-service.ts";
8
+ import { localFabricPath } from "./local-paths.ts";
9
+
10
+ const MAX_REQUEST_BYTES = 40 * 1024;
11
+ const ENDPOINT_FILENAME = "owner-endpoint.json";
12
+
13
+ export type LocalTaskAction = "propose" | "status" | "review" | "run" | "review-result";
14
+
15
+ interface OwnerEndpoint {
16
+ schemaVersion: 1;
17
+ repositoryRoot: string;
18
+ pid: number;
19
+ port: number;
20
+ token: string;
21
+ }
22
+
23
+ function endpointPath(repositoryRoot: string): string {
24
+ return localFabricPath(repositoryRoot, ENDPOINT_FILENAME);
25
+ }
26
+
27
+ function processIsAlive(pid: number): boolean {
28
+ try { process.kill(pid, 0); return true; } catch (error) {
29
+ return (error as NodeJS.ErrnoException).code === "EPERM";
30
+ }
31
+ }
32
+
33
+ function readEndpoint(repositoryRoot: string): OwnerEndpoint | null {
34
+ const path = endpointPath(repositoryRoot);
35
+ if (!existsSync(path)) return null;
36
+ if (lstatSync(path).isSymbolicLink()) {
37
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner endpoint is a symbolic link");
38
+ }
39
+ const bytes = readFileSync(path);
40
+ if (bytes.length > 2_048) throw new AgentFabricError("AF_INVALID_STATE", "Local owner endpoint is oversized");
41
+ let value: unknown;
42
+ try { value = JSON.parse(bytes.toString("utf8")); } catch {
43
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner endpoint is invalid JSON");
44
+ }
45
+ const item = value as Partial<OwnerEndpoint>;
46
+ if (!item || item.schemaVersion !== 1 || item.repositoryRoot !== repositoryRoot ||
47
+ !Number.isSafeInteger(item.pid) || (item.pid ?? 0) <= 0 ||
48
+ !Number.isSafeInteger(item.port) || (item.port ?? 0) < 1 || (item.port ?? 0) > 65535 ||
49
+ typeof item.token !== "string" || !/^[0-9a-f]{64}$/u.test(item.token)) {
50
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner endpoint is inconsistent");
51
+ }
52
+ if (!processIsAlive(item.pid!)) {
53
+ // A killed owner cannot hold PGlite. Remove only the exact endpoint bytes
54
+ // read above; a replacement means another owner is starting.
55
+ if (readFileSync(path).equals(bytes)) unlinkSync(path);
56
+ return null;
57
+ }
58
+ return item as OwnerEndpoint;
59
+ }
60
+
61
+ function authorized(request: IncomingMessage, endpoint: OwnerEndpoint): boolean {
62
+ const supplied = request.headers.authorization;
63
+ const expected = `Bearer ${endpoint.token}`;
64
+ return request.headers.origin === undefined &&
65
+ request.headers.host === `127.0.0.1:${endpoint.port}` &&
66
+ typeof supplied === "string" && supplied.length === expected.length &&
67
+ timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
68
+ }
69
+
70
+ async function readBody(request: IncomingMessage): Promise<Record<string, unknown>> {
71
+ if (request.headers["content-type"] !== "application/json") {
72
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner request must be JSON");
73
+ }
74
+ let bytes = 0;
75
+ const chunks: Buffer[] = [];
76
+ for await (const chunk of request) {
77
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
78
+ bytes += part.length;
79
+ if (bytes > MAX_REQUEST_BYTES) {
80
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner request exceeds 40 KiB");
81
+ }
82
+ chunks.push(part);
83
+ }
84
+ let value: unknown;
85
+ try { value = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch {
86
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner request is invalid JSON");
87
+ }
88
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
89
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner request is not an object");
90
+ }
91
+ return value as Record<string, unknown>;
92
+ }
93
+
94
+ async function dispatch(service: LocalTaskService, action: LocalTaskAction, request: Record<string, unknown>): Promise<LocalTaskStatus> {
95
+ const keys = Object.keys(request).sort().join(",");
96
+ if (action === "propose") {
97
+ if (keys !== "proposal") throw new AgentFabricError("AF_INVALID_STATE", "Proposal request has unexpected fields");
98
+ return service.propose(request.proposal);
99
+ }
100
+ if (keys !== "taskId" || typeof request.taskId !== "string") {
101
+ throw new AgentFabricError("AF_INVALID_STATE", "Task request requires only taskId");
102
+ }
103
+ if (action === "status") return service.status(request.taskId);
104
+ if (action === "review") return service.review(request.taskId);
105
+ if (action === "run") return service.run(request.taskId);
106
+ return service.reviewResult(request.taskId);
107
+ }
108
+
109
+ export interface LocalTaskOwnerServer {
110
+ repositoryRoot: string;
111
+ port: number;
112
+ close(): Promise<void>;
113
+ }
114
+
115
+ /** One PGlite owner shared by CLI clients and the read/proposal MCP adapter. */
116
+ export async function serveLocalTasks(
117
+ repositoryRoot: string,
118
+ suppliedService?: LocalTaskService,
119
+ ): Promise<LocalTaskOwnerServer> {
120
+ const service = suppliedService ?? await LocalTaskService.open(repositoryRoot);
121
+ const root = service.repositoryRoot;
122
+ if (root !== realpathSync(repositoryRoot)) {
123
+ if (!suppliedService) await service.close();
124
+ throw new AgentFabricError("AF_INVALID_STATE", "Local owner service belongs to another repository");
125
+ }
126
+ const path = endpointPath(root);
127
+ let listener: ReturnType<typeof createServer> | undefined;
128
+ let published: OwnerEndpoint | undefined;
129
+ try {
130
+ if (readEndpoint(root)) {
131
+ throw new AgentFabricError("AF_CONFLICT", "A local Agent Fabric owner is already running");
132
+ }
133
+ const token = randomBytes(32).toString("hex");
134
+ listener = createServer((request, response: ServerResponse) => {
135
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
136
+ response.setHeader("Cache-Control", "no-store");
137
+ if (!published || !authorized(request, published)) {
138
+ response.writeHead(403).end(JSON.stringify({ ok: false, error: "unauthorized" }));
139
+ return;
140
+ }
141
+ const action = request.url?.slice("/v1/".length) as LocalTaskAction;
142
+ if (request.method !== "POST" || !request.url?.startsWith("/v1/") ||
143
+ !["propose", "status", "review", "run", "review-result"].includes(action)) {
144
+ response.writeHead(404).end(JSON.stringify({ ok: false, error: "unknown_action" }));
145
+ return;
146
+ }
147
+ void readBody(request).then((body) => dispatch(service, action, body)).then((status) => {
148
+ if (!response.destroyed) response.writeHead(200).end(JSON.stringify({ ok: true, status }));
149
+ }).catch((error: unknown) => {
150
+ if (!response.destroyed) response.writeHead(isAgentFabricError(error) ? 400 : 500).end(JSON.stringify({
151
+ ok: false, code: isAgentFabricError(error) ? error.code : "AF_INVALID_STATE",
152
+ error: error instanceof Error ? error.message : "Local owner request failed",
153
+ }));
154
+ });
155
+ });
156
+ await new Promise<void>((resolve, reject) => {
157
+ listener!.once("error", reject);
158
+ listener!.listen(0, "127.0.0.1", resolve);
159
+ });
160
+ const port = (listener.address() as AddressInfo).port;
161
+ published = { schemaVersion: 1, repositoryRoot: root, pid: process.pid, port, token };
162
+ mkdirSync(dirname(path), { recursive: true });
163
+ writeFileSync(path, JSON.stringify(published), { flag: "wx", mode: 0o600 });
164
+ let closed = false;
165
+ return {
166
+ repositoryRoot: root, port,
167
+ async close() {
168
+ if (closed) return;
169
+ closed = true;
170
+ await new Promise<void>((resolve) => listener!.close(() => resolve()));
171
+ if (existsSync(path) && readFileSync(path, "utf8") === JSON.stringify(published)) unlinkSync(path);
172
+ if (!suppliedService) await service.close();
173
+ },
174
+ };
175
+ } catch (error) {
176
+ if (listener?.listening) await new Promise<void>((resolve) => listener!.close(() => resolve()));
177
+ if (!suppliedService) await service.close();
178
+ throw error;
179
+ }
180
+ }
181
+
182
+ export async function requestLocalTask(
183
+ repositoryRoot: string,
184
+ action: LocalTaskAction,
185
+ body: Record<string, unknown>,
186
+ ): Promise<LocalTaskStatus | null> {
187
+ const endpoint = readEndpoint(repositoryRoot);
188
+ if (!endpoint) return null;
189
+ let response: Response;
190
+ try {
191
+ response = await fetch(`http://127.0.0.1:${endpoint.port}/v1/${action}`, {
192
+ method: "POST", headers: { Authorization: `Bearer ${endpoint.token}`, "Content-Type": "application/json" },
193
+ body: JSON.stringify(body), redirect: "manual",
194
+ signal: AbortSignal.timeout(action === "run" ? 130_000 :
195
+ action === "review" || action === "review-result" ? 310_000 : 10_000),
196
+ });
197
+ } catch {
198
+ throw new AgentFabricError("AF_INVALID_STATE", "Local Agent Fabric owner is unreachable; restart it before retrying");
199
+ }
200
+ if (response.status >= 300) {
201
+ const result = await response.json().catch(() => ({})) as { code?: string; error?: string };
202
+ throw new AgentFabricError("AF_INVALID_STATE", result.error ?? "Local owner request failed");
203
+ }
204
+ const result = await response.json() as { ok?: boolean; status?: LocalTaskStatus };
205
+ if (!result.ok || !result.status) throw new AgentFabricError("AF_INVALID_STATE", "Local owner returned no task status");
206
+ return result.status;
207
+ }