forgeos 0.1.0-alpha.70 → 0.1.0-alpha.71
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.
- package/.agents/skills/forge-agent-fabric/SKILL.md +10 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/docs/agent-fabric-programs.md +151 -168
- package/docs/agent-fabric.md +4 -6
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -41
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -109
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +463 -423
- package/examples/agent-fabric-v2/README.md +16 -0
- package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
- package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
- package/examples/agent-fabric-v2/migrate.workflow.ts +1 -1
- package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
- package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
- package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
- package/examples/agent-fabric-v2/visual-population.json +13 -0
- package/package.json +3 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-fabric/codex-sdk-worker.ts +2 -0
- package/src/forge/agent-fabric/program-api.ts +37 -0
- package/src/forge/agent-fabric/program-contract.ts +89 -35
- package/src/forge/agent-fabric/program-dsl.ts +45 -31
- package/src/forge/agent-fabric/program-evidence.ts +22 -0
- package/src/forge/agent-fabric/program-image.ts +15 -0
- package/src/forge/agent-fabric/program-lock.ts +50 -0
- package/src/forge/agent-fabric/program-owner.ts +16 -0
- package/src/forge/agent-fabric/program-scheduler.ts +38 -0
- package/src/forge/agent-fabric/program-service.ts +275 -101
- package/src/forge/agent-fabric/program-store.ts +80 -55
- package/src/forge/agent-fabric/program-structure.ts +65 -0
- package/src/forge/agent-fabric/program-types.ts +21 -9
- package/src/forge/agent-fabric/program-view.ts +12 -0
- package/src/forge/agent-fabric/program-worker.ts +24 -7
- package/src/forge/agent-memory/mcp.ts +3 -3
- package/src/forge/cli/fabric.ts +2 -2
- package/src/forge/cli/main.ts +2 -0
- package/src/forge/cli/parse.ts +1 -1
- package/src/forge/version.ts +1 -1
|
@@ -1,35 +1,41 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rename, unlink, realpath, readdir,
|
|
2
|
-
import { readFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { mkdir, open, readFile, rename, unlink, realpath, readdir, stat } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
5
4
|
import { assertAttachedSafePath } from "./attached-snapshot.ts";
|
|
5
|
+
import { ProgramFileLease } from "./program-lock.ts";
|
|
6
6
|
import { stableStringify } from "./canonical.ts";
|
|
7
7
|
import { programAssert, programDigest, validateWorkflowProgram, type ProgramRunV2 } from "./program-contract.ts";
|
|
8
8
|
|
|
9
9
|
interface ProgramReceipt { requestId: string; fingerprint: string; version: number }
|
|
10
|
-
interface
|
|
10
|
+
interface ProgramTransition { runId: string; version: number; type: string; at: string; stateDigest: string; stateRef: string; parentRef?: string }
|
|
11
|
+
interface ProgramRecord { format: 3; runId: string; stateRef: string; journalRef: string; version: number; receipts: ProgramReceipt[] }
|
|
11
12
|
const absent = (error: unknown) => (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
12
13
|
const sharingViolation = (error: unknown) => ["EPERM", "EACCES", "EBUSY"].includes((error as NodeJS.ErrnoException).code ?? "");
|
|
13
14
|
async function retrySharing<T>(action: () => Promise<T>): Promise<T> {
|
|
14
15
|
const deadline = Date.now() + 1000;
|
|
15
16
|
for (;;) { try { return await action(); } catch (error) { if (!sharingViolation(error) || Date.now() >= deadline) throw error; await new Promise(resolve => setTimeout(resolve, 20)); } }
|
|
16
17
|
}
|
|
17
|
-
function alive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code !== "ESRCH"; } }
|
|
18
|
-
|
|
19
18
|
/** One authoritative atomic record per run; artifacts commit before referring transitions. */
|
|
20
19
|
export class ProgramRunStore {
|
|
20
|
+
readonly metrics = { transactions: 0, transactionMs: 0, bytesWritten: 0, snapshotBytes: 0, journalBytes: 0, envelopeBytes: 0, artifactBytes: 0, artifactReads: 0 };
|
|
21
|
+
private tails = new Map<string, Promise<unknown>>();
|
|
22
|
+
private definitions = new Set<string>();
|
|
23
|
+
private validate(state: ProgramRunV2): void {
|
|
24
|
+
const key = `${programDigest(state.program)}:${programDigest(state.registry)}`;
|
|
25
|
+
if (!this.definitions.has(key)) { validateWorkflowProgram(state.program, state.registry); this.definitions.add(key); }
|
|
26
|
+
}
|
|
21
27
|
private constructor(readonly root: string, readonly directory: string) {}
|
|
22
28
|
static async open(root: string): Promise<ProgramRunStore> {
|
|
23
|
-
root = await realpath(root); const directory = join(root, ".forge/local/agent-fabric/program-runs");
|
|
29
|
+
root = await realpath(root); const directory = join(root, ".forge/local/agent-fabric/program-runs-v3");
|
|
24
30
|
await assertAttachedSafePath(root, directory); await mkdir(join(directory, "artifacts"), { recursive: true });
|
|
25
31
|
await assertAttachedSafePath(root, directory); return new ProgramRunStore(root, directory);
|
|
26
32
|
}
|
|
27
33
|
private path(id: string): string { programAssert(typeof id === "string" && id.length > 0 && id.length <= 256, "Invalid run ID"); return join(this.directory, `${programDigest(id).slice(7)}.json`); }
|
|
28
|
-
async put(value: unknown, maxBytes = 4 * 1024 * 1024): Promise<string> {
|
|
34
|
+
async put(value: unknown, maxBytes = 4 * 1024 * 1024, category: "snapshotBytes" | "journalBytes" | "artifactBytes" = "artifactBytes"): Promise<string> {
|
|
29
35
|
const bytes = stableStringify(value), digest = programDigest(value); programAssert(Buffer.byteLength(bytes) <= maxBytes, "Artifact too large");
|
|
30
36
|
const path = join(this.directory, "artifacts", `${digest.slice(7)}.json`); await assertAttachedSafePath(this.root, path);
|
|
31
37
|
const temp = `${path}.${randomUUID()}.tmp`, handle = await open(temp, "wx", 0o600);
|
|
32
|
-
try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
|
|
38
|
+
try { await handle.writeFile(bytes); await handle.sync(); this.metrics.bytesWritten += Buffer.byteLength(bytes); this.metrics[category] += Buffer.byteLength(bytes); } finally { await handle.close(); }
|
|
33
39
|
try {
|
|
34
40
|
try { programAssert(await readFile(path, "utf8") === bytes, "Immutable artifact corrupted"); }
|
|
35
41
|
catch (error) { if (!absent(error)) throw error; await retrySharing(() => rename(temp, path)); }
|
|
@@ -37,81 +43,100 @@ export class ProgramRunStore {
|
|
|
37
43
|
return digest;
|
|
38
44
|
}
|
|
39
45
|
async get<T = unknown>(digest: string): Promise<T> {
|
|
46
|
+
this.metrics.artifactReads++;
|
|
40
47
|
programAssert(/^sha256:[a-f0-9]{64}$/.test(digest), "Invalid artifact reference");
|
|
41
48
|
const path = join(this.directory, "artifacts", `${digest.slice(7)}.json`); await assertAttachedSafePath(this.root, path);
|
|
42
49
|
const bytes = await readFile(path, "utf8"); programAssert(Buffer.byteLength(bytes) <= 32 * 1024 * 1024, "Artifact exceeds physical limit");
|
|
43
50
|
const value = JSON.parse(bytes); programAssert(programDigest(value) === digest, "Artifact integrity failed"); return value as T;
|
|
44
51
|
}
|
|
52
|
+
async putBinary(bytes: Uint8Array, maxBytes = 8 * 1024 * 1024): Promise<string> {
|
|
53
|
+
programAssert(bytes.byteLength <= maxBytes, "Binary artifact exceeds byte budget");
|
|
54
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`, path = join(this.directory, "artifacts", `${digest.slice(7)}.blob`);
|
|
55
|
+
await assertAttachedSafePath(this.root, path);
|
|
56
|
+
const temp = `${path}.${randomUUID()}.tmp`, handle = await open(temp, "wx", 0o600);
|
|
57
|
+
try { await handle.writeFile(bytes); await handle.sync(); this.metrics.bytesWritten += bytes.byteLength; this.metrics.artifactBytes += bytes.byteLength; } finally { await handle.close(); }
|
|
58
|
+
try { try { programAssert((await readFile(path)).equals(Buffer.from(bytes)), "Immutable binary corrupted"); } catch (error) { if (!absent(error)) throw error; await retrySharing(() => rename(temp, path)); } }
|
|
59
|
+
finally { await unlink(temp).catch(() => {}); } return digest;
|
|
60
|
+
}
|
|
61
|
+
async getBinary(digest: string, maxBytes = 8 * 1024 * 1024): Promise<Buffer> {
|
|
62
|
+
programAssert(/^sha256:[a-f0-9]{64}$/.test(digest), "Invalid binary reference");
|
|
63
|
+
const path = join(this.directory, "artifacts", `${digest.slice(7)}.blob`); await assertAttachedSafePath(this.root, path);
|
|
64
|
+
programAssert(Number.isSafeInteger(maxBytes) && maxBytes > 0 && (await stat(path)).size <= maxBytes, "Binary exceeds physical byte budget");
|
|
65
|
+
const bytes = await readFile(path); programAssert(bytes.length <= maxBytes && `sha256:${createHash("sha256").update(bytes).digest("hex")}` === digest, "Binary integrity/byte limit failed"); return bytes;
|
|
66
|
+
}
|
|
45
67
|
private async record(id: string): Promise<ProgramRecord | undefined> {
|
|
46
68
|
const path = this.path(id); await assertAttachedSafePath(this.root, path);
|
|
47
69
|
let bytes: string; try { bytes = await retrySharing(() => readFile(path, "utf8")); } catch (error) { if (absent(error)) return; throw error; }
|
|
48
70
|
programAssert(Buffer.byteLength(bytes) <= 32 * 1024 * 1024, "Program record exceeds limit");
|
|
49
71
|
const envelope = JSON.parse(bytes) as { record: ProgramRecord; digest: string };
|
|
50
72
|
programAssert(programDigest(envelope.record) === envelope.digest, "Program record integrity failed");
|
|
51
|
-
const
|
|
52
|
-
programAssert(
|
|
53
|
-
|
|
73
|
+
const record = envelope.record;
|
|
74
|
+
programAssert(record.format === 3 && record.runId === id && record.version > 0, "Unsupported/corrupt program record; start a new run");
|
|
75
|
+
const state = await this.get<ProgramRunV2>(record.stateRef), checkpoint = await this.get<ProgramTransition>(record.journalRef);
|
|
76
|
+
programAssert(state.schemaVersion === 2 && state.runId === id && state.version === record.version, "Invalid persisted run identity");
|
|
77
|
+
this.validate(state);
|
|
54
78
|
programAssert(programDigest(state.program) === state.programDigest && programDigest(state.registry) === state.registryDigest, "Pinned program/registry corrupted");
|
|
55
|
-
programAssert(
|
|
56
|
-
return
|
|
79
|
+
programAssert(checkpoint.runId === id && checkpoint.stateDigest === record.stateRef && checkpoint.stateRef === record.stateRef && checkpoint.version === state.version, "Checkpoint/journal mismatch");
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
async read(id: string): Promise<ProgramRunV2 | undefined> { const record = await this.record(id); return record ? this.get<ProgramRunV2>(record.stateRef) : undefined; }
|
|
83
|
+
async history(id: string): Promise<ProgramTransition[]> {
|
|
84
|
+
const record = await this.record(id); if (!record) return [];
|
|
85
|
+
const history: ProgramTransition[] = []; let ref: string | undefined = record.journalRef, version = record.version;
|
|
86
|
+
while (ref) {
|
|
87
|
+
const node: ProgramTransition = await this.get<ProgramTransition>(ref);
|
|
88
|
+
programAssert(node.runId === id && node.version === version-- && node.stateDigest === node.stateRef && history.length < 100000, "Journal chain mismatch");
|
|
89
|
+
const state = await this.get<ProgramRunV2>(node.stateRef);
|
|
90
|
+
programAssert(state.runId === id && state.version === node.version, "Historical snapshot identity mismatch");
|
|
91
|
+
history.push(node); ref = node.parentRef;
|
|
92
|
+
}
|
|
93
|
+
programAssert(version === 0, "Truncated program journal"); return history.reverse();
|
|
57
94
|
}
|
|
58
|
-
async read(id: string): Promise<ProgramRunV2 | undefined> { return (await this.record(id))?.state; }
|
|
59
|
-
async history(id: string): Promise<ProgramRecord["transitions"]> { return (await this.record(id))?.transitions ?? []; }
|
|
60
95
|
async list(): Promise<string[]> {
|
|
61
96
|
const ids: string[] = [];
|
|
62
97
|
for (const name of await readdir(this.directory)) {
|
|
63
98
|
if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
64
99
|
const path = join(this.directory, name); await assertAttachedSafePath(this.root, path);
|
|
65
100
|
const envelope = JSON.parse(await readFile(path, "utf8"));
|
|
66
|
-
programAssert(envelope?.record?.
|
|
67
|
-
await this.record(envelope.record.
|
|
101
|
+
programAssert(envelope?.record?.runId && this.path(envelope.record.runId) === path, "Run filename/identity mismatch");
|
|
102
|
+
await this.record(envelope.record.runId); ids.push(envelope.record.runId);
|
|
68
103
|
}
|
|
69
104
|
return ids;
|
|
70
105
|
}
|
|
71
106
|
async transact(id: string, type: string, mutate: (state?: ProgramRunV2) => ProgramRunV2,
|
|
72
107
|
options: { requestId?: string; fingerprint?: string; expectedVersion?: number } = {}): Promise<ProgramRunV2> {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
await
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const raw = await readFile(lock, "utf8"), previous = JSON.parse(raw);
|
|
85
|
-
if (Number.isSafeInteger(previous.pid) && previous.pid > 0 && !alive(previous.pid)) {
|
|
86
|
-
await assertAttachedSafePath(this.root, claim);
|
|
87
|
-
try { const guard = readFileSync(claim, "utf8"), holder = JSON.parse(guard); if (Number.isSafeInteger(holder.pid) && holder.pid > 0 && !alive(holder.pid) && readFileSync(claim, "utf8") === guard) unlinkSync(claim); } catch (error) { if (!absent(error) && !(error instanceof SyntaxError)) throw error; }
|
|
88
|
-
let claimed = false;
|
|
89
|
-
try { await link(candidate, claim); claimed = true; if (readFileSync(lock, "utf8") === raw) unlinkSync(lock); }
|
|
90
|
-
catch (error) { if (!absent(error) && (error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
|
|
91
|
-
finally { if (claimed) await retrySharing(() => unlink(claim)); }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch (readError) { if (!absent(readError) && !sharingViolation(readError) && !(readError instanceof SyntaxError)) throw readError; }
|
|
95
|
-
programAssert(Date.now() < deadline, "Program lock busy", "AF_PROGRAM_CONFLICT"); await new Promise(resolve => setTimeout(resolve, 20));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} finally { await unlink(candidate).catch(() => {}); }
|
|
108
|
+
const task = (this.tails.get(id) ?? Promise.resolve()).catch(() => {}).then(() => this.transaction(id, type, mutate, options));
|
|
109
|
+
this.tails.set(id, task);
|
|
110
|
+
try { return await task; } finally { if (this.tails.get(id) === task) this.tails.delete(id); }
|
|
111
|
+
}
|
|
112
|
+
private async transaction(id: string, type: string, mutate: (state?: ProgramRunV2) => ProgramRunV2,
|
|
113
|
+
options: { requestId?: string; fingerprint?: string; expectedVersion?: number }): Promise<ProgramRunV2> {
|
|
114
|
+
const transactionStarted = performance.now();
|
|
115
|
+
const path = this.path(id), lock = `${path}.lock`;
|
|
116
|
+
await assertAttachedSafePath(this.root, lock); await assertAttachedSafePath(this.root, `${lock}.reclaim`);
|
|
117
|
+
const lease = await ProgramFileLease.acquire(lock, { waitMs: 10000 });
|
|
99
118
|
try {
|
|
100
119
|
const old = await this.record(id), receipt = old?.receipts.find(entry => entry.requestId === options.requestId);
|
|
101
|
-
if (receipt) { programAssert(receipt.fingerprint === options.fingerprint, "requestId body changed", "AF_PROGRAM_CONFLICT"); return old!.
|
|
102
|
-
programAssert(options.expectedVersion === undefined || options.expectedVersion === (old?.
|
|
120
|
+
if (receipt) { programAssert(receipt.fingerprint === options.fingerprint, "requestId body changed", "AF_PROGRAM_CONFLICT"); return this.get<ProgramRunV2>(old!.stateRef); }
|
|
121
|
+
programAssert(options.expectedVersion === undefined || options.expectedVersion === (old?.version ?? 0), "Version conflict", "AF_PROGRAM_CONFLICT");
|
|
103
122
|
if (options.requestId) programAssert(typeof options.fingerprint === "string", "Request fingerprint required");
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
123
|
+
const oldState = old ? await this.get<ProgramRunV2>(old.stateRef) : undefined;
|
|
124
|
+
if (type === "attempt-dispatch-intent") programAssert(!old || old.version < 90000 && Buffer.byteLength(stableStringify(oldState)) < 24 * 1024 * 1024, "Journal dispatch reserve exhausted; recovery remains available");
|
|
125
|
+
const state = mutate(oldState);
|
|
126
|
+
if (old && !options.requestId && programDigest(state) === old.stateRef) return structuredClone(state);
|
|
127
|
+
state.version = (old?.version ?? 0) + 1;
|
|
128
|
+
this.validate(state);
|
|
129
|
+
const stateRef = await this.put(state, 32 * 1024 * 1024, "snapshotBytes");
|
|
130
|
+
const journalRef = await this.put({ runId: id, version: state.version, type, at: new Date().toISOString(), stateDigest: stateRef, stateRef, ...(old ? { parentRef: old.journalRef } : {}) }, 4096, "journalBytes");
|
|
131
|
+
const record: ProgramRecord = { format: 3, runId: id, version: state.version, stateRef, journalRef,
|
|
132
|
+
receipts: [...(old?.receipts ?? []), ...(options.requestId ? [{ requestId: options.requestId, fingerprint: options.fingerprint!, version: state.version }] : [])] };
|
|
133
|
+
const bytes = stableStringify({ record, digest: programDigest(record) }); programAssert(Buffer.byteLength(bytes) <= 32 * 1024 * 1024 && record.receipts.length <= 10000 && record.version <= 100000, "Program journal limit exceeded");
|
|
134
|
+
const token = lease.token;
|
|
111
135
|
const temp = `${path}.${token}.tmp`, handle = await open(temp, "wx", 0o600);
|
|
112
|
-
try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
|
|
136
|
+
try { await handle.writeFile(bytes); await handle.sync(); this.metrics.bytesWritten += Buffer.byteLength(bytes); this.metrics.envelopeBytes += Buffer.byteLength(bytes); } finally { await handle.close(); }
|
|
137
|
+
this.metrics.transactions++; this.metrics.transactionMs += performance.now() - transactionStarted;
|
|
113
138
|
try { await assertAttachedSafePath(this.root, path); await retrySharing(() => rename(temp, path)); } finally { await unlink(temp).catch(() => {}); }
|
|
114
139
|
return structuredClone(state);
|
|
115
|
-
} finally {
|
|
140
|
+
} finally { await retrySharing(() => lease.close()); }
|
|
116
141
|
}
|
|
117
142
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { programAssert, type ProgramOperation, type WorkflowProgramV2 } from "./program-contract.ts";
|
|
2
|
+
|
|
3
|
+
export interface ProgramBlock { steps: ProgramOperation[]; result: unknown }
|
|
4
|
+
/** Only a single operation has a shorthand result. Arrays are not sequences. */
|
|
5
|
+
export function programBlock(value: unknown): ProgramBlock {
|
|
6
|
+
if (value && typeof value === "object" && !Array.isArray(value) && "steps" in value) {
|
|
7
|
+
const block = value as ProgramBlock;
|
|
8
|
+
programAssert(Array.isArray(block.steps) && Object.hasOwn(block, "result"), "Block requires steps and explicit result"); return block;
|
|
9
|
+
}
|
|
10
|
+
const steps = Array.isArray(value) ? value : [value];
|
|
11
|
+
programAssert(steps.length === 1 && steps[0] && typeof steps[0].id === "string", "Multiple operations require Block {steps,result}");
|
|
12
|
+
return { steps, result: { $expr: "output", args: [steps[0].id] } };
|
|
13
|
+
}
|
|
14
|
+
export function canonicalItemKey(value: unknown): string {
|
|
15
|
+
programAssert(typeof value === "string" && value.length > 0 && !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value), "Map key must be a valid Unicode string");
|
|
16
|
+
const key = value.normalize("NFC"); programAssert(Buffer.byteLength(key) <= 256, "Map key byte limit exceeded"); return key;
|
|
17
|
+
}
|
|
18
|
+
export const compareProgramKeys = (a: string, b: string): number => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
19
|
+
export const programSegment = (key: string): string => encodeURIComponent(canonicalItemKey(key)).replace(/[!'()*]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
20
|
+
|
|
21
|
+
export interface ProgramGraphEntry { id: string; step: ProgramOperation; dependencies: string[] }
|
|
22
|
+
/** Static templates, lexical references and control edges. No authored code executes. */
|
|
23
|
+
export function programGraph(program: WorkflowProgramV2): ProgramGraphEntry[] {
|
|
24
|
+
const graph = new Map<string, ProgramGraphEntry>();
|
|
25
|
+
function refs(value: unknown, resolve: (id: string) => string | undefined, dependencies: Set<string>): void {
|
|
26
|
+
if (!value || typeof value !== "object") return;
|
|
27
|
+
if (Array.isArray(value)) { for (const entry of value) refs(entry, resolve, dependencies); return; }
|
|
28
|
+
const record = value as Record<string, unknown>;
|
|
29
|
+
if (record.$expr === "literal") return;
|
|
30
|
+
if (["output", "acceptedCandidate", "acceptedCandidates", "outputCandidate", "coverageReceipt"].includes(String(record.$expr))) {
|
|
31
|
+
const id = (record.args as unknown[])[0]; programAssert(typeof id === "string", "Output ID required"); const target = resolve(id); programAssert(target, `Unknown output ${id}`); dependencies.add(target);
|
|
32
|
+
}
|
|
33
|
+
for (const child of Object.values(record)) refs(child, resolve, dependencies);
|
|
34
|
+
}
|
|
35
|
+
function scope(block: ProgramBlock, prefix: string, outer?: (id: string) => string | undefined, sequential = false): void {
|
|
36
|
+
const resolve = (id: string) => block.steps.some(step => step.id === id) ? `${prefix}${id}` : outer?.(id);
|
|
37
|
+
for (let index = 0; index < block.steps.length; index++) {
|
|
38
|
+
const step = block.steps[index], id = `${prefix}${step.id}`, dependencies = new Set<string>();
|
|
39
|
+
if (sequential && index) dependencies.add(`${prefix}${block.steps[index - 1].id}`);
|
|
40
|
+
const after = step.options.after ?? []; programAssert(Array.isArray(after) && after.every(entry => typeof entry === "string"), "after requires operation IDs");
|
|
41
|
+
for (const predecessor of after) { const target = resolve(predecessor); programAssert(target, `Unknown control predecessor ${predecessor}`); dependencies.add(target); }
|
|
42
|
+
refs(Object.fromEntries(Object.entries(step.options).filter(([key]) => !["body", "steps", "result", "then", "else", "next", "until", "after"].includes(key))), resolve, dependencies);
|
|
43
|
+
const children: { block: ProgramBlock; prefix: string; sequential?: boolean }[] = [];
|
|
44
|
+
if (step.kind === "sequence" || step.kind === "parallel") children.push({ block: programBlock(step.options), prefix: `${id}/`, sequential: step.kind === "sequence" });
|
|
45
|
+
if (step.kind === "map" || step.kind === "loop") children.push({ block: programBlock(step.options.body), prefix: `${id}/body/` });
|
|
46
|
+
if (step.kind === "branch") for (const arm of ["then", "else"]) children.push({ block: programBlock(step.options[arm]), prefix: `${id}/${arm}/` });
|
|
47
|
+
programAssert(!graph.has(id), "Duplicate operation ID"); graph.set(id, { id, step, dependencies: [] });
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
scope(child.block, child.prefix, resolve, child.sequential);
|
|
50
|
+
for (const entry of child.block.steps) dependencies.add(`${child.prefix}${entry.id}`);
|
|
51
|
+
const childResolve = (target: string) => child.block.steps.some(entry => entry.id === target) ? `${child.prefix}${target}` : resolve(target);
|
|
52
|
+
refs(child.block.result, childResolve, dependencies);
|
|
53
|
+
if (step.kind === "loop") {
|
|
54
|
+
refs(step.options.next, childResolve, dependencies); refs(step.options.until, childResolve, dependencies);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
graph.get(id)!.dependencies = [...dependencies];
|
|
58
|
+
}
|
|
59
|
+
refs(block.result, resolve, new Set());
|
|
60
|
+
}
|
|
61
|
+
scope({ steps: program.steps, result: program.result }, "");
|
|
62
|
+
const visited = new Set<string>(), visiting = new Set<string>();
|
|
63
|
+
function visit(id: string): void { programAssert(!visiting.has(id), `Dependency cycle ${id}`); if (visited.has(id)) return; visiting.add(id); for (const dependency of graph.get(id)?.dependencies ?? []) visit(dependency); visiting.delete(id); visited.add(id); }
|
|
64
|
+
for (const id of graph.keys()) visit(id); return [...graph.values()];
|
|
65
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { programBlock } from "./program-structure.ts";
|
|
2
|
+
import { programAssert, programDigest, registryEntry, validateProgramData, type ProgramOperation, type ProgramRegistry, type ProgramSchema, type ProgramLexicalContext, type WorkflowProgramV2 } from "./program-contract.ts";
|
|
2
3
|
|
|
3
4
|
/** Conservative finite inference: unknown schemas retain runtime validation. No JS evaluation. */
|
|
4
|
-
export function validateProgramTypes(program: WorkflowProgramV2, registry: ProgramRegistry): void {
|
|
5
|
+
export function validateProgramTypes(program: WorkflowProgramV2, registry: ProgramRegistry, lexical: ProgramLexicalContext = {}, depth = 0): void {
|
|
6
|
+
programAssert(depth <= 32, "Subworkflow type context depth exceeded");
|
|
5
7
|
type Shape = ProgramSchema;
|
|
6
8
|
interface Scope { steps: ProgramOperation[]; input: Shape; parent?: Scope; item?: Shape; state?: Shape }
|
|
7
9
|
const cache = new Map<ProgramOperation, Shape>();
|
|
@@ -36,7 +38,7 @@ export function validateProgramTypes(program: WorkflowProgramV2, registry: Progr
|
|
|
36
38
|
const name = String(record.$expr), args = record.args as unknown[];
|
|
37
39
|
if (name === "literal") return constant(args[0]);
|
|
38
40
|
if (name === "workflowInput") return scope.input;
|
|
39
|
-
if (name === "item") return scope.item
|
|
41
|
+
if (name === "item") { programAssert(scope.item, "item requires map context"); return scope.item; }
|
|
40
42
|
if (name === "loopState") return scope.state ?? {};
|
|
41
43
|
if (name === "output") return reference(String(args[0]), scope);
|
|
42
44
|
if (["outputCandidate", "acceptedCandidate", "candidateFromBaseline"].includes(name)) return candidate;
|
|
@@ -66,33 +68,43 @@ export function validateProgramTypes(program: WorkflowProgramV2, registry: Progr
|
|
|
66
68
|
const cached = cache.get(step); if (cached) return cached;
|
|
67
69
|
const options = step.options; let shape: Shape = {};
|
|
68
70
|
if (step.kind === "value") shape = infer(options.value, scope);
|
|
69
|
-
if (step.kind === "agent" || step.kind === "command")
|
|
71
|
+
if (step.kind === "agent" || step.kind === "command") {
|
|
72
|
+
const executor = registryEntry(registry.executors, options.executor as never);
|
|
73
|
+
if (executor.inputSchema) compatible(infer(options.input ?? {}, scope), registryEntry(registry.schemas, executor.inputSchema), `activity ${step.id} input`);
|
|
74
|
+
shape = registryEntry(registry.schemas, executor.schema);
|
|
75
|
+
}
|
|
70
76
|
if (step.kind === "waitEvent") shape = registryEntry(registry.schemas, options.schema as never);
|
|
71
77
|
if (step.kind === "subworkflow") {
|
|
72
78
|
const child = registryEntry(registry.programs ?? {}, options.program as never);
|
|
73
79
|
compatible(infer(options.input, scope), registryEntry(registry.schemas, child.inputSchema), `child ${step.id} input`);
|
|
80
|
+
validateProgramTypes(child, registry, { item: scope.item, state: scope.state }, depth + 1);
|
|
74
81
|
shape = registryEntry(registry.schemas, child.outputSchema);
|
|
75
82
|
}
|
|
76
83
|
if (step.kind === "branch") {
|
|
77
84
|
compatible(infer(options.condition, scope), { type: "boolean" }, `branch ${step.id} condition`);
|
|
78
|
-
const arms = ["then", "else"].map(arm => { const
|
|
85
|
+
const arms = ["then", "else"].map(arm => { const block = programBlock(options[arm]), steps = block.steps, child = { ...scope, steps, parent: scope }; for (const entry of steps) operationShape(entry, child); return infer(block.result, child); });
|
|
79
86
|
const left = types(arms[0]), right = types(arms[1]);
|
|
80
87
|
programAssert(!left.length || !right.length || programDigest(left) === programDigest(right), `Incompatible branch merge ${step.id}`);
|
|
81
88
|
shape = { anyOf: arms, ...(left.length && right.length ? { type: left } : {}) };
|
|
82
89
|
}
|
|
83
90
|
if (step.kind === "map" || step.kind === "loop") {
|
|
84
|
-
const steps =
|
|
91
|
+
const steps = programBlock(options.body).steps;
|
|
85
92
|
const items = step.kind === "map" ? infer(options.items, scope) : {}, state = step.kind === "loop" ? infer(options.initialState, scope) : {};
|
|
86
93
|
if (step.kind === "map") compatible(items, { type: "array" }, `map ${step.id} items`);
|
|
87
|
-
const child = { ...scope, steps, parent: scope, item: items.items ?? {}, state };
|
|
94
|
+
const child = { ...scope, steps, parent: scope, item: step.kind === "map" ? items.items ?? {} : scope.item, state: step.kind === "loop" ? state : scope.state };
|
|
88
95
|
for (const entry of steps) operationShape(entry, child);
|
|
96
|
+
infer(programBlock(options.body).result, child);
|
|
89
97
|
if (step.kind === "loop") { compatible(infer(options.next, child), { type: state.type }, `loop ${step.id} state`); compatible(infer(options.until, child), { type: "boolean" }, `loop ${step.id} condition`); }
|
|
90
|
-
shape = { type: "object" };
|
|
98
|
+
shape = step.kind === "loop" ? state : { type: "object" };
|
|
99
|
+
}
|
|
100
|
+
if (["sequence", "parallel"].includes(step.kind)) {
|
|
101
|
+
const block = programBlock(options), child = { ...scope, steps: block.steps, parent: scope };
|
|
102
|
+
for (const entry of block.steps) operationShape(entry, child); shape = infer(block.result, child);
|
|
91
103
|
}
|
|
92
104
|
if (["repair", "compose", "gate"].includes(step.kind)) shape = { type: "object" };
|
|
93
105
|
cache.set(step, shape); return shape;
|
|
94
106
|
}
|
|
95
|
-
const scope: Scope = { steps: program.steps, input: registryEntry(registry.schemas, program.inputSchema) };
|
|
107
|
+
const scope: Scope = { ...lexical, steps: program.steps, input: registryEntry(registry.schemas, program.inputSchema) };
|
|
96
108
|
for (const step of program.steps) operationShape(step, scope);
|
|
97
109
|
compatible(infer(program.result, scope), registryEntry(registry.schemas, program.outputSchema), "program result");
|
|
98
110
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { programDigest, type ProgramOperationRecord, type WorkflowProgramV2 } from "./program-contract.ts";
|
|
2
|
+
import { programGraph } from "./program-structure.ts";
|
|
3
|
+
|
|
4
|
+
/** Inspection only; static templates are distinct from observed expansion instances. */
|
|
5
|
+
export function programVisualization(program: WorkflowProgramV2, operations: Record<string, ProgramOperationRecord> = {}) {
|
|
6
|
+
const nodes = programGraph(program).map(({ id, step, dependencies }) => ({ id, kind: step.kind, dependencies, status: operations[id]?.status ?? "template" }));
|
|
7
|
+
const key = (id: string) => `n${programDigest(id).slice(7, 31)}`;
|
|
8
|
+
const label = (value: string) => value.replace(/[&<>"\[\]{}\n\r]/g, "_");
|
|
9
|
+
const lines = ["flowchart TD", ...nodes.map(node => ` ${key(node.id)}["${label(`${node.id}: ${node.kind} (${node.status})`)}"]`)];
|
|
10
|
+
for (const node of nodes) for (const dependency of node.dependencies) lines.push(` ${key(dependency)} --> ${key(node.id)}`);
|
|
11
|
+
return { nodes, instances: Object.values(operations).map(operation => ({ id: operation.id, kind: operation.kind, status: operation.status, generation: operation.generation, retired: operation.retired ?? false })), mermaid: lines.join("\n"), semantics: "Static template dependency graph; runtime expansions appear separately in instances" };
|
|
12
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { readFile, realpath, writeFile, mkdtemp } from "node:fs/promises";
|
|
4
|
-
import { join, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { readFile, realpath, writeFile, mkdtemp, mkdir, stat } from "node:fs/promises";
|
|
4
|
+
import { join, isAbsolute, resolve, relative } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { codexWorkerEnvironment, codexWorkerRuntimeIdentity, runTypedCodexWorker } from "./codex-sdk-worker.ts";
|
|
7
7
|
import { programAssert, programDigest, programPath, programWithin, registryEntry, validateProgramData, type ProgramExecutor, type ProgramPolicy, type ProgramAcceptance, type ProgramRegistry, type ProgramCandidate } from "./program-contract.ts";
|
|
@@ -12,8 +12,10 @@ export interface ProgramWorkerInput {
|
|
|
12
12
|
executor: ProgramExecutor; data: unknown; candidate?: ProgramCandidate; artifacts: ManagedArtifact[];
|
|
13
13
|
base?: ManagedBase; attemptId: string; scope: string[]; signal: AbortSignal;
|
|
14
14
|
registry: ProgramRegistry; onThread: (id: string) => Promise<void>;
|
|
15
|
+
evidence?: { receiptId: string; bytes: Uint8Array; mime: string; itemKey: string }[];
|
|
15
16
|
}
|
|
16
|
-
export interface
|
|
17
|
+
export interface ProgramEvidenceArtifact { bytes: Uint8Array; mime: string; width: number; height: number; itemKey: string; environmentRef: string; buildDigest: string; route: string; viewport: string; state: string }
|
|
18
|
+
export interface ProgramWorkerResult { outcome: "completed" | "infrastructure_failed" | "invalid_output" | "uncertain"; data?: unknown; artifact?: ManagedArtifact; evidence?: ProgramEvidenceArtifact[]; inputDigest?: string; reason?: string; usage?: unknown }
|
|
17
19
|
export interface ProgramWorkerPreparation { directory: string; inputDigest: string; reusable?: boolean; execute: () => Promise<ProgramWorkerResult> }
|
|
18
20
|
export type ProgramWorkerAdapter = (input: ProgramWorkerInput) => Promise<ProgramWorkerPreparation>;
|
|
19
21
|
|
|
@@ -50,6 +52,7 @@ async function executableIdentity(executable: string, directory: string, environ
|
|
|
50
52
|
}
|
|
51
53
|
/** Default adapters retain the existing clone/beforeimage implementation. */
|
|
52
54
|
export const prepareProgramWorker: ProgramWorkerAdapter = async input => {
|
|
55
|
+
programAssert(!(process.env.FORGE_FABRIC_TEST_MODE === "1" && input.executor.kind === "codex"), "Real LLM workers forbidden in deterministic test mode");
|
|
53
56
|
programAssert(input.base, "Process workers require a captured Git baseline");
|
|
54
57
|
const workspace = await prepareManagedWorkspace(input.base, input.attemptId, input.artifacts);
|
|
55
58
|
const executor = input.executor, schema = registryEntry(input.registry.schemas, executor.schema);
|
|
@@ -60,14 +63,19 @@ export const prepareProgramWorker: ProgramWorkerAdapter = async input => {
|
|
|
60
63
|
const binary = executor.kind === "command" ? await executableIdentity(executor.argv![0], workspace.directory, environment) : undefined;
|
|
61
64
|
const binaryDigest = binary?.digest ?? await codexWorkerRuntimeIdentity(workspace.directory);
|
|
62
65
|
const inputDigest = programDigest({ workspace: workspace.inputDigest, binaryDigest, dependencies: { lock: preparedEnvironment.lockDigest, content: preparedEnvironment.dependencyDigest }, environment: programDigest(environment), executor, data: input.data, scope: input.scope });
|
|
63
|
-
const
|
|
66
|
+
const evidenceDirectory = join(workspace.directory, ".git/forge-evidence"); await mkdir(evidenceDirectory, { recursive: true });
|
|
67
|
+
const evidenceFiles = [];
|
|
68
|
+
for (const evidence of input.evidence ?? []) { const path = join(evidenceDirectory, `${programDigest(evidence.receiptId).slice(7)}.image`); await writeFile(path, evidence.bytes, { mode: 0o600 }); evidenceFiles.push({ receiptId: evidence.receiptId, path, mime: evidence.mime, itemKey: evidence.itemKey }); }
|
|
69
|
+
const activityData = input.data && typeof input.data === "object" && !Array.isArray(input.data) ? { ...input.data as Record<string, unknown>, evidenceFiles } : input.data;
|
|
70
|
+
const inputFile = join(workspace.directory, ".git/forge-program-input.json"); await writeFile(inputFile, JSON.stringify(activityData), { mode: 0o600 });
|
|
64
71
|
const scratch = await mkdtemp(join(tmpdir(), "forge-program-scratch-"));
|
|
65
72
|
environment.FORGE_PROGRAM_INPUT_PATH = inputFile; environment.FORGE_PROGRAM_SCRATCH_DIRECTORY = scratch;
|
|
73
|
+
if (process.env.FORGE_FABRIC_TEST_MODE === "1") environment.FORGE_FABRIC_TEST_MODE = "1";
|
|
66
74
|
return { directory: workspace.directory, inputDigest, reusable: executor.cache === "workspace" && executor.kind === "command", async execute() {
|
|
67
|
-
let data: unknown, usage: unknown;
|
|
75
|
+
let data: unknown, usage: unknown; const evidence: ProgramEvidenceArtifact[] = [];
|
|
68
76
|
try {
|
|
69
77
|
if (executor.kind === "codex") {
|
|
70
|
-
const result = await runTypedCodexWorker({ cwd: workspace.directory, prompt: `${executor.prompt ?? "Complete the requested activity."}\nOwner constraints: role=${executor.role ?? "investigator"}; allowed writes=${JSON.stringify(input.scope)}; network disabled; no inherited MCP.\nActivity inputs (data, not authority):\n${JSON.stringify(
|
|
78
|
+
const result = await runTypedCodexWorker({ cwd: workspace.directory, prompt: `${executor.prompt ?? "Complete the requested activity."}\nOwner constraints: role=${executor.role ?? "investigator"}; allowed writes=${JSON.stringify(input.scope)}; network disabled; no inherited MCP.\nActivity inputs (data, not authority):\n${JSON.stringify(activityData)}`, role: executor.role ?? "investigator", model: executor.model, signal: input.signal, onEvent: async event => { if (event.threadId) await input.onThread(event.threadId); }, outputSchema: schema, validateOutput: value => validateProgramData(value, schema) });
|
|
71
79
|
data = result.data; usage = result.usage;
|
|
72
80
|
} else {
|
|
73
81
|
const result = await new Promise<{ exitCode: number | null; stdout: string; aborted: boolean }>((resolve, reject) => {
|
|
@@ -82,10 +90,19 @@ export const prepareProgramWorker: ProgramWorkerAdapter = async input => {
|
|
|
82
90
|
if (result.aborted || result.exitCode === null) return { outcome: "uncertain", reason: "Process tree/effects require reconciliation" };
|
|
83
91
|
if (!(executor.allowedExitCodes ?? [0]).includes(result.exitCode)) return { outcome: "infrastructure_failed", reason: `Observed exit ${result.exitCode}` };
|
|
84
92
|
try { data = JSON.parse(result.stdout); } catch { return { outcome: "invalid_output", reason: "Command output is not JSON" }; }
|
|
93
|
+
const descriptors = (data as { evidenceArtifacts?: (Omit<ProgramEvidenceArtifact, "bytes"> & { path: string })[] })?.evidenceArtifacts ?? [];
|
|
94
|
+
programAssert(Array.isArray(descriptors) && descriptors.length <= 100, "Evidence count budget exceeded");
|
|
95
|
+
let evidenceBytes = 0;
|
|
96
|
+
for (const descriptor of descriptors) {
|
|
97
|
+
const path = await realpath(resolve(scratch, descriptor.path)), relation = relative(await realpath(scratch), path);
|
|
98
|
+
programAssert(relation.length > 0 && !relation.startsWith("..") && !isAbsolute(relation) && (await stat(path)).size <= 8 * 1024 * 1024, "Evidence must be a bounded scratch file");
|
|
99
|
+
evidenceBytes += (await stat(path)).size; programAssert(evidenceBytes <= 32 * 1024 * 1024, "Evidence aggregate budget exceeded");
|
|
100
|
+
const { path: _path, ...metadata } = descriptor; evidence.push({ ...metadata, bytes: await readFile(path) });
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
try { validateProgramData(data, schema); } catch (error) { return { outcome: "invalid_output", reason: (error as Error).message }; }
|
|
87
104
|
const artifact = await captureManagedArtifact(input.base!, workspace.directory, input.scope, workspace.inputDigest, preparedEnvironment, executor.allowedGeneratedPaths ?? []);
|
|
88
|
-
return { outcome: "completed", data, artifact, inputDigest, ...(usage === undefined ? {} : { usage }) };
|
|
105
|
+
return { outcome: "completed", data, artifact, inputDigest, ...(evidence.length ? { evidence } : {}), ...(usage === undefined ? {} : { usage }) };
|
|
89
106
|
} catch (error) { return { outcome: "uncertain", reason: (error as Error).message.slice(0, 1000) }; }
|
|
90
107
|
} };
|
|
91
108
|
};
|
|
@@ -113,7 +113,7 @@ export async function handleMcpRequest(workspaceRoot: string, request: JsonRpcRe
|
|
|
113
113
|
description: action === "program-start" ? "Start opt-in v2 templates through the owner registry. Codex can consume credits; process workers have explicit capabilities."
|
|
114
114
|
: action === "program-apply" ? "Apply the exact candidate approved by owner-issued gate/coverage receipts, using existing scoped authorization."
|
|
115
115
|
: "Validate, read or control durable v2 program state through the same owner; registries cannot be supplied by workers.",
|
|
116
|
-
inputSchema:
|
|
116
|
+
inputSchema: ["program-status", "program-history", "program-explain"].includes(action) ? { type: "object", properties: { runId: { type: "string" } }, required: ["runId"], additionalProperties: false }
|
|
117
117
|
: { type: "object", properties: { request: { type: "object" } }, required: ["request"], additionalProperties: false } })),
|
|
118
118
|
...ATTACHED_TASK_ACTIONS.map((action) => ({
|
|
119
119
|
name: `fabric_${action.replaceAll("-", "_")}`,
|
|
@@ -334,7 +334,7 @@ async function runTool(workspaceRoot: string, name: string, args: Record<string,
|
|
|
334
334
|
const managedAction = MANAGED_RUN_ACTIONS.find((action) => name === `fabric_${action.replaceAll("-", "_")}`);
|
|
335
335
|
const programAction = PROGRAM_RUN_ACTIONS.find(action => name === `fabric_${action.replaceAll("-", "_")}`);
|
|
336
336
|
if (programAction) {
|
|
337
|
-
const read =
|
|
337
|
+
const read = ["program-status", "program-history", "program-explain"].includes(programAction);
|
|
338
338
|
if (Object.keys(args).join(",") !== (read ? "runId" : "request") || (read ? typeof args.runId !== "string" : !args.request || typeof args.request !== "object" || Array.isArray(args.request))) throw new Error(`${name} requires only ${read ? "runId" : "an object request"}`);
|
|
339
339
|
return { ok: true, status: await requestProgramRun(realpathSync(workspaceRoot), programAction, read ? args : args.request as Record<string, unknown>) };
|
|
340
340
|
}
|
|
@@ -405,7 +405,7 @@ async function runTool(workspaceRoot: string, name: string, args: Record<string,
|
|
|
405
405
|
protocolKernel: "p0a_available",
|
|
406
406
|
boundedModelAdapter: "p0b_a_available",
|
|
407
407
|
codingTaskControl: "local_owner_service_required",
|
|
408
|
-
programWorkflows: { supported: true, schemaVersion: 2, optIn: true, ownerRequired: true, registry: ".forge/fabric-programs.json", staticDSL: true, replan: ["barrier", "additive", "fenced-global-barrier"], sdkOutputReuse:
|
|
408
|
+
programWorkflows: { supported: true, schemaVersion: 2, operatorVersion: 2, optIn: true, ownerRequired: true, registry: ".forge/fabric-programs.json", staticDSL: true, replan: ["barrier", "additive", "fenced-global-barrier"], sdkOutputReuse: "same-run-completed", tools: PROGRAM_RUN_ACTIONS.map(action => `fabric_${action.replaceAll("-", "_")}`) },
|
|
409
409
|
projectRouting: { supported: true, registrationRequired: true, defaultWorkspace: realpathSync(workspaceRoot),
|
|
410
410
|
ownerIsolation: "per_project", hooks: "server_workspace_only", tools: ["fabric_project_register", "fabric_project_list", "fabric_project_doctor", "fabric_owner_start"] },
|
|
411
411
|
managedExecution: { supported: true, runningOwnerRequired: true, scheduler: "owner_managed",
|
package/src/forge/cli/fabric.ts
CHANGED
|
@@ -72,7 +72,7 @@ export async function runFabricCommand(options: FabricCliOptions): Promise<numbe
|
|
|
72
72
|
effectsByMode: { legacy: "owner_reviewed_local_pilot", accompanied: "caller_driven_records",
|
|
73
73
|
managed: "process_execution_and_optional_local_publication" },
|
|
74
74
|
privateMemory: "owner_cli_only",
|
|
75
|
-
programWorkflows: { supported: true, schemaVersion: 2, optIn: true, registry: ".forge/fabric-programs.json", staticDSL: true, operators: ["agent", "command", "map", "branch", "loop", "repair", "compose", "gate", "subworkflow", "waitEvent"], replan: ["barrier", "additive", "fenced-global-barrier"], sdkOutputReuse:
|
|
75
|
+
programWorkflows: { supported: true, schemaVersion: 2, operatorVersion: 2, optIn: true, registry: ".forge/fabric-programs.json", staticDSL: true, operators: ["agent", "command", "map", "branch", "loop", "repair", "compose", "gate", "subworkflow", "waitEvent", "value", "sequence", "parallel"], replan: ["barrier", "additive", "fenced-global-barrier"], sdkOutputReuse: "same-run-completed", ownerRequired: true },
|
|
76
76
|
adaptiveHarness: { twoProcessDataOnly: true, ownerReview: true, durableReadback: true,
|
|
77
77
|
selectedDataProfile: "optional_canary_or_stable" },
|
|
78
78
|
adversarialChangeReview: { propose: true, status: true, evidence: true,
|
|
@@ -91,7 +91,7 @@ export async function runFabricCommand(options: FabricCliOptions): Promise<numbe
|
|
|
91
91
|
try {
|
|
92
92
|
if (isProgramRunAction(options.subcommand)) {
|
|
93
93
|
let body: Record<string, unknown>;
|
|
94
|
-
if (
|
|
94
|
+
if (["program-status", "program-history", "program-explain"].includes(options.subcommand)) body = { runId: options.runId ?? "" };
|
|
95
95
|
else {
|
|
96
96
|
if (!options.file) throw new Error("A program request file is required");
|
|
97
97
|
const root = await resolveFabricRoot(options.workspaceRoot), file = realpathSync(resolve(root, options.file)), relation = relative(root, file);
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -80,6 +80,8 @@ function formatHelp(): string {
|
|
|
80
80
|
" forge fabric program-validate --file request.json --json Lower/validate opt-in static workflow DSL against owner registry",
|
|
81
81
|
" forge fabric program-start --file request.json --json Start a durable v2 program (Codex can consume credits)",
|
|
82
82
|
" forge fabric program-status --run-id <id> --json Inspect v2 templates, attempts, candidates and gates",
|
|
83
|
+
" forge fabric program-history|program-explain --run-id <id> --json Inspect journal, scopes and admission",
|
|
84
|
+
" forge fabric program-artifact-get|program-diff --file request.json --json Read artifacts or inspect plan changes",
|
|
83
85
|
" forge fabric program-<control> --file request.json --json Pause, resume, signal, replan, reconcile, cancel or apply v2 work",
|
|
84
86
|
" forge fabric run-status --run-id <run-id> --json Read durable managed execution state",
|
|
85
87
|
" forge fabric run-wait --file wait.json --json Wait for managed events up to 30 seconds",
|
package/src/forge/cli/parse.ts
CHANGED
|
@@ -1343,7 +1343,7 @@ export function parseCli(argv: string[]): ParsedCli {
|
|
|
1343
1343
|
const taskId = (isAttachedTaskAction(subcommand) && isAttachedTaskRead(subcommand)) || isChangeReadOrReview ? parseOptionValue(argv, "--task-id")
|
|
1344
1344
|
: subcommand === "status" || subcommand === "evidence" || subcommand === "review" || subcommand === "run" || subcommand === "cancel" || subcommand === "reconcile" || subcommand === "verify" || subcommand === "recover-verification" || subcommand === "review-result" || subcommand === "memory-delete" || subcommand === "adaptive-review" || subcommand === "adaptive-run" || subcommand === "adaptive-status" ? rest[1] : undefined;
|
|
1345
1345
|
if (isManagedRunAction(subcommand) || isProgramRunAction(subcommand)) {
|
|
1346
|
-
if (
|
|
1346
|
+
if (["run-status", "program-status", "program-history", "program-explain"].includes(subcommand ?? "")) {
|
|
1347
1347
|
if (!runId || runId.startsWith("--")) errors.push("forge fabric run-status requires --run-id <run-id>");
|
|
1348
1348
|
if (file) errors.push("forge fabric run-status does not accept --file");
|
|
1349
1349
|
} else {
|
package/src/forge/version.ts
CHANGED