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
|
@@ -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", "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
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?:
|
|
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 ===
|
|
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[]> = {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
}
|