forgeos 0.1.0-alpha.71 → 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 (38) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/docs/agent-fabric-programs.md +34 -2
  4. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md +75 -0
  5. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md +137 -0
  6. package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md +56 -0
  7. package/examples/agent-fabric-v2/templates/README.md +46 -0
  8. package/examples/agent-fabric-v2/templates/bugfix.json +12 -0
  9. package/examples/agent-fabric-v2/templates/migration.json +13 -0
  10. package/examples/agent-fabric-v2/templates/review.json +12 -0
  11. package/package.json +5 -1
  12. package/scripts/summarize-fabric-benchmark.mjs +6 -0
  13. package/src/forge/_generated/releaseManifest.json +1 -1
  14. package/src/forge/_generated/releaseManifest.ts +3 -3
  15. package/src/forge/agent-fabric/claude-program-worker.ts +95 -0
  16. package/src/forge/agent-fabric/codex-sdk-worker.ts +10 -4
  17. package/src/forge/agent-fabric/index.ts +5 -0
  18. package/src/forge/agent-fabric/local-task-server.ts +2 -1
  19. package/src/forge/agent-fabric/managed-environment.ts +14 -8
  20. package/src/forge/agent-fabric/managed-workspace.ts +4 -0
  21. package/src/forge/agent-fabric/program-api.ts +1 -1
  22. package/src/forge/agent-fabric/program-authoring.ts +48 -0
  23. package/src/forge/agent-fabric/program-benchmark.ts +76 -0
  24. package/src/forge/agent-fabric/program-capabilities.ts +14 -0
  25. package/src/forge/agent-fabric/program-contract.ts +19 -3
  26. package/src/forge/agent-fabric/program-observation.ts +63 -0
  27. package/src/forge/agent-fabric/program-runtime-options.ts +23 -0
  28. package/src/forge/agent-fabric/program-scheduler.ts +25 -6
  29. package/src/forge/agent-fabric/program-service.ts +172 -35
  30. package/src/forge/agent-fabric/program-store.ts +9 -1
  31. package/src/forge/agent-fabric/program-templates.ts +96 -0
  32. package/src/forge/agent-fabric/program-view.ts +15 -1
  33. package/src/forge/agent-fabric/program-worker.ts +73 -20
  34. package/src/forge/agent-memory/mcp.ts +8 -2
  35. package/src/forge/cli/fabric.ts +4 -2
  36. package/src/forge/cli/main.ts +2 -0
  37. package/src/forge/cli/parse.ts +3 -1
  38. package/src/forge/version.ts +1 -1
@@ -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;
@@ -183,14 +184,19 @@ async function runTypedCodexWorkerInternal(input: CodexWorkerInput & { outputSch
183
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 } : {}) };
184
185
  const thread = input.threadId ? client.resumeThread(input.threadId, threadOptions) : client.startThread(threadOptions);
185
186
  let threadId = input.threadId ?? thread.id ?? "", finalMessage: string | undefined, completed = false, count = 0, observedUsage: CodexWorkerUsage | undefined;
186
- 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` });
187
192
  for await (const event of stream.events) {
188
193
  checkAbort(input.signal); if (++count > 10000) fail("AF_CODEX_EVENT_LIMIT", "Typed worker event budget exceeded");
189
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");
190
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 }); }
191
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; }
192
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 }); }
193
- 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" });
194
200
  }
195
201
  checkAbort(input.signal); if (!completed || !threadId || !finalMessage) fail("AF_CODEX_INCOMPLETE", "Typed worker completion not observed");
196
202
  let data: unknown; try { data = JSON.parse(finalMessage); input.validateOutput(data); } catch { fail("AF_CODEX_REPORT", "Typed worker output schema invalid"); }
@@ -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; }
@@ -192,6 +192,10 @@ export async function captureManagedArtifact(base: ManagedBase, workspaceDirecto
192
192
  }
193
193
  const artifact = { digest: artifactDigest(files), files }; validateArtifact(artifact, writeScope); return artifact;
194
194
  }
195
+ /** A bounded observation of the complete managed source delta, never a termination attestation. */
196
+ export async function observeManagedWorkspace(base: ManagedBase, directory: string, inputDigest: string, environment?: ManagedEnvironment, generatedPaths: string[] = []): Promise<string> {
197
+ return (await captureManagedArtifact(base, directory, base.scope, inputDigest, environment, generatedPaths)).digest;
198
+ }
195
199
  export async function publishManagedArtifacts(base: ManagedBase, artifacts: ManagedArtifact[], controls?: { beforeWrite?: () => Promise<void> }): Promise<{ digest: string; changedFiles: string[] }> {
196
200
  await validateBase(base); const before = await inventory(base.root, base.scope);
197
201
  assert(before.digest === base.digest && await currentContextMatches(base) && await git(base.root, ["rev-parse", "HEAD"]) === base.head, "source changed since capture (including readonly environment context)");
@@ -26,7 +26,7 @@ export interface WorkflowOptions {
26
26
  branch: Common & { condition: boolean | ProgramExpr; then: Body; else: Body };
27
27
  loop: Common & { initialState: Data; maxRounds: number; body: Body; next: Data; until: boolean | ProgramExpr };
28
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" };
29
+ compose: Common & { candidates: Data; onConflict?: "needs-resolution"; resolver?: WorkflowRef<"executor">; resolverInput?: Data; resolverWriteScope?: Data };
30
30
  gate: Common & { candidate: Data; coverage?: Data; authorization?: Data };
31
31
  subworkflow: Common & { program: WorkflowRef<"program">; input: Data };
32
32
  waitEvent: Common & { type: string; correlation: Data; schema: WorkflowRef<"schema">; subject?: Data; timeoutMs?: number };
@@ -0,0 +1,48 @@
1
+ import { programDigest, programValueBytes, programWithin, refKey, registryEntry, validateWorkflowProgram, type ProgramRegistry, type ProgramSchema, type WorkflowProgramV2 } from "./program-contract.ts";
2
+ import { programGraph } from "./program-structure.ts";
3
+ import { buildWorkflowTemplate, type ProgramTemplateKind, type ProgramTemplateOptions } from "./program-templates.ts";
4
+
5
+ export interface ProgramAuthorDiagnostic { code: string; message: string; operationId?: string }
6
+ export interface ProgramAuthorProposal {
7
+ valid: boolean; program?: WorkflowProgramV2; digest?: string; diagnostics: ProgramAuthorDiagnostic[];
8
+ summary?: { criteria: string[]; requiredChecks: string[]; writeScope: string[]; humanApproval: boolean; operations: { id: string; kind: string; dependencies: string[] }[] };
9
+ }
10
+ export function validateProgramProposal(program: WorkflowProgramV2, registry: ProgramRegistry): ProgramAuthorProposal {
11
+ try {
12
+ validateWorkflowProgram(program, registry);
13
+ const acceptance = registryEntry(registry.acceptance, program.acceptance);
14
+ const policy = registryEntry(registry.policies, program.policy);
15
+ return { valid: true, program: structuredClone(program), digest: programDigest(program), diagnostics: [], summary: {
16
+ criteria: [...acceptance.criteria], requiredChecks: [...new Set([...acceptance.requiredChecks, ...(acceptance.requiredChecksByScope?.item ?? []), ...(acceptance.requiredChecksByScope?.final ?? [])])],
17
+ writeScope: [...new Set([...acceptance.writeScope, ...policy.writeScope])].filter(path => programWithin(path, acceptance.writeScope) && programWithin(path, policy.writeScope)), humanApproval: acceptance.requireHumanApproval === true,
18
+ operations: programGraph(program).map(entry => ({ id: entry.id, kind: entry.step.kind, dependencies: entry.dependencies })),
19
+ } };
20
+ } catch (error) {
21
+ return { valid: false, diagnostics: [{ code: (error as { code?: string }).code ?? "AF_PROGRAM_INVALID", message: error instanceof Error ? error.message : String(error) }] };
22
+ }
23
+ }
24
+ export function proposeWorkflowTemplate(kind: ProgramTemplateKind, options: ProgramTemplateOptions, registry: ProgramRegistry): ProgramAuthorProposal {
25
+ try { programValueBytes(options, 40000, 128); return validateProgramProposal(buildWorkflowTemplate(kind, options, registry), registry); }
26
+ catch (error) { return { valid: false, diagnostics: [{ code: "AF_PROGRAM_TEMPLATE_INVALID", message: error instanceof Error ? error.message : String(error) }] }; }
27
+ }
28
+
29
+ /** Generate authoring declarations from the owner's schemas, without evaluating authored code. */
30
+ export function generateRegistryAuthorTypes(registry: ProgramRegistry): string {
31
+ programValueBytes(registry, 4 * 1024 * 1024);
32
+ const shape = (schema: ProgramSchema, depth = 0): string => {
33
+ if (depth > 32) throw new Error("Schema depth exceeded");
34
+ if (Object.hasOwn(schema, "const")) return JSON.stringify(schema.const);
35
+ if (schema.enum?.length) return schema.enum.map(value => JSON.stringify(value)).join(" | ");
36
+ if (schema.anyOf?.length) return schema.anyOf.map(value => shape(value, depth + 1)).join(" | ");
37
+ const types = schema.type ? Array.isArray(schema.type) ? schema.type : [schema.type] : [];
38
+ if (!types.length) return "unknown";
39
+ return types.map(type => {
40
+ if (type === "object") return `{ ${[...Object.entries(schema.properties ?? {}).map(([key, value]) => `${JSON.stringify(key)}${schema.required?.includes(key) ? "" : "?"}: ${shape(value, depth + 1)}`), ...(schema.additionalProperties === false ? [] : ["[key: string]: unknown"])].join("; ")} }`;
41
+ if (type === "array") return `Array<${shape(schema.items ?? {}, depth + 1)}>`;
42
+ return type === "integer" ? "number" : type;
43
+ }).join(" | ");
44
+ };
45
+ const schemas = Object.entries(registry.schemas).sort(([a], [b]) => a.localeCompare(b));
46
+ const refs = (entries: Record<string, unknown>) => Object.keys(entries).sort().map(key => JSON.stringify(key)).join(" | ") || "never";
47
+ return `// Generated owner registry authoring types; runtime owner validation remains authoritative.\nexport interface OwnerSchemas {\n${schemas.map(([key, value]) => ` ${JSON.stringify(key)}: ${shape(value)};`).join("\n")}\n}\nexport interface OwnerExecutors {\n${Object.entries(registry.executors).sort(([a], [b]) => a.localeCompare(b)).map(([key, executor]) => ` ${JSON.stringify(key)}: { input: ${executor.inputSchema ? `OwnerSchemas[${JSON.stringify(refKey(executor.inputSchema))}]` : "unknown"}; output: OwnerSchemas[${JSON.stringify(refKey(executor.schema))}] };`).join("\n")}\n}\nexport type OwnerPolicyKey = ${refs(registry.policies)};\nexport type OwnerAcceptanceKey = ${refs(registry.acceptance)};\n`;
48
+ }
@@ -0,0 +1,76 @@
1
+ import { programAssert, programDigest, programValueBytes } from "./program-contract.ts";
2
+
3
+ export interface ProgramBenchmarkObservation {
4
+ taskId: string; runtimeVersion: string; model: string; toolsDigest: string; contextDigest: string; contractDigest: string;
5
+ category: "runtime" | "coding"; budget: string;
6
+ timingMs: Partial<Record<"queue" | "preparation" | "execution" | "capture" | "persistence" | "integration", number>>;
7
+ inputTokens?: number; outputTokens?: number; usageStatus: "observed" | "estimated" | "unknown";
8
+ externalEvaluation?: { assessor: string; criteriaDigest: string; independent: boolean; success: boolean; escapedDefects: number; unnecessaryChanges: number };
9
+ humanInterventions: number; repeatedActivities: number; retainedBytes?: number;
10
+ }
11
+ export interface ProgramBenchmarkReport { schemaVersion: 1; benchmarkId: string; observations: ProgramBenchmarkObservation[]; limitations: string[] }
12
+
13
+ /** Data validation only: producing a report never executes a model or workflow. */
14
+ export function validateBenchmarkReport(report: ProgramBenchmarkReport): void {
15
+ programValueBytes(report, 4 * 1024 * 1024);
16
+ programAssert(report?.schemaVersion === 1 && typeof report.benchmarkId === "string" && report.benchmarkId.length > 0, "Benchmark identity required");
17
+ programAssert(Array.isArray(report.observations) && report.observations.length > 0 && Array.isArray(report.limitations) && report.limitations.every(value => typeof value === "string"), "Benchmark observations and limitations required");
18
+ const identities = new Set<string>();
19
+ const count = (value: unknown) => Number.isSafeInteger(value) && Number(value) >= 0;
20
+ for (const entry of report.observations) {
21
+ programAssert(entry && typeof entry === "object", "Benchmark observation required");
22
+ for (const key of ["taskId", "runtimeVersion", "model", "toolsDigest", "contextDigest", "contractDigest", "budget"] as const) programAssert(typeof entry[key] === "string" && entry[key].length > 0, `Benchmark ${key} required`);
23
+ programAssert(!identities.has(entry.taskId), "Duplicate benchmark task identity"); identities.add(entry.taskId);
24
+ programAssert(["runtime", "coding"].includes(entry.category) && ["observed", "estimated", "unknown"].includes(entry.usageStatus), "Invalid benchmark category or usage status");
25
+ programAssert(entry.timingMs && typeof entry.timingMs === "object" && !Array.isArray(entry.timingMs), "Timing observations required");
26
+ for (const [key, value] of Object.entries(entry.timingMs)) programAssert(["queue", "preparation", "execution", "capture", "persistence", "integration"].includes(key) && typeof value === "number" && Number.isFinite(value) && value >= 0, "Invalid timing observation");
27
+ for (const value of [entry.humanInterventions, entry.repeatedActivities]) programAssert(count(value), "Invalid benchmark counter");
28
+ for (const value of [entry.inputTokens, entry.outputTokens, entry.retainedBytes]) if (value !== undefined) programAssert(count(value), "Invalid optional benchmark counter");
29
+ if (entry.usageStatus === "unknown") programAssert(entry.inputTokens === undefined && entry.outputTokens === undefined, "Unknown consumption cannot be represented as measured tokens");
30
+ else programAssert(entry.inputTokens !== undefined && entry.outputTokens !== undefined, "Observed or estimated consumption requires both token counters");
31
+ if (entry.externalEvaluation) {
32
+ const assessment = entry.externalEvaluation;
33
+ programAssert(typeof assessment.assessor === "string" && assessment.assessor.length > 0 && typeof assessment.criteriaDigest === "string" && assessment.criteriaDigest.length > 0, "External assessment provenance required");
34
+ programAssert(typeof assessment.independent === "boolean" && typeof assessment.success === "boolean" && count(assessment.escapedDefects) && count(assessment.unnecessaryChanges), "Invalid external assessment");
35
+ }
36
+ }
37
+ }
38
+ /** Pair by task identity, never by array position; unmatched/confounded trials fail eligibility. */
39
+ export function compareBenchmarkReports(left: ProgramBenchmarkReport, right: ProgramBenchmarkReport) {
40
+ validateBenchmarkReport(left); validateBenchmarkReport(right);
41
+ programAssert(left.benchmarkId !== right.benchmarkId, "Paired reports require distinct benchmark identities");
42
+ const leftById = new Map(left.observations.map(entry => [entry.taskId, entry]));
43
+ const rightById = new Map(right.observations.map(entry => [entry.taskId, entry]));
44
+ const differences: { taskId: string; fields: string[] }[] = [];
45
+ const unmatchedTasks = [...new Set([...leftById.keys(), ...rightById.keys()])].filter(id => !leftById.has(id) || !rightById.has(id)).sort();
46
+ const pairs = left.observations.filter(entry => rightById.has(entry.taskId)).map(entry => {
47
+ const counterpart = rightById.get(entry.taskId)!;
48
+ const fields: string[] = (["category", "model", "toolsDigest", "contextDigest", "contractDigest", "budget"] as const).filter(key => entry[key] !== counterpart[key]);
49
+ if (entry.externalEvaluation?.criteriaDigest !== counterpart.externalEvaluation?.criteriaDigest) fields.push("criteriaDigest");
50
+ if (fields.length) differences.push({ taskId: entry.taskId, fields: [...new Set(fields)] });
51
+ return { taskId: entry.taskId, leftVersion: entry.runtimeVersion, rightVersion: counterpart.runtimeVersion };
52
+ });
53
+ const leftSummary = summarizeBenchmarkReport(left), rightSummary = summarizeBenchmarkReport(right);
54
+ const metrics = (report: ProgramBenchmarkReport) => {
55
+ const sum = (values: number[], integral = false) => { const total = values.reduce((total, value) => total + value, 0); programAssert(Number.isFinite(total) && (!integral || Number.isSafeInteger(total)), "Benchmark aggregate exceeds numeric limits"); return total; };
56
+ const verified = report.observations.filter(entry => entry.category === "coding" && entry.externalEvaluation?.independent && entry.externalEvaluation.success && entry.externalEvaluation.escapedDefects === 0).length;
57
+ const observedTokens = report.observations.every(entry => entry.usageStatus === "observed") ? sum(report.observations.flatMap(entry => [entry.inputTokens!, entry.outputTokens!]), true) : null;
58
+ const stages = ["queue", "preparation", "execution", "capture", "persistence", "integration"] as const;
59
+ const timeMs = report.observations.every(entry => stages.every(stage => entry.timingMs[stage] !== undefined)) ? sum(report.observations.flatMap(entry => stages.map(stage => entry.timingMs[stage]!))) : null;
60
+ return { verifiedTasks: verified, observedTokens, timeMs, tokensPerVerifiedTask: observedTokens !== null && verified ? observedTokens / verified : null, timeMsPerVerifiedTask: timeMs !== null && verified ? timeMs / verified : null, humanInterventions: sum(report.observations.map(entry => entry.humanInterventions), true), repeatedActivities: sum(report.observations.map(entry => entry.repeatedActivities), true) };
61
+ };
62
+ return { schemaVersion: 1, pairs, unmatchedTasks, differences, left: { ...leftSummary, metrics: metrics(left) }, right: { ...rightSummary, metrics: metrics(right) },
63
+ comparisonEligible: unmatchedTasks.length === 0 && differences.length === 0 && leftSummary.comparisonEligible && rightSummary.comparisonEligible,
64
+ limitations: ["Paired report validation does not execute benchmarks or verify assessor independence. Model, tools, context, contract, budget and criteria differences prevent a controlled superiority claim.", ...left.limitations, ...right.limitations] };
65
+ }
66
+ export function summarizeBenchmarkReport(report: ProgramBenchmarkReport) {
67
+ validateBenchmarkReport(report);
68
+ const coding = report.observations.filter(entry => entry.category === "coding");
69
+ const independentlyAssessed = coding.filter(entry => entry.externalEvaluation?.independent);
70
+ const success = independentlyAssessed.filter(entry => entry.externalEvaluation?.success && entry.externalEvaluation.escapedDefects === 0).length;
71
+ return { digest: programDigest(report), tasks: report.observations.length, independentlyAssessed: independentlyAssessed.length,
72
+ verifiedSuccessRate: independentlyAssessed.length ? success / independentlyAssessed.length : null,
73
+ unknownUsageTasks: report.observations.filter(entry => entry.usageStatus === "unknown").length,
74
+ comparisonEligible: coding.length > 0 && independentlyAssessed.length === coding.length,
75
+ limitations: [...report.limitations, "Eligibility does not prove superiority; comparison requires matched workloads and declared model/tool/context differences."] };
76
+ }
@@ -0,0 +1,14 @@
1
+ /** Shared public capability boundary; reports support, not real-provider certification. */
2
+ export const programWorkflowExtensions = Object.freeze({
3
+ authoring: { templates: ["review", "bugfix", "migration"], ownerValidated: true, typedRegistry: true, dispatch: false },
4
+ runtimeConfig: ".forge/fabric-runtime.json",
5
+ scheduler: { fairness: "round-robin", defaultOwnerCapacity: 4, maxOwnerCapacity: 32 },
6
+ resources: { enforcement: "admission-only", unknownUsageExplicit: true, providerHardCap: false },
7
+ observations: { durable: true, deduplicated: true, invalidOutputRetainsUsage: true },
8
+ executors: ["codex", "command", "claude"],
9
+ claude: { optIn: true, isolation: "cooperative", network: "host", realProviderVerified: false },
10
+ nativeImages: ["codex"],
11
+ threadRecovery: "codex-observed-compatible-original-workspace",
12
+ conflictResolution: "owner-registered-resolver-fresh-final-assessment",
13
+ externalQualityEvaluated: false,
14
+ });
@@ -3,6 +3,7 @@ import { stableStringify } from "./canonical.ts";
3
3
  import { programGraph } from "./program-structure.ts";
4
4
  import { validateVisualPopulation } from "./program-evidence.ts";
5
5
  import { validateProgramTypes } from "./program-types.ts";
6
+ import type { ProgramWorkerObservation, ProgramUsageSummary } from "./program-observation.ts";
6
7
 
7
8
  export type ProgramValue = null | boolean | number | string | ProgramValue[] | { [key: string]: ProgramValue };
8
9
  export interface ProgramRef { id: string; version: string }
@@ -20,16 +21,18 @@ export type ProgramSchema = { type?: string | string[]; enum?: ProgramValue[]; c
20
21
  minimum?: number; maximum?: number; anyOf?: ProgramSchema[] };
21
22
  export interface ProgramLexicalContext { item?: ProgramSchema; state?: ProgramSchema }
22
23
  export interface ProgramExecutor {
23
- id: string; version: string; kind: "command" | "codex"; effect: "read" | "isolated-write" | "idempotent-effect" | "non-idempotent-effect";
24
+ id: string; version: string; kind: "command" | "codex" | "claude"; effect: "read" | "isolated-write" | "idempotent-effect" | "non-idempotent-effect";
24
25
  argv?: string[]; prompt?: string; role?: "implementer" | "reviewer" | "investigator" | "decision";
25
26
  model?: string; timeoutMs: number; writeScope: string[]; allowedGeneratedPaths?: string[];
26
27
  network: "disabled" | "host"; isolation: "cooperative" | "sandbox"; schema: ProgramRef;
27
28
  allowedExitCodes?: number[]; environment?: Record<string, string>;
29
+ tokenAccounting?: "none" | "provider";
28
30
  inputSchema?: ProgramRef;
29
31
  dependencies?: "none" | "auto";
30
32
  cache?: "none" | "workspace";
31
33
  }
32
34
  export interface ProgramPolicy {
35
+ resources?: { maxTokens: number; reserveTokensPerAttempt: number; finalReserveTokens?: number; unknownUsage: "block" | "allow" };
33
36
  allowFencedReplan?: boolean;
34
37
  captureScope?: string[];
35
38
  id: string; version: string; maxItems: number; concurrency: number; maxAttempts: number;
@@ -58,6 +61,7 @@ export interface ProgramRegistry {
58
61
  populations: Record<string, ProgramPopulation>; programs?: Record<string, WorkflowProgramV2>;
59
62
  }
60
63
  export interface ProgramAssessment {
64
+ resolutionReceiptIds?: string[];
61
65
  receiptId?: string; generation?: number; operationId?: string;
62
66
  semanticDigest?: string; inputDigest?: string;
63
67
  phase?: "item" | "final"; obligationIds?: string[]; satisfiedObligationIds?: string[]; unsatisfiedObligationIds?: string[];
@@ -78,6 +82,11 @@ export interface ProgramAttempt {
78
82
  outcome: "running" | "completed" | "infrastructure_failed" | "invalid_output" | "uncertain" | "canceled_confirmed";
79
83
  outputRef?: string; threadId?: string; reason?: string;
80
84
  reservation?: { scopes: { id: string; limit: number }[]; status: "held" | "released"; ownerEpoch: string };
85
+ observations?: Record<string, ProgramWorkerObservation>;
86
+ usageApplicability?: "not-applicable" | "provider";
87
+ usageSummary?: ProgramUsageSummary;
88
+ resourceReservation?: { tokens: number; status: "held" | "released"; phase?: "work" | "final" };
89
+ recovery?: { directory: string; inputDigest: string; workspaceDigest: string; threadId?: string; terminationObserved?: boolean; generationCompatible?: boolean };
81
90
  usage?: unknown;
82
91
  }
83
92
  export interface ProgramOperationRecord {
@@ -111,6 +120,7 @@ export interface ProgramRunV2 {
111
120
  waits: Record<string, { generation: number; deadlineAt?: string; signalId?: string; outputRef?: string }>;
112
121
  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 }>;
113
122
  collections: Record<string, { seal: string; coverageReceiptId: string | null; generation: number; ids: string[]; acceptedAssessmentIds: string[]; acceptedCandidateIds: string[]; obligationsSatisfied: boolean }>;
123
+ compositionResolutions?: Record<string, { receiptId: string; candidateId: string; candidateDigest: string; originalCandidateIds: string[]; attemptId: string; operationId: string; generation: number; semanticDigest: string; inputDigest: string; populationDigest: string; acceptanceDigest: string }>;
114
124
  resultRef?: string; gateRef?: string; intent?: ProgramApplyIntent; receiptRef?: string;
115
125
  acceptedCandidate?: ProgramCandidate;
116
126
  scopeResults?: Record<string, { outcomes: { id: string; status: string; reason?: string }[]; status: "completed" | "waiting" | "failed"; resultRef?: string }>;
@@ -212,6 +222,11 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
212
222
  const policy = registryEntry(registry.policies, program.policy), acceptance = registryEntry(registry.acceptance, program.acceptance);
213
223
  for (const field of ["maxItems", "concurrency", "maxAttempts", "maxOperations", "maxDepth", "deadlineMs", "maxOutputBytes"] as const) programAssert(Number.isSafeInteger(policy[field]) && policy[field] > 0, `Invalid limit ${field}`);
214
224
  programAssert(policy.concurrency <= 32 && policy.maxDepth <= 32 && policy.maxItems <= 10000 && policy.maxAttempts <= 10000 && policy.maxOperations <= 20000 && policy.maxOutputBytes <= 4 * 1024 * 1024, "Physical limits exceeded");
225
+ if (policy.resources) {
226
+ const budget = policy.resources;
227
+ for (const value of [budget.maxTokens, budget.reserveTokensPerAttempt, budget.finalReserveTokens ?? 0]) programAssert(Number.isSafeInteger(value) && value >= 0, "Invalid resource budget");
228
+ programAssert(budget.maxTokens > 0 && budget.reserveTokensPerAttempt > 0 && (budget.finalReserveTokens ?? 0) <= budget.maxTokens && ["block", "allow"].includes(budget.unknownUsage), "Invalid resource policy");
229
+ }
215
230
  for (const path of [...policy.writeScope, ...acceptance.writeScope, ...(policy.captureScope ?? [])]) programPath(path);
216
231
  programAssert(acceptance.criteria.length > 0 && new Set(acceptance.criteria).size === acceptance.criteria.length, "Acceptance criteria required and unique");
217
232
  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); }
@@ -235,7 +250,7 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
235
250
  map: ["items", "key", "coverage", "concurrency", "completion", "quorum", "body", "order"],
236
251
  branch: ["condition", "then", "else"], loop: ["initialState", "maxRounds", "body", "next", "until"],
237
252
  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"],
253
+ compose: ["candidates", "onConflict", "resolver", "resolverInput", "resolverWriteScope"], gate: ["candidate", "coverage", "authorization"], subworkflow: ["program", "input"],
239
254
  waitEvent: ["schema", "type", "correlation", "subject", "timeoutMs"], sequence: ["steps", "result"], parallel: ["steps", "result", "onFailure"],
240
255
  };
241
256
  kinds.add("sequence"); kinds.add("parallel");
@@ -255,7 +270,8 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
255
270
  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
271
  else programAssert(step.options.quorum === undefined, "Quorum only applies to quorum completion");
257
272
  }
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); }
273
+ if (["agent", "command"].includes(step.kind)) { const executor = registryEntry(registry.executors, step.options.executor as ProgramRef); programAssert(executor.tokenAccounting === undefined || executor.tokenAccounting === "provider" || executor.tokenAccounting === "none" && executor.kind === "command", "Token accounting exemption only applies to owner-declared nonprovider commands"); if (executor.inputSchema) registryEntry(registry.schemas, executor.inputSchema); }
274
+ if (step.kind === "compose" && step.options.resolver) programAssert(registryEntry(registry.executors, step.options.resolver as ProgramRef).effect === "isolated-write", "Conflict resolver must produce an isolated candidate");
259
275
  if (step.kind === "repair") {
260
276
  registryEntry(registry.executors, step.options.implement as ProgramRef); registryEntry(registry.executors, step.options.review as ProgramRef);
261
277
  programAssert(["implement-first", "assess-first"].includes(String(step.options.entryMode)), "Repair entryMode required");
@@ -0,0 +1,63 @@
1
+ import { programAssert, programDigest, programValueBytes, type ProgramAttempt } from "./program-contract.ts";
2
+
3
+ /** Adapter observations are data, not completion or acceptance authority. */
4
+ export interface ProgramWorkerObservation {
5
+ id: string; type: string; source: string; at: string;
6
+ usage?: { input_tokens: number; cached_input_tokens?: number; output_tokens: number };
7
+ semantics?: "incremental" | "cumulative";
8
+ metadata?: Record<string, unknown>;
9
+ }
10
+ export interface ProgramUsageSummary {
11
+ inputTokens: number; outputTokens: number; cachedInputTokens: number;
12
+ totalTokens: number; certainty: "observed" | "unknown" | "not-applicable";
13
+ }
14
+ export function recordProgramObservation(attempt: ProgramAttempt, observation: ProgramWorkerObservation): void {
15
+ programValueBytes(observation, 16384, 128);
16
+ programAssert(typeof observation.id === "string" && observation.id.length > 0 && observation.id.length <= 256 &&
17
+ typeof observation.type === "string" && observation.type.length <= 128 && typeof observation.source === "string" && observation.source.length <= 128 &&
18
+ Number.isFinite(Date.parse(observation.at)), "Invalid worker observation");
19
+ programAssert(observation.semantics === undefined || ["incremental", "cumulative"].includes(observation.semantics), "Invalid usage semantics");
20
+ if (observation.usage) {
21
+ const usage = observation.usage;
22
+ programAssert(Number.isSafeInteger(usage.input_tokens) && usage.input_tokens >= 0 && Number.isSafeInteger(usage.output_tokens) && usage.output_tokens >= 0, "Usage requires nonnegative safe input/output counters");
23
+ programAssert(usage.cached_input_tokens === undefined || Number.isSafeInteger(usage.cached_input_tokens) && usage.cached_input_tokens >= 0 && usage.cached_input_tokens <= usage.input_tokens, "Invalid cached usage counter");
24
+ }
25
+ const observations = attempt.observations ??= {};
26
+ if (observations[observation.id]) { programAssert(programDigest(observations[observation.id]) === programDigest(observation), "Observation identity changed"); return; }
27
+ programAssert(Object.keys(observations).length < 1024, "Attempt observation budget exceeded");
28
+ const next = { ...attempt, observations: { ...observations, [observation.id]: structuredClone(observation) } };
29
+ const summary = summarizeProgramUsage(next);
30
+ observations[observation.id] = next.observations[observation.id];
31
+ attempt.usageSummary = summary;
32
+ }
33
+ export function summarizeProgramUsage(attempt: ProgramAttempt): ProgramUsageSummary {
34
+ let inputTokens = 0, outputTokens = 0, cachedInputTokens = 0, observed = false;
35
+ const cumulative = new Map<string, NonNullable<ProgramWorkerObservation["usage"]>>();
36
+ const semantics = new Map<string, string>();
37
+ for (const observation of Object.values(attempt.observations ?? {})) {
38
+ if (!observation.usage) continue; observed = true;
39
+ const mode = observation.semantics ?? "incremental";
40
+ programAssert(!semantics.has(observation.source) || semantics.get(observation.source) === mode, "Usage source mixes cumulative and incremental semantics");
41
+ semantics.set(observation.source, mode);
42
+ if (observation.semantics === "cumulative") {
43
+ const previous = cumulative.get(observation.source);
44
+ cumulative.set(observation.source, { input_tokens: Math.max(previous?.input_tokens ?? 0, observation.usage.input_tokens), output_tokens: Math.max(previous?.output_tokens ?? 0, observation.usage.output_tokens), cached_input_tokens: Math.max(previous?.cached_input_tokens ?? 0, observation.usage.cached_input_tokens ?? 0) });
45
+ } else { inputTokens += observation.usage.input_tokens; outputTokens += observation.usage.output_tokens; cachedInputTokens += observation.usage.cached_input_tokens ?? 0; }
46
+ }
47
+ for (const usage of cumulative.values()) { inputTokens += usage.input_tokens; outputTokens += usage.output_tokens; cachedInputTokens += usage.cached_input_tokens ?? 0; }
48
+ if (!observed && attempt.usage && typeof attempt.usage === "object") {
49
+ const usage = attempt.usage as { input_tokens?: number; output_tokens?: number; cached_input_tokens?: number };
50
+ if (Number.isSafeInteger(usage.input_tokens) && Number.isSafeInteger(usage.output_tokens) && usage.input_tokens! >= 0 && usage.output_tokens! >= 0) {
51
+ inputTokens = usage.input_tokens!; outputTokens = usage.output_tokens!;
52
+ cachedInputTokens = Number.isSafeInteger(usage.cached_input_tokens) && usage.cached_input_tokens! >= 0 ? usage.cached_input_tokens! : 0; observed = true;
53
+ }
54
+ }
55
+ programAssert([inputTokens, outputTokens, cachedInputTokens, inputTokens + outputTokens].every(value => Number.isSafeInteger(value) && value >= 0), "Usage aggregate exceeds safe integer range");
56
+ return { inputTokens, outputTokens, cachedInputTokens, totalTokens: inputTokens + outputTokens, certainty: observed ? "observed" : attempt.usageApplicability === "not-applicable" ? "not-applicable" : "unknown" };
57
+ }
58
+ /** Held reservations and unknown completed calls remain liabilities after owner restart. */
59
+ export function programResourceLiability(attempt: ProgramAttempt): number {
60
+ const usage = summarizeProgramUsage(attempt);
61
+ const reserve = attempt.resourceReservation?.tokens ?? 0;
62
+ return usage.totalTokens + (attempt.resourceReservation?.status === "held" ? Math.max(0, reserve - usage.totalTokens) : usage.certainty === "unknown" ? reserve : 0);
63
+ }
@@ -0,0 +1,23 @@
1
+ import { lstatSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ProgramError } from "./program-contract.ts";
4
+
5
+ export interface ProgramRuntimeOptions { ownerCapacity?: number; ownerMaxTokens?: number }
6
+
7
+ /** Owner configuration is local trusted configuration, never a workflow request override. */
8
+ export function readProgramRuntimeOptions(root: string): ProgramRuntimeOptions {
9
+ const directory = join(root, ".forge"), path = join(directory, "fabric-runtime.json");
10
+ if (lstatSync(directory, { throwIfNoEntry: false })?.isSymbolicLink()) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration directory cannot be a symbolic link");
11
+ const stat = lstatSync(path, { throwIfNoEntry: false });
12
+ if (!stat) return {};
13
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration must be a regular file up to 16 KiB");
14
+ const value: unknown = JSON.parse(readFileSync(path, "utf8"));
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration must be an object");
16
+ const record = value as Record<string, unknown>;
17
+ if (record.schemaVersion !== 1 || Object.keys(record).some(key => !["schemaVersion", "ownerCapacity", "ownerMaxTokens"].includes(key))) throw new ProgramError("AF_PROGRAM_INVALID", "Unsupported runtime configuration");
18
+ for (const key of ["ownerCapacity", "ownerMaxTokens"] as const) {
19
+ const number = record[key];
20
+ if (number !== undefined && (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1 || (key === "ownerCapacity" && number > 32))) throw new ProgramError("AF_PROGRAM_INVALID", `Invalid ${key}`);
21
+ }
22
+ return { ownerCapacity: record.ownerCapacity as number | undefined, ownerMaxTokens: record.ownerMaxTokens as number | undefined };
23
+ }
@@ -8,8 +8,15 @@ export class ProgramActivityScheduler {
8
8
  private active = new Map<string, number>();
9
9
  private scopes = new Map<string, number>();
10
10
  private count = 0;
11
- private lastRun = "";
12
- constructor(readonly ownerLimit = 4) { programAssert(Number.isSafeInteger(ownerLimit) && ownerLimit > 0 && ownerLimit <= 32, "Invalid owner capacity"); }
11
+ private runOrder: string[] = [];
12
+ private cursor = 0;
13
+ private capacity: number;
14
+ get ownerLimit(): number { return this.capacity; }
15
+ constructor(ownerLimit = 4) { programAssert(Number.isSafeInteger(ownerLimit) && ownerLimit > 0 && ownerLimit <= 32, "Invalid owner capacity"); this.capacity = ownerLimit; }
16
+ setOwnerCapacity(ownerLimit: number): void {
17
+ programAssert(Number.isSafeInteger(ownerLimit) && ownerLimit > 0 && ownerLimit <= 32, "Invalid owner capacity");
18
+ this.capacity = ownerLimit; this.drain();
19
+ }
13
20
  restore(runId: string, scopes: ActivityScope[]): () => void {
14
21
  this.count++; this.active.set(runId, (this.active.get(runId) ?? 0) + 1);
15
22
  for (const scope of scopes) this.scopes.set(scope.id, (this.scopes.get(scope.id) ?? 0) + 1);
@@ -20,19 +27,31 @@ export class ProgramActivityScheduler {
20
27
  return new Promise((resolve, reject) => {
21
28
  const ticket: Ticket = { runId, runLimit, scopes, resolve, reject, signal, abort: () => { this.tickets = this.tickets.filter(entry => entry !== ticket); reject(new Error("Activity admission canceled")); this.drain(); } };
22
29
  if (signal.aborted) { ticket.abort(); return; }
23
- signal.addEventListener("abort", ticket.abort, { once: true }); this.tickets.push(ticket); this.drain();
30
+ signal.addEventListener("abort", ticket.abort, { once: true }); this.tickets.push(ticket);
31
+ if (!this.runOrder.includes(runId)) this.runOrder.push(runId);
32
+ this.drain();
24
33
  });
25
34
  }
26
35
  private drain(): void {
27
36
  while (this.count < this.ownerLimit) {
28
37
  const eligible = this.tickets.filter(ticket => !ticket.signal.aborted && (this.active.get(ticket.runId) ?? 0) < ticket.runLimit && ticket.scopes.every(scope => (this.scopes.get(scope.id) ?? 0) < scope.limit));
29
- const ticket = eligible.find(entry => entry.runId !== this.lastRun) ?? eligible[0]; if (!ticket) return;
38
+ let ticket: Ticket | undefined;
39
+ for (let offset = 0; offset < this.runOrder.length; offset++) {
40
+ const index = (this.cursor + offset) % this.runOrder.length;
41
+ ticket = eligible.find(entry => entry.runId === this.runOrder[index]);
42
+ // Keep the next ordinal unwrapped: newly queued runs after a lone run
43
+ // must get their turn before the previously admitted run repeats.
44
+ if (ticket) { this.cursor = index + 1; break; }
45
+ }
46
+ if (!ticket) return;
30
47
  this.tickets.splice(this.tickets.indexOf(ticket), 1); ticket.signal.removeEventListener("abort", ticket.abort);
31
- this.lastRun = ticket.runId; this.count++; this.active.set(ticket.runId, (this.active.get(ticket.runId) ?? 0) + 1);
48
+ this.count++; this.active.set(ticket.runId, (this.active.get(ticket.runId) ?? 0) + 1);
32
49
  for (const scope of ticket.scopes) this.scopes.set(scope.id, (this.scopes.get(scope.id) ?? 0) + 1);
33
50
  let released = false;
34
51
  ticket.resolve(() => { if (released) return; released = true; this.count--; this.active.set(ticket.runId, this.active.get(ticket.runId)! - 1); for (const scope of ticket.scopes) this.scopes.set(scope.id, this.scopes.get(scope.id)! - 1); this.drain(); });
35
52
  }
36
53
  }
37
- snapshot(): { active: number; queued: number; ownerLimit: number } { return { active: this.count, queued: this.tickets.length, ownerLimit: this.ownerLimit }; }
54
+ snapshot(): { active: number; queued: number; ownerLimit: number; runs: { runId: string; active: number; queued: number }[] } {
55
+ return { active: this.count, queued: this.tickets.length, ownerLimit: this.ownerLimit, runs: [...new Set([...this.active.keys(), ...this.tickets.map(ticket => ticket.runId)])].map(runId => ({ runId, active: this.active.get(runId) ?? 0, queued: this.tickets.filter(ticket => ticket.runId === runId).length })) };
56
+ }
38
57
  }