forgeos 0.1.0-alpha.7 → 0.1.0-alpha.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/forge-agent-fabric/SKILL.md +178 -0
- package/.agents/skills/forge-agent-fabric/references/managed-workflows.md +95 -0
- package/.agents/skills/forge-agent-fabric/references/repository-analysis.md +102 -0
- package/.agents/skills/forge-agent-fabric/scripts/fabric.mjs +46 -0
- package/.agents/skills/forge-agent-fabric/scripts/repository.mjs +44 -0
- package/.npmignore +10 -0
- package/AGENTS.md +142 -90
- package/CHANGELOG.md +870 -9
- package/LICENSE +21 -0
- package/README.md +121 -6
- package/adapters/go/README.md +23 -0
- package/adapters/go/go.mod +3 -0
- package/adapters/go/http.go +149 -0
- package/adapters/go/registry.go +234 -0
- package/adapters/go/types.go +136 -0
- package/adapters/java/README.md +68 -0
- package/adapters/java/pom.xml +47 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
- package/adapters/java-spring-boot-starter/README.md +32 -0
- package/adapters/java-spring-boot-starter/pom.xml +49 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
- package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/bin/forge.mjs +18 -0
- package/docs/agent-fabric-programs.md +151 -0
- package/docs/agent-fabric.md +582 -0
- package/docs/architecture/agent-fabric/PORTABLE_PROJECTS_PLAN.md +136 -0
- package/docs/architecture/agent-fabric/README.md +180 -0
- package/docs/architecture/agent-fabric/REPOSITORY_MANIFEST_PLAN.md +284 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +590 -0
- package/docs/cair-protocol.md +103 -0
- package/docs/changelog.md +1070 -0
- package/docs/forge-protocol.md +189 -0
- package/docs/repository-agent-benchmark.md +179 -0
- package/docs/repository-analysis.md +139 -0
- package/docs/repository-context-quality.md +179 -0
- package/docs/repository-runtime-observation.md +82 -0
- package/examples/agent-fabric-v2/README.md +70 -0
- package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
- package/examples/agent-fabric-v2/make-request.mjs +4 -0
- package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
- package/examples/agent-fabric-v2/migrate.workflow.ts +35 -0
- package/examples/agent-fabric-v2/registry.example.json +256 -0
- package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
- package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
- package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
- package/examples/agent-fabric-v2/visual-population.json +13 -0
- package/examples/agent-fabric-v2/workers/check.mjs +2 -0
- package/examples/agent-fabric-v2/workers/common.mjs +7 -0
- package/examples/agent-fabric-v2/workers/discover.mjs +2 -0
- package/examples/agent-fabric-v2/workers/migrate.mjs +4 -0
- package/examples/agent-fabric-v2/workers/review.mjs +3 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +60 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
- package/package.json +60 -5
- package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
- package/schemas/forge-manifest.schema.json +389 -0
- package/scripts/field-test-forgeos.mjs +1021 -0
- package/scripts/install-agent-fabric-skill.mjs +105 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1592 -125
- package/src/forge/agent-adapters/types.ts +225 -1
- package/src/forge/agent-fabric/adapter.ts +131 -0
- package/src/forge/agent-fabric/attached-snapshot.ts +55 -0
- package/src/forge/agent-fabric/attached-task-contract.ts +105 -0
- package/src/forge/agent-fabric/attached-task-service.ts +127 -0
- package/src/forge/agent-fabric/attached-task-store.ts +109 -0
- package/src/forge/agent-fabric/authority.ts +298 -0
- package/src/forge/agent-fabric/canonical.ts +135 -0
- package/src/forge/agent-fabric/codex-adversarial-review.ts +229 -0
- package/src/forge/agent-fabric/codex-sdk-worker.ts +203 -0
- package/src/forge/agent-fabric/conductor.ts +754 -0
- package/src/forge/agent-fabric/dictionary.ts +12 -0
- package/src/forge/agent-fabric/errors.ts +32 -0
- package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
- package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
- package/src/forge/agent-fabric/index.ts +48 -0
- package/src/forge/agent-fabric/journal.ts +116 -0
- package/src/forge/agent-fabric/local-adaptive-digest-worker.mjs +26 -0
- package/src/forge/agent-fabric/local-adaptive-harness.ts +343 -0
- package/src/forge/agent-fabric/local-adaptive-service.ts +398 -0
- package/src/forge/agent-fabric/local-adaptive-worker.ts +146 -0
- package/src/forge/agent-fabric/local-approval-window.ts +211 -0
- package/src/forge/agent-fabric/local-change-review-service.ts +374 -0
- package/src/forge/agent-fabric/local-coding-worker.ts +285 -0
- package/src/forge/agent-fabric/local-control-store.ts +239 -0
- package/src/forge/agent-fabric/local-effect-broker.ts +348 -0
- package/src/forge/agent-fabric/local-evolution-approval.ts +81 -0
- package/src/forge/agent-fabric/local-evolution-profile.ts +87 -0
- package/src/forge/agent-fabric/local-evolution-registry.ts +427 -0
- package/src/forge/agent-fabric/local-evolution-service.ts +200 -0
- package/src/forge/agent-fabric/local-intelligence.ts +281 -0
- package/src/forge/agent-fabric/local-paths.ts +18 -0
- package/src/forge/agent-fabric/local-task-contract.ts +203 -0
- package/src/forge/agent-fabric/local-task-inbox.ts +543 -0
- package/src/forge/agent-fabric/local-task-server.ts +374 -0
- package/src/forge/agent-fabric/local-task-service.ts +982 -0
- package/src/forge/agent-fabric/local-verification.ts +551 -0
- package/src/forge/agent-fabric/managed-environment.ts +205 -0
- package/src/forge/agent-fabric/managed-run-contract.ts +103 -0
- package/src/forge/agent-fabric/managed-run-service.ts +401 -0
- package/src/forge/agent-fabric/managed-run-store.ts +165 -0
- package/src/forge/agent-fabric/managed-workspace.ts +253 -0
- package/src/forge/agent-fabric/p0a.ts +104 -0
- package/src/forge/agent-fabric/p0b-model-adapter.ts +434 -0
- package/src/forge/agent-fabric/planning.ts +155 -0
- package/src/forge/agent-fabric/program-api.ts +37 -0
- package/src/forge/agent-fabric/program-contract.ts +283 -0
- package/src/forge/agent-fabric/program-dsl.ts +126 -0
- package/src/forge/agent-fabric/program-evidence.ts +22 -0
- package/src/forge/agent-fabric/program-image.ts +15 -0
- package/src/forge/agent-fabric/program-lock.ts +50 -0
- package/src/forge/agent-fabric/program-owner.ts +16 -0
- package/src/forge/agent-fabric/program-scheduler.ts +38 -0
- package/src/forge/agent-fabric/program-service.ts +793 -0
- package/src/forge/agent-fabric/program-store.ts +142 -0
- package/src/forge/agent-fabric/program-structure.ts +65 -0
- package/src/forge/agent-fabric/program-types.ts +110 -0
- package/src/forge/agent-fabric/program-view.ts +12 -0
- package/src/forge/agent-fabric/program-worker.ts +108 -0
- package/src/forge/agent-fabric/project-profile.ts +50 -0
- package/src/forge/agent-fabric/project-registry.ts +120 -0
- package/src/forge/agent-fabric/project-runtime.ts +85 -0
- package/src/forge/agent-fabric/readiness.ts +26 -0
- package/src/forge/agent-fabric/reducer.ts +863 -0
- package/src/forge/agent-fabric/repository-context.ts +83 -0
- package/src/forge/agent-fabric/resource-ledger.ts +266 -0
- package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
- package/src/forge/agent-fabric/types.ts +424 -0
- package/src/forge/agent-fabric/validation.ts +500 -0
- package/src/forge/agent-fabric/workflow-engine.ts +218 -0
- package/src/forge/agent-fabric/workflow-task-actions.ts +104 -0
- package/src/forge/agent-fabric/workflow-templates.ts +11 -0
- package/src/forge/agent-memory/bridge.ts +1728 -0
- package/src/forge/agent-memory/context-pack.ts +284 -0
- package/src/forge/agent-memory/hook-runner.ts +331 -0
- package/src/forge/agent-memory/mcp.ts +597 -0
- package/src/forge/agent-memory/normalize.ts +498 -0
- package/src/forge/agent-memory/redaction.ts +103 -0
- package/src/forge/agent-memory/sources/claude-code.ts +51 -0
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +343 -0
- package/src/forge/agent-memory/sources/codex.ts +161 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +215 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +896 -0
- package/src/forge/brownfield-import/types.ts +127 -0
- package/src/forge/cair/action-journal.ts +61 -0
- package/src/forge/cair/action-parser.ts +314 -0
- package/src/forge/cair/action-validator.ts +40 -0
- package/src/forge/cair/actions.ts +1818 -0
- package/src/forge/cair/format.ts +77 -0
- package/src/forge/cair/index.ts +118 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +255 -0
- package/src/forge/cli/adaptive.ts +51 -0
- package/src/forge/cli/ai.ts +16 -1
- package/src/forge/cli/auth.ts +459 -15
- package/src/forge/cli/authmd.ts +441 -0
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/build.ts +20 -4
- package/src/forge/cli/changed.ts +418 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1859 -20
- package/src/forge/cli/db.ts +209 -2
- package/src/forge/cli/deploy.ts +1339 -0
- package/src/forge/cli/deps.ts +150 -23
- package/src/forge/cli/dev.ts +1137 -53
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/doctor.ts +297 -0
- package/src/forge/cli/env-placeholders.ts +62 -0
- package/src/forge/cli/evolution.ts +40 -0
- package/src/forge/cli/fabric.ts +253 -0
- package/src/forge/cli/field-test.ts +568 -0
- package/src/forge/cli/golden-path.ts +487 -0
- package/src/forge/cli/handoff.ts +336 -0
- package/src/forge/cli/index.ts +1 -0
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/main.ts +196 -3
- package/src/forge/cli/new.ts +169 -17
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +398 -6
- package/src/forge/cli/parse.ts +1377 -38
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/query.ts +32 -0
- package/src/forge/cli/release.ts +35 -11
- package/src/forge/cli/repository.ts +151 -0
- package/src/forge/cli/run.ts +42 -0
- package/src/forge/cli/secrets.ts +137 -1
- package/src/forge/cli/seed.ts +581 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2282 -0
- package/src/forge/cli/verify.ts +1458 -32
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +3509 -0
- package/src/forge/compiler/agent-contract/build.ts +728 -64
- package/src/forge/compiler/agent-contract/types.ts +45 -2
- package/src/forge/compiler/ai-registry/parse.ts +71 -2
- package/src/forge/compiler/api-surface/build.ts +47 -0
- package/src/forge/compiler/app-graph/build.ts +112 -9
- package/src/forge/compiler/app-graph/extract.ts +107 -0
- package/src/forge/compiler/app-graph/module-graph.ts +73 -78
- package/src/forge/compiler/app-graph/parser.ts +24 -24
- package/src/forge/compiler/app-graph/profile.ts +26 -0
- package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
- package/src/forge/compiler/app-graph/versions.ts +1 -1
- package/src/forge/compiler/classifier/capabilities.ts +3 -2
- package/src/forge/compiler/classifier/classify.ts +32 -8
- package/src/forge/compiler/classifier/secrets.ts +3 -2
- package/src/forge/compiler/classifier/signals.ts +91 -1
- package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
- package/src/forge/compiler/client-sdk/render-client.ts +295 -13
- package/src/forge/compiler/data-graph/build.ts +29 -1
- package/src/forge/compiler/data-graph/parse.ts +20 -4
- package/src/forge/compiler/data-graph/sql/ddl.ts +144 -31
- package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
- package/src/forge/compiler/data-graph/sql/types.ts +1 -0
- package/src/forge/compiler/diagnostics/codes.ts +38 -0
- package/src/forge/compiler/diagnostics/create.ts +28 -2
- package/src/forge/compiler/diagnostics/index.ts +2 -0
- package/src/forge/compiler/emitter/barrel.ts +3 -0
- package/src/forge/compiler/emitter/render.ts +9 -0
- package/src/forge/compiler/external-manifest/registry.ts +217 -0
- package/src/forge/compiler/external-manifest/types.ts +91 -0
- package/src/forge/compiler/external-manifest/validate.ts +373 -0
- package/src/forge/compiler/frontend-graph/build.ts +142 -14
- package/src/forge/compiler/integration/add.ts +991 -33
- package/src/forge/compiler/integration/plan.ts +43 -12
- package/src/forge/compiler/integration/render.ts +29 -0
- package/src/forge/compiler/integration/snapshot.ts +2 -0
- package/src/forge/compiler/integration/templates/convex.ts +70 -0
- package/src/forge/compiler/integration/templates/index.ts +1 -0
- package/src/forge/compiler/integration/templates/render.ts +38 -0
- package/src/forge/compiler/integration/templates/types.ts +3 -0
- package/src/forge/compiler/integration/templates/workos.ts +1439 -0
- package/src/forge/compiler/make-registry/build.ts +8 -9
- package/src/forge/compiler/orchestrator/fast-check.ts +4 -0
- package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
- package/src/forge/compiler/orchestrator/manifest.ts +1 -1
- package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
- package/src/forge/compiler/orchestrator/plan.ts +249 -10
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +101 -33
- package/src/forge/compiler/orchestrator/serialize.ts +33 -8
- package/src/forge/compiler/orchestrator/types.ts +1 -1
- package/src/forge/compiler/package-graph/compiler.ts +3 -1
- package/src/forge/compiler/package-graph/constants.ts +1 -1
- package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
- package/src/forge/compiler/package-manager/adapter.ts +4 -1
- package/src/forge/compiler/package-manager/commands.ts +4 -0
- package/src/forge/compiler/package-manager/executor.ts +30 -1
- package/src/forge/compiler/package-manager/version.ts +36 -12
- package/src/forge/compiler/package-upgrades/planner.ts +155 -18
- package/src/forge/compiler/package-upgrades/types.ts +2 -0
- package/src/forge/compiler/policy-registry/build.ts +47 -2
- package/src/forge/compiler/policy-registry/parse.ts +32 -2
- package/src/forge/compiler/recipes/definitions.ts +63 -0
- package/src/forge/compiler/recipes/index.ts +2 -0
- package/src/forge/compiler/recipes/profiles.ts +81 -0
- package/src/forge/compiler/recipes/registry.ts +6 -0
- package/src/forge/compiler/test-graph/build.ts +11 -3
- package/src/forge/compiler/types/app-graph.ts +8 -2
- package/src/forge/compiler/types/cli.ts +76 -1
- package/src/forge/compiler/types/dev-manifest.ts +4 -0
- package/src/forge/compiler/types/emit.ts +2 -0
- package/src/forge/compiler/types/frontend-graph.ts +2 -2
- package/src/forge/compiler/types/integration.ts +1 -0
- package/src/forge/compiler/types/package-graph.ts +6 -0
- package/src/forge/compiler/types/policy-registry.ts +3 -1
- package/src/forge/delta/broker-runner.mjs +5 -0
- package/src/forge/delta/broker.ts +429 -0
- package/src/forge/delta/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +238 -0
- package/src/forge/delta/git-observer.ts +67 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +25 -0
- package/src/forge/delta/process-identity.ts +37 -0
- package/src/forge/delta/queue-status.ts +190 -0
- package/src/forge/delta/recorder.ts +540 -0
- package/src/forge/delta/redaction.ts +50 -0
- package/src/forge/delta/schema.ts +240 -0
- package/src/forge/delta/session.ts +142 -0
- package/src/forge/delta/status.ts +1139 -0
- package/src/forge/delta/store.ts +3329 -0
- package/src/forge/delta/timeline.ts +178 -0
- package/src/forge/dev/server.ts +1061 -22
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +285 -33
- package/src/forge/dev-console/types.ts +47 -1
- package/src/forge/impact/index.ts +222 -30
- package/src/forge/impact/types.ts +33 -1
- package/src/forge/intent/index.ts +35 -16
- package/src/forge/make/fields.ts +26 -0
- package/src/forge/make/index.ts +42 -6
- package/src/forge/make/templates.ts +211 -4
- package/src/forge/make/types.ts +2 -1
- package/src/forge/policy.ts +1 -1
- package/src/forge/react/index.ts +160 -2
- package/src/forge/refactor/index.ts +1 -0
- package/src/forge/repair/rules/index.ts +2 -2
- package/src/forge/repository-analysis/adapters/common.ts +72 -0
- package/src/forge/repository-analysis/adapters/docker.ts +185 -0
- package/src/forge/repository-analysis/adapters/java.ts +133 -0
- package/src/forge/repository-analysis/adapters/typescript.ts +350 -0
- package/src/forge/repository-analysis/adapters/vue.ts +71 -0
- package/src/forge/repository-analysis/analyze.ts +434 -0
- package/src/forge/repository-analysis/benchmark.ts +336 -0
- package/src/forge/repository-analysis/cache-gc.ts +154 -0
- package/src/forge/repository-analysis/check-selection.ts +123 -0
- package/src/forge/repository-analysis/context-node.ts +17 -0
- package/src/forge/repository-analysis/context.ts +456 -0
- package/src/forge/repository-analysis/http-resolution.ts +135 -0
- package/src/forge/repository-analysis/identity.ts +45 -0
- package/src/forge/repository-analysis/index.ts +18 -0
- package/src/forge/repository-analysis/nuxt-resolution.ts +188 -0
- package/src/forge/repository-analysis/quality.ts +60 -0
- package/src/forge/repository-analysis/retrieval.ts +309 -0
- package/src/forge/repository-analysis/runtime-artifacts.ts +195 -0
- package/src/forge/repository-analysis/runtime-observation.ts +325 -0
- package/src/forge/repository-analysis/scanner.ts +174 -0
- package/src/forge/repository-analysis/storage.ts +193 -0
- package/src/forge/repository-analysis/test-associations.ts +59 -0
- package/src/forge/repository-analysis/types.ts +99 -0
- package/src/forge/repository-manifest/index.ts +178 -0
- package/src/forge/repository-manifest/types.ts +63 -0
- package/src/forge/review/index.ts +162 -12
- package/src/forge/review/types.ts +15 -0
- package/src/forge/runtime/ai/providers.ts +19 -4
- package/src/forge/runtime/auth/config.ts +17 -0
- package/src/forge/runtime/auth/evaluate.ts +15 -2
- package/src/forge/runtime/auth/resolve.ts +61 -6
- package/src/forge/runtime/auth/types.ts +1 -0
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/generated-client.ts +13 -2
- package/src/forge/runtime/db/memory-adapter.ts +139 -32
- package/src/forge/runtime/db/pglite-adapter.ts +285 -2
- package/src/forge/runtime/executor.ts +129 -16
- package/src/forge/runtime/external/bridge.ts +663 -0
- package/src/forge/runtime/runner/run-entry.ts +16 -7
- package/src/forge/runtime/telemetry/buffer.ts +4 -1
- package/src/forge/runtime/telemetry/scrubber.ts +35 -5
- package/src/forge/runtime/webhooks/security.ts +12 -7
- package/src/forge/server.ts +71 -7
- package/src/forge/ui/index.ts +831 -19
- package/src/forge/ui/types.ts +3 -1
- package/src/forge/version.ts +1 -1
- package/src/forge/vue/index.ts +422 -0
- package/src/forge/workspace/baseline.ts +112 -0
- package/src/forge/workspace/change-summary.ts +292 -0
- package/src/forge/workspace/forge-cli.ts +42 -0
- package/src/forge/workspace/git-summary.ts +393 -0
- package/templates/agent-workroom/AGENTS.md +29 -0
- package/templates/agent-workroom/README.md +34 -0
- package/templates/agent-workroom/forge.config.ts +3 -0
- package/templates/agent-workroom/package.json +34 -0
- package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
- package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
- package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
- package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
- package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
- package/templates/agent-workroom/src/forge/schema.ts +54 -0
- package/templates/agent-workroom/src/policies.ts +6 -0
- package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
- package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
- package/templates/agent-workroom/tsconfig.json +16 -0
- package/templates/agent-workroom/web/index.html +13 -0
- package/templates/agent-workroom/web/package.json +21 -0
- package/templates/agent-workroom/web/src/App.tsx +345 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +20 -0
- package/templates/agent-workroom/web/src/main.tsx +13 -0
- package/templates/agent-workroom/web/src/styles.css +545 -0
- package/templates/agent-workroom/web/tsconfig.json +27 -0
- package/templates/agent-workroom/web/vite.config.ts +36 -0
- package/templates/b2b-support-web/package.json +2 -0
- package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/web/app/page.tsx +2 -2
- package/templates/b2b-support-web/web/lib/forge.ts +9 -2
- package/templates/b2b-support-web/web/next.config.ts +24 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +2 -1
- package/templates/minimal-web/pnpm-workspace.yaml +2 -0
- package/templates/minimal-web/web/index.html +1 -0
- package/templates/minimal-web/web/src/App.tsx +12 -6
- package/templates/minimal-web/web/src/lib/forge.ts +9 -2
- package/templates/minimal-web/web/src/styles.css +34 -0
- package/templates/minimal-web/web/vite.config.ts +36 -0
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/.yarnrc.yml +1 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +37 -0
- package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +22 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +6 -0
- package/templates/vendor-access/.vscode/settings.json +14 -0
- package/templates/vendor-access/README.md +30 -0
- package/templates/vendor-access/forge.config.ts +3 -0
- package/templates/vendor-access/package.json +35 -0
- package/templates/vendor-access/pnpm-workspace.yaml +2 -0
- package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
- package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
- package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
- package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
- package/templates/vendor-access/src/forge/schema.ts +74 -0
- package/templates/vendor-access/src/policies.ts +11 -0
- package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/tsconfig.json +17 -0
- package/templates/vendor-access/web/index.html +13 -0
- package/templates/vendor-access/web/package.json +21 -0
- package/templates/vendor-access/web/src/App.tsx +573 -0
- package/templates/vendor-access/web/src/lib/forge.ts +20 -0
- package/templates/vendor-access/web/src/main.tsx +205 -0
- package/templates/vendor-access/web/src/styles.css +682 -0
- package/templates/vendor-access/web/tsconfig.json +18 -0
- package/templates/vendor-access/web/vite.config.ts +36 -0
- package/src/forge/_generated/actionSubscriptions.json +0 -2
- package/src/forge/_generated/actionSubscriptions.ts +0 -10
- package/src/forge/_generated/agentAdapterManifest.json +0 -2
- package/src/forge/_generated/agentAdapterManifest.ts +0 -73
- package/src/forge/_generated/agentContract.json +0 -2
- package/src/forge/_generated/agentContract.ts +0 -7829
- package/src/forge/_generated/agentQuickstart.md +0 -34
- package/src/forge/_generated/agentTools.json +0 -2
- package/src/forge/_generated/agentTools.md +0 -16
- package/src/forge/_generated/agentTools.ts +0 -12
- package/src/forge/_generated/aiContext.ts +0 -125
- package/src/forge/_generated/aiModels.json +0 -2
- package/src/forge/_generated/aiModels.ts +0 -51
- package/src/forge/_generated/aiProviders.json +0 -2
- package/src/forge/_generated/aiProviders.ts +0 -23
- package/src/forge/_generated/aiRegistry.json +0 -2
- package/src/forge/_generated/aiRegistry.ts +0 -31
- package/src/forge/_generated/api.json +0 -2
- package/src/forge/_generated/api.ts +0 -8
- package/src/forge/_generated/appGraph.json +0 -2
- package/src/forge/_generated/appGraph.ts +0 -14937
- package/src/forge/_generated/appMap.md +0 -55
- package/src/forge/_generated/artifactManifest.json +0 -2
- package/src/forge/_generated/artifactManifest.ts +0 -7
- package/src/forge/_generated/authClaims.json +0 -2
- package/src/forge/_generated/authClaims.ts +0 -13
- package/src/forge/_generated/authConfig.json +0 -2
- package/src/forge/_generated/authConfig.ts +0 -17
- package/src/forge/_generated/authContext.ts +0 -23
- package/src/forge/_generated/authRegistry.json +0 -2
- package/src/forge/_generated/authRegistry.ts +0 -25
- package/src/forge/_generated/buildInfo.json +0 -2
- package/src/forge/_generated/buildInfo.ts +0 -9
- package/src/forge/_generated/capabilityMap.json +0 -2
- package/src/forge/_generated/capabilityMap.md +0 -15
- package/src/forge/_generated/capabilityMap.ts +0 -17
- package/src/forge/_generated/client.ts +0 -282
- package/src/forge/_generated/clientApi.ts +0 -9
- package/src/forge/_generated/clientManifest.json +0 -2
- package/src/forge/_generated/clientManifest.ts +0 -39
- package/src/forge/_generated/clientTypes.ts +0 -78
- package/src/forge/_generated/configRegistry.json +0 -2
- package/src/forge/_generated/configRegistry.ts +0 -4
- package/src/forge/_generated/dataGraph.json +0 -2
- package/src/forge/_generated/dataGraph.ts +0 -8
- package/src/forge/_generated/db.json +0 -2
- package/src/forge/_generated/db.ts +0 -2
- package/src/forge/_generated/dbSecurityManifest.json +0 -2
- package/src/forge/_generated/dbSecurityManifest.ts +0 -15
- package/src/forge/_generated/dbSessionContext.json +0 -2
- package/src/forge/_generated/dbSessionContext.ts +0 -39
- package/src/forge/_generated/deployManifest.json +0 -2
- package/src/forge/_generated/deployManifest.ts +0 -14
- package/src/forge/_generated/devManifest.json +0 -2
- package/src/forge/_generated/devManifest.ts +0 -62
- package/src/forge/_generated/envSchema.json +0 -2
- package/src/forge/_generated/envSchema.ts +0 -59
- package/src/forge/_generated/frontendGraph.json +0 -2
- package/src/forge/_generated/frontendGraph.ts +0 -27
- package/src/forge/_generated/importGuards.json +0 -2
- package/src/forge/_generated/importGuards.ts +0 -686
- package/src/forge/_generated/index.ts +0 -68
- package/src/forge/_generated/liveProductionManifest.json +0 -2
- package/src/forge/_generated/liveProductionManifest.ts +0 -23
- package/src/forge/_generated/liveProtocol.json +0 -2
- package/src/forge/_generated/liveProtocol.ts +0 -21
- package/src/forge/_generated/liveQueryRegistry.json +0 -2
- package/src/forge/_generated/liveQueryRegistry.ts +0 -9
- package/src/forge/_generated/liveTransportConfig.json +0 -2
- package/src/forge/_generated/liveTransportConfig.ts +0 -19
- package/src/forge/_generated/makeRegistry.json +0 -2
- package/src/forge/_generated/makeRegistry.ts +0 -177
- package/src/forge/_generated/makeTemplates.json +0 -2
- package/src/forge/_generated/makeTemplates.ts +0 -66
- package/src/forge/_generated/mockMap.json +0 -2
- package/src/forge/_generated/mockMap.ts +0 -7
- package/src/forge/_generated/operationPlaybooks.md +0 -167
- package/src/forge/_generated/packageGraph.json +0 -2
- package/src/forge/_generated/packageGraph.ts +0 -249334
- package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
- package/src/forge/_generated/permissionMatrix.json +0 -2
- package/src/forge/_generated/permissionMatrix.ts +0 -7
- package/src/forge/_generated/policyRegistry.json +0 -2
- package/src/forge/_generated/policyRegistry.ts +0 -11
- package/src/forge/_generated/queryRegistry.json +0 -2
- package/src/forge/_generated/queryRegistry.ts +0 -9
- package/src/forge/_generated/react.d.ts +0 -22
- package/src/forge/_generated/react.ts +0 -29
- package/src/forge/_generated/reactManifest.json +0 -2
- package/src/forge/_generated/reactManifest.ts +0 -19
- package/src/forge/_generated/rlsPolicies.json +0 -2
- package/src/forge/_generated/rlsPolicies.sql +0 -34
- package/src/forge/_generated/rlsPolicies.ts +0 -6
- package/src/forge/_generated/runtimeGraph.json +0 -2
- package/src/forge/_generated/runtimeGraph.ts +0 -8
- package/src/forge/_generated/runtimeMatrix.json +0 -2
- package/src/forge/_generated/runtimeMatrix.ts +0 -334130
- package/src/forge/_generated/runtimeRegistry.ts +0 -2
- package/src/forge/_generated/runtimeRules.md +0 -91
- package/src/forge/_generated/secretRegistry.json +0 -2
- package/src/forge/_generated/secretRegistry.ts +0 -50
- package/src/forge/_generated/secretsContext.ts +0 -11
- package/src/forge/_generated/serverApi.ts +0 -10
- package/src/forge/_generated/sourceMapManifest.json +0 -2
- package/src/forge/_generated/sourceMapManifest.ts +0 -7
- package/src/forge/_generated/sqlPlan.json +0 -2
- package/src/forge/_generated/sqlPlan.ts +0 -88
- package/src/forge/_generated/subscriptionManifest.json +0 -2
- package/src/forge/_generated/subscriptionManifest.ts +0 -7
- package/src/forge/_generated/symbolicationManifest.json +0 -2
- package/src/forge/_generated/symbolicationManifest.ts +0 -17
- package/src/forge/_generated/telemetryRegistry.json +0 -2
- package/src/forge/_generated/telemetryRegistry.ts +0 -9
- package/src/forge/_generated/telemetrySinks.json +0 -2
- package/src/forge/_generated/telemetrySinks.ts +0 -11
- package/src/forge/_generated/tenantScope.json +0 -2
- package/src/forge/_generated/tenantScope.ts +0 -8
- package/src/forge/_generated/testGraph.json +0 -2
- package/src/forge/_generated/testGraph.ts +0 -3452
- package/src/forge/_generated/testPlanRegistry.json +0 -2
- package/src/forge/_generated/testPlanRegistry.ts +0 -33
- package/src/forge/_generated/uiRoutes.json +0 -2
- package/src/forge/_generated/uiRoutes.ts +0 -16
- package/src/forge/_generated/uiScenarios.json +0 -2
- package/src/forge/_generated/uiScenarios.ts +0 -30
- package/src/forge/_generated/uiTestManifest.json +0 -2
- package/src/forge/_generated/uiTestManifest.ts +0 -27
- package/src/forge/_generated/workflowRegistry.json +0 -2
- package/src/forge/_generated/workflowRegistry.ts +0 -9
- package/src/forge/_generated/workflowSubscriptions.json +0 -2
- package/src/forge/_generated/workflowSubscriptions.ts +0 -10
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import type { AddressInfo } from "node:net";
|
|
5
|
+
import type { LocalCodingTaskProposal } from "./local-task-contract.ts";
|
|
6
|
+
import type { LocalMemoryEntry } from "./local-intelligence.ts";
|
|
7
|
+
import type { Digest } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export interface LocalApprovalView {
|
|
10
|
+
taskId: string;
|
|
11
|
+
repositoryRoot: string;
|
|
12
|
+
proposal: Readonly<LocalCodingTaskProposal>;
|
|
13
|
+
proposalDigest: Digest;
|
|
14
|
+
memory?: readonly LocalMemoryEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LocalPatchReviewView {
|
|
18
|
+
taskId: string;
|
|
19
|
+
repositoryRoot: string;
|
|
20
|
+
baseCommit: string;
|
|
21
|
+
diffDigest: Digest;
|
|
22
|
+
diff: string;
|
|
23
|
+
verification: "diff_check_passed" | "diff_check_failed";
|
|
24
|
+
sandboxVerification?: { state: "started" | "finished"; outcome?: string; evidenceDigest?: Digest };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface LocalVerificationRecoveryBase {
|
|
28
|
+
kind: "verification-recovery";
|
|
29
|
+
taskId: string;
|
|
30
|
+
repositoryRoot: string;
|
|
31
|
+
diffDigest: Digest;
|
|
32
|
+
requestDigest: Digest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type LocalVerificationRecoveryView = LocalVerificationRecoveryBase & (
|
|
36
|
+
| { mode: "clear" }
|
|
37
|
+
| { mode: "continue"; remainingCommands: readonly { path: string; timeoutMs: number }[] }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export type LocalApprovalDecision = "approved" | "rejected";
|
|
41
|
+
|
|
42
|
+
interface ApprovalWindowOptions {
|
|
43
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeHtml(value: string): string {
|
|
48
|
+
return value.replace(/[&<>"']/gu, (character) => ({
|
|
49
|
+
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
50
|
+
})[character] ?? character);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function row(label: string, value: string): string {
|
|
54
|
+
return `<div class="row"><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderPage(view: LocalApprovalView, token: string): string {
|
|
58
|
+
const task = view.proposal;
|
|
59
|
+
const expires = new Date(task.limits.expiresAt).toISOString();
|
|
60
|
+
const fields = [
|
|
61
|
+
row("Repositório", view.repositoryRoot),
|
|
62
|
+
row("Identidade", task.repositoryId),
|
|
63
|
+
row("Commit base", task.baseCommit),
|
|
64
|
+
row("Objetivo", task.goal),
|
|
65
|
+
row("Aceite", task.acceptanceCriteria.join(" • ")),
|
|
66
|
+
row("Fora do escopo", task.nonObjectives.join(" • ") || "Nenhum"),
|
|
67
|
+
row("Leitura", task.sourcePaths.join(" • ")),
|
|
68
|
+
row("Memória privada selecionada (dados sem autoridade)", view.memory?.map((entry) =>
|
|
69
|
+
`${entry.id}: ${entry.text} (fonte ${entry.sourceSnapshotDigest}; expira ${new Date(entry.expiresAt).toISOString()})`).join(" • ") || "Nenhuma"),
|
|
70
|
+
row("Escrita", task.writablePaths.join(" • ")),
|
|
71
|
+
row("Alvo local", task.requestedModelTargetId),
|
|
72
|
+
row("Modelo aprovado", task.requestedModelId ?? "Desconhecido (proposta antiga)"),
|
|
73
|
+
row("Limites", `${task.limits.maximumAttempts} tentativa(s); ${task.limits.maximumWallClockMs} ms; ${task.limits.maximumOutputTokens} tokens; ${task.limits.maximumContextBytes} bytes de contexto; ${task.limits.maximumPatchBytes} bytes de patch`),
|
|
74
|
+
row("Expira", expires),
|
|
75
|
+
row("Comandos de verificação", task.verification
|
|
76
|
+
? `${task.verification.commands.map((command) => command.kind === "node-test-file" ? `node --test ${command.path} (${command.timeoutMs} ms)` : `git diff --check (${command.timeoutMs} ms)`).join(" • ")} | imagem ${task.verification.imageId}`
|
|
77
|
+
: "Nenhum"),
|
|
78
|
+
row("Revisão SHA-256", view.proposalDigest),
|
|
79
|
+
].join("");
|
|
80
|
+
return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forge · Aprovar tarefa</title><style>
|
|
81
|
+
:root{font-family:system-ui,sans-serif;color-scheme:light;background:#f4f6f8;color:#14202b}body{margin:0;padding:32px 16px}main{max-width:750px;margin:auto;background:white;border:1px solid #d8e0e7;border-radius:16px;padding:28px;box-shadow:0 10px 30px #20304016}h1{margin:0 0 8px;font-size:1.65rem}p{line-height:1.5;color:#42576a}.badge{display:inline-block;background:#e5f1ed;color:#13694c;border-radius:8px;padding:5px 9px;font-size:.8rem;font-weight:700}dl{margin:24px 0}.row{display:grid;grid-template-columns:165px 1fr;gap:12px;border-top:1px solid #e4e9ed;padding:12px 0}dt{font-weight:700;color:#42576a}dd{margin:0;overflow-wrap:anywhere}form{display:flex;gap:12px}button{cursor:pointer;border:0;border-radius:9px;padding:12px 20px;font-size:1rem;font-weight:700}button[value=approved]{background:#0a7050;color:white}button[value=rejected]{background:#dfe5e9;color:#14202b}@media(max-width:560px){.row{grid-template-columns:1fr;gap:4px}form{flex-direction:column}}
|
|
82
|
+
</style></head><body><main><span class="badge">Forge Agent Fabric · piloto local</span><h1>Revisar tarefa</h1><p>Esta decisão autoriza a tentativa local e os comandos de verificação exibidos. Ela não publica nem faz merge.</p><p><strong>${escapeHtml(view.taskId)}</strong></p><dl>${fields}</dl><form method="post" action="/decision/${token}"><input type="hidden" name="digest" value="${escapeHtml(view.proposalDigest)}"><button type="submit" name="decision" value="approved">Aprovar revisão</button><button type="submit" name="decision" value="rejected">Rejeitar</button></form></main></body></html>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderPatchPage(view: LocalPatchReviewView, token: string): string {
|
|
86
|
+
return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forge · Aceitar resultado</title><style>
|
|
87
|
+
:root{font-family:system-ui,sans-serif;color-scheme:light;background:#f4f6f8;color:#14202b}body{margin:0;padding:24px}main{max-width:1000px;margin:auto;background:white;padding:24px;border-radius:12px}pre{white-space:pre;overflow:auto;max-height:60vh;padding:16px;background:#15202b;color:#eaf2f7;border-radius:8px}form{display:flex;gap:12px}button{padding:12px 18px;cursor:pointer}strong{overflow-wrap:anywhere}
|
|
88
|
+
</style></head><body><main><h1>Revisar resultado</h1><p>Esta decisão registra seu aceite do diff. Ela não faz merge, publica ou altera o checkout original.</p><p>Repositório: <strong>${escapeHtml(view.repositoryRoot)}</strong></p><p>Commit base: <strong>${escapeHtml(view.baseCommit)}</strong></p><p>Tarefa: <strong>${escapeHtml(view.taskId)}</strong></p><p>Verificação Git: <strong>${escapeHtml(view.verification)}</strong></p><p>Verificação isolada: <strong>${escapeHtml(view.sandboxVerification ? `${view.sandboxVerification.state}: ${view.sandboxVerification.outcome ?? "incerta"}; ${view.sandboxVerification.evidenceDigest ?? "sem recibo"}` : "não solicitada")}</strong></p><p>Digest do diff: <strong>${escapeHtml(view.diffDigest)}</strong></p><pre>${escapeHtml(view.diff)}</pre><form method="post" action="/decision/${token}"><input type="hidden" name="digest" value="${escapeHtml(view.diffDigest)}"><button type="submit" name="decision" value="approved">Aceitar este diff</button><button type="submit" name="decision" value="rejected">Rejeitar</button></form></main></body></html>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderVerificationRecoveryPage(view: LocalVerificationRecoveryView, token: string): string {
|
|
92
|
+
const continuation = view.mode === "continue";
|
|
93
|
+
const explanation = continuation
|
|
94
|
+
? "Os comandos anteriores têm recibos duráveis. Aprovar executa somente os testes restantes do mesmo perfil aprovado, em contêineres isolados. Nenhum comando já iniciado será repetido; um início incerto continuará bloqueado."
|
|
95
|
+
: "O registro durável confirma que nenhum contêiner foi iniciado nesta tentativa. Aprovar remove somente esta intenção de verificação e permite iniciar uma nova tentativa com o mesmo perfil aprovado.";
|
|
96
|
+
const remaining = continuation
|
|
97
|
+
? `<h2>Testes restantes</h2><ul>${view.remainingCommands.map((command) =>
|
|
98
|
+
`<li>${escapeHtml(command.path)} (${command.timeoutMs} ms)</li>`).join("")}</ul>` : "";
|
|
99
|
+
const approveLabel = continuation ? "Executar testes restantes" : "Permitir nova verificação";
|
|
100
|
+
return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forge · Recuperar verificação</title></head><body><main><h1>Recuperar verificação</h1><p>${explanation}</p><p>Repositório: <strong>${escapeHtml(view.repositoryRoot)}</strong></p><p>Tarefa: <strong>${escapeHtml(view.taskId)}</strong></p><p>Diff: <strong>${escapeHtml(view.diffDigest)}</strong></p><p>Solicitação: <strong>${escapeHtml(view.requestDigest)}</strong></p>${remaining}<form method="post" action="/decision/${token}"><input type="hidden" name="digest" value="${escapeHtml(view.requestDigest)}"><button type="submit" name="decision" value="approved">${approveLabel}</button><button type="submit" name="decision" value="rejected">Manter bloqueada</button></form></main></body></html>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function openBrowser(url: string): Promise<void> {
|
|
104
|
+
const command = process.platform === "win32" ? "explorer.exe" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
105
|
+
await new Promise<void>((resolve, reject) => {
|
|
106
|
+
const child = spawn(command, [url], { detached: true, stdio: "ignore", windowsHide: false });
|
|
107
|
+
child.once("error", reject);
|
|
108
|
+
child.once("spawn", () => { child.unref(); resolve(); });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Human-facing cooperative approval. It is not protection from same-account shell/UI control. */
|
|
113
|
+
export async function requestLocalApproval(
|
|
114
|
+
view: LocalApprovalView,
|
|
115
|
+
options: ApprovalWindowOptions = {},
|
|
116
|
+
): Promise<LocalApprovalDecision> {
|
|
117
|
+
return requestLocalDecision(view, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function requestLocalPatchAcceptance(
|
|
121
|
+
view: LocalPatchReviewView,
|
|
122
|
+
options: ApprovalWindowOptions = {},
|
|
123
|
+
): Promise<LocalApprovalDecision> {
|
|
124
|
+
return requestLocalDecision(view, options);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function requestLocalVerificationRecovery(
|
|
128
|
+
view: LocalVerificationRecoveryView,
|
|
129
|
+
options: ApprovalWindowOptions = {},
|
|
130
|
+
): Promise<LocalApprovalDecision> {
|
|
131
|
+
return requestLocalDecision(view, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function requestLocalDecision(
|
|
135
|
+
view: LocalApprovalView | LocalPatchReviewView | LocalVerificationRecoveryView,
|
|
136
|
+
options: ApprovalWindowOptions,
|
|
137
|
+
): Promise<LocalApprovalDecision> {
|
|
138
|
+
const boundDigest = "kind" in view ? view.requestDigest :
|
|
139
|
+
"proposal" in view ? view.proposalDigest : view.diffDigest;
|
|
140
|
+
const token = randomBytes(32).toString("hex");
|
|
141
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
142
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) {
|
|
143
|
+
throw new Error("Invalid local approval timeout");
|
|
144
|
+
}
|
|
145
|
+
let settle!: (decision: LocalApprovalDecision) => void;
|
|
146
|
+
let fail!: (reason: Error) => void;
|
|
147
|
+
const outcome = new Promise<LocalApprovalDecision>((resolve, reject) => { settle = resolve; fail = reject; });
|
|
148
|
+
let decided = false;
|
|
149
|
+
let origin = "";
|
|
150
|
+
const server = createServer((request, response) => {
|
|
151
|
+
response.setHeader("Cache-Control", "no-store");
|
|
152
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
153
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
154
|
+
response.setHeader("X-Frame-Options", "DENY");
|
|
155
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'");
|
|
156
|
+
if (request.headers.host !== origin.slice("http://".length)) {
|
|
157
|
+
response.writeHead(400).end();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (request.method === "GET" && request.url === `/${token}` && !decided) {
|
|
161
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
162
|
+
response.writeHead(200).end("kind" in view ? renderVerificationRecoveryPage(view, token) :
|
|
163
|
+
"proposal" in view ? renderPage(view, token) : renderPatchPage(view, token));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Chrome sends Origin: null for a same-origin form POST when the page uses
|
|
167
|
+
// Referrer-Policy: no-referrer. Fetch metadata distinguishes that browser
|
|
168
|
+
// case from a cross-site form while the unguessable token binds the decision.
|
|
169
|
+
const sameOriginPost = request.headers.origin === origin ||
|
|
170
|
+
(request.headers.origin === "null" && request.headers["sec-fetch-site"] === "same-origin");
|
|
171
|
+
if (request.method !== "POST" || request.url !== `/decision/${token}` || decided ||
|
|
172
|
+
!sameOriginPost ||
|
|
173
|
+
request.headers["content-type"] !== "application/x-www-form-urlencoded") {
|
|
174
|
+
response.writeHead(403).end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let body = "";
|
|
178
|
+
request.on("data", (chunk: Buffer) => {
|
|
179
|
+
body += chunk.toString("utf8");
|
|
180
|
+
if (body.length > 2048) request.destroy();
|
|
181
|
+
});
|
|
182
|
+
request.on("end", () => {
|
|
183
|
+
const values = new URLSearchParams(body);
|
|
184
|
+
const decision = values.get("decision");
|
|
185
|
+
if (values.size !== 2 || values.get("digest") !== boundDigest ||
|
|
186
|
+
(decision !== "approved" && decision !== "rejected")) {
|
|
187
|
+
response.writeHead(400).end();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
decided = true;
|
|
191
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
192
|
+
response.writeHead(200).end(`<html lang="pt-BR"><meta charset="utf-8"><title>Forge</title><p>Decisão registrada: ${decision === "approved" ? "aprovada" : "rejeitada"}. Você pode fechar esta janela.</p></html>`);
|
|
193
|
+
response.once("finish", () => settle(decision));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
await new Promise<void>((resolve, reject) => {
|
|
197
|
+
server.once("error", reject);
|
|
198
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
199
|
+
});
|
|
200
|
+
const address = server.address() as AddressInfo;
|
|
201
|
+
origin = `http://127.0.0.1:${address.port}`;
|
|
202
|
+
const timer = setTimeout(() => fail(new Error("Local approval timed out")), timeoutMs);
|
|
203
|
+
try {
|
|
204
|
+
await (options.openBrowser ?? openBrowser)(`${origin}/${token}`);
|
|
205
|
+
return await outcome;
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
server.closeAllConnections();
|
|
209
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { sha256Digest, stableStringify } from "./canonical.ts";
|
|
6
|
+
import { runCodexAdversarialReview } from "./codex-adversarial-review.ts";
|
|
7
|
+
import { AgentFabricError } from "./errors.ts";
|
|
8
|
+
import { localFabricPath } from "./local-paths.ts";
|
|
9
|
+
|
|
10
|
+
const MAX_DIFF_BYTES = 1024 * 1024;
|
|
11
|
+
const MAX_PATHS = 100;
|
|
12
|
+
const MAX_ROUNDS = 8;
|
|
13
|
+
|
|
14
|
+
export interface LocalChangeProposal {
|
|
15
|
+
objective: string;
|
|
16
|
+
acceptanceCriteria: string[];
|
|
17
|
+
implementer: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Contract extends LocalChangeProposal {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
changeId: string;
|
|
23
|
+
repositoryRoot: string;
|
|
24
|
+
baseCommit: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
digest: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RoundIntent {
|
|
30
|
+
schemaVersion: 1;
|
|
31
|
+
round: number;
|
|
32
|
+
baseCommit: string;
|
|
33
|
+
contractDigest: string;
|
|
34
|
+
diffDigest: string;
|
|
35
|
+
diffBytes: number;
|
|
36
|
+
changedPaths: string[];
|
|
37
|
+
requestDigest: string;
|
|
38
|
+
reviewer: "codex-cli";
|
|
39
|
+
startedAt: string;
|
|
40
|
+
digest: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RoundResult {
|
|
44
|
+
schemaVersion: 1;
|
|
45
|
+
intentDigest: string;
|
|
46
|
+
finishedAt: string;
|
|
47
|
+
state: "reported" | "inconclusive";
|
|
48
|
+
report?: unknown;
|
|
49
|
+
outputDigest?: string;
|
|
50
|
+
usage?: unknown;
|
|
51
|
+
error?: string;
|
|
52
|
+
digest: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LocalChangeStatus {
|
|
56
|
+
changeId: string;
|
|
57
|
+
state: "proposed" | "needs_review" | "review_uncertain" | "changes_requested" | "inconclusive" | "ready";
|
|
58
|
+
contractDigest: string;
|
|
59
|
+
baseCommit: string;
|
|
60
|
+
currentDiffDigest: string | null;
|
|
61
|
+
latestRound: number;
|
|
62
|
+
reviewedDiffDigest: string | null;
|
|
63
|
+
reviewerVerdict: string | null;
|
|
64
|
+
canAccept: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function git(root: string, args: string[], options: { index?: string; input?: Buffer; maxBuffer?: number } = {}): Buffer {
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync("git", ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=NUL", ...args], {
|
|
70
|
+
cwd: root, env: options.index ? { ...process.env, GIT_INDEX_FILE: options.index } : process.env,
|
|
71
|
+
input: options.input, encoding: "buffer", windowsHide: true, timeout: 30_000,
|
|
72
|
+
maxBuffer: options.maxBuffer ?? MAX_DIFF_BYTES + 4096, stdio: ["pipe", "pipe", "ignore"],
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
throw new AgentFabricError("AF_INVALID_STATE", `Git operation failed: ${args[0] ?? "unknown"}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function gitText(root: string, args: string[], options?: { index?: string; maxBuffer?: number }): string {
|
|
80
|
+
return git(root, args, options).toString("utf8").trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function inertFilterOptions(root: string): string[] {
|
|
84
|
+
let names = "";
|
|
85
|
+
try {
|
|
86
|
+
names = execFileSync("git", ["config", "--name-only", "--get-regexp",
|
|
87
|
+
"^filter\\..*\\.(process|smudge|clean)$"], {
|
|
88
|
+
cwd: root, encoding: "utf8", windowsHide: true, timeout: 10_000,
|
|
89
|
+
maxBuffer: 64 * 1024, stdio: ["ignore", "pipe", "ignore"],
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if ((error as { status?: number }).status !== 1) {
|
|
93
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Cannot inspect Git filters");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const options: string[] = [];
|
|
97
|
+
for (const name of new Set(names.split(/\r?\n/u).filter(Boolean))) {
|
|
98
|
+
if (!/^filter\.[a-z0-9_.-]+\.(?:process|smudge|clean)$/iu.test(name)) {
|
|
99
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Unsupported Git filter configuration");
|
|
100
|
+
}
|
|
101
|
+
options.push("-c", `${name}=`);
|
|
102
|
+
}
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function digestBytes(bytes: Buffer): string { return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; }
|
|
107
|
+
|
|
108
|
+
function checkedObject(value: unknown): Record<string, unknown> {
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new AgentFabricError("AF_INVALID_STATE", "Invalid change proposal");
|
|
110
|
+
return value as Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateProposal(value: unknown): LocalChangeProposal {
|
|
114
|
+
const object = checkedObject(value);
|
|
115
|
+
if (Object.keys(object).sort().join(",") !== "acceptanceCriteria,implementer,objective" ||
|
|
116
|
+
typeof object.objective !== "string" || !object.objective.trim() || object.objective.length > 4000 ||
|
|
117
|
+
!Array.isArray(object.acceptanceCriteria) || object.acceptanceCriteria.length < 1 || object.acceptanceCriteria.length > 20 ||
|
|
118
|
+
!object.acceptanceCriteria.every((item) => typeof item === "string" && item.trim() && item.length <= 1000) ||
|
|
119
|
+
typeof object.implementer !== "string" || !/^[a-z][a-z0-9-]{1,63}$/u.test(object.implementer)) {
|
|
120
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Change proposal requires bounded objective, acceptanceCriteria, and implementer");
|
|
121
|
+
}
|
|
122
|
+
return object as unknown as LocalChangeProposal;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sealed<T extends object>(value: T): T & { digest: string } {
|
|
126
|
+
return { ...value, digest: sha256Digest(stableStringify(value)) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readSealed<T extends { digest: string }>(path: string): T {
|
|
130
|
+
const bytes = readFileSync(path);
|
|
131
|
+
if (bytes.length > MAX_DIFF_BYTES) throw new AgentFabricError("AF_CONFLICT", "Change record exceeds limit");
|
|
132
|
+
let value: T;
|
|
133
|
+
try { value = JSON.parse(bytes.toString("utf8")) as T; }
|
|
134
|
+
catch { throw new AgentFabricError("AF_CONFLICT", "Change record is invalid JSON"); }
|
|
135
|
+
if (!value || typeof value !== "object" || typeof value.digest !== "string") {
|
|
136
|
+
throw new AgentFabricError("AF_CONFLICT", "Change record is invalid");
|
|
137
|
+
}
|
|
138
|
+
const { digest, ...unsigned } = value;
|
|
139
|
+
if (sha256Digest(stableStringify(unsigned)) !== digest) throw new AgentFabricError("AF_CONFLICT", "Change record digest mismatch");
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writeNew(path: string, value: unknown): void {
|
|
144
|
+
writeFileSync(path, Buffer.isBuffer(value) ? value : JSON.stringify(value), { flag: "wx", mode: 0o600 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function recordPath(root: string, id: string, filename: string): string {
|
|
148
|
+
if (!/^change:[0-9a-f-]{36}$/u.test(id)) throw new AgentFabricError("AF_INVALID_STATE", "Invalid change ID");
|
|
149
|
+
return localFabricPath(root, "change-reviews", id.slice(7), filename);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function roundName(round: number, suffix: string): string { return `round-${String(round).padStart(3, "0")}.${suffix}`; }
|
|
153
|
+
|
|
154
|
+
function reviewPrompt(contract: Contract, diffDigest: string, paths: string[]): string {
|
|
155
|
+
return `Adversarially review this exact change against its acceptance criteria. Report only concrete findings. Do not edit files.\nObjective: ${contract.objective}\nAcceptance criteria: ${JSON.stringify(contract.acceptanceCriteria)}\nBase commit: ${contract.baseCommit}\nDiff digest: ${diffDigest}\nChanged paths: ${JSON.stringify(paths)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface CapturedDiff { bytes: Buffer; digest: string; paths: string[] }
|
|
159
|
+
|
|
160
|
+
function capture(root: string, baseCommit: string): CapturedDiff {
|
|
161
|
+
if (gitText(root, ["rev-parse", "HEAD"]) !== baseCommit) {
|
|
162
|
+
throw new AgentFabricError("AF_CONFLICT", "Change checkout moved from its pinned base commit");
|
|
163
|
+
}
|
|
164
|
+
// A separate index captures staged, unstaged, and non-ignored new files without changing the user's index.
|
|
165
|
+
const tempParent = localFabricPath(root, "change-reviews", "tmp");
|
|
166
|
+
mkdirSync(tempParent, { recursive: true, mode: 0o700 });
|
|
167
|
+
const temp = mkdtempSync(join(tempParent, "index-"));
|
|
168
|
+
try {
|
|
169
|
+
const index = join(temp, "index");
|
|
170
|
+
git(root, ["read-tree", baseCommit], { index });
|
|
171
|
+
git(root, [...inertFilterOptions(root), "add", "-A", "--", ".", ":(exclude).forge/local/**", ":(exclude).forge/delta/**"], { index });
|
|
172
|
+
const paths = git(root, ["diff", "--cached", "--name-only", "-z", baseCommit], { index }).toString("utf8").split("\0").filter(Boolean);
|
|
173
|
+
if (paths.length > MAX_PATHS || paths.some((path) => path.startsWith(".forge/local/") || path.startsWith(".forge/delta/") || path.includes("\0"))) {
|
|
174
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Change contains too many paths or local state");
|
|
175
|
+
}
|
|
176
|
+
for (const path of paths) {
|
|
177
|
+
const mode = gitText(root, ["ls-files", "-s", "--", path], { index }).split(" ")[0];
|
|
178
|
+
if (mode && mode !== "100644" && mode !== "100755") {
|
|
179
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Change contains a symlink or submodule");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
git(root, ["diff", "--cached", "--check", baseCommit], { index });
|
|
183
|
+
const bytes = git(root, ["diff", "--cached", "--no-ext-diff", "--no-textconv", "--binary", "--full-index", baseCommit], { index });
|
|
184
|
+
if (bytes.length > MAX_DIFF_BYTES || bytes.includes(Buffer.from("GIT binary patch"))) {
|
|
185
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Change diff exceeds limit or contains binary data");
|
|
186
|
+
}
|
|
187
|
+
return { bytes, digest: digestBytes(bytes), paths };
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(temp, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function materialize(root: string, id: string, round: number, base: string, diff: CapturedDiff): string {
|
|
194
|
+
const checkout = localFabricPath(root, "change-reviews", id.slice(7), `checkout-${round}`);
|
|
195
|
+
const hooks = localFabricPath(root, "change-reviews", "empty-hooks");
|
|
196
|
+
mkdirSync(hooks, { recursive: true });
|
|
197
|
+
git(root, ["-c", `core.hooksPath=${hooks}`, "worktree", "add", "--detach", "--no-checkout", "--", checkout, base]);
|
|
198
|
+
git(checkout, ["-c", `core.hooksPath=${hooks}`, ...inertFilterOptions(checkout), "reset", "--hard", base]);
|
|
199
|
+
git(checkout, [...inertFilterOptions(checkout), "apply", "--index", "--binary", "-"], { input: diff.bytes });
|
|
200
|
+
const staged = git(checkout, ["diff", "--cached", "--no-ext-diff", "--no-textconv", "--binary", "--full-index", base]);
|
|
201
|
+
if (digestBytes(staged) !== diff.digest) throw new AgentFabricError("AF_CONFLICT", "Review snapshot differs from captured change");
|
|
202
|
+
return checkout;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function verifySnapshot(checkout: string, base: string, expected: string): void {
|
|
206
|
+
if (gitText(checkout, ["rev-parse", "HEAD"]) !== base ||
|
|
207
|
+
git(checkout, ["diff", "--no-ext-diff", "--binary"]).length ||
|
|
208
|
+
git(checkout, ["ls-files", "--others", "--exclude-standard"]).length ||
|
|
209
|
+
digestBytes(git(checkout, ["diff", "--cached", "--no-ext-diff", "--no-textconv", "--binary", "--full-index", base])) !== expected) {
|
|
210
|
+
throw new AgentFabricError("AF_CONFLICT", "Reviewer snapshot was modified");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class LocalChangeReviewService {
|
|
215
|
+
private constructor(readonly repositoryRoot: string,
|
|
216
|
+
private readonly reviewRunner: typeof runCodexAdversarialReview) {}
|
|
217
|
+
|
|
218
|
+
static async open(repositoryRoot: string, options: { reviewRunner?: typeof runCodexAdversarialReview } = {}): Promise<LocalChangeReviewService> {
|
|
219
|
+
const root = realpathSync(repositoryRoot);
|
|
220
|
+
if (realpathSync(gitText(root, ["rev-parse", "--show-toplevel"])) !== root) {
|
|
221
|
+
throw new AgentFabricError("AF_INVALID_STATE", "Change review requires a repository root");
|
|
222
|
+
}
|
|
223
|
+
return new LocalChangeReviewService(root, options.reviewRunner ?? runCodexAdversarialReview);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async close(): Promise<void> { /* filesystem records do not retain handles */ }
|
|
227
|
+
|
|
228
|
+
private contract(id: string): Contract {
|
|
229
|
+
const contract = readSealed<Contract>(recordPath(this.repositoryRoot, id, "contract.json"));
|
|
230
|
+
if (contract.changeId !== id || contract.repositoryRoot !== this.repositoryRoot || contract.schemaVersion !== 1) {
|
|
231
|
+
throw new AgentFabricError("AF_CONFLICT", "Change contract identity mismatch");
|
|
232
|
+
}
|
|
233
|
+
return contract;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private withLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
|
|
237
|
+
const path = recordPath(this.repositoryRoot, id, "operation.lock");
|
|
238
|
+
let fd: number;
|
|
239
|
+
try { fd = openSync(path, "wx", 0o600); }
|
|
240
|
+
catch { throw new AgentFabricError("AF_CONFLICT", "Change operation already active or interrupted; inspect evidence before recovery"); }
|
|
241
|
+
return fn().finally(() => { closeSync(fd); unlinkSync(path); });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async propose(value: unknown): Promise<LocalChangeStatus> {
|
|
245
|
+
const request = validateProposal(value);
|
|
246
|
+
const changeId = `change:${randomUUID()}`;
|
|
247
|
+
const directory = localFabricPath(this.repositoryRoot, "change-reviews", changeId.slice(7));
|
|
248
|
+
mkdirSync(localFabricPath(this.repositoryRoot, "change-reviews"), { recursive: true, mode: 0o700 });
|
|
249
|
+
mkdirSync(directory, { recursive: false, mode: 0o700 });
|
|
250
|
+
const contract = sealed({ schemaVersion: 1 as const, ...request, changeId,
|
|
251
|
+
repositoryRoot: this.repositoryRoot, baseCommit: gitText(this.repositoryRoot, ["rev-parse", "HEAD"]),
|
|
252
|
+
createdAt: new Date().toISOString() });
|
|
253
|
+
writeNew(join(directory, "contract.json"), contract);
|
|
254
|
+
return this.status(changeId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async status(id: string): Promise<LocalChangeStatus> {
|
|
258
|
+
const contract = this.contract(id);
|
|
259
|
+
const directory = localFabricPath(this.repositoryRoot, "change-reviews", id.slice(7));
|
|
260
|
+
const names = readdirSync(directory).filter((name) => /^round-\d{3}\.intent\.json$/u.test(name)).sort();
|
|
261
|
+
if (names.length > MAX_ROUNDS) throw new AgentFabricError("AF_CONFLICT", "Change has too many review rounds");
|
|
262
|
+
let last: RoundIntent | undefined;
|
|
263
|
+
let result: RoundResult | undefined;
|
|
264
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
265
|
+
const intent = readSealed<RoundIntent>(join(directory, names[index]!));
|
|
266
|
+
if (intent.round !== index + 1 || intent.baseCommit !== contract.baseCommit ||
|
|
267
|
+
intent.contractDigest !== contract.digest || intent.schemaVersion !== 1) {
|
|
268
|
+
throw new AgentFabricError("AF_CONFLICT", "Review round chain is inconsistent");
|
|
269
|
+
}
|
|
270
|
+
const prompt = reviewPrompt(contract, intent.diffDigest, intent.changedPaths);
|
|
271
|
+
if (intent.requestDigest !== sha256Digest(stableStringify({ contractDigest: contract.digest,
|
|
272
|
+
diffDigest: intent.diffDigest, round: intent.round, prompt }))) {
|
|
273
|
+
throw new AgentFabricError("AF_CONFLICT", "Review request digest mismatch");
|
|
274
|
+
}
|
|
275
|
+
const diff = readFileSync(join(directory, roundName(intent.round, "patch")));
|
|
276
|
+
if (diff.length !== intent.diffBytes || digestBytes(diff) !== intent.diffDigest) {
|
|
277
|
+
throw new AgentFabricError("AF_CONFLICT", "Recorded review diff was modified");
|
|
278
|
+
}
|
|
279
|
+
last = intent;
|
|
280
|
+
const resultPath = join(directory, roundName(intent.round, "result.json"));
|
|
281
|
+
result = existsSync(resultPath) ? readSealed<RoundResult>(resultPath) : undefined;
|
|
282
|
+
if (result && result.intentDigest !== intent.digest) throw new AgentFabricError("AF_CONFLICT", "Review result is detached from its intent");
|
|
283
|
+
if (result?.state === "reported") {
|
|
284
|
+
const report = result.report as { requestDigest?: string; verdict?: string; findings?: unknown[] } | undefined;
|
|
285
|
+
if (!report || report.requestDigest !== intent.requestDigest ||
|
|
286
|
+
!["pass", "changes_requested"].includes(report.verdict ?? "") ||
|
|
287
|
+
!Array.isArray(report.findings) ||
|
|
288
|
+
(report.verdict === "pass" ? report.findings.length !== 0 : report.findings.length === 0) ||
|
|
289
|
+
!/^sha256:[0-9a-f]{64}$/u.test(result.outputDigest ?? "")) {
|
|
290
|
+
throw new AgentFabricError("AF_CONFLICT", "Stored reviewer report is invalid");
|
|
291
|
+
}
|
|
292
|
+
} else if (result && result.state !== "inconclusive") {
|
|
293
|
+
throw new AgentFabricError("AF_CONFLICT", "Stored reviewer state is invalid");
|
|
294
|
+
}
|
|
295
|
+
if (!result && index < names.length - 1) throw new AgentFabricError("AF_CONFLICT", "New round follows unfinished review");
|
|
296
|
+
}
|
|
297
|
+
const current = capture(this.repositoryRoot, contract.baseCommit);
|
|
298
|
+
const same = !!last && current.digest === last.diffDigest;
|
|
299
|
+
const verdict = result?.state === "reported" && result.report && typeof result.report === "object"
|
|
300
|
+
? (result.report as { verdict?: string }).verdict ?? null : null;
|
|
301
|
+
const state: LocalChangeStatus["state"] = !last ? "proposed" : !same ? "needs_review" : !result ? "review_uncertain"
|
|
302
|
+
: result.state === "inconclusive" ? "inconclusive" : verdict === "pass" ? "ready" : "changes_requested";
|
|
303
|
+
return { changeId: id, state, contractDigest: contract.digest, baseCommit: contract.baseCommit,
|
|
304
|
+
currentDiffDigest: current.bytes.length ? current.digest : null, latestRound: last?.round ?? 0,
|
|
305
|
+
reviewedDiffDigest: last?.diffDigest ?? null, reviewerVerdict: verdict, canAccept: state === "ready" };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async evidence(id: string): Promise<{ status: LocalChangeStatus; rounds: unknown[] }> {
|
|
309
|
+
const status = await this.status(id);
|
|
310
|
+
const directory = localFabricPath(this.repositoryRoot, "change-reviews", id.slice(7));
|
|
311
|
+
const rounds: unknown[] = [];
|
|
312
|
+
for (let round = 1; round <= status.latestRound; round += 1) {
|
|
313
|
+
const intent = readSealed<RoundIntent>(join(directory, roundName(round, "intent.json")));
|
|
314
|
+
const resultPath = join(directory, roundName(round, "result.json"));
|
|
315
|
+
const result = existsSync(resultPath) ? readSealed<RoundResult>(resultPath) : undefined;
|
|
316
|
+
rounds.push({ round, diffDigest: intent.diffDigest, changedPaths: intent.changedPaths,
|
|
317
|
+
requestDigest: intent.requestDigest, reviewer: intent.reviewer, startedAt: intent.startedAt,
|
|
318
|
+
state: result?.state ?? "uncertain", report: result?.report ?? null, error: result?.error ?? null,
|
|
319
|
+
outputDigest: result?.outputDigest ?? null, usage: result?.usage ?? null });
|
|
320
|
+
}
|
|
321
|
+
return { status, rounds };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async review(id: string): Promise<LocalChangeStatus> {
|
|
325
|
+
const contract = this.contract(id);
|
|
326
|
+
if (contract.implementer === "codex-cli") throw new AgentFabricError("AF_CONFLICT", "Implementer cannot review its own change");
|
|
327
|
+
return this.withLock(id, async () => {
|
|
328
|
+
const previous = await this.status(id);
|
|
329
|
+
if (previous.state === "review_uncertain") throw new AgentFabricError("AF_CONFLICT", "Interrupted review requires owner inspection");
|
|
330
|
+
if (previous.latestRound >= MAX_ROUNDS) throw new AgentFabricError("AF_RESOURCE_EXHAUSTED", "Review round limit reached");
|
|
331
|
+
const diff = capture(this.repositoryRoot, contract.baseCommit);
|
|
332
|
+
if (!diff.bytes.length) throw new AgentFabricError("AF_INVALID_STATE", "No change diff to review");
|
|
333
|
+
if (previous.reviewedDiffDigest === diff.digest && previous.state !== "inconclusive") {
|
|
334
|
+
throw new AgentFabricError("AF_CONFLICT", "This exact diff was already reviewed");
|
|
335
|
+
}
|
|
336
|
+
const round = previous.latestRound + 1;
|
|
337
|
+
const directory = localFabricPath(this.repositoryRoot, "change-reviews", id.slice(7));
|
|
338
|
+
const prompt = reviewPrompt(contract, diff.digest, diff.paths);
|
|
339
|
+
const requestDigest = sha256Digest(stableStringify({ contractDigest: contract.digest, diffDigest: diff.digest, round, prompt }));
|
|
340
|
+
const intent = sealed({ schemaVersion: 1 as const, round, baseCommit: contract.baseCommit,
|
|
341
|
+
contractDigest: contract.digest, diffDigest: diff.digest, diffBytes: diff.bytes.length,
|
|
342
|
+
changedPaths: diff.paths, requestDigest, reviewer: "codex-cli" as const,
|
|
343
|
+
startedAt: new Date().toISOString() });
|
|
344
|
+
const patchPath = join(directory, roundName(round, "patch"));
|
|
345
|
+
if (existsSync(patchPath)) {
|
|
346
|
+
// An orphan patch means a prior process stopped before recording its intent,
|
|
347
|
+
// which is before model dispatch. The operation lock prevents a live peer.
|
|
348
|
+
if (!lstatSync(patchPath).isFile() || existsSync(join(directory, roundName(round, "intent.json")))) {
|
|
349
|
+
throw new AgentFabricError("AF_CONFLICT", "Review patch already has an intent or unsafe file type");
|
|
350
|
+
}
|
|
351
|
+
if (!readFileSync(patchPath).equals(diff.bytes)) unlinkSync(patchPath);
|
|
352
|
+
}
|
|
353
|
+
if (!existsSync(patchPath)) writeNew(patchPath, diff.bytes);
|
|
354
|
+
writeNew(join(directory, roundName(round, "intent.json")), intent);
|
|
355
|
+
// From this point a crash leaves an uncertain durable intent. Never auto-dispatch a duplicate paid turn.
|
|
356
|
+
let outcome: Awaited<ReturnType<typeof runCodexAdversarialReview>>;
|
|
357
|
+
try {
|
|
358
|
+
const checkout = materialize(this.repositoryRoot, id, round, contract.baseCommit, diff);
|
|
359
|
+
outcome = await this.reviewRunner({ workspaceRoot: checkout, prompt, requestDigest,
|
|
360
|
+
timeoutMs: 120_000, maxOutputBytes: 256 * 1024 });
|
|
361
|
+
verifySnapshot(checkout, contract.baseCommit, diff.digest);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
outcome = { state: "inconclusive", error: error instanceof Error ? error.message : "Review failed" };
|
|
364
|
+
}
|
|
365
|
+
const result = sealed({ schemaVersion: 1 as const, intentDigest: intent.digest,
|
|
366
|
+
finishedAt: new Date().toISOString(), state: outcome.state,
|
|
367
|
+
...(outcome.outputDigest ? { outputDigest: outcome.outputDigest } : {}),
|
|
368
|
+
...(outcome.usage ? { usage: outcome.usage } : {}),
|
|
369
|
+
...(outcome.state === "reported" ? { report: outcome.report } : { error: outcome.error ?? "Review inconclusive" }) });
|
|
370
|
+
writeNew(join(directory, roundName(round, "result.json")), result);
|
|
371
|
+
return this.status(id);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|