forgeos 0.1.0-alpha.70 → 0.1.0-alpha.72

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 (60) hide show
  1. package/.agents/skills/forge-agent-fabric/SKILL.md +10 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/docs/agent-fabric-programs.md +183 -168
  5. package/docs/agent-fabric.md +4 -6
  6. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md +75 -0
  7. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md +137 -0
  8. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md +56 -0
  9. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -41
  10. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
  11. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
  12. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -109
  13. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +463 -423
  14. package/examples/agent-fabric-v2/README.md +16 -0
  15. package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
  16. package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
  17. package/examples/agent-fabric-v2/migrate.workflow.ts +1 -1
  18. package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
  19. package/examples/agent-fabric-v2/templates/README.md +46 -0
  20. package/examples/agent-fabric-v2/templates/bugfix.json +12 -0
  21. package/examples/agent-fabric-v2/templates/migration.json +13 -0
  22. package/examples/agent-fabric-v2/templates/review.json +12 -0
  23. package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
  24. package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
  25. package/examples/agent-fabric-v2/visual-population.json +13 -0
  26. package/package.json +7 -1
  27. package/scripts/summarize-fabric-benchmark.mjs +6 -0
  28. package/src/forge/_generated/releaseManifest.json +1 -1
  29. package/src/forge/_generated/releaseManifest.ts +3 -3
  30. package/src/forge/agent-fabric/claude-program-worker.ts +95 -0
  31. package/src/forge/agent-fabric/codex-sdk-worker.ts +12 -4
  32. package/src/forge/agent-fabric/index.ts +5 -0
  33. package/src/forge/agent-fabric/local-task-server.ts +2 -1
  34. package/src/forge/agent-fabric/managed-environment.ts +14 -8
  35. package/src/forge/agent-fabric/managed-workspace.ts +4 -0
  36. package/src/forge/agent-fabric/program-api.ts +37 -0
  37. package/src/forge/agent-fabric/program-authoring.ts +48 -0
  38. package/src/forge/agent-fabric/program-benchmark.ts +76 -0
  39. package/src/forge/agent-fabric/program-capabilities.ts +14 -0
  40. package/src/forge/agent-fabric/program-contract.ts +106 -36
  41. package/src/forge/agent-fabric/program-dsl.ts +45 -31
  42. package/src/forge/agent-fabric/program-evidence.ts +22 -0
  43. package/src/forge/agent-fabric/program-image.ts +15 -0
  44. package/src/forge/agent-fabric/program-lock.ts +50 -0
  45. package/src/forge/agent-fabric/program-observation.ts +63 -0
  46. package/src/forge/agent-fabric/program-owner.ts +16 -0
  47. package/src/forge/agent-fabric/program-runtime-options.ts +23 -0
  48. package/src/forge/agent-fabric/program-scheduler.ts +57 -0
  49. package/src/forge/agent-fabric/program-service.ts +423 -112
  50. package/src/forge/agent-fabric/program-store.ts +88 -55
  51. package/src/forge/agent-fabric/program-structure.ts +65 -0
  52. package/src/forge/agent-fabric/program-templates.ts +96 -0
  53. package/src/forge/agent-fabric/program-types.ts +21 -9
  54. package/src/forge/agent-fabric/program-view.ts +26 -0
  55. package/src/forge/agent-fabric/program-worker.ts +93 -23
  56. package/src/forge/agent-memory/mcp.ts +9 -3
  57. package/src/forge/cli/fabric.ts +4 -2
  58. package/src/forge/cli/main.ts +4 -0
  59. package/src/forge/cli/parse.ts +3 -1
  60. 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,46 @@
1
+ # Owner-bound authoring templates
2
+
3
+ These JSON requests select the review, bugfix and migration templates. `program-author`
4
+ validates references against the owner's registry and returns a proposal without running
5
+ workers, replacing policies, applying changes or spending model tokens. Start is a
6
+ separate action. The supplied registry's workers and migration criteria are text-file
7
+ fixtures; replace them with real project contracts and verification commands for coding.
8
+
9
+ Review begins with assessment and requests an empty write scope. A negative assessment
10
+ cannot change files; bounded follow-up may end exhausted or stalled. Bugfix assesses
11
+ first, avoiding edits to an already accepted baseline. Migration consumes
12
+ `input.items: [{id, allowedPaths}]`, seals coverage against the owner population, repairs
13
+ each item only after a negative assessment, composes accepted candidates and assesses the integrated candidate before gate. An optional owner-authorized resolver can be selected in migration options; its candidate requires fresh final population evaluation.
14
+
15
+ Each repair preserves mandatory checks. Scoped acceptance must bind `component` to
16
+ `item` and `assessment` to `final`; templates do not mutate these owner bindings.
17
+ Human approval, when required, remains outstanding at the gate. Apply remains separate.
18
+ `program-author-types` exports owner schema/executor declarations for author assistance;
19
+ runtime validation is still authoritative.
20
+
21
+ ## Independent benchmark reports
22
+
23
+ `program-benchmark.ts` defines a versioned observation report. Record identity of runtime,
24
+ model, tools, context, contracts and budget for each task. Queue/preparation/execution/
25
+ capture/persistence/integration timings are optional observations; omitted means absent,
26
+ not zero. Consumption is observed, estimated or unknown. Independent evaluation requires
27
+ an assessor and criteria digest fixed independently of the implementer. Owner gate
28
+ approval alone does not qualify as external success.
29
+
30
+ Read and summarize an already collected report with:
31
+
32
+ ```sh
33
+ node --import tsx scripts/summarize-fabric-benchmark.mjs report.json
34
+ ```
35
+
36
+ This command only validates data. It does not run workflows, providers or benchmarks.
37
+ The existing deterministic benchmark scripts remain runtime references and do not prove
38
+ superiority over Claude or coding quality. Real evaluations are a separate explicit run.
39
+
40
+ ## CI scope
41
+
42
+ PRs changing only explicitly covered authoring, observation, benchmark or adapter modules use a small critical suite. Missing critical files fail closed.
43
+ Core owner/store/workspace/services, shared or unknown paths and main runs retain the full Agent Fabric suite. Workflow
44
+ contracts, typecheck, compiler checks, lint and the separate security gate remain.
45
+ Ubuntu/Node 22 CLI smoke runs once in verify; the matrix adds Windows 22 for PRs and
46
+ Ubuntu 24 on main. Manual CI includes macOS.
@@ -0,0 +1,12 @@
1
+ {
2
+ "kind": "bugfix",
3
+ "options": {
4
+ "id": "bugfix-workflow",
5
+ "inputSchema": { "id": "input", "version": "v1" },
6
+ "outputSchema": { "id": "gate", "version": "v1" },
7
+ "policy": { "id": "local", "version": "v1" },
8
+ "acceptance": { "id": "migration", "version": "v1" },
9
+ "implement": { "id": "migrate", "version": "v1" },
10
+ "review": { "id": "review", "version": "v1" }
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "migration",
3
+ "options": {
4
+ "id": "migration-workflow",
5
+ "inputSchema": { "id": "discovery", "version": "v1" },
6
+ "outputSchema": { "id": "gate", "version": "v1" },
7
+ "policy": { "id": "local", "version": "v1" },
8
+ "acceptance": { "id": "migration", "version": "v1" },
9
+ "implement": { "id": "migrate", "version": "v1" },
10
+ "review": { "id": "review", "version": "v1" },
11
+ "population": { "id": "files", "version": "v1" }
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "kind": "review",
3
+ "options": {
4
+ "id": "review-workflow",
5
+ "inputSchema": { "id": "input", "version": "v1" },
6
+ "outputSchema": { "id": "gate", "version": "v1" },
7
+ "policy": { "id": "local", "version": "v1" },
8
+ "acceptance": { "id": "migration", "version": "v1" },
9
+ "implement": { "id": "migrate", "version": "v1" },
10
+ "review": { "id": "review", "version": "v1" }
11
+ }
12
+ }
@@ -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.72",
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",
@@ -12,6 +12,7 @@
12
12
  "bin/",
13
13
  "scripts/field-test-forgeos.mjs",
14
14
  "scripts/install-agent-fabric-skill.mjs",
15
+ "scripts/summarize-fabric-benchmark.mjs",
15
16
  ".agents/skills/forge-agent-fabric/",
16
17
  "docs/cair-protocol.md",
17
18
  "docs/forge-protocol.md",
@@ -26,6 +27,11 @@
26
27
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md",
27
28
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md",
28
29
  "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md",
30
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md",
31
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md",
32
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md",
33
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md",
34
+ "docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md",
29
35
  "examples/go-billing/",
30
36
  "examples/agent-fabric-v2/",
31
37
  "examples/java-billing/",
@@ -0,0 +1,6 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { summarizeBenchmarkReport, compareBenchmarkReports } from "../src/forge/agent-fabric/program-benchmark.ts";
3
+ const path = process.argv[2];
4
+ if (!path) throw new Error("Usage: node --import tsx scripts/summarize-fabric-benchmark.mjs REPORT.json [COUNTERPART.json]");
5
+ const report = JSON.parse(await readFile(path, "utf8"));
6
+ console.log(JSON.stringify(process.argv[3] ? compareBenchmarkReports(report, JSON.parse(await readFile(process.argv[3], "utf8"))) : summarizeBenchmarkReport(report), null, 2));
@@ -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.72","releaseId":"forgeos@0.1.0-alpha.72+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.72 input=4096e19589f57b9568accde0ae34aa0782c1598f54261e161e1ff5ac7cc857b9 content=d749e9d32ae5d270f7ba002ac58ee67da6d8e1a0dd704ad5e9a05a4decf4d59f
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.72",
23
+ "releaseId": "forgeos@0.1.0-alpha.72+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -0,0 +1,95 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import type { ProgramWorkerObservation } from "./program-observation.ts";
4
+
5
+ /** The CLI is cooperative: neither network denial nor process-tree termination is attested. */
6
+ export const claudeProgramCapabilities = Object.freeze({ adapter: "claude", protocol: "stream-json", minimumVersion: "2.1.259", isolation: "cooperative", network: "host", structuredOutput: true, threadResume: true, nativeImages: false, hardTokenLimit: false, processTreeTermination: false });
7
+ export function validateClaudeProgramVersion(output: string): string {
8
+ const match = /^\s*(\d+)\.(\d+)\.(\d+)(?:\s|$)/.exec(output);
9
+ if (!match) throw new Error("Claude CLI version identity unavailable");
10
+ const [major, minor, patch] = match.slice(1).map(Number);
11
+ if (![major, minor, patch].every(Number.isSafeInteger) || major! < 2 || major === 2 && (minor! < 1 || minor === 1 && patch! < 259)) throw new Error("Claude CLI requires version >=2.1.259 for restricted unattended protocol");
12
+ return `${major}.${minor}.${patch}`;
13
+ }
14
+ export interface ClaudeProgramInput {
15
+ binary: string; cwd: string; prompt: string; schema: unknown; model?: string; threadId?: string;
16
+ writable: boolean; signal: AbortSignal; environment: NodeJS.ProcessEnv;
17
+ onThread: (id: string) => Promise<void>;
18
+ onEvent: (event: Omit<ProgramWorkerObservation, "id" | "at">) => Promise<void>;
19
+ }
20
+ export function claudeProgramArguments(input: Pick<ClaudeProgramInput, "schema" | "model" | "threadId" | "writable">): string[] {
21
+ const tools = input.writable ? "Read,Glob,Grep,Edit,Write" : "Read,Glob,Grep";
22
+ return ["--print", "--output-format", "stream-json", "--verbose", "--json-schema", JSON.stringify(input.schema),
23
+ "--restricted", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands",
24
+ "--settings", '{"disableAllHooks":true}', "--setting-sources", "", "--permission-mode", "dontAsk", "--permission-prompts", "none",
25
+ "--tools", tools, "--allowedTools", tools, ...(input.model ? ["--model", input.model] : []), ...(input.threadId ? ["--resume", input.threadId] : [])];
26
+ }
27
+ /** Parse only the documented operational envelope; never persist assistant/tool text. */
28
+ export function claudeProgramResult(event: Record<string, unknown>): { data: unknown; usage?: ProgramWorkerObservation["usage"] } {
29
+ if (event.type !== "result" || event.subtype !== "success" || event.is_error === true || !("structured_output" in event)) throw new Error("Claude structured completion not observed");
30
+ return { data: event.structured_output, usage: claudeProgramUsage(event) };
31
+ }
32
+ export function claudeProgramUsage(event: Record<string, unknown>): ProgramWorkerObservation["usage"] | undefined {
33
+ const raw = event.usage as Record<string, unknown> | undefined;
34
+ if (!raw) return undefined;
35
+ for (const key of ["input_tokens", "output_tokens"]) if (!Number.isSafeInteger(raw[key]) || (raw[key] as number) < 0) throw new Error("Invalid Claude usage counters");
36
+ for (const key of ["cache_read_input_tokens", "cache_creation_input_tokens"]) if (raw[key] !== undefined && (!Number.isSafeInteger(raw[key]) || (raw[key] as number) < 0)) throw new Error("Invalid Claude cache usage");
37
+ const cached = (raw.cache_read_input_tokens as number | undefined) ?? 0;
38
+ const inputTokens = (raw.input_tokens as number) + cached + ((raw.cache_creation_input_tokens as number | undefined) ?? 0);
39
+ if (!Number.isSafeInteger(inputTokens)) throw new Error("Claude aggregate usage exceeds safe counter bound");
40
+ return { input_tokens: inputTokens, cached_input_tokens: cached, output_tokens: raw.output_tokens as number };
41
+ }
42
+ export async function runClaudeProgramWorker(input: ClaudeProgramInput): Promise<{ data: unknown; usage?: ProgramWorkerObservation["usage"] }> {
43
+ if (process.env.FORGE_FABRIC_TEST_MODE === "1") throw new Error("Real Claude workers forbidden in deterministic test mode");
44
+ if (input.signal.aborted) throw new Error("Claude canceled before dispatch");
45
+ const identity = await promisify(execFile)(input.binary, ["--version"], { cwd: input.cwd, env: input.environment, windowsHide: true, timeout: 10000, maxBuffer: 4096, signal: input.signal }).catch(() => { throw new Error("Claude CLI version inspection failed; model dispatch refused"); });
46
+ const version = validateClaudeProgramVersion(identity.stdout);
47
+ await input.onEvent({ type: "adapter.version", source: "claude", metadata: { version, minimumVersion: claudeProgramCapabilities.minimumVersion } });
48
+ if (input.signal.aborted) throw new Error("Claude canceled before model dispatch");
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn(input.binary, claudeProgramArguments(input), { cwd: input.cwd, env: input.environment, shell: false, windowsHide: true });
51
+ let pending = "", bytes = 0, count = 0, resultObserved = false, sessionId = input.threadId, final: ReturnType<typeof claudeProgramResult> | undefined;
52
+ let queue = Promise.resolve(), failure: unknown, settled = false, grace: ReturnType<typeof setTimeout> | undefined;
53
+ const finish = (error?: unknown) => { if (settled) return; settled = true; if (grace) clearTimeout(grace); input.signal.removeEventListener("abort", abort); if (error) reject(error); else resolve(final!); };
54
+ const abort = () => { child.kill(); grace ??= setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); child.unref(); finish(new Error("Claude termination/effects unknown; reconcile before retry")); }, 1000); };
55
+ input.signal.addEventListener("abort", abort, { once: true });
56
+ const consume = (line: string) => {
57
+ queue = queue.then(async () => {
58
+ if (settled || failure) return;
59
+ if (++count > 10000) throw new Error("Claude event budget exceeded");
60
+ const event = JSON.parse(line) as Record<string, unknown>;
61
+ if (typeof event.session_id === "string") {
62
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(event.session_id) || sessionId && event.session_id !== sessionId) throw new Error("Claude session identity mismatch");
63
+ if (!sessionId) { sessionId = event.session_id; await input.onThread(sessionId); }
64
+ }
65
+ if (event.type === "result") {
66
+ if (settled) return;
67
+ if (resultObserved) throw new Error("Duplicate Claude result");
68
+ resultObserved = true;
69
+ const usage = claudeProgramUsage(event);
70
+ if (usage) await input.onEvent({ type: "usage", source: "claude", usage, semantics: "incremental" });
71
+ final = claudeProgramResult(event);
72
+ }
73
+ }).catch(error => { failure = error; abort(); });
74
+ };
75
+ child.stdout.setEncoding("utf8");
76
+ child.stdout.on("data", (chunk: string) => {
77
+ bytes += Buffer.byteLength(chunk);
78
+ if (bytes > 4 * 1024 * 1024) { failure = new Error("Claude output budget exceeded"); abort(); return; }
79
+ pending += chunk; let index: number;
80
+ while ((index = pending.indexOf("\n")) >= 0) { const line = pending.slice(0, index).trim(); pending = pending.slice(index + 1); if (line) consume(line); }
81
+ });
82
+ child.stderr.on("data", () => {});
83
+ child.once("error", () => finish(new Error("Claude executable unavailable or launch failed")));
84
+ child.once("close", async code => {
85
+ if (pending.trim()) consume(pending.trim()); await queue;
86
+ try {
87
+ await input.onEvent({ type: "process.closed", source: "claude", metadata: { exitCode: code, rootTerminationObserved: true, processTreeTermination: false } });
88
+ finish(failure || input.signal.aborted || code !== 0 || !final ? new Error("Claude completion/effects require reconciliation") : undefined);
89
+ } catch (error) { finish(error); }
90
+ });
91
+ child.stdin.on("error", () => { failure = new Error("Claude input delivery failed"); abort(); });
92
+ child.stdin.end(input.prompt);
93
+ if (input.signal.aborted) abort();
94
+ });
95
+ }
@@ -1,4 +1,4 @@
1
- import { Codex, type CodexOptions, type ThreadOptions, type TurnOptions, type ThreadEvent } from "@openai/codex-sdk";
1
+ import { Codex, type CodexOptions, type ThreadOptions, type TurnOptions, type ThreadEvent, type Input } from "@openai/codex-sdk";
2
2
  import { realpath, stat, readFile } from "node:fs/promises";
3
3
  import { createHash } from "node:crypto";
4
4
  import { homedir } from "node:os";
@@ -13,6 +13,7 @@ export interface CodexWorkerEvent { type: string; threadId?: string; summary?: s
13
13
  export interface CodexWorkerInput {
14
14
  cwd: string; prompt: string; role: "implementer" | "reviewer" | "investigator" | "decision";
15
15
  model?: string; threadId?: string; signal: AbortSignal;
16
+ images?: { path: string }[];
16
17
  onEvent: (event: CodexWorkerEvent) => Promise<void> | void;
17
18
  }
18
19
  export interface CodexWorkerOutput {
@@ -22,7 +23,7 @@ export interface CodexWorkerOutput {
22
23
  }
23
24
  export interface CodexWorkerThread {
24
25
  readonly id: string | null;
25
- runStreamed(prompt: string, options: TurnOptions): Promise<{ events: AsyncIterable<ThreadEvent> }>;
26
+ runStreamed(prompt: Input, options: TurnOptions): Promise<{ events: AsyncIterable<ThreadEvent> }>;
26
27
  }
27
28
  export interface CodexWorkerClient {
28
29
  startThread(options: ThreadOptions): CodexWorkerThread;
@@ -165,6 +166,7 @@ async function runCodexWorkerInternal(input: CodexWorkerInput, factory: CodexWor
165
166
  }
166
167
 
167
168
  export async function runCodexWorker(input: CodexWorkerInput, factory?: CodexWorkerFactory): Promise<CodexWorkerOutput> {
169
+ if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
168
170
  try { return await runCodexWorkerInternal(input, factory ?? ((options) => isolatedCodexClient(options, input.cwd, input.signal))); }
169
171
  catch (error) {
170
172
  if (error instanceof CodexWorkerError) throw error;
@@ -182,20 +184,26 @@ async function runTypedCodexWorkerInternal(input: CodexWorkerInput & { outputSch
182
184
  const threadOptions: ThreadOptions = { workingDirectory: cwd, sandboxMode: input.role === "implementer" ? "workspace-write" : "read-only", networkAccessEnabled: false, webSearchMode: "disabled", approvalPolicy: "never", ...(input.model ? { model: input.model } : {}) };
183
185
  const thread = input.threadId ? client.resumeThread(input.threadId, threadOptions) : client.startThread(threadOptions);
184
186
  let threadId = input.threadId ?? thread.id ?? "", finalMessage: string | undefined, completed = false, count = 0, observedUsage: CodexWorkerUsage | undefined;
185
- const stream = await thread.runStreamed(`${input.prompt}\nReturn only data matching the requested output schema. Never include credentials.`, { outputSchema: input.outputSchema, signal: input.signal });
187
+ const prompt = `${input.prompt}\nReturn only data matching the requested output schema. Never include credentials.`;
188
+ if (input.images && (input.images.length > 100 || input.images.some(image => !text(image.path, 4096)))) fail("AF_CODEX_INPUT", "Invalid image inputs");
189
+ const content: Input = input.images?.length ? [{ type: "text", text: prompt }, ...input.images.map(image => ({ type: "local_image" as const, path: image.path }))] : prompt;
190
+ const stream = await thread.runStreamed(content, { outputSchema: input.outputSchema, signal: input.signal });
191
+ if (input.images?.length) await input.onEvent({ type: "input.images.delivered", summary: `${input.images.length} images supplied through SDK local_image; understanding is not attested` });
186
192
  for await (const event of stream.events) {
187
193
  checkAbort(input.signal); if (++count > 10000) fail("AF_CODEX_EVENT_LIMIT", "Typed worker event budget exceeded");
188
194
  if (!["thread.started", "turn.started", "item.completed", "item.started", "item.updated", "turn.completed", "turn.failed", "error"].includes(event.type)) fail("AF_CODEX_EVENT", "Unknown typed event");
189
195
  if (event.type === "thread.started") { if (!text(event.thread_id, 128) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/u.test(event.thread_id) || threadId && threadId !== event.thread_id) fail("AF_CODEX_EVENT", "Thread identity changed"); threadId = event.thread_id; await input.onEvent({ type: event.type, threadId }); }
190
196
  else if (event.type === "item.completed" && event.item.type === "agent_message") { if (completed || !text(event.item.text, 4 * 1024 * 1024)) fail("AF_CODEX_REPORT", "Invalid typed message"); finalMessage = event.item.text; }
191
197
  else if (event.type === "turn.completed") { if (completed) fail("AF_CODEX_EVENT", "Duplicate completion"); completed = true; observedUsage = usage(event.usage); await input.onEvent({ type: event.type, usage: observedUsage }); }
192
- else if (event.type === "turn.failed" || event.type === "error") fail("AF_CODEX_TURN_FAILED", "Typed worker failed; reconcile before retry");
198
+ else if (event.type === "turn.failed" || event.type === "error") { await input.onEvent({ type: event.type, summary: "Typed worker failed; reconcile before retry" }); fail("AF_CODEX_TURN_FAILED", "Typed worker failed; reconcile before retry"); }
199
+ else await input.onEvent({ type: event.type, summary: "Typed SDK activity observed" });
193
200
  }
194
201
  checkAbort(input.signal); if (!completed || !threadId || !finalMessage) fail("AF_CODEX_INCOMPLETE", "Typed worker completion not observed");
195
202
  let data: unknown; try { data = JSON.parse(finalMessage); input.validateOutput(data); } catch { fail("AF_CODEX_REPORT", "Typed worker output schema invalid"); }
196
203
  return { threadId, data, ...(observedUsage ? { usage: observedUsage } : {}) };
197
204
  }
198
205
  export async function runTypedCodexWorker(input: CodexWorkerInput & { outputSchema: unknown; validateOutput: (data: unknown) => void }, factory?: CodexWorkerFactory): Promise<{ threadId: string; data: unknown; usage?: CodexWorkerUsage }> {
206
+ if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
199
207
  try { return await runTypedCodexWorkerInternal(input, factory); }
200
208
  catch (error) { if (error instanceof CodexWorkerError) throw error; fail("AF_CODEX_EXECUTION", "Typed SDK or persistence failed; reconcile before retry"); }
201
209
  }
@@ -44,5 +44,10 @@ export { lowerWorkflowSource, PROGRAM_DSL_VERSION } from "./program-dsl.ts";
44
44
  export { ProgramRunService, PROGRAM_RUN_ACTIONS } from "./program-service.ts";
45
45
  export type { ProgramRunAction } from "./program-service.ts";
46
46
  export { ProgramRunStore } from "./program-store.ts";
47
+ export * from "./program-authoring.ts";
48
+ export * from "./program-templates.ts";
49
+ export * from "./program-runtime-options.ts";
50
+ export * from "./program-benchmark.ts";
51
+ export * from "./program-observation.ts";
47
52
  export { validateProgramCapabilities } from "./program-worker.ts";
48
53
  export type { ProgramWorkerAdapter, ProgramWorkerInput, ProgramWorkerResult } from "./program-worker.ts";
@@ -14,6 +14,7 @@ import { localFabricPath } from "./local-paths.ts";
14
14
  import { applyFabricProfile } from "./project-profile.ts";
15
15
  import { ProgramRunService, PROGRAM_RUN_ACTIONS, type ProgramRunAction } from "./program-service.ts";
16
16
  import { ProgramError } from "./program-contract.ts";
17
+ import { readProgramRuntimeOptions } from "./program-runtime-options.ts";
17
18
 
18
19
  const MAX_REQUEST_BYTES = 40 * 1024;
19
20
  const ENDPOINT_FILENAME = "owner-endpoint.json";
@@ -200,7 +201,7 @@ export async function serveLocalTasks(
200
201
  attachedService = await AttachedTaskService.open(root);
201
202
  const { ManagedRunService: ManagedService } = await import("./managed-run-service.ts");
202
203
  managedService = await ManagedService.open(root);
203
- programService = await ProgramRunService.open(root);
204
+ programService = await ProgramRunService.open(root, undefined, undefined, readProgramRuntimeOptions(root));
204
205
  const token = randomBytes(32).toString("hex");
205
206
  listener = createServer((request, response: ServerResponse) => {
206
207
  response.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -8,7 +8,8 @@ import { tmpdir } from "node:os";
8
8
  import { stableStringify } from "./canonical.ts";
9
9
 
10
10
  type Manager = "npm" | "pnpm" | "yarn" | "bun" | "none";
11
- export interface ManagedEnvironmentOptions { mode?: "auto" | "none"; ignoreScripts?: boolean; registry?: string; timeoutMs?: number; cacheDirectory?: string; signal?: AbortSignal }
11
+ export interface ManagedEnvironmentOptions { mode?: "auto" | "none"; ignoreScripts?: boolean; registry?: string; timeoutMs?: number; cacheDirectory?: string; signal?: AbortSignal;
12
+ onTiming?: (measurement: { phase: "cache.verify" | "cache.copy" | "dependencies.install"; elapsedMs: number }) => Promise<void> | void }
12
13
  export interface ManagedEnvironment { schemaVersion: 1; directory: string; manager: Manager; lockDigest: string; dependencyDigest: string; derivedPaths: string[]; cacheHit: boolean }
13
14
  const execute = promisify(execFile);
14
15
  const hash = (value: string | Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
@@ -134,6 +135,11 @@ export async function verifyManagedEnvironment(environment: ManagedEnvironment):
134
135
  assert(await dependencyDigest(environment.directory, environment.derivedPaths) === environment.dependencyDigest, "prepared dependencies were modified; result is invalid");
135
136
  }
136
137
  export async function prepareManagedEnvironment(directory: string, options: ManagedEnvironmentOptions = {}): Promise<ManagedEnvironment> {
138
+ const timed = async <T>(phase: "cache.verify" | "cache.copy" | "dependencies.install", operation: () => Promise<T>): Promise<T> => {
139
+ const start = performance.now();
140
+ try { return await operation(); }
141
+ finally { await options.onTiming?.({ phase, elapsedMs: performance.now() - start }); }
142
+ };
137
143
  directory = resolve(directory); aborted(options.signal);
138
144
  assert(!(await lstat(directory)).isSymbolicLink(), "workspace root cannot be a symlink");
139
145
  const none = (): ManagedEnvironment => ({ schemaVersion: 1, directory, manager: "none", lockDigest: hash("none"), dependencyDigest: hash(stableStringify([])), derivedPaths: [], cacheHit: false });
@@ -160,9 +166,9 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
160
166
  const cache = join(cacheRoot, lockDigest.slice(7)), marker = join(cache, "environment.json");
161
167
  if (await exists(marker)) {
162
168
  const cached = JSON.parse(await readFile(marker, "utf8")) as ManagedEnvironment;
163
- assert(cached.lockDigest === lockDigest && cached.dependencyDigest === await dependencyDigest(join(cache, "tree"), cached.derivedPaths, options.signal), "dependency cache integrity failed");
164
- await copyDependencyPaths(join(cache, "tree"), directory, cached.derivedPaths, options.signal);
165
- const environment = { ...cached, directory, cacheHit: true }; await verifyManagedEnvironment(environment); return environment;
169
+ await timed("cache.verify", async () => { assert(cached.lockDigest === lockDigest && cached.dependencyDigest === await dependencyDigest(join(cache, "tree"), cached.derivedPaths, options.signal), "dependency cache integrity failed"); });
170
+ await timed("cache.copy", () => copyDependencyPaths(join(cache, "tree"), directory, cached.derivedPaths, options.signal));
171
+ const environment = { ...cached, directory, cacheHit: true }; await timed("cache.verify", () => verifyManagedEnvironment(environment)); return environment;
166
172
  }
167
173
  const configBackups: { path: string; bytes?: Buffer }[] = [];
168
174
  try {
@@ -178,7 +184,7 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
178
184
  : Number.parseInt(version) >= 2 ? ["install", "--immutable", ...(ignore ? ["--mode=skip-builds"] : [])] : ["install", "--frozen-lockfile", "--non-interactive", `--registry=${registry}`, ...(ignore ? ["--ignore-scripts"] : [])];
179
185
  env.YARN_NODE_LINKER = "node-modules"; env.YARN_NM_MODE = "classic"; env.YARN_ENABLE_GLOBAL_CACHE = "false"; env.YARN_NPM_REGISTRY_SERVER = registry; env.YARN_ENABLE_SCRIPTS = ignore ? "false" : "true";
180
186
  env.NPM_CONFIG_CACHE = join(cacheRoot, "download-cache"); env.BUN_INSTALL_CACHE_DIR = join(cacheRoot, "bun-download-cache"); env.COREPACK_HOME = join(cacheRoot, "corepack-cache");
181
- await execute(command.executable, [...command.prefix, ...args], { cwd: directory, env, windowsHide: true, timeout, signal: options.signal, maxBuffer: 1024 * 1024 });
187
+ await timed("dependencies.install", () => execute(command.executable, [...command.prefix, ...args], { cwd: directory, env, windowsHide: true, timeout, signal: options.signal, maxBuffer: 1024 * 1024 }));
182
188
  } catch (error) { if (options.signal?.aborted) throw new Error("managed environment: preparation canceled"); throw new Error(`managed environment: frozen ${manager} install failed: ${(error as Error).message.slice(0, 2000)}`); }
183
189
  finally { for (const config of configBackups) await writeFile(config.path, config.bytes!); }
184
190
  aborted(options.signal); const derivedPaths = await derivePaths(directory);
@@ -186,12 +192,12 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
186
192
  derivedPaths.sort(); await portableLinks(directory, derivedPaths); const dependency = await dependencyDigest(directory, derivedPaths, options.signal);
187
193
  const environment: ManagedEnvironment = { schemaVersion: 1, directory, manager, lockDigest, dependencyDigest: dependency, derivedPaths, cacheHit: false };
188
194
  stage = join(cacheRoot, `${lockDigest.slice(7)}-${randomUUID()}.stage`); await mkdir(join(stage, "tree"), { recursive: true });
189
- await copyDependencyPaths(directory, join(stage, "tree"), derivedPaths, options.signal);
190
- assert(await dependencyDigest(join(stage, "tree"), derivedPaths, options.signal) === dependency, "cache copy integrity failed");
195
+ await timed("cache.copy", () => copyDependencyPaths(directory, join(stage!, "tree"), derivedPaths, options.signal));
196
+ await timed("cache.verify", async () => { assert(await dependencyDigest(join(stage!, "tree"), derivedPaths, options.signal) === dependency, "cache copy integrity failed"); });
191
197
  try {
192
198
  await rename(stage, cache); uncommittedCache = cache;
193
199
  await rebaseCacheJunctions(join(cache, "tree"), join(stage, "tree"), derivedPaths);
194
- assert(await dependencyDigest(join(cache, "tree"), derivedPaths, options.signal) === dependency, "published cache integrity failed");
200
+ await timed("cache.verify", async () => { assert(await dependencyDigest(join(cache, "tree"), derivedPaths, options.signal) === dependency, "published cache integrity failed"); });
195
201
  const markerTemp = join(cache, "environment.json.tmp");
196
202
  await writeFile(markerTemp, stableStringify(environment)); await rename(markerTemp, join(cache, "environment.json")); uncommittedCache = undefined;
197
203
  } catch (error) { if (uncommittedCache || !["EEXIST", "ENOTEMPTY", "EPERM"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error; }