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.
Files changed (41) hide show
  1. package/.agents/skills/forge-agent-fabric/SKILL.md +10 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +6 -0
  4. package/docs/agent-fabric-programs.md +151 -168
  5. package/docs/agent-fabric.md +4 -6
  6. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -41
  7. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
  8. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
  9. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -109
  10. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +463 -423
  11. package/examples/agent-fabric-v2/README.md +16 -0
  12. package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
  13. package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
  14. package/examples/agent-fabric-v2/migrate.workflow.ts +1 -1
  15. package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
  16. package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
  17. package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
  18. package/examples/agent-fabric-v2/visual-population.json +13 -0
  19. package/package.json +3 -1
  20. package/src/forge/_generated/releaseManifest.json +1 -1
  21. package/src/forge/_generated/releaseManifest.ts +3 -3
  22. package/src/forge/agent-fabric/codex-sdk-worker.ts +2 -0
  23. package/src/forge/agent-fabric/program-api.ts +37 -0
  24. package/src/forge/agent-fabric/program-contract.ts +89 -35
  25. package/src/forge/agent-fabric/program-dsl.ts +45 -31
  26. package/src/forge/agent-fabric/program-evidence.ts +22 -0
  27. package/src/forge/agent-fabric/program-image.ts +15 -0
  28. package/src/forge/agent-fabric/program-lock.ts +50 -0
  29. package/src/forge/agent-fabric/program-owner.ts +16 -0
  30. package/src/forge/agent-fabric/program-scheduler.ts +38 -0
  31. package/src/forge/agent-fabric/program-service.ts +275 -101
  32. package/src/forge/agent-fabric/program-store.ts +80 -55
  33. package/src/forge/agent-fabric/program-structure.ts +65 -0
  34. package/src/forge/agent-fabric/program-types.ts +21 -9
  35. package/src/forge/agent-fabric/program-view.ts +12 -0
  36. package/src/forge/agent-fabric/program-worker.ts +24 -7
  37. package/src/forge/agent-memory/mcp.ts +3 -3
  38. package/src/forge/cli/fabric.ts +2 -2
  39. package/src/forge/cli/main.ts +2 -0
  40. package/src/forge/cli/parse.ts +1 -1
  41. package/src/forge/version.ts +1 -1
@@ -52,3 +52,19 @@ The owner registry is project configuration, not workflow input. `cache: none` i
52
52
  default. Only commands declared to depend entirely on their captured workspace inputs
53
53
  may opt into `cache: workspace`; the owner must account for external inputs. SDK output
54
54
  reuse is disabled until its effective instruction closure can be attested.
55
+
56
+ ## Current workflow runtime and UI example
57
+
58
+ `ui-audit.workflow.ts` uses the current operatorVersion 2 architecture directly. `run-ui-fixture.ts` is a disposable local integration example: twenty HTML pages, three known defects, command-only agents, item and final checks, capture receipts, compose and gate. Run `node bin/forge-bun.mjs run examples/agent-fabric-v2/run-ui-fixture.ts` from the Forge checkout. It sets the no-LLM fuse and never applies to a user project.
59
+
60
+ `runUiFixture(count, defects, browserPath?)` also accepts an explicit Chromium/Edge executable for real local headless screenshots; the default uses valid PNG fixtures. Both reviewers are deterministic checks of fixture attributes, not visual AI evaluation. The worker's cooperative process capability must be owner-authorized.
61
+
62
+ The static lowerer and TS author API share the same example including `as const`, `satisfies`, typed refs and scoped checks. Arbitrary TS execution is unsupported. Legacy file-conversion example remains a separate usage example, not a compatibility interpreter. See docs/agent-fabric-programs.md for all fifty author constructors and fifteen controls.
63
+
64
+ ## Typed reusable subworkflow and visual cases
65
+
66
+ `item-child.workflow.ts` is a finite typed inspection program. Lower it and register its IR as `inspect-item@v2`; the root `map-child.workflow.ts` calls it with explicit input and a map quota of one. Supply owner schemas `path@v2` (object path:string), `inspection@v2` (object ok:boolean), `any@v2`, policy `local@v2`, acceptance `data@v2`, and executor `inspect@v2` with inputSchema path@v2/output schema inspection@v2. These are templates, not automatically installed executors. Child quotas/cancellation inherit from the root; keep the same policy/acceptance.
67
+
68
+ `visual-population.json` demonstrates separate desktop/ready and mobile/error cases for one route. Replace the baseline placeholder with the owner-captured digest and enumerate all required scenarios. Matching itemKey alone cannot close a visual case when route, viewport, state or dimensions differ. Image header validation is not proof of visual correctness.
69
+
70
+ Use program-explain to retrieve the template graph, observed expansions and a Mermaid diagram. Format 3 runs live in program-runs-v3; older checkpoint directories are preserved and not loaded.
@@ -0,0 +1,11 @@
1
+ import { defineWorkflow, schemaRef, policyRef, acceptanceRef, executorRef, workflowInput, agent, output } from "forgeos/agent-fabric/workflows";
2
+
3
+ // Register this IR as inspect-item@v2. Owner schemas/executor remain authoritative.
4
+ const inspect = agent("inspect", {
5
+ executor: executorRef<{ path: string }, { ok: boolean }>("inspect", "v2"),
6
+ input: workflowInput<{ path: string }>()
7
+ });
8
+ export default defineWorkflow({
9
+ id: "inspect-item", version: 2, mode: "data", inputSchema: schemaRef("path", "v2"), outputSchema: schemaRef("inspection", "v2"),
10
+ policy: policyRef("local", "v2"), acceptance: acceptanceRef("data", "v2"), steps: [inspect], result: output(inspect)
11
+ });
@@ -0,0 +1,10 @@
1
+ import { defineWorkflow, schemaRef, policyRef, acceptanceRef, programRef, map, subworkflow, item, field, output } from "forgeos/agent-fabric/workflows";
2
+
3
+ export default defineWorkflow({
4
+ id: "inspect-files", version: 2, mode: "data", inputSchema: schemaRef("any", "v2"), outputSchema: schemaRef("any", "v2"),
5
+ policy: policyRef("local", "v2"), acceptance: acceptanceRef("data", "v2"),
6
+ steps: [map("files", {
7
+ items: [{ path: "src/a.ts" }, { path: "src/b.ts" }], key: field(item(), "path"), concurrency: 1, completion: "all-required",
8
+ body: subworkflow("inspect", { program: programRef<{ path: string }, { ok: boolean }>("inspect-item", "v2"), input: item<{ path: string }>() })
9
+ })], result: output("files")
10
+ });
@@ -1,7 +1,7 @@
1
1
  import { defineWorkflow, schemaRef, policyRef, acceptanceRef, populationRef, executorRef, recipeRef, agent, map, field, item, output, population, coverageFor, repair, candidateFromBaseline, compose, acceptedCandidates, outputCandidate, acceptedCandidate, gate, coverageReceipt } from "forgeos/agent-fabric/workflows";
2
2
 
3
3
  const recipe = {
4
- recipe: recipeRef("repair", "v1"),
4
+ recipe: recipeRef("repair", "v2"),
5
5
  implement: executorRef("migrate", "v1"), review: executorRef("review", "v1"),
6
6
  checks: ["check@v1"], maxRepairRounds: 3, maxAssessmentAttempts: 6,
7
7
  maxInfrastructureAttempts: 2,
@@ -0,0 +1,48 @@
1
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
2
+ import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ProgramRunService } from "../../src/forge/agent-fabric/program-service.ts";
7
+ import { programDigest, type ProgramRegistry, type ProgramRunV2 } from "../../src/forge/agent-fabric/program-contract.ts";
8
+ import { lowerWorkflowSource, schemaRef } from "../../src/forge/agent-fabric/program-dsl.ts";
9
+
10
+ export async function runUiFixture(count = 20, defects = [2, 9, 17], browserPath?: string) {
11
+ const root = await mkdtemp(join(tmpdir(), "forge-ui-fixture-"));
12
+ let service: ProgramRunService | undefined;
13
+ const startedAt = performance.now();
14
+ try {
15
+ await mkdir(join(root, "src")); await mkdir(join(root, ".forge"));
16
+ for (let index = 0; index < count; index++) await writeFile(join(root, "src", `ui-${String(index).padStart(2, "0")}.html`), `<!doctype html><html><title>UI ${index}</title><body data-fixture="${defects.includes(index) ? "bad" : "good"}"><h1>Local fixture ${index}</h1></body></html>`);
17
+ for (const args of [["init", "--quiet"], ["add", "src"], ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--quiet", "-m", "UI fixture baseline"]]) {
18
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", windowsHide: true }); if (result.status) throw new Error(result.stderr);
19
+ }
20
+ const registry: ProgramRegistry = { schemas: { "any@v2": {} }, executors: {}, policies: {}, acceptance: {}, populations: {} };
21
+ const worker = fileURLToPath(new URL("./ui-fixture-worker.cjs", import.meta.url));
22
+ for (const id of ["discover", "capture", "implement", "review", "local-check", "global-check"]) registry.executors[`${id}@v2`] = {
23
+ id, version: "v2", kind: "command", effect: id === "implement" ? "isolated-write" : "read", role: id === "implement" ? "implementer" : id === "review" ? "reviewer" : "investigator",
24
+ argv: [process.execPath, worker, id, ...(id === "capture" && browserPath ? [browserPath] : [])], timeoutMs: id === "capture" && browserPath ? 120000 : 10000, writeScope: id === "implement" ? ["src"] : [], network: "host", isolation: "cooperative", schema: schemaRef("any", "v2")
25
+ };
26
+ registry.policies["local@v2"] = { id: "local", version: "v2", concurrency: 4, maxItems: 100, maxAttempts: 500, maxOperations: 2000, maxDepth: 12, deadlineMs: 240000, maxOutputBytes: 4 * 1024 * 1024, writeScope: ["src"], executors: Object.keys(registry.executors), allowNetwork: true, allowCooperativeCommands: true };
27
+ registry.acceptance["ui@v2"] = { id: "ui", version: "v2", criteria: ["Known fixture attributes are correct"], requireReview: true, allowNoWork: false, writeScope: ["src"], requiredChecks: [],
28
+ requiredChecksByScope: { item: ["local-check@v2"], final: ["global-check@v2"] }, assessmentBindings: { page: "item", final: "final" },
29
+ obligations: [{ id: "layout", scope: "item", criteria: ["known fixture constraint"], requiredEvidence: ["capture"] }, { id: "integration", scope: "final", criteria: ["all pages correct together"] }] };
30
+ registry.populations["pages@v2"] = { id: "pages", version: "v2", members: [], baselineDigest: "owner-capture", inventoryRoots: ["src"], extensions: [".html"], exclusions: [], allowNoWork: false, evidence: "owner filesystem inventory" };
31
+ await writeFile(join(root, ".forge/fabric-programs.json"), JSON.stringify(registry));
32
+ const source = await readFile(new URL("./ui-audit.workflow.ts", import.meta.url), "utf8"), program = lowerWorkflowSource(source);
33
+ service = await ProgramRunService.open(root);
34
+ const initial = await service.execute("program-start", { requestId: "ui-fixture", program, input: {} }) as ProgramRunV2;
35
+ let run: ProgramRunV2;
36
+ do { await new Promise(resolve => setTimeout(resolve, 20)); run = (await service.store.read(initial.runId))!; } while (run.status === "executing");
37
+ if (run.status !== "acceptance-ready") throw new Error(`UI fixture failed: ${run.status}: ${run.reason}`);
38
+ const assessment = Object.values(run.assessments).find(receipt => receipt.phase === "final")!;
39
+ const implementations = Object.values(run.attempts).filter(attempt => attempt.operationId.endsWith("/implement"));
40
+ const beforeApply = await readFile(join(root, "src", `ui-${String(defects[0] ?? 0).padStart(2, "0")}.html`), "utf8");
41
+ const report = { datasetDigest: programDigest({ count, defects }), pages: count, knownDefects: defects.length, implementations: implementations.length, attempts: run.totalAttempts,
42
+ captures: Object.keys(run.artifactRecords ?? {}).length, finalObligations: assessment.satisfiedObligationIds?.length, finalCaptures: assessment.evidenceRefs?.length,
43
+ destinationPreserved: beforeApply.includes(`data-fixture="${defects.length ? "bad" : "good"}"`), llmCalls: 0, elapsedMs: Math.round(performance.now() - startedAt), storage: service.store.metrics,
44
+ captureMode: browserPath ? "local-headless-browser" : "png-fixture", limitations: [browserPath ? "Local browser rendering; no visual model evaluation" : "PNG fixtures; no browser rendering or visual model evaluation", "Cooperative command isolation", "No automatic apply; no Claude benchmark"] };
45
+ return report;
46
+ } finally { await service?.close(); await rm(root, { recursive: true, force: true }); }
47
+ }
48
+ if (import.meta.main) { process.env.FORGE_FABRIC_TEST_MODE = "1"; console.log(JSON.stringify(await runUiFixture(), null, 2)); }
@@ -0,0 +1,28 @@
1
+ import { defineWorkflow, schemaRef, executorRef, policyRef, acceptanceRef, populationRef, recipeRef, agent, map, repair, compose, gate, field, item, output, population, coverageFor, candidateFromBaseline, acceptedCandidates, outputCandidate, acceptedCandidate, coverageReceipt, type WorkflowOptions } from "forgeos/agent-fabric/workflows";
2
+
3
+ const repairPolicy = {
4
+ recipe: recipeRef("repair", "v2"), implement: executorRef("implement", "v2"), review: executorRef("review", "v2"), evidence: [executorRef("capture", "v2")],
5
+ entryMode: "assess-first", maxRepairRounds: 3, maxAssessmentAttempts: 6, maxInfrastructureAttempts: 2,
6
+ progressPolicy: { unchangedCandidateRounds: 2, repeatedFindingsRounds: 2 }
7
+ } as const;
8
+ const itemRepair = {
9
+ ...repairPolicy, evidence: [executorRef("capture", "v2")], assessmentScope: "item", checks: ["local-check@v2"], initialCandidate: candidateFromBaseline(item()), writeScope: field(item(), "allowedPaths")
10
+ } satisfies WorkflowOptions["repair"];
11
+
12
+ export default defineWorkflow({
13
+ id: "ui-audit", version: 1, operatorVersion: 2, mode: "candidate", inputSchema: schemaRef("any", "v2"), outputSchema: schemaRef("any", "v2"),
14
+ policy: policyRef("local", "v2"), acceptance: acceptanceRef("ui", "v2"), population: populationRef("pages", "v2"),
15
+ steps: [
16
+ agent("discover", { executor: executorRef("discover", "v2"), input: {} }),
17
+ map("pages", {
18
+ items: field(output("discover"), "items"), key: field(item(), "id"), coverage: coverageFor(population(), output("discover")), completion: "all-required", concurrency: 4,
19
+ body: repair("page", itemRepair)
20
+ }),
21
+ compose("integrated", { candidates: acceptedCandidates("pages"), onConflict: "needs-resolution" }),
22
+ repair("final", {
23
+ ...repairPolicy, evidence: [executorRef("capture", "v2")], assessmentScope: "final", checks: ["global-check@v2"],
24
+ initialCandidate: outputCandidate("integrated"), writeScope: ["src"]
25
+ }),
26
+ gate("ready", { candidate: acceptedCandidate("final"), coverage: coverageReceipt("pages") })
27
+ ], result: output("ready")
28
+ });
@@ -0,0 +1,36 @@
1
+ // Deterministic fixture worker. Does not call an LLM or make network requests.
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const { pathToFileURL } = require('node:url');
6
+ const role = process.argv[2];
7
+ const data = JSON.parse(fs.readFileSync(process.env.FORGE_PROGRAM_INPUT_PATH, 'utf8'));
8
+ const context = data.assessmentContext;
9
+ const inventory = () => fs.readdirSync('src').filter(name => name.endsWith('.html')).sort().map(name => ({ id: 'src/' + name, allowedPaths: ['src/' + name] }));
10
+ const items = data.workItem ? [data.workItem] : inventory();
11
+ const invalid = () => items.filter(item => !fs.readFileSync(item.id, 'utf8').includes('data-fixture="good"'));
12
+ let result;
13
+ if (role === 'discover') result = { items: inventory() };
14
+ else if (role === 'implement') {
15
+ for (const item of invalid()) fs.writeFileSync(item.id, fs.readFileSync(item.id, 'utf8').replace('data-fixture="bad"', 'data-fixture="good"'));
16
+ result = { summary: 'Repaired known fixture attributes' };
17
+ } else if (role === 'review') {
18
+ const findings = invalid().map(item => ({ itemKey: item.id, reason: 'Known deterministic fixture defect' }));
19
+ result = { verdict: findings.length ? 'changes_requested' : 'approved', findings, coveredObligationIds: context.obligationIds };
20
+ } else if (role === 'local-check' || role === 'global-check') result = { passed: invalid().length === 0 };
21
+ else if (role === 'capture') {
22
+ // A valid PNG fixture proves artifact transport/binding, not browser rendering.
23
+ const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aGfoAAAAASUVORK5CYII=', 'base64');
24
+ result = { evidenceArtifacts: items.map((item, index) => {
25
+ const name = 'capture-' + index + '.png', file = path.join(process.env.FORGE_PROGRAM_SCRATCH_DIRECTORY, name);
26
+ const browser = process.argv[3];
27
+ if (browser) {
28
+ const profile = path.join(process.env.FORGE_PROGRAM_SCRATCH_DIRECTORY, 'profile-' + index);
29
+ const rendered = spawnSync(browser, ['--headless', '--disable-gpu', '--disable-extensions', '--disable-background-networking', '--no-first-run', '--no-default-browser-check', '--hide-scrollbars', '--window-size=800,600', '--user-data-dir=' + profile, '--screenshot=' + file, pathToFileURL(path.resolve(item.id)).href], { windowsHide: true, timeout: 15000, stdio: 'pipe' });
30
+ if (rendered.error || rendered.status !== 0 || !fs.existsSync(file)) throw new Error('Headless browser capture failed');
31
+ } else fs.writeFileSync(file, bytes);
32
+ const image = fs.readFileSync(file), width = image.readUInt32BE(16), height = image.readUInt32BE(20);
33
+ return { path: name, itemKey: item.id, buildDigest: data.candidateDigest, environmentRef: context.environmentRef, mime: 'image/png', width, height, route: '/' + item.id, viewport: width + 'x' + height, state: browser ? 'local-browser-render' : 'deterministic-fixture' };
34
+ }) };
35
+ } else throw new Error('Unknown fixture role');
36
+ console.log(JSON.stringify(result));
@@ -0,0 +1,13 @@
1
+ {
2
+ "id": "checkout-cases",
3
+ "version": "v2",
4
+ "members": ["checkout-desktop-ready", "checkout-mobile-error"],
5
+ "exclusions": [],
6
+ "baselineDigest": "OWNER_MUST_SUPPLY_CAPTURED_BASELINE_DIGEST",
7
+ "evidence": "Owner catalog of required checkout scenarios; extend explicitly for every route, viewport and state",
8
+ "allowNoWork": false,
9
+ "visualCases": [
10
+ { "itemKey": "checkout-desktop-ready", "route": "/checkout", "viewport": "desktop", "state": "ready", "width": 1440, "height": 900 },
11
+ { "itemKey": "checkout-mobile-error", "route": "/checkout", "viewport": "mobile", "state": "error", "width": 390, "height": 844 }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.70",
3
+ "version": "0.1.0-alpha.71",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,6 +26,8 @@
26
26
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md",
27
27
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md",
28
28
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md",
29
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md",
30
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md",
29
31
  "examples/go-billing/",
30
32
  "examples/agent-fabric-v2/",
31
33
  "examples/java-billing/",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.70","releaseId":"forgeos@0.1.0-alpha.70+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.71","releaseId":"forgeos@0.1.0-alpha.71+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.70 input=c9c2b146d0cb312a96b8d2ba635c2e26634645a24895b1d003e2f65ae17a3e02 content=0b7cd4e138639e7ccdf2e6723e47f5d231465b6da9b553bb37ba21b68ceb2c5b
1
+ // @forge-generated generator=0.1.0-alpha.71 input=c1757bb141bbcdd4d235a6d1af230b3a767ec7e4354f7fc3afb49fd46e893f06 content=cd16693ecfd3d8410340a10ca8d2fbd0c9a99361acd460af97c00a8c58a5032d
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.70",
23
- "releaseId": "forgeos@0.1.0-alpha.70+unknown",
22
+ "packageVersion": "0.1.0-alpha.71",
23
+ "releaseId": "forgeos@0.1.0-alpha.71+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -165,6 +165,7 @@ async function runCodexWorkerInternal(input: CodexWorkerInput, factory: CodexWor
165
165
  }
166
166
 
167
167
  export async function runCodexWorker(input: CodexWorkerInput, factory?: CodexWorkerFactory): Promise<CodexWorkerOutput> {
168
+ if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
168
169
  try { return await runCodexWorkerInternal(input, factory ?? ((options) => isolatedCodexClient(options, input.cwd, input.signal))); }
169
170
  catch (error) {
170
171
  if (error instanceof CodexWorkerError) throw error;
@@ -196,6 +197,7 @@ async function runTypedCodexWorkerInternal(input: CodexWorkerInput & { outputSch
196
197
  return { threadId, data, ...(observedUsage ? { usage: observedUsage } : {}) };
197
198
  }
198
199
  export async function runTypedCodexWorker(input: CodexWorkerInput & { outputSchema: unknown; validateOutput: (data: unknown) => void }, factory?: CodexWorkerFactory): Promise<{ threadId: string; data: unknown; usage?: CodexWorkerUsage }> {
200
+ if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
199
201
  try { return await runTypedCodexWorkerInternal(input, factory); }
200
202
  catch (error) { if (error instanceof CodexWorkerError) throw error; fail("AF_CODEX_EXECUTION", "Typed SDK or persistence failed; reconcile before retry"); }
201
203
  }
@@ -0,0 +1,37 @@
1
+ import type { ProgramExpr, ProgramOperation, ProgramRef, WorkflowProgramV2 } from "./program-contract.ts";
2
+ import type { ProgramBlock } from "./program-structure.ts";
3
+
4
+ declare const registryKind: unique symbol;
5
+ declare const inputType: unique symbol;
6
+ declare const outputType: unique symbol;
7
+ export type WorkflowExpression<T = unknown> = ProgramExpr & { readonly [outputType]?: T };
8
+ export type WorkflowValue<T> = T | WorkflowExpression<T> | (T extends readonly (infer E)[] ? WorkflowValue<E>[] : T extends object ? { [K in keyof T]: WorkflowValue<T[K]> } : never);
9
+ export type WorkflowResolved<T> = T extends WorkflowExpression<infer V> ? V : T extends readonly unknown[] ? { [K in keyof T]: WorkflowResolved<T[K]> } : T extends object ? { [K in keyof T]: WorkflowResolved<T[K]> } : T;
10
+ export type WorkflowRef<K extends string, Input = unknown, Output = unknown> = ProgramRef & { readonly [registryKind]: K; readonly [inputType]?: Input; readonly [outputType]?: Output };
11
+ export type WorkflowDefinition = Omit<WorkflowProgramV2, "schemaVersion" | "inputSchema" | "outputSchema" | "policy" | "acceptance" | "population"> & {
12
+ inputSchema: WorkflowRef<"schema">; outputSchema: WorkflowRef<"schema">;
13
+ policy: WorkflowRef<"policy">; acceptance: WorkflowRef<"acceptance">; population?: WorkflowRef<"population">;
14
+ };
15
+ type Data = unknown;
16
+ export type WorkflowCollection<T> = { items: ({ key: string; outcome: "completed"; value: T } | { key: string; outcome: "failed" | "waiting"; reason?: string })[]; results: T[]; failures: { id: string; reason: string }[]; seal: string; coverage: unknown; status: "completed" | "partial" | "no-work" };
17
+ export type WorkflowBody<T> = WorkflowOperation<keyof WorkflowOptions, T> | (ProgramBlock & { result: WorkflowValue<T> });
18
+ type Body = ProgramOperation | ProgramBlock;
19
+ interface Common { after?: string[]; label?: string }
20
+ interface Activity extends Common { executor: WorkflowRef<"executor">; input?: Data; candidate?: Data; writeScope?: Data }
21
+ export interface WorkflowOptions {
22
+ value: Common & { value: Data };
23
+ agent: Activity;
24
+ command: Activity;
25
+ map: Common & { items: Data; key: Data; coverage?: Data; completion: "all-required" | "partial" | "quorum"; quorum?: { minAccepted: number }; concurrency?: number; order?: "key" | "input"; body: Body };
26
+ branch: Common & { condition: boolean | ProgramExpr; then: Body; else: Body };
27
+ loop: Common & { initialState: Data; maxRounds: number; body: Body; next: Data; until: boolean | ProgramExpr };
28
+ repair: Common & { recipe: WorkflowRef<"recipe">; implement: WorkflowRef<"executor">; review: WorkflowRef<"executor">; checks: Data; input?: Data; evidence?: WorkflowRef<"executor">[]; assessmentScope?: "item" | "final"; entryMode: "implement-first" | "assess-first"; initialCandidate: Data; writeScope: Data; maxRepairRounds: number; maxAssessmentAttempts: number; maxInfrastructureAttempts: number; progressPolicy: { unchangedCandidateRounds: number; repeatedFindingsRounds: number } };
29
+ compose: Common & { candidates: Data; onConflict?: "needs-resolution" };
30
+ gate: Common & { candidate: Data; coverage?: Data; authorization?: Data };
31
+ subworkflow: Common & { program: WorkflowRef<"program">; input: Data };
32
+ waitEvent: Common & { type: string; correlation: Data; schema: WorkflowRef<"schema">; subject?: Data; timeoutMs?: number };
33
+ sequence: Common & ProgramBlock;
34
+ parallel: Common & ProgramBlock & { onFailure: "collect-all" | "cancel-siblings" };
35
+ }
36
+ export type WorkflowOperation<K extends keyof WorkflowOptions = keyof WorkflowOptions, Output = unknown> = ProgramOperation & { kind: K; readonly [outputType]?: Output };
37
+ export type WorkflowActivityOptions<Input, Output> = Omit<WorkflowOptions["agent"], "executor" | "input"> & { executor: WorkflowRef<"executor", Input, Output>; input?: WorkflowValue<NoInfer<Input>> };
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { stableStringify } from "./canonical.ts";
3
+ import { programGraph } from "./program-structure.ts";
4
+ import { validateVisualPopulation } from "./program-evidence.ts";
3
5
  import { validateProgramTypes } from "./program-types.ts";
4
6
 
5
7
  export type ProgramValue = null | boolean | number | string | ProgramValue[] | { [key: string]: ProgramValue };
@@ -7,7 +9,8 @@ export interface ProgramRef { id: string; version: string }
7
9
  export interface ProgramExpr { $expr: string; args: unknown[] }
8
10
  export interface ProgramOperation { kind: string; id: string; options: Record<string, unknown> }
9
11
  export interface WorkflowProgramV2 {
10
- operatorVersion?: 1;
12
+ operatorVersion?: 2;
13
+ mode?: "data" | "candidate";
11
14
  schemaVersion: 2; id: string; version: number; inputSchema: ProgramRef; outputSchema: ProgramRef;
12
15
  acceptance: ProgramRef; population?: ProgramRef; policy: ProgramRef; steps: ProgramOperation[]; result: unknown;
13
16
  }
@@ -15,12 +18,14 @@ export type ProgramSchema = { type?: string | string[]; enum?: ProgramValue[]; c
15
18
  properties?: Record<string, ProgramSchema>; required?: string[]; additionalProperties?: boolean;
16
19
  items?: ProgramSchema; minItems?: number; maxItems?: number; minLength?: number; maxLength?: number;
17
20
  minimum?: number; maximum?: number; anyOf?: ProgramSchema[] };
21
+ export interface ProgramLexicalContext { item?: ProgramSchema; state?: ProgramSchema }
18
22
  export interface ProgramExecutor {
19
23
  id: string; version: string; kind: "command" | "codex"; effect: "read" | "isolated-write" | "idempotent-effect" | "non-idempotent-effect";
20
24
  argv?: string[]; prompt?: string; role?: "implementer" | "reviewer" | "investigator" | "decision";
21
25
  model?: string; timeoutMs: number; writeScope: string[]; allowedGeneratedPaths?: string[];
22
26
  network: "disabled" | "host"; isolation: "cooperative" | "sandbox"; schema: ProgramRef;
23
27
  allowedExitCodes?: number[]; environment?: Record<string, string>;
28
+ inputSchema?: ProgramRef;
24
29
  dependencies?: "none" | "auto";
25
30
  cache?: "none" | "workspace";
26
31
  }
@@ -34,8 +39,15 @@ export interface ProgramPolicy {
34
39
  export interface ProgramAcceptance {
35
40
  id: string; version: string; criteria: string[]; writeScope: string[];
36
41
  requiredChecks: string[]; requireReview: boolean; allowNoWork: boolean;
42
+ requiredChecksByScope?: { item: string[]; final: string[] };
43
+ assessmentBindings?: Record<string, "item" | "final">;
44
+ obligations?: { id: string; scope: "item" | "final"; criteria: string[]; requiredEvidence?: string[] }[];
45
+ allowPartial?: boolean;
46
+ requireHumanApproval?: boolean;
37
47
  }
48
+ export interface ProgramVisualCase { itemKey: string; route: string; viewport: string; state: string; width: number; height: number }
38
49
  export interface ProgramPopulation {
50
+ visualCases?: ProgramVisualCase[];
39
51
  inventoryRoots?: string[]; extensions?: string[];
40
52
  id: string; version: string; members: string[]; exclusions: { id: string; reason: string }[];
41
53
  baselineDigest: string; evidence: string; allowNoWork: boolean;
@@ -48,6 +60,8 @@ export interface ProgramRegistry {
48
60
  export interface ProgramAssessment {
49
61
  receiptId?: string; generation?: number; operationId?: string;
50
62
  semanticDigest?: string; inputDigest?: string;
63
+ phase?: "item" | "final"; obligationIds?: string[]; satisfiedObligationIds?: string[]; unsatisfiedObligationIds?: string[];
64
+ environmentRef?: string; evidenceRefs?: string[]; contractDigest?: string;
51
65
  candidateDigest: string; verdict: "approved" | "changes_requested" | "inconclusive";
52
66
  findings: unknown[]; checks: { id: string; passed: boolean }[]; attemptIds: string[];
53
67
  }
@@ -63,6 +77,7 @@ export interface ProgramAttempt {
63
77
  executorDigest: string; startedAt: string; completedAt?: string;
64
78
  outcome: "running" | "completed" | "infrastructure_failed" | "invalid_output" | "uncertain" | "canceled_confirmed";
65
79
  outputRef?: string; threadId?: string; reason?: string;
80
+ reservation?: { scopes: { id: string; limit: number }[]; status: "held" | "released"; ownerEpoch: string };
66
81
  usage?: unknown;
67
82
  }
68
83
  export interface ProgramOperationRecord {
@@ -70,12 +85,12 @@ export interface ProgramOperationRecord {
70
85
  retired?: boolean;
71
86
  dependencies?: string[];
72
87
  id: string; semanticDigest: string; inputDigest: string; generation: number;
73
- status: "running" | "completed" | "skipped" | "needs-attention" | "uncertain";
88
+ status: "declared" | "ready" | "running" | "waiting" | "completed" | "skipped" | "needs-attention" | "uncertain";
74
89
  outputRef?: string; attempts: string[]; reason?: string; reusedFromGeneration?: number;
75
90
  }
76
91
  export interface ProgramSignal {
77
92
  id: string; target: string; generation: number; type: string; correlation: string;
78
- payloadRef: string; expiresAt?: string; status: "pending" | "consumed" | "pending-after-apply" | "stale";
93
+ payloadRef: string; authorization?: string; subjectDigest?: string; expiresAt?: string; status: "pending" | "consumed" | "pending-after-apply" | "stale";
79
94
  }
80
95
  export interface ProgramApplyIntent {
81
96
  requestId: string; requestDigest: string;
@@ -88,18 +103,37 @@ export interface ProgramRunV2 {
88
103
  program: WorkflowProgramV2; registry: ProgramRegistry; programDigest: string; registryDigest: string;
89
104
  inputRef: string; baselineDigest: string; baseRef?: string; createdAt: string; deadlineAt: string;
90
105
  version: number; semanticVersion: number; revision: number; planRevision: number;
91
- status: "executing" | "paused" | "needs-attention" | "completed" | "acceptance-ready" | "applying" | "applied" | "apply-uncertain" | "canceled";
106
+ status: "executing" | "waiting" | "paused" | "needs-attention" | "completed" | "acceptance-ready" | "applying" | "applied" | "apply-uncertain" | "canceled";
92
107
  operations: Record<string, ProgramOperationRecord>; attempts: Record<string, ProgramAttempt>;
93
108
  seals: Record<string, string>; signals: ProgramSignal[]; candidates: Record<string, ProgramCandidate>;
94
109
  assessments: Record<string, ProgramAssessment>; coverageReceipts: Record<string, { populationDigest: string; baselineDigest: string; ids: string[]; status: string }>;
95
110
  deltas: Record<string, { artifactRef: string; inputCandidateDigest: string; outputCandidateDigest: string; producerAttemptId: string }>;
96
111
  waits: Record<string, { generation: number; deadlineAt?: string; signalId?: string; outputRef?: string }>;
97
- repairs: Record<string, { semanticDigest: string; inputDigest: string; candidate: ProgramCandidate; rounds: number; assessments: number; infrastructure: number; unchanged: number; repeated: number; priorFindings: string; feedback: unknown; phase: "implement" | "assess" | "assessment-active" | "decide" | "accepted" | "exhausted" | "stalled"; assessment?: ProgramAssessment }>;
112
+ repairs: Record<string, { semanticDigest: string; inputDigest: string; candidate: ProgramCandidate; rounds: number; assessments: number; infrastructure: number; unchanged: number; repeated: number; priorFindings: string; feedback: unknown; phase: "implement" | "implementation-active" | "assess" | "assessment-active" | "decide" | "accepted" | "exhausted" | "stalled"; assessment?: ProgramAssessment }>;
98
113
  collections: Record<string, { seal: string; coverageReceiptId: string | null; generation: number; ids: string[]; acceptedAssessmentIds: string[]; acceptedCandidateIds: string[]; obligationsSatisfied: boolean }>;
99
114
  resultRef?: string; gateRef?: string; intent?: ProgramApplyIntent; receiptRef?: string;
100
115
  acceptedCandidate?: ProgramCandidate;
116
+ scopeResults?: Record<string, { outcomes: { id: string; status: string; reason?: string }[]; status: "completed" | "waiting" | "failed"; resultRef?: string }>;
117
+ artifactRecords?: Record<string, ProgramCaptureReceipt>;
118
+ queue?: { id: string; generation: number; at: string }[];
101
119
  totalAttempts: number; reason?: string;
102
120
  }
121
+ export interface ProgramAssessmentContext {
122
+ visualCases?: ProgramVisualCase[];
123
+ runId: string; invocationId: string; assessmentId: string; phase: "item" | "final";
124
+ candidateRef: ProgramCandidate; obligationIds: string[]; itemKey?: string;
125
+ environmentRef: string; evidenceRefs: string[]; input: unknown;
126
+ }
127
+ export type ProgramCollectionItem = { key: string; outcome: "completed"; value: unknown } | { key: string; outcome: "failed" | "waiting"; reason?: string };
128
+ export interface ProgramCollectionResult {
129
+ items: ProgramCollectionItem[]; results: unknown[]; failures: { id: string; reason: string }[];
130
+ seal: string; coverage: unknown; status: "completed" | "partial" | "no-work";
131
+ }
132
+ export interface ProgramCaptureReceipt {
133
+ receiptId: string; artifactRef: string; candidateDigest: string; environmentRef: string;
134
+ itemKey: string; buildDigest: string; mime: string; width: number; height: number;
135
+ route: string; viewport: string; state: string; producerAttemptId: string;
136
+ }
103
137
  export class ProgramError extends Error {
104
138
  constructor(readonly code: string, message: string) { super(message); this.name = "ProgramError"; }
105
139
  }
@@ -158,9 +192,10 @@ export function programPath(path: string): void {
158
192
  programAssert(path !== ".git" && !path.startsWith(".git/") && !path.startsWith(".forge/local/"), "Protected path");
159
193
  }
160
194
  export function programWithin(path: string, scope: string[]): boolean { return scope.some(prefix => path === prefix || path.startsWith(`${prefix}/`)); }
161
- export function validateWorkflowProgram(program: WorkflowProgramV2, registry: ProgramRegistry, ancestry: Set<string> = new Set()): void {
195
+ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: ProgramRegistry, ancestry: Set<string> = new Set(), lexical: ProgramLexicalContext = {}): void {
162
196
  programAssert(program?.schemaVersion === 2 && typeof program.id === "string" && program.id.length > 0 && Number.isSafeInteger(program.version) && program.version > 0, "Program v2 required");
163
- programAssert(program.operatorVersion === undefined || program.operatorVersion === 1, "Unsupported operator version");
197
+ programAssert(program.operatorVersion === undefined || program.operatorVersion === 2, "Unsupported operator version");
198
+ programAssert(program.mode === undefined || ["data", "candidate"].includes(program.mode), "Invalid workflow mode");
164
199
  programValueBytes(program, 4 * 1024 * 1024); programValueBytes(registry, 4 * 1024 * 1024);
165
200
  const identity = programDigest(program); programAssert(!ancestry.has(identity), "Recursive subworkflow cycle"); ancestry = new Set([...ancestry, identity]); programAssert(ancestry.size <= 32, "Subworkflow depth exceeded");
166
201
  const schemaKeys = new Set(["type", "enum", "const", "properties", "required", "additionalProperties", "items", "minItems", "maxItems", "minLength", "maxLength", "minimum", "maximum", "anyOf"]);
@@ -179,11 +214,9 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
179
214
  programAssert(policy.concurrency <= 32 && policy.maxDepth <= 32 && policy.maxItems <= 10000 && policy.maxAttempts <= 10000 && policy.maxOperations <= 20000 && policy.maxOutputBytes <= 4 * 1024 * 1024, "Physical limits exceeded");
180
215
  for (const path of [...policy.writeScope, ...acceptance.writeScope, ...(policy.captureScope ?? [])]) programPath(path);
181
216
  programAssert(acceptance.criteria.length > 0 && new Set(acceptance.criteria).size === acceptance.criteria.length, "Acceptance criteria required and unique");
182
- if (program.population) { const population = registryEntry(registry.populations, program.population); programAssert(population.members.length <= policy.maxItems && new Set(population.members).size === population.members.length && population.evidence.length > 0, "Population inventory invalid"); }
217
+ if (program.population) { const population = registryEntry(registry.populations, program.population); programAssert(population.members.length <= policy.maxItems && new Set(population.members).size === population.members.length && population.evidence.length > 0, "Population inventory invalid"); validateVisualPopulation(population); }
183
218
  const kinds = new Set(["agent", "command", "map", "branch", "loop", "compose", "gate", "repair", "subworkflow", "waitEvent", "value"]);
184
- let count = 0;
185
219
  const expressions: Record<string, [number, number]> = { workflowInput: [0, 0], item: [0, 0], loopState: [0, 0], output: [1, 1], field: [2, 2], literal: [1, 1], object: [1, 1], array: [0, 10000], eq: [2, 2], and: [1, 100], or: [1, 100], not: [1, 1], concat: [1, 100], filter: [3, 3], unique: [2, 2], sort: [2, 2], take: [2, 2], length: [1, 1], population: [0, 0], acceptance: [0, 0], acceptedCandidate: [1, 1], acceptedCandidates: [1, 1], outputCandidate: [1, 1], candidateFromBaseline: [0, 1], coverageFor: [2, 2], coverageReceipt: [1, 1], acceptanceWriteScope: [1, 1], approvedChecksFor: [1, 1], acceptanceChecks: [1, 1] };
186
- const graph = new Map<string, Set<string>>();
187
220
  function inspect(value: unknown, resolve: (id: string) => string | undefined, dependencies: Set<string>, depth = 0): void {
188
221
  programAssert(depth <= 32, "Expression nesting exceeded");
189
222
  if (!value || typeof value !== "object") return;
@@ -197,33 +230,54 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
197
230
  }
198
231
  for (const entry of Object.values(object)) inspect(entry, resolve, dependencies, depth + 1);
199
232
  }
200
- const options: Record<string, string[]> = { value: ["value"], agent: ["executor", "input", "candidate", "writeScope"], command: ["executor", "input", "candidate", "writeScope"], map: ["items", "key", "coverage", "concurrency", "completion", "quorum", "body"], branch: ["condition", "then", "else"], loop: ["initialState", "maxRounds", "body", "next", "until"], repair: ["recipe", "entryMode", "initialCandidate", "writeScope", "implement", "review", "checks", "maxRepairRounds", "maxAssessmentAttempts", "maxInfrastructureAttempts", "progressPolicy", "onExhausted", "onUncertain", "preserveCandidateHistory"], compose: ["candidates", "onConflict"], gate: ["candidate", "obligations", "coverage"], subworkflow: ["program", "input"], waitEvent: ["schema", "type", "correlation", "timeoutMs", "onTimeout"] };
201
- function walk(steps: ProgramOperation[], depth: number, prefix = "", parentResolve?: (id: string) => string | undefined): void {
202
- programAssert(Array.isArray(steps) && depth <= policy.maxDepth, "Invalid template depth"); const ids = new Set<string>();
203
- const resolve = (id: string): string | undefined => steps.some(step => step.id === id) ? `${prefix}${id}` : parentResolve?.(id);
204
- for (const step of steps) {
205
- programAssert(step && kinds.has(step.kind) && /^[A-Za-z][A-Za-z0-9_-]{0,79}$/.test(step.id) && !ids.has(step.id) && step.options && typeof step.options === "object", "Invalid/duplicate operation"); ids.add(step.id);
206
- programAssert(Object.keys(step.options).every(key => options[step.kind].includes(key)), `Unknown ${step.kind} option`);
207
- programAssert(++count <= policy.maxOperations, "Template limit exceeded");
208
- const dependencies = new Set<string>(); graph.set(`${prefix}${step.id}`, dependencies);
209
- inspect(Object.fromEntries(Object.entries(step.options).filter(([key]) => !["body", "then", "else", "next", "until"].includes(key))), resolve, dependencies);
210
- if (step.kind === "map" || step.kind === "loop") { const body = Array.isArray(step.options.body) ? step.options.body as ProgramOperation[] : [step.options.body as ProgramOperation]; const childPrefix = `${prefix}${step.id}/body/`; walk(body, depth + 1, childPrefix, resolve); for (const child of body) dependencies.add(`${childPrefix}${child.id}`); const childResolve = (id: string) => body.some(step => step.id === id) ? `${childPrefix}${id}` : resolve(id); if (step.kind === "loop") { inspect(step.options.next, childResolve, dependencies); inspect(step.options.until, childResolve, dependencies); } }
211
- if (step.kind === "map") programAssert(["all-required", "partial", "quorum"].includes(String(step.options.completion)), "Explicit map completion required");
212
- if (step.kind === "branch") { walk(step.options.then as ProgramOperation[], depth + 1, `${prefix}${step.id}/then/`, resolve); walk(step.options.else as ProgramOperation[], depth + 1, `${prefix}${step.id}/else/`, resolve); for (const arm of ["then", "else"]) for (const child of step.options[arm] as ProgramOperation[]) dependencies.add(`${prefix}${step.id}/${arm}/${child.id}`); }
213
- if (["agent", "command"].includes(step.kind)) registryEntry(registry.executors, step.options.executor as ProgramRef);
214
- if (step.kind === "repair") { registryEntry(registry.executors, step.options.implement as ProgramRef); registryEntry(registry.executors, step.options.review as ProgramRef); programAssert(["implement-first", "assess-first"].includes(String(step.options.entryMode)), "Repair entryMode required"); programAssert((step.options.recipe as ProgramRef)?.id === "repair" && (step.options.recipe as ProgramRef)?.version === "v1", "Unsupported repair recipe"); }
215
- if (step.kind === "repair") { programAssert(step.options.onExhausted === undefined || step.options.onExhausted === "needs-attention", "Unsupported exhaustion policy"); programAssert(step.options.onUncertain === undefined || step.options.onUncertain === "reconcile", "Unsupported uncertainty policy"); programAssert(step.options.preserveCandidateHistory === undefined || step.options.preserveCandidateHistory === true, "Candidate history is always preserved"); }
216
- if (step.kind === "compose") programAssert(step.options.onConflict === undefined || step.options.onConflict === "needs-resolution", "Unsupported composition conflict policy");
217
- if (step.kind === "waitEvent") programAssert(step.options.onTimeout === undefined || step.options.onTimeout === "needs-attention", "Unsupported timeout policy");
218
- if (step.kind === "waitEvent") registryEntry(registry.schemas, step.options.schema as ProgramRef);
219
- if (step.kind === "subworkflow") { const child = registryEntry(registry.programs ?? {}, step.options.program as ProgramRef); programAssert(programDigest(child.policy) === programDigest(program.policy) && programDigest(child.acceptance) === programDigest(program.acceptance), "Child cannot replace inherited policy/acceptance"); validateWorkflowProgram(child, registry, ancestry); }
233
+ const options: Record<string, string[]> = {
234
+ value: ["value"], agent: ["executor", "input", "candidate", "writeScope"], command: ["executor", "input", "candidate", "writeScope"],
235
+ map: ["items", "key", "coverage", "concurrency", "completion", "quorum", "body", "order"],
236
+ branch: ["condition", "then", "else"], loop: ["initialState", "maxRounds", "body", "next", "until"],
237
+ repair: ["recipe", "entryMode", "initialCandidate", "writeScope", "implement", "review", "checks", "input", "assessmentScope", "evidence", "maxRepairRounds", "maxAssessmentAttempts", "maxInfrastructureAttempts", "progressPolicy"],
238
+ compose: ["candidates", "onConflict"], gate: ["candidate", "coverage", "authorization"], subworkflow: ["program", "input"],
239
+ waitEvent: ["schema", "type", "correlation", "subject", "timeoutMs"], sequence: ["steps", "result"], parallel: ["steps", "result", "onFailure"],
240
+ };
241
+ kinds.add("sequence"); kinds.add("parallel");
242
+ const entries = programGraph(program);
243
+ programAssert(entries.length <= policy.maxOperations, "Template limit exceeded");
244
+ for (const entry of entries) {
245
+ const step = entry.step;
246
+ programAssert(step && kinds.has(step.kind) && /^[A-Za-z][A-Za-z0-9_-]{0,79}$/.test(step.id) && step.options && typeof step.options === "object", "Invalid operation");
247
+ programAssert(Object.keys(step.options).every(key => [...options[step.kind], "after", "label"].includes(key)), `Unknown ${step.kind} option`);
248
+ programAssert(entries.filter(other => other.id === entry.id).length === 1, "Duplicate operation ID");
249
+ inspect(Object.fromEntries(Object.entries(step.options).filter(([key]) => !["body", "steps", "result", "then", "else", "next", "until"].includes(key))), id => id, new Set());
250
+ programAssert(entry.id.split("/").length <= policy.maxDepth * 3 + 1, "Template depth exceeded");
251
+ if (step.kind === "map") {
252
+ programAssert(["all-required", "partial", "quorum"].includes(String(step.options.completion)), "Explicit map completion required");
253
+ programAssert(step.options.order === undefined || ["key", "input"].includes(String(step.options.order)), "Invalid map order");
254
+ if (step.options.completion !== "all-required") programAssert(acceptance.allowPartial, "Partial/quorum requires owner acceptance permission");
255
+ if (step.options.completion === "quorum") programAssert(step.options.quorum && typeof step.options.quorum === "object" && Number.isSafeInteger((step.options.quorum as { minAccepted: number }).minAccepted) && (step.options.quorum as { minAccepted: number }).minAccepted > 0, "Quorum minAccepted required");
256
+ else programAssert(step.options.quorum === undefined, "Quorum only applies to quorum completion");
257
+ }
258
+ if (["agent", "command"].includes(step.kind)) { const executor = registryEntry(registry.executors, step.options.executor as ProgramRef); if (executor.inputSchema) registryEntry(registry.schemas, executor.inputSchema); }
259
+ if (step.kind === "repair") {
260
+ registryEntry(registry.executors, step.options.implement as ProgramRef); registryEntry(registry.executors, step.options.review as ProgramRef);
261
+ programAssert(["implement-first", "assess-first"].includes(String(step.options.entryMode)), "Repair entryMode required");
262
+ programAssert((step.options.recipe as ProgramRef)?.id === "repair" && (step.options.recipe as ProgramRef)?.version === "v2", "Unsupported repair recipe");
263
+ const phase = step.options.assessmentScope as "item" | "final" | undefined;
264
+ programAssert(phase === undefined || phase === "item" || phase === "final", "Invalid assessment scope");
265
+ if (acceptance.requiredChecksByScope) {
266
+ programAssert(phase && acceptance.assessmentBindings?.[step.id] === phase, "Assessment scope must match owner operation binding");
267
+ if (phase === "item") programAssert(lexical.item || entries.some(ancestor => ancestor.step.kind === "map" && entry.id.startsWith(`${ancestor.id}/body/`)), "Item assessment requires a map context");
268
+ }
269
+ const required = phase && acceptance.requiredChecksByScope ? acceptance.requiredChecksByScope[phase] : acceptance.requiredChecks;
270
+ programAssert(Array.isArray(step.options.checks) && required.every(check => (step.options.checks as unknown[]).includes(check)), "Repair cannot omit required checks");
271
+ for (const ref of (step.options.evidence ?? []) as ProgramRef[]) programAssert(registryEntry(registry.executors, ref).effect === "read", "Evidence executor must be readonly");
220
272
  }
273
+ if (step.kind === "parallel") programAssert(["collect-all", "cancel-siblings"].includes(String(step.options.onFailure)), "Parallel failure policy required");
274
+ if (step.kind === "compose") programAssert(step.options.onConflict === undefined || step.options.onConflict === "needs-resolution", "Unsupported conflict policy");
275
+ if (step.kind === "waitEvent") registryEntry(registry.schemas, step.options.schema as ProgramRef);
276
+ if (step.kind === "subworkflow") { const child = registryEntry(registry.programs ?? {}, step.options.program as ProgramRef); programAssert(programDigest(child.policy) === programDigest(program.policy) && programDigest(child.acceptance) === programDigest(program.acceptance), "Child cannot replace inherited policy/acceptance"); programAssert(!child.population || program.population && programDigest(child.population) === programDigest(program.population), "Child cannot replace inherited population"); validateWorkflowProgram(child, registry, ancestry, { ...lexical, ...(entries.some(ancestor => ancestor.step.kind === "map" && entry.id.startsWith(`${ancestor.id}/body/`)) ? { item: {} } : {}), ...(entries.some(ancestor => ancestor.step.kind === "loop" && entry.id.startsWith(`${ancestor.id}/body/`)) ? { state: {} } : {}) }); }
221
277
  }
222
- walk(program.steps, 0); programAssert(Object.hasOwn(program, "result"), "Explicit program result required");
278
+ programAssert(Object.hasOwn(program, "result"), "Explicit program result required");
223
279
  inspect(program.result, id => program.steps.some(step => step.id === id) ? id : undefined, new Set());
224
- const visiting = new Set<string>(), visited = new Set<string>();
225
- 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) ?? []) visit(dependency); visiting.delete(id); visited.add(id); }
226
- for (const id of graph.keys()) visit(id);
227
- validateProgramTypes(program, registry);
280
+ if (acceptance.requiredChecksByScope) programAssert(acceptance.obligations?.length && new Set(acceptance.obligations.map(obligation => obligation.id)).size === acceptance.obligations.length, "Scoped acceptance requires unique owner obligations");
281
+ validateProgramTypes(program, registry, lexical);
228
282
  programDigest(program); programDigest(registry);
229
283
  }