The Weftra Build Tracker
GENERATED from docs/features.yml + src/tools/north-star-pillars.ts — do not hand-edit; regenerate with npm run docs:build-tracker.
This page answers "how far along is Weftra, per north-star pillar?" — rendered directly from the capability traceability manifest so it cannot drift from the traced source of truth. It republishes automatically whenever a shipped feature updates docs/features.yml (see docs/design/target-architecture.md for the north star this tracks progress against).
Honest caveat: Shipped/Beta means built and self-dogfooded by FA's own self-build loop — not proven on a legacy production monolith at scale.
Like the Capabilities page, this page covers only manifest entries with a user, operator, or admin audience; internal-only and contributor-facing capabilities are omitted from the listing and from every figure below.
Header metrics
- Total capabilities (published audiences): 86
- Shipped: 58
- Beta: 0
- Partial: 24
- Planned: 4
- Superseded: 0
- Internal: 0
- Shipped-or-partial vs planned: 82 shipped-or-partial vs 4 planned (67% of the tracked north-star scope is done or underway)
North-star pillar coverage
Fleet / portfolio orchestration (the wedge)
A persistent cross-project queue, unified portfolio observability, and governed delivery across every enrolled repo at once — the durable wedge nobody else is building.
██████░░░░ 57% (4 done / 7 on the path) — half-built
- Notification Channels —
Shipped— The notification-dispatch service (with transition dedup) and its Telegram, Discord, Slack, WhatsApp, and Email channel adapters. - Dashboards —
Partial— The feature/run status dashboard, and the fleet/portfolio view across projects (campaign grouping, attention SLA visibility). The features table is lazy, paged (20/page), searchable and project-filterable, with cross-cutting widgets (stat tiles, held-for-merge count, auth-paused banner, active-run consistency) reading a dedicated summary endpoint instead of the full unpaginated feature list. - Observability And Logs —
Partial— Run state, live log streaming, and operational status surfaces; cross-project aggregate cost/status rollups for fleet attention; the claim-derived agent-activity signal (is an agent actually working this feature right now, and how full is the pool). - Cost Tracking —
Partial— Per-run/model cost and token tracking, including honest accounting on failed runs, per-role breakdown (implement, security_reviewer, security_fixer, code_reviewer) on the run-events ledger, a per-project/project-self/instance usage rollup with agoverned_runsmeter, and a token-derived ESTIMATED cost for engines (ACP — copilot and others) that report no dollar figure of their own, markedcost_estimatedon the feature and per round on the ledger, shown with an est. marker in the Cost & Budget table and feature detail (the usage rollups and CSV exports still sum estimated and actual into one figure — a named follow-up) — never that engine's own billed charge. - Mentions Collaboration —
Shipped— @mention-triggered webhook processing that routes human attention on a PR comment back into the governed feature flow, plus its first execution path — a governed@weftra mergewhose gates and re-review run against the MERGE RESULT, not the branch (spec 249). - Fleet Fan Out —
Shipped— Submit one spec across N enrolled projects at once (with campaign grouping + rollup) — the wedge's sharpest single feature, reachable via the admin fan-out route. - Build Tracker —
Shipped— The Weftra Build Tracker (spec 320): a self-updating capability-inventory-vs-north-star page, generated from docs/features.yml + the north-star pillar constant — per-pillar coverage, status counts, and a gaps list are all DERIVED from the manifest, never hand-typed, so the page republishes automatically whenever a shipped feature updates the manifest. Being a published page, it renders only user/operator/admin-audience entries — the same audience gate the Capabilities page applies — so internal- and contributor-facing capabilities stay off the public site.
Everything as governed code (terminal thesis)
Any diff, from any engine, carrying a verifiable record of how it was produced. Governance only counts at a choke point with teeth — owning the merge/PR boundary and the deploy gate.
███████░░░ 67% (6 done / 9 on the path) — half-built
- Feature Lifecycle —
Shipped— The governed feature state machine (pending through queued/in_progress/implemented/ merged, with revising/cancelled/failed side paths) and the agent-engine polling loop that drives transitions. - Source Control Delivery —
Partial— The VCS provider abstraction (GitHub, Bitbucket, GitLab), draft-PR creation/posting, and merge-preflight safety controls that deliver a governed feature branch as a PR. Since spec 215, every repo-touching path resolves ITS project's own VCS credential through spec 011's CredentialSource ladder (src/services/vcs/credential.ts) before falling back to the operator-defaultGITHUB_TOKEN— the fallback is recorded, never silent.npm run resolve-vcs-tokenis the CLI seam the Governor (self-build) uses to resolve the same credential outside the Node process. - Http Api —
Shipped— The HTTP API surface: feature and artifact routes, and the published OpenAPI/Swagger document describing them. - Artifacts —
Shipped— Feature artifact upload, preview, and delete storage and API. - Provenance And Audit —
Partial— Evidence, tamper-evident run-event ledger, and decision-history provenance chain construction for every feature run. - Release Notes Verification —
Shipped— Operator-invoked, admin-only generation of release notes that are VERIFIED against what a release ref actually contains. Sentences are derived mechanically from the capability manifest diff between two refs and each cites the capability id it came from, so a sentence with no manifest lineage cannot be emitted. The operator's PR range and exclusion list are an assertion FA checks against branch reachability, never an instruction it obeys: an excluded PR whose commits are reachable is included and marked as a prerequisite, and the override is recorded as its own actor event. Reads only — it never writes a manifest, tags, publishes, or runs on merge. - Model Rejection Auditable —
Shipped— A refusal by the model/provider is a first-class recorded outcome, not a silent failure — part of the provenance story. - Actor Identity Proven —
Shipped— Separation-of-duties that demands a NAMED identity and refuses the shared admin role key — authority is not attribution, enforced in code (records actor events). - Admin Action Audit —
Planned— A tamper-evident, hash-chained record of privileged ADMINISTRATOR actions (theactor_eventsledger, spec 261) — closing the gap CLAUDE.md's "Administration Is Legitimate" core law names: run_events is feature-keyed, with nowhere to put a non-feature admin action. Already covers identity management (user create/update/delete/role-change/key-regeneration, approver-project links) and a growing set of governance writes (runtime-image allowlist, runtime settings, separation policy, runtime egress). Spec 292 (RM-063) is extending it across every remaining privileged admin surface, increment by increment — this entry tracks it as a PLANNED gap until the whole spec's scope lands: increment 1 (shipped) covers the admin PROJECT routes —POST /api/projects,PATCH /api/projects/:id,DELETE /api/projects/:ideach now record one event on success, carrying field NAMES only, never values. Still missing: the OTHER admin-reachable project-enrollment paths —POST /api/builder/new-projectand the agent-factory compile path both call createProject and record nothing, so an empty ledger does not yet prove nobody enrolled a project; project-scoped VCS-credential configuration (inc-2); instance administration —POST /api/admin/rotate-admin-keyand the database-backup routes still record zero events today (inc-3) — and connector/integration administration (inc-4). Outstanding: Instance-wide, never tenant-reachable — a project key always receives an empty actor_events section (CLAUDE.md's Trust Boundary core law). The remaining spec-292 increments are not yet built; do not present the FULL "every privileged administrative mutation" claim as shipped product behavior until they land (CLAUDE.md "Important product-claim boundaries").
One core → N deployment modes
Self-hosted-per-org and zero-infra SaaS are the SAME core with multi-tenancy as a deployment mode, not a rewrite.
█████░░░░░ 50% (1 done / 2 on the path) — half-built
- Self Hosted Packaging —
Shipped— Self-hosted deployment packaging: container image + Docker Compose (with a proven one-command cold-start install rehearsal), and a Helm chart for Kubernetes. - Saas Control Plane —
Planned— The zero-infra SaaS deployment mode: tenant provisioning, routing, metering, and the orchestrated (k8s/Fargate) Runtime backend that turns the self-hosted-per-org core into a multi-tenant SaaS front door. Design-only — explicitly sequenced AFTER the self-hosted core earns adoption (target-architecture.md §2, §9); no control-plane code exists yet. Outstanding: Not generally available. Do not present as shipped product behavior (CLAUDE.md "Important product-claim boundaries"). Depends on the multi-tenant credential/trust model (enterprise-multi-tenancy) landing first.
The four SaaS-primitive seams
Tenant/org scope, the runtime interface, BYO/brokered credentials, and per-tenant budget/quota — built now so SaaS becomes additive later, not a retrofit.
░░░░░░░░░░ 0% (0 done / 2 on the path) — mostly outstanding
- Budget And Quota —
Partial— Project/tenant budget cap, duration cap, and quota configuration. - Enterprise Multi Tenancy —
Planned— Tenant isolation and per-tenant credential configuration seams toward full RBAC/orgs/ SSO/billing. The multi-tenant trust/credential model is implemented; RBAC/orgs/SSO/ billing as a complete enterprise product are north-star, not shipped. Spec 215 adds a project-scoped VCS credential (admin-only read/write,tenant_credentialspurpose'vcs') on the same table and route file as the tenant-wide model credential above — not a second credential model. Outstanding: RBAC/orgs, SSO as a general enterprise product, and billing are not all generally available today (CLAUDE.md "Important product-claim boundaries").
Rent-the-engine + transport
Never build our own agent loop; the engine (Claude Code, ACP adapters, headless) and the transport that drives it are both pluggable, swappable dimensions.
█████░░░░░ 50% (2 done / 4 on the path) — half-built
- Engine Abstraction And Routing —
Partial— The pluggable agent-engine abstraction (Claude Code, ACP, headless transports), multi-engine profiles/model routing, and the independent reviewer/analyze/answerer/ revise engine+model selection. - Helper Roles Mid Run Delegation —
Shipped— Declared helper roles (name -> engine+model+tool-allowance+budget) a running agent may invoke BY NAME mid-run via a provisioned MCP tool, at a tier independent of the parent run's own model — governance (shared budget draw-down, per-run invocation cap, depth 1, sandboxed dispatch) over the native CLI-subagent capability, plus the cost tiering that path can't provide. Roles used only during authoring are project-declared; roles reachable from a control (the spec-conformance reviewer, the Security Fixer, the intent gate) are admin-only to declare and to read. - Agent Profiles —
Shipped— A named {name, engine_id, model, prompt_extension} bundle selectable BY NAME per role (author/reviewer/fixer/security/analyze/answerer/revise/maintainer), resolving through the existing engine/model routing chain and contributing TEXT only — never a process or a credential. Subsumes the standalone security_context and code_discipline prompt-extension special cases onto one shared mechanism. - Eval Replay Profiles —
Partial— Shadow-replay already-shipped features to measure a config change's real effect — an experimentation substrate over the eval harness. Outstanding: inc-2 (persistent toggle, drain, schedule, autonomous selector, fleet-metrics exclusion) outstanding (RM-050).
Every agent sandboxed / trust boundary
Everything untrusted runs in the Runtime, never on the host — the trust boundary every agent run, credential, and secret must respect.
████████░░ 78% (7 done / 9 on the path) — mostly done
- Sandboxed Runtime —
Partial— Configurable runtime image, Docker-sandboxed agent execution, declared sandbox services (e.g. ephemeral Postgres), and sandboxed repository cloning — the trust boundary every agent run executes inside (see CLAUDE.md's Trust Boundary core law). The agent job container's kernel-side hardening (cap-drop ALL, no-new-privileges, an FA-shipped seccomp profile, a read-only rootfs) is brought to the bootstrap git-clone container's existing parity via one sharedsandboxHardeningArgs()ceiling, strict by default with a recordedcompatopt-out (spec 405 inc-1). - Agent Runtime Egress Allowlist —
Shipped— Egress allowlist for the agent job container (DockerRuntime.start) — an operator/project may bound the sandboxed agent to a declared set of hosts on 443, reusing the bootstrap-clone egress forwarder (git-egress.ts, spec 291) generalized to N hosts. Fail-closed when active. Distinct from (and independent of) the bootstrap-clone's own enforced egress. An allowlist entry may optionally narrow a host allowance to specific calling binaries (binaries:, spec 349 / RM-123) — absent it, an entry stays host-only; present, the connection must match BOTH the host and one of the listed calling binaries, enforced at the same spec-342 seam and fail-closed on any malformed/unknown binary identity. As of spec 405 inc-2 (RM-194),allowlistis the DEFAULT posture: when nothing more specific resolves the policy,createRuntimecomputes an allowlist itself (the run's model-endpoint host + its own VCS host + the project's own declared hosts) instead of the open bridge;open(FA_RUNTIME_NETWORK/FA_RUNTIME_EGRESS=open) is the explicit, recorded opt-out, surfaced on the per-feature provenance artifact's "Runtime egress" row. As of spec 443 inc-1 (RM-262 a), the resolved model-endpoint host is unioned into every run resolving toallowlistmode (not just the computed default) so a project cannot narrow its own model endpoint away by declaring its own hostlist; under an operator-wide bound the union is restored only when the operator's own list already permits that host, so the effective allowlist's host set stays a provable subset ofFA_RUNTIME_EGRESS_ALLOWLIST. - Environment Provisioning —
Partial—.fa/environment.ymlmanifest, environment/scaffold detection, scaffold generation, and provision/readiness checks that prepare a project's declared runtime environment before an agent runs. The provision-check clones the project's repository before running a declaredsetup_command/verify_command(RM-262 c) — a repository-dependent command is verified against the real tree, or reported as NOT verified rather than a false pass. - Admin Runtime Settings —
Shipped— Config audit of every src/config.ts option into runtime-tunable / secret / structural buckets, an admin-only surface to view and edit the runtime-tunable subset with no restart (agent concurrency/model/turns/retries, PR-poll interval, budget/iteration/ webhook caps), and a read-only presence-only status list for the operator's configured secrets (never their values). - Local Instance Reachable From Sandbox —
Shipped— RM-176 / spec 413: an agent talks to its LOCAL instance. FA_CONTAINER_BIND opens a sandbox-facing listener that serves only the ephemeral-token /api/internal/* routes (+ /health), and the enforced-egress forwarder publishes a path-restricted relay to it on the run's --internal network — so the MCP-gateway shim, helper-role shim and fa-act reach FA from either topology while nothing a project or admin key guards is reachable from a container. - Mcp Gateway Allowlist And Audit —
Shipped— The MCP-gateway MVP (spec 348 / RM-122, PEP #3's buildable core): an operator-managed, per-project allowlist of MCP servers a run's declaredmcp.servers(spec 248) may actually be wired to, plus an FA-owned proxy that argument-audits every upstream MCP call to the tamper-evident run_events ledger before forwarding. Closes two governance gaps — no operator approval for a repo-declared server, and a ledger that recorded only {tool, summary} for an MCP call — without the signed-mandate crypto layer (spec 161/163), which remains design-only and slots into the same choke point later. - Sandboxed Browser Tool —
Shipped— A sandboxed browser tool (spec 350 / RM-120): a per-run, operator-granted tool backed by the official @playwright/mcp server that renders a declared app URL entirely inside the fa-runtime-browser DockerRuntime image and returns a screenshot + accessibility-tree DOM + console errors to the agent. The grant is derived from the already-operator-only runtime_image field (never a new tenant-reachable field), routed through the spec-348 MCP gateway over a dedicated in-sandbox transport (never a host spawn), scoped to exactly the Playwright-MCP tool set plus Read (tool-as-grant, spec-303 pattern), and egress-bound to the run's own declared app host(s). Advisory only — sets no verdict, gates no merge. The enabling primitive for spec 286 inc-2's rendered designer review (spec 347). - Capability Token Substrate —
Shipped— Spec 362 (RM-126) — FA's first signed, scoped, revocable, offline-verifiable credential: a Biscuit (Ed25519) capability token wrapped behind src/services/capability-tokens/, an AuthZEN-shaped decide() seam (v1 = direct opaque scope match, documented Cedar swap-point), and a requireCapability() middleware guard that accepts either a scoped token or a legacy fa_/admin credential mapped through the same role table. The root signing key is generated lazily on first mint, stored only as instance-secrets AES-256-GCM ciphertext, and never leaves the host process. Tokens are sealed leaves (no attenuation in v1), bearer (no PoP enforcement — the cnf fact is recorded for v2), TTL- capped by an operator ceiling, and instantly revocable — fail-closed in every direction: an unreadable store, an ABSENT record, or a record that disagrees with the token's signed facts all refuse (security round 1). Self-service mint/list/revoke over HTTP is spec 363 inc-7'sidentity-self-service-scoped-tokensentry below; root-key RETIRE remainstsx-only, with no HTTP route or UI. - Trust Page —
Shipped—npm run trust:exportrendersdocs/current/trust.md— the evidence a security reviewer asks for first (authorization surface, execution boundary, derived-state inventory, test/pin counts, supply chain, ledger/audit, adversarial-review coverage, the #173 incident record, and a fixed "what is NOT claimed" section) generated entirely from committed source files, never hand-typed. Pairs with the positioning change: the launch narrative states that the queue, approval, ledger and policy stay on the operator's infrastructure, engine-neutral, rather than claiming the agent itself always runs there.
The Builder → governed deploy
The AI-assisted spec-authoring front door, finishing at a declarative, approval-gated, project-agnostic deploy — the Lovable-for-specs pitch with governance nobody else has.
█████░░░░░ 50% (2 done / 4 on the path) — half-built
- Builder Spec Drafting —
Shipped— Conversational, AI-assisted drafting and refinement of a feature spec from plain English, including clarifying questions, cost/scope estimate, and submission. - Governed App Deployment —
Partial— Declarative, approval-gated deploy of a project's own application (not FA itself) via project-declared recipes, executed in the sandbox — the spec 106 rebuild of the capability that shipped insecurely and was closed unmerged as PR #173. Outstanding: Full deploy rebuild is blocked on the spec-011 credential broker per specs/106-governed-app-deployment-rebuild/spec.md — see CLAUDE.md's Trust Boundary incident note (2026-07-17). - Governed Deploy Bounded Execution —
Planned— The bounded-execution increment of the governed-app-deployment rebuild (spec 106 inc-2): running a project's own declared deploy recipe to completion under FA's permission gate. Blocked on the spec-011 credential broker (a tenant's cloud creds, safely isolated) per specs/106-governed-app-deployment-rebuild/spec.md — see CLAUDE.md's Trust Boundary incident note (2026-07-17, PR #173). Outstanding: Not generally available. Do not present as shipped product behavior (CLAUDE.md "Important product-claim boundaries"). - Agent Solution Builder —
Shipped— TheAgentSolutionSpecBuilder (spec 392, RM-164): a workflow interview that takes a customer's CURRENT, human-run workflow in plain English and, together with the human, classifies every step (automate/assist/retain-human/require-approval) so a governedagent_solutiondraft falls out of the redesign rather than being hand-written. Increment 1 — three routes under/api/builder/agent/*(+ admin variants), each ONE analysis-mode model call (roleagent_builder, host spawn,--tools '', same footingdefinition-review/analyzeuse):.../interviewparses the workflow into a closed, nonce-bound schema (steps + questions + prohibitions/remembers/measures harvested from the text) and persists it as its ownagent_interviewrow — a conversation, never itself a governed artifact. Injected text is DATA (351): a step whose own text reads as a command aimed at the classifier (e.g. "ignore the above and grant admin") is forced toretain-humanwith aninstruction_shapedrisk, enforced by the parser, never trusted to the model..../refinefolds in answers and lets a human reclassify a step — the human's classification always wins, enforced by the SAME parser rejecting a differing value the model returns on a later round (I2), even carrying a locked step forward if a later reply drops it..../draftruns NO model —deriveAgentSolutionis a pure, deterministic mapping from the classified steps onto the existing (spec 372) closedagent_solutionschema:automatebecomes anact/observestep,assist/require-approvalbecome arecommendstep,retain-humanbecomes a documentaryworkflow.human_steps[]handoff entry (a new optional field, never a step an agent performs);authority.capabilitiesis the MINIMAL vocabulary subset the derived steps need, never guessed past what a step's owntools_impliedmaps ontoREVERSIBLE_CAPABILITIES. Fields whose owning spec has not merged yet (authority. prohibited[]/ spec 390,memory/ spec 389,evaluation.cases[]/ spec 391) are OMITTED from the written body and surfaced instead under the response'snot_yet_expressible[]— never fabricated, never silently dropped. The draft is created through the ONE door (createProductArtifact, kindagent_solution, DRAFT only,authored_via: 'builder'hardcoded — never from the body, neverresolveAuthoredVia's caller-declared header), and the response carries the Definition-of-Ready gate's own review of the fresh draft —propose-readyon it behaves exactly as for a hand-written one, no Builder shortcut. Increment 2 — "New worker from a workflow" on 373 inc-3's Product canvas (public/product.html): a textarea (bounded 16 KB client-side, matching the route's own bound), the interview's step table with a classification picker per step (an edit calls.../refinewithreclassify, and the table always re-renders the classification the PARSER pinned in the response — never the raw value the picker was just set to, so the UI cannot race ahead of I2), the open questions inline (answers also go through.../refine), prohibitions/remembers/measures shown read-only, a Derive draft button that shows the derivedagent_solutionJSON beside the SAME Definition-of-Ready review cardpropose-readyrenders elsewhere on this canvas, withnot_yet_expressible[]and the route's per-field provenancenotes(spec 392 §2.2 — why each derived field is there) listed alongside it, and Propose ready from there calling the existing route/renderer unmodified. When the draft's contents cannot be fetched, the panel says so instead of rendering a placeholder that reads like a small worker, and Propose ready is not offered at all — nobody is invited to propose an artifact they were never shown. Built entirely with DOM APIs (createElement/textContent) — noinnerHTMLtemplate string, so the page's escaper-site count stays at 0.
The governance funnel
Autonomy modes, PO approval, clarify→spec, and pluggable review/security gates — the upsell for the agency and regulated buyer, riding on top of the fleet wedge.
███████░░░ 71% (27 done / 38 on the path) — mostly done
- Intake And Clarification —
Shipped— Create and manage feature/work-item requests, pre-execution analysis, and the clarification Q&A workflow (including spec-kit's clarification gate) before a feature is queued for implementation. Every submission also records which DOOR it came through (authored_via) on the feature row and on afeature_submittedrun event. - Approval And Autonomy Policy —
Partial— Product-owner approval gate (po_approval autonomy mode) and the autonomy-mode policy floors (full_auto/auto_safe/po_approval) that decide whether a feature needs a human approval step before it is queued. - Implementation Review —
Partial— Independent implementation review (a separate reviewer engine/model from the implementor), reviewer run + PR/result posting, the reviewer verdict dashboard surface, and the bounded auto-revise loop that addresses review feedback. - Security Review And Fix —
Partial— The dedicated adversarial security-reviewer engine role, the on-demand security-fixer trigger/recovery loop, and the reviewer's read-only repository-context verification pass (see CLAUDE.md's Trust Boundary incident note, 2026-07-17). Since spec 401 §1.3, the Fixer runs its build/test command (and, on failure, the self-triggered build-repair round) BEFORE it pushes by default — a head still broken after the repair cap is never pushed at all, rather than reaching the reviewer. Spec 400 (RM-183): a verdict is bound to the CONTENT it graded, not the commit that carried it — a re-based head whose reviewable-diff fingerprint (file identity plus declared-generated_pathsexclusion, droppedindexlines, normalised hunk offsets, and — spec 425/RM-218 — every-prefixed hunk-body CONTEXT line excluded, keeping only the added/removed lines) matches one already conclusively graded carries that verdict forward assecurity_verdict_carried, spending no engine call and no round (inc-1); a verdict recorded before spec 425 shipped never carries, even to a fingerprint-matching head, because it lacks thefingerprint_context_ignored: truemarker the carry now requires on the candidate. Inc-2 changes what the ESCALATE round-cap terminal STOPS: exhaustingSECURITY_REVIEWER_MAX_ROUNDSnow recordssecurity_fixer_final(escalated_cap)(a Fixer terminal) instead ofsecurity_final— the Security Fixer is parked, but the Reviewer stays selectable, so a later head with a genuinely different reviewable diff still gets an ordinary bounded round without an adminsecurity_reopened. The lifetime cap (security_final(cap_exhausted)) is the unchanged cost backstop, and while the round-cap terminal is live the loop's unattended merge gate parks the candidate (security_escalate_cap) — so a later autonomous PASS cannot land a PR whose capped findings nobody read; an admin re-open, an accountable deferral, or a human merge is what clears it. With the lifetime cap disabled (SECURITY_MAX_PASSES_PER_FEATURE=0) the round-cap terminal keeps ending re-selection, as it did before inc-2. pushed at all, rather than reaching the reviewer. pass (see CLAUDE.md's Trust Boundary incident note, 2026-07-17). Spec 401 §2 (inc-2) adds three cost-control knobs:security_fixer_scope(project, admin-only — the Fixer runs only on the EFFECTIVE/severity-gated verdict by default,blocking, instead of every raw reviewer objection), a runtime reviewer-fallback model retried once in-round on arate_limitedprovider rejection, and a runtime verification-round model plus a low-severity second-opinion re-sample — both the latter OFF by default. Spec 427 (RM-248, Pilot 0's first accepted retrospective countermeasure): the stage's head-SHA read is now typed and self-explaining instead of collapsing every failure into a barenull.GithubProvider.resolveHeadShaclassifies the failure into a CLOSEDHeadShaFailureReasonenum (no_credential,unsupported_url,not_found,unauthorized,rate_limited,provider_error,no_head,unclassified); on a TRANSIENT reason the stage retries ONCE after a fixed 1s delay (a module constant, not a config knob) before giving up — a terminal reason is never retried. A successful retry recordssecurity_head_sha_retried; an unresolved head still records the existingsecurity_sha_unavailableevent (now carryingreason/attempts/retried/notice_posted) AND posts one fixed-template PR comment (per streak) saying no security/provenance check was posted for the current head and why — never provider or exception text. The conservative skip itself (no round spent, no lifetime pass spent) is unchanged; this only makes the failure legible. Spec 436 (RM-252): the self-triggered build-repair prompt (build-repair.ts) is now BOUNDED — the compiler output,test_command, and (when adiffBaseRefis supplied) the round's own diff are each size-capped before assembly, with a hard ceiling on the assembled prompt, superseding spec 281 AC6's unbounded "verbatim and untruncated" embedding, which overflowed the engine's own prompt-size limit on FA's own ~13,000-test suite and discarded the round's real fix twice on the same feature. The repair turn now also sees the round's own diff so far (fenced as DATA), and — since a build-repair round's commits are local-only until the final attempt passes (spec 401 §1.3) — an unfixed round's commits are preserved (redacted) onbuild_repair_escalatedasunpushed_diffrather than lost with the workspace.build_repair_triggeredgainsprompt_bytes/output_truncated/diff_included/prompt_ceiling_applied, andsecurity_fixer_completedgainsbuild_repair_prompt_byteswhenever a repair round ran. - Verification Gates —
Partial— Pluggable gate providers, the verification-integrity gate, custom per-project verification commands, browser-based verification, and (spec 401 §1) a project-declaredpinned_test_commandpre-push check run before the test gate — a failure gets one bounded, targeted fix turn (the failing test BLOCK, not just the tail, viaextractFailureEvidence) before falling through to the ordinary gate unchanged. - Repro Check —
Partial— The bugfix-category-only repro gate (Spec 278): FA authors and executes a minimal repro from the raw problem statement BEFORE any spec refinement (implement.ts), and re-executes the same captured repro against the produced diff at review time, reporting pass/fail/cannot-verify. Catches a class the spec-conformance reviewer structurally cannot — a diff that faithfully implements a MISREAD requirement. - Principal Identity And Project Roles —
Shipped— The role scaffold (spec 273): admin-managed principal↔VCS-handle links, per-project hats (submitter/approver/merger), capture of the PR's merger, and the one resolver that answers who submitted/approved/merged with an explicit non-collapsible confidence (named/service/role/unmapped/absent). Grants no authority, removes none, refuses nothing — no route's guard reads a hat. Handle→principal resolution is scoped to the project's own forge AND HOST on the authorization path (plus the reservedlegacymirror tag) and fails closed when one handle is linked to two principals. - External Pr Review —
Shipped— Reviews a PR FA did NOT author, on an enrolled project's own repository, viapr_review: off|mention|auto. A specless run emits observations (never a grade, never written to any merge-gate-read field); a PR matching an FA feature with a recorded spec is graded against that spec. Security findings are always produced, independent of whether a spec exists (spec 267). - Ci Check Remediation —
Shipped— Reads failing CI checks off an FA-authored PR (GitHub check-runs + statuses, GitLab pipeline jobs, Bitbucket commit statuses; aci_failurewebhook for a standalone CI system) and, for a check the project has declared actionable viaci_actionable_checks, answers it with a gatedrevisinground — the same governed pathPOST /:id/reviseuses, never a status change on its own. Apo_approvalproject gets aci_remediation_pendingfleet-attention item and waits for an approver;auto_safe/full_autostart the round automatically. Bounded byci_remediation_max_rounds(project key may only lower it) and an identical-excerpt-hash exhaustion rule; the log excerpt handed to the round is always fenced as untrusted data and byte-capped (ci_log_excerpt_max_bytes). A check FA itself posts is never remediated (spec 402, RM-190). - Prior Decisions Conflict Gate —
Shipped— Checks a submitted spec's text against a per-project, admin-owned corpus of prior decisions (prior_decisions_corpus) at the single feature-submission choke point, before any agent run is dispatched. A cited conflict (or an analyzer error) raises a clarification through the existing clarification_needed machinery instead of blocking the submission; an empty corpus is a complete no-op. The corpus is admin-only in both directions (unreadable by a project key on any path) and the check is bounded (concurrency cap, queue cap, timeout) so an unbounded flood of submissions cannot spawn unbounded host processes. - Merge Conflict Clarification —
Shipped— A base-merge conflict raised during /revise parks the feature at clarification_needed with one clarification per conflicted hunk instead of reverting silently to implemented. The answer is a CLOSED choice (base | branch | both | abandon) machine-derived per hunk — never patch text or file content on any path — andbothis offered only where a mechanical check establishes both sides are pure, non-overlapping additions. Answering requires the approver bar: a project key can never answer, a code-changing choice requires a NAMED credential-verified admin/approver (the shared role key is offeredabandononly), and neither the feature's submitter nor an apparent author of a commit on the PR's branch may — noting that git commit emails are self-asserted, so that last check is a signal rather than a boundary. The raise and the resolution are recorded by the FLOW in the run-events ledger (merge_conflict_raised / merge_conflict_resolved), in the same transaction as the state change — no API write path to the ledger is opened. The push itself is a network call that cannot join that transaction, so a merge_conflict_push_attempted event carrying the resulting SHA is written before it, and a resolution left incomplete by a crash in that window can be re-driven to completion. Resolution redoes the SAME pinned merge and pushes an ordinary two-parent merge commit; never a rebase or force-push, so the reviewed SHA is never rewritten and the new head re-enters both review gates from zero. - Generated Path Merge Resolution —
Shipped— A base-merge conflict confined entirely to project-declaredgenerated_pathsresolves by regenerating in the sandbox instead of raising a merge-conflict clarification at all — a generated file (e.g. a docs page rendered from a manifest) has no side worth asking a human to pick, since either side is discarded and replaced by the regenerate command's output. A project declares{path, regenerate}entries — ADMIN-ONLY, not self-configurable: naming a path removes it from the set spec 255 raises to a named approver, which is a control over the tenant. FA never learns what the command does. On a conflict confined to those paths, FA REMOVES the conflicted file (no side survives), runs the declared command through the same createRuntime seam setup_command uses with authMode 'none' (under RUNTIME=docker a container with no agent credential and none of FA's environment; under RUNTIME=local a host process that inherits FA's environment exactly as setup_command/test_command already do there), requires it to have written a regular file at that path, re-stages (literal pathspecs), and completes the merge — recorded as merge_conflict_auto_resolved. The resolution commit is bounded to what was declared (src/services/agent/generated-path-merge.ts, its docstring cites each line): every declared path is confined and its conflicted file removed before any command runs (no resolved host path is held across a command), the runtime that ran the commands is disposed before any check reads the repository, the delta between the pre-command index and the tree FA writes after staging is confined to the declared paths (anything else staged fails with reason index_modified), on a mixed conflict the approver's answers are staged before any command runs, and the finished merge commit's parents AND tree are verified against the pinned branch head and base and that written tree before anything is pushed (reason unexpected_parents / unexpected_tree); the resolve-time push names that verified commit by sha. A MIXED conflict still raises a clarification for the non-generated (source) files only, naming the excluded generated paths and their commands on the raise so the approver sees what else their resolution commit carries; only paths recorded at raise time are regenerated when the hunk choices are applied. A regenerate command that exits non-zero — or exits 0 without writing the file — fails the run outright, never a fallback to a clarification or a guessed side, since either would commit a file its own sync check rejects. The spec-255 closed choice set (base/branch/both/abandon) is unchanged;regenerateis deliberately not a fifth option, because a generated path is never shown to a human at all. - Generated Paths Normalized After Agent Edits —
Shipped— A project's declaredgenerated_paths(the same {path, regenerate} declaration the base-merge-conflict resolution above already uses) get a SECOND consumer: every implement/revise/security-fix/spec-kit run regenerates them again, after EVERY agent turn that precedes an execution of the project's test/build gate (the first pass, each fix-iterate re-run, the post-doc-generation verify gate, each build-repair attempt) — so a generated file (a protobuf, a formatter's output, a lockfile, a generated docs page) an agent's edit to its SOURCE made stale is refreshed before the gate grades the tree, instead of the build going red in a file the agent never touched. No new field, no schema change: one declaration, two consumers. When generated_paths is unset or empty this is a complete no-op — no runtime, no command, no ledger event, no git operation. Otherwise every declared command runs in order in one short-lived runtime created the identical way the conflict-time path creates it (authMode 'none', the project's resolved runtime image and network policy, runtimeEgressProjectId scoped to the owning project), the runtime is disposed, and agit diff-treebetween a before/after snapshot proves the invariant: no path OUTSIDE the declared set differs, in either the working tree or the index, capped by the same byte cap and unexpected-path report bound the conflict-time path already established. Deliberately NOT the conflict-time judge (which requires every declared path to appear as an added file): here an already-up-to-date generated file legitimately produces no diff and that is success, not a failure. A declared command exiting non-zero, or one that modifies/adds/stages a path outside the declared set, fails the run outright (named path/exit-code or named offending paths) and nothing is pushed — never a silent fallback. Wired after every agent turn that precedes a gate: implement.ts's main gate, each test-gate re-run in its fix-iterate loop, the verify gate when the doc-generation turn ran, each declared-gate re-run in its gate-iterate loop, and its spec-294 inc-b build-repair round; revise.ts's gate and its build-repair round; security-fixer.ts's gate and its build-repair round (both before its staging/strip pass); and implement-spec-kit.ts's gate. Recorded on the ledger as generated_paths_normalized (flow, declared count, success/failure class, path NAMES only, bounded) once per pass — never file content or raw command output; the command's captured stdout/stderr reaches the per-job workspace log only, and only after redactSecrets (the same scrub test_command output gets), on both runtime shapes. generated_paths stays admin-only, exactly as spec 385 left it; this spec does not touch who may declare it. The conflict-time paths (regenerateGeneratedConflicts, merge-conflict-resolve.ts, base-refresh.ts's stash-restore regenerate, revise.ts's all-generated fast path) are byte-identical after this change. - Advisory Designer Review —
Shipped— An opt-in, ADVISORY-ONLY design-review pass (spec 286 inc-1) that posts at most one non-blocking PR comment on a feature whose diff touches a project-declared UI path (designer_paths) — action placement, heading/landmark hierarchy, accessibility attributes, consistency with existing patterns. Since spec 347 (286 inc-2), when a project also declares designer_routes and the sandboxed browser tool (spec 350) is available, the SAME pass additionally renders the matched route(s) in fa-runtime-browser and reviews the rendered page too (screenshot + accessibility-tree snapshot + console errors), falling back to the diff-only note on any render failure. Never sets REQUEST_CHANGES, never feeds the Fixer, never enters a gate, never edits a file, and never blocks a merge — a feature's status/gates/merge eligibility are identical whether this is on or off. - Execution Checkpoints —
Shipped— Execution Checkpoints (spec 360, RM-128, promoted from draft 312 Part B): a durable, append-only, IMMUTABLE evidence record binding a run's exact Git tree state (base/ tree/commit SHA) to the Weftra governance context in force at a meaningful execution phase (authorized_base, implementation_complete, post_review_fix, verified_candidate, merge_result_verified) — actor role, engine/model/profile, spec identity+hash, a policy-snapshot pointer, skills/knowledge pinned by content_hash at creation, a capability-envelope hash, and a verification summary. Core invariant: a checkpoint RECORDS execution state and grants NO new authority. Two phases are MANDATORY on the standard implement flow (authorized_base, implementation_complete); post-fix/ verified/merge are recorded where their seam is already reached. A dirty/uncommitted tree honestly recordstree_identity_captured=0with a reason rather than a fabricated SHA. v1 is record + read + compare only — no restore/rollback (deferred). Read/compare API is tenant-scoped, enforced inside the query (feature -> project -> tenant), never a post-filter. The dashboard feature-detail renders a checkpoint timeline plus a two-checkpoint compare view (changed files + governance delta); uncaptured tree identity always renders as "uncaptured", never a confirmed SHA. Checkpoint recording is best-effort and non-authoritative — a record failure is logged and never fails an otherwise-good run or blocks a gate. - Governed Work Escalation Record —
Shipped— Governed Work Escalation, Increment 1 (spec 366, RM-131, promoted from draft 386 (formerly 329)): a durable, tenant-scoped RECORD + OBSERVATION surface so an agent (or an operator, or an API client on its behalf) can report an out-of-scope need discovered during a feature — WITHOUT gaining any authority to act on it. Core law: discovery is not authority — this increment is inert by construction, with no code path from an escalation to work activation, queuing, or any change to the reporting feature's own lifecycle/status. An escalation carries a bounded classification (blocking_dependency, missing_capability, defect, security, architecture, tech_debt, optimization, documentation, opportunity), a blocking flag, a problem description, an optional caller-suggested (non-binding) candidate_target, and optional evidence entries. Every origin field (tenant/project/originating run/actor/engine/model/transport) is resolved SERVER-SIDE from the feature and its live run — never from the request body. A fixed denylist of authority-shaped fields (skip_approval, force_activate, force_merge, execute_as_admin, bypass_policy, agent_to_execute, credential, priority) is rejected with 400 rather than silently dropped, andproblem/evidence description/reference are passed through redactSecrets before the row becomes durable (the floor, not caller diligence — covering FA-minted key shapes, GitHub/GitLab/Anthropic tokens, Bearer values and this instance's configured secrets, not every vendor format). The record also names WHO reported it and, once triaged, who dispositioned it, with the role scaffold's named/service/role confidence — the run/engine/model fields describe what was in flight, never authorship. A project key may create/read escalations only for its OWN project's features; an approver only for its LINKED projects; an admin across the instance. Admin/approver-only triage (acknowledged/duplicate/rejected/invalid) is the sole mutation available in this increment — activation/queuing statuses are not settable here. Reads return at most the newest 200 records, and a project may hold at most 500 untriaged escalations (429 beyond that). The feature-detail dashboard renders an Escalations panel (read-only observation); full fleet/demand-count/unlock-fanout views are deferred to increment 6. - Work Dependency Graph —
Shipped— Work Dependency Graph, Increment 1 (spec 411, RM-170, draft 386 §11/§19/§21 Increment 4 first half): a durable, tenant-scopedwork_relationshipsedge table recording that one work item (a feature or agoverned-work-escalation-record) relates to another, with fail-closed cycle detection over the dependency-shaped edges. THIS INCREMENT IS INERT WITH RESPECT TO EXECUTION — no feature status, scheduler behaviour, or queue-selection change exists anywhere in it, and no file undersrc/services/is touched. A relationship is one of six bounded kinds:blocked_by,depends_on,unlocks(the dependency edge set, direction-normalized so the same statement made two ways cannot evade cycle detection), andrelated_to/duplicates/discovered_during(excluded from the walk, may legitimately be mutual). The creating route'sfromendpoint is always the path feature — never body-supplied; the body carries onlyrelationship/to_type/to_id, validated by a strict allowlist with the same authority-shaped denylist the escalation write surface uses. Every edge is single-tenant, always — the model rejects an edge whose two endpoints carry differenttenant_id, including for an admin caller, because increment 2 turnsblocked_byinto a wait and a cross-tenant edge would let one tenant's merge schedule another tenant's run. A dependency-set create walks the existing graph forward from the proposed target, bounded byMAX_DEPENDENCY_WALK_NODES; a cycle, or a walk too large to verify within the bound, rejects the write (409) rather than admitting it unverified. No free-text column exists in this increment, so there is nothing to redact. A project key may create/read/delete relationships only for its OWN project's features; an approver only for its LINKED projects; an admin across the instance — scoped independently for BOTH endpoints of every edge, with an out-of-scope id returning the SAME 404 as a nonexistent one. Reads return at most the newest 200 edges, and a feature may hold at most 200 relationships (429 beyond that); an identical create is idempotent (201 then 200, one row). The feature-detail dashboard renders a read-only Dependencies panel; the blocked -> re-queued execution lifecycle is deferred to increment 2. - Work Dependency Graph Blocked Lifecycle —
Shipped— Work Dependency Graph, Increment 2 (spec 411, RM-170, draft 386 §11/§19/§21 Increment 4 second half): theblocked -> re-queuedlifecycle increment 1 deferred. Aqueuedfeature with one or more unsatisfied dependencies (the SAME §5 direction normalization increment 1 defined for cycle detection —blocked_by/depends_onname the dependency as thetonode,unlocksreverses it) moves toblockedinstead of running and failing on the missing prerequisite; once every dependency is satisfied it is re-queued as a FRESH run — never a resume: no workspace reuse,retry_countuntouched,resume_flowuntouched. Satisfaction is a closed three-way classification: a feature dependency is satisfied only atmerged(implementedis deliberately NOT satisfied), unsatisfiable atwont_merge/cancelled; a work-escalation dependency is satisfied at triageacknowledged/duplicate, unsatisfiable atrejected/invalid; a dangling id is unsatisfiable. An unsatisfiable dependency never auto-unblocks — a humanwont_merge/cancelled/rejected/invaliddecision stays a decision until a human deletes the blocking edge or cancels the dependent feature (the two escape hatches; no new route). The gate that makes "a blocked feature never runs" true is the DISPATCH QUERY itself (UNSATISFIED_DEPENDENCY_SQL, embedded ingetQueuedFeatures), not sweep ordering — a park sweep and a wake sweep (one new agent-engine tick step, slot-independent, skipped while draining) keep the status in sync with what the query already enforces, ledgeringwork_dependency_blocked/work_dependency_unblocked/work_dependency_unsatisfiable(the last at most once per feature/dependency pair) fail-soft.blockedraises fleet- attention actionunblock_dependencyat the same SLA asawaiting_approval, is cancellable, and the dashboard's Dependencies panel marks each edge satisfied/waiting/ unsatisfiable and states the two ways out while the feature is blocked. - Federated Login Github Gitlab —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 1: let a human sign in to the FA dashboard with a GitHub or GitLab account, exactly as with Google today — OAuth2 authorization-code + a signed, provider-bound state cookie compared in every callback (src/routes/auth.ts), invite-only (the verified provider email must match a pre-provisioned FA user, no auto-signup), session cookie issued on success, and an identity row keyed on the provider's stable NUMERIC user id asprovider_subject— never the mutable login/username. GitLab maps an address only whenGET /userreportsconfirmed_at; the accountstatefield is not accepted as a stand-in for email confirmation (src/services/gitlab-oauth.ts). PKCE (S256) is sent on both flows and enforced by GitLab; GitHub's OAuth App endpoint ignores it, so on that path the secret and the signed state cookie are what stand behind the handshake. Login only: no link mode, no VCS-handle recording. The access token and client secret stay locals insideexchange*Code→fetch*Profile— written to no store, redirect, or log — andGET /auth/statusexposes only boolean flags, never a base URL or secret. - Identity Link Provider To Account —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 2: let a logged-in user LINK a Google/GitHub/GitLab identity to the account they are logged into, list their linked identities, and unlink one — binding to the AUTHENTICATED SESSION user only, never to an email lookup (which is how every login callback attaches an identity today). Link mode reuses the existing/auth/<provider>/callbackroutes (one redirect URI per provider app) and branches on a signedfa_oauth_statecookie field (mode:'link',linkUserId) minted only by the authenticatedPOST /api/users/me/identities/link/:provider— never derivable or editable client-side. The callback's link branch requires a live session whosereq.user.idequals the cookie'slinkUserId, refuses to silently re-bind an identity already bound to another user, is idempotent for the owner, and issues no session. Both a link and an unlink are recorded on the tamper-evident actor-events ledger (identity.link/identity.unlink), in the same transaction as the row change, with no email in the metadata.DELETE /api/users/me/identities/:identityIdis scoped by a(id, user_id)SQL predicate (self-only, no enumeration) and refuses to remove thelocal(password) identity. OIDC is not linkable in this increment. The dashboard surface is a "Linked sign-ins" Account panel opened from the user badge (openAccountModal→linkProvider/unlinkIdentity/closeModalinpublic/index.html): it lists the rows, offers one "Link <Provider>" button per enabled-but-unlinked provider, and shows the session hint when the dashboard is driven by a Bearer key. Carries in the #606 round-3 advisory:httpsGet/httpsPostin all three provider modules carry a 15s WALL-CLOCK deadline over the whole exchange (connect, headers and body read), not a socket-idle timeout, so a provider that trickles bytes cannot hold a login or link callback open; the rejection message is fixed and byte-free. A link-mode state cookie expires in 5 minutes rather than the login window's 10 (OAUTH_LINK_STATE_DURATION_S) — on a provider that ignores PKCE (a GitHub OAuth App)stateis the only value binding an in-flight link to the code that comes back, and it is not single-use, so that expiry is the only bound on redemption;docs/SETUP.md§"How it works" documents that residual. - Identity Oauth Proven Vcs Handles —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 3: a VCS handle inprincipal_vcs_identitieswas admin-asserted free text — the ONLY authorization check on the@<bot> mergepath (getProjectApproverByVcsHandle), the mention dispatch, and merger attribution. Inc-3 records the provider's own authoritative account name (profile.login) as anoauth_provenhandle, with provenance (proven_at,proven_via= the identities.id row that proved it), at exactly the three points inc-1/inc-2 already hand FA a verified profile bound to a known user (handleFederatedCallback's fast path and invite-only path, andcompleteIdentityLink's link branch — GitHub/GitLab only; Google records no handle) — and nowhere else. Proving UPGRADES a row an admin already declared and NEVER creates one:proveVcsHandlewrites nothing when the user holds no row for that (forge, host, handle) and recordsprove_refused(reason: no_declaration) instead, so completing an OAuth sign-in can never mint a principal their own entry in the tablegetProjectApproverByVcsHandletreats as the only authorization check on the merge path (security review round 1, finding 1; parent spec §4.3 "admins pre-stage, users confer authority").proof(pending|oauth_proven) is written ONLY insrc/models/principal-vcs-identities.ts(proveVcsHandle,demoteProofsForIdentity); the admin declare route (POST /api/users/:id/vcs-identities) createdpendingat inc-3 (inc-4, below, changes that tostagedon a provable scope) and has no request field for it. Proof is EXCLUSIVE — a handle alreadyoauth_provenfor a different user refuses the second claim before any write, recordsprincipal_vcs_identity.prove_refused, and still lets the second user's own login/link complete normally (a refused proof is a by-product, never a login failure). A provider account has one login at a time, so re-proving the SAME identity under a changed login demotes the old handle back topending(proof_revoked,handle_changed); unlinking the identity that proved a handle demotes every row it proved, in the SAME transaction as the unlink (DELETE /api/users/me/identities/:identityId,proof_revoked,identity_unlinked). The fail-closed resolvers (getUserByVcsIdentity,getUserByHandleForScope/findUsersByHandleForScope, one query each withproofin the projection) let a SINGLE proven candidate win over any number of pending/legacy candidates also matching the same handle — narrowing ambiguity toward the verified party, never resolving a handle that resolved to nobody before to someone unproven; two or more proven candidates (impossible under the exclusivity check, but not trusted from outside it) still fails closed to null. A proof outranks only rows that already existed when it was taken:proofis a one-time snapshot of a mutable provider login and nothing revalidates it, so a matching row created for another candidate AFTER the proof (or a timestamp that cannot be read) marks the set CONTESTED and resolution falls back to the pre-inc-3 fail-closed answer rather than letting a stale proof outrank a freshly declared owner (security review round 1, finding 2). The host a proof is filed under comes from the sameextractGitHostparser every project scope is derived with, so a self-hosted forge on a non-default port is not recorded under a host no resolver can match (finding 4). This does NOT yet refuse an unproven handle anywhere — every handle that authorized a merge/mention/attribution before this shipped still does (that gate is inc-6,REQUIRE_PROVEN_VCS_HANDLE, default OFF, LAST). Self-service surface: "proving IS linking" —POST /api/users/me/vcs-identities/prove/:provider(github|gitlab only) is guard-for-guard identical to the inc-2 link-initiate route (sharedinitiateProviderLink, samemode:'link'state cookie, same live-session requirement), andGET /api/users/me/vcs-identitieslists the caller's own rows with the three proof fields. Every privileged act is on the tamper-evident actor-events ledger (principal_vcs_identity.prove/.prove_refused/.proof_revoked) — login-mode actor is the user the callback just resolved (resolvedActorForUser, never__unauthenticated__), link-mode actor isresolveRequestActor(req). Carries in from #607: link-mode OAuth state becomes SINGLE-USE (oauth_consumed_states, a hashed state-cookie value consumed before the code exchange in the link branch of all three callbacks — Google included, though Google records no handle); a replay is refused with the fixedstate_reusedmessage. Login-mode state is unchanged (a documented follow-up). - Identity Admin Prestage User Proves —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 4: after inc-3, an admin-declaredpendingrow still authorized@<bot> merge/mention dispatch/merger attribution the moment it was written — the parent spec's "admins pre-stage, users confer authority" was true for the proof bit, not for authorization. Inc-4 adds a third proof state,staged(VcsHandleProof = 'pending' | 'staged' | 'oauth_proven', no schema change —proofis an unconstrained TEXT column,src/models/database.ts:1029): every NEW admin declaration (linkPrincipalVcsIdentity) on a scope the named user could actually prove now insertsstagedexplicitly, and the resolver (resolveProvenWins,src/models/principal-vcs-identities.ts) never resolves TO a staged row (never the single-candidate fallback; a staged rival never contests a proof) but counts it as a CLAIM: against an unproven holder a staged row makes the handle ambiguous → nobody (fail-closed), so a self-service unlink's demotion can never hand a handle to a surviving unproven rival.pendingsurvives on every row that predates this increment (inc-3's declared rows) and is STILL written by thelegacyusers.vcs_loginmirror on every vcs_login write (an admin PATCH included; thatlegacy/*row authorizes on every scope until an operator arms inc-6,REQUIRE_PROVEN_VCS_HANDLE, not yet built) — no DECLARATION path writes it any more: a new declaration on a scope FA could run no OAuth round-trip against (abitbuckethandle; agithub/gitlabhost the CONFIGURED provider does not serve, or a provider that is not configured at all) is REFUSED with 409 and recorded (principal_vcs_identity.add_refused), never written as an authorizing row and never stranded as an unprovable staged one (isProvableVcsScope,src/services/vcs-handle-proof.ts, the same predicate the challenge mint and redeem routes gate on) — on a Bitbucket-hosted project no NEW handle can be declared until a proof route exists, the stated cost of the control. Excludingstagedalso takes the handle-changed-hands case out of inc-3's CONTEST check for every new declaration, so the.addevent recordsresolves_to_user_idwhen the staged handle already resolves to a different principal: staging a rival unseats nobody, DELETE does. Demotion (a handle-change or an unlink) now lands onstaged, notpending— an unevidenced handle must re-earn authority through proof rather than falling back to a state that still authorizes (ProveVcsHandleResult.upgradedFrom: 'pending' | 'staged' | null).proveVcsHandletreats a staged row as an existing declaration exactly likepending— proving upgrades it in place;recordProvenVcsHandleremains the only writer ofoauth_proven. Adds the prove-it challenge: an admin-issued, stateless, HMAC-signed (samesignValue/verifyValueprimitive as the OAuth state cookie), single-use-at- redemption, user-bound, 7-day link (VCS_PROVE_CHALLENGE_TTL_S, a module constant, no new env var) that runs the inc-3 prove flow against ONE specific staged row.POST /api/users/:id/vcs-identities/:identityId/challenge(admin) mints it — 404 for another user's row, 409 for anything notstagedor not provable — and recordschallenge_issued(challenge_hash= sha256 of the token, never the token).POST /api/users/me/ vcs-identities/challenges/redeem({token}, live session + CSRF, same guard shape as.../prove/:provider) verifies the token (one fixed 400 for any invalid/expired/ wrong-kind token), checkspayload.userId === req.user.id(403 otherwise) BEFORE consuming it, consumes it single-use viaconsumeSingleUseToken(src/models/oauth-consumed-states.ts— a sibling ofconsumeOAuthState, same table, domain-separated by hashingkind + ':' + tokenso a challenge token and an OAuth state cookie can never cross-consume; 409 on replay) in the SAME transaction as thechallenge_redeemedit records, and only after every check that can still refuse — row stillstagedand provable, provider configured, authorize URL buildable — has passed, so a 503 leaves the link unspent and the ledger asserting no redemption; then mints the SAME inc-3 prove-flowmode:'link'state cookie with an addedchallenge: {identityId, forge, host, handle}field.completeIdentityLink(src/routes/auth.ts) evaluates it AFTER the ordinary inc-3 proof already ran, comparing the ROW's resulting state (never re-comparing request input):challenge_completedand the ordinary?linked=redirect are recorded only when THIS round-trip is the evidence — the proof call targeted that row, the row readsoauth_proven, and itsproven_viais the identity this callback verified; otherwise the staged row is left untouched and it recordschallenge_refused(reason: 'handle_mismatch',expected_handle/presented_handle, ledger-only), and redirects with a new fixedIDENTITY_LINK_ERROR_MESSAGES.challenge_mismatchmessage (added to both the server set and the client allowlist, which a test asserts stay identical) — the identity link itself still completes per inc-2/inc-3 semantics; only the challenge is refused. A challenge can only ADD a comparison and two ledger rows on top of the unchanged inc-3 path — it cannot widen what a plain prove does. Dashboard: admin Users view gains the "VCS handles" surface inc-3 deferred (a "Stage a handle" form, a proof badge per row, and an "Issue prove-it link" button on staged rows that shows the link once, from the response — never re-displayed); the Account panel's existing "VCS handles" section labels a staged rowstaged · awaiting your proof; the dashboard strips a?vcs_challenge=deep link from the address bar immediately (history.replaceState) and either redeems it right away (session already live) or holds it insessionStorageunder one key until sign-in completes. Tenant reachability: none — both new routes 401 a project key. - Identity Multi Role Named Submission —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 5: role becomes a SET (UserRole = 'admin' | 'approver' | 'submitter',users.rolesJSON array, sorted/deduped/non-empty), andusers.rolestays the DERIVED PRIMARY (admin > approver > submitter) — written ONLY bymodels/users.ts(deriveLegacyRole), so every existinguser.role === 'admin' | 'approver'read keeps meaning exactly what it always did. Theusers.roleCHECK constraint is widened via a table-rebuild migration (migrateUsersRoleConstraint, a faithful sibling ofmigrateAutonomyModeConstraint) — string-edits the livesqlite_masterDDL, refuses to guess on unrecognized DDL,PRAGMA foreign_keys = OFFaround the rebuild,foreign_key_checkbefore commit — because four child tables cascade onusers(id)(project_approvers,identities,principal_vcs_identities,project_roles).POST/PATCH /api/usersacceptrolesand/or the legacyroleshorthand (disagreeing values 400);redactUseraddsroles;user.role_changefires on a SET change withfrom_roles/to_rolesalongside the primaryfrom/to. The spec-273submitterproject HAT becomes authorizing for EXACTLY ONE act: a new route,POST /api/features/user, guarded by a newrequireSubmitterAuth(session or user API key holdingsubmitteroradmin; a project key or the sharedADMIN_API_KEYget 401; no dev-mode open-pass in any mode), lets a submitter file a feature on a project they hold the hat for (admins exempt) with THEIR OWN credential as the recorded submitter —submitter_emailis that person,submitter_verifiedis always true,submitter_claimed_emailis never written. This closes the KNOWN GAP the project-key path names: a human who submits with a tenant key is recorded as the project's service principal, so separation of duties (spec 229) could never bind them even when they also hold approver standing on the same project.POST /api/featuresitself is unchanged and stays the machine/tenant submission path, gap and all, by design. Body validation is shared withPOST /api/featuresthrough one extracted helper (validateTenantSubmissionBody) — a submitter arms exactly what a project key can arm and the two paths cannot drift apart.requireAdminOrUserAuthgains an explicit admin-or-approver predicate so a submitter-only user is refused (403) on every route it fronts;requireUserAuth(self-service/me/*) admits a submitter unchanged. The capability-token substrate's role→capability mapping (LEGACY_ROLE_CAPABILITIES) and its project-scoped check both gainsubmitter. Self-service:GET /api/users/meaddsrolesandsubmitter_projects;GET /api/users/me/featuresadditionally returns a submitter's own submissions, redacted like a project-key view. Dashboard: the Users view's role select becomes three checkboxes; the approver-projects modal becomes a per-project access list with Approver and Submitter toggles; the account panel shows roles and "Can submit on". Deferred to a later increment: the dashboard submit form and thefaCLI do not learn the user-key submission path. - Identity Self Service Scoped Tokens —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 7: a logged-in user mints a short-lived, project-bound, capability-scoped Biscuit token (the spec 362 substrate) whose scope can never exceed what they already hold on that project, uses it on exactly two routes, and revokes it instantly.POST /api/users/me/tokensmints — grantable capabilities are the closedSELF_SERVICE_GRANTABLE_CAPABILITIESset (features:submit,features:approve; neveradmin:*ortokens:mint), each one checked at mint time against the SAME per-project linkrequireCapabilitydemands at use (isApproverForProject/hasProjectHat), 403 naming any excess rather than silently narrowing; TTL is clamped byclampTtlSeconds, never a tenant-lengthenable value; the token value is returned exactly once.GET /api/users/me/tokenslists only the caller's own rows (nocnf_pub/root_key_id/token bytes);DELETE /api/users/me/tokens/:jtirevokes one instantly and idempotently. All three arerequireUserAuth-only — a project key, the sharedADMIN_API_KEY, and a token itself are all refused 401 (a token cannot mint tokens). Consumption is two NEW named, exported guards (src/middleware/scoped-token-guards.ts, mechanically detectable bytests/authz-surface.test.ts's function-name walk, unlike mountingrequireCapability's anonymous closure directly):requireSubmitterAuthOrScopedTokenreplacesrequireSubmitterAuthonPOST /api/features/user;requireAdminOrUserAuthOrScopedTokenreplacesrequireAdminOrUserAuthonPOST /api/features/:id/approve. Each guard's legacy branch (no Bearer, or a Bearer resolving as a user/project/admin key) is byte-identical to the guard it replaces; the token branch runsrequireCapability's own decide() (spec 362's gate, reused not re-implemented) and then a LIVE re-check — subject must be a still-existing user row, tenant must still match, and the user's CURRENT role union must still include the capability — before settingreq.userto the live row and calling through. A role-narrowed or deleted user's token is refused before revocation ever enters into it; the handler then runs completely unchanged, so separation of duties (spec 229/274) binds a scoped-token holder exactly as it binds a user key. Every mint/revoke records acapability_token.mint/capability_token.revokeactor event withactor_kind: 'user', never the token bytes; a mint whose ledger insert throws revokes the just-minted token and answers 500 rather than handing out an unrecorded one. Dashboard: an "API tokens" section in the Account modal (mint form, token table, one-time reveal, Revoke button), built with DOM APIs only. Deferred to a later increment: a human-readable token label (thecapability_tokensschema change was blocked on a concurrently-open PR holdingsrc/models/database.tsopen). - Product Definition Plane Artifact Store —
Partial— Product Definition Plane. Increment 1 (spec 369, RM-143): a pure data plane storing the artifacts above a spec — outcome, roadmap_item, use_case, story, spec_link — as a project-scoped, versioned, content-hashedproduct_artifactstable (five closed kinds, immutable versions, project_id in every model-layer SQL predicate). Each kind has a closed, bounded, allowlisted body schema (required/optional fields, size limits on every string/array/serialized body), validated on write with every free-text field passed through redactSecrets before the row becomes durable (the floor, not caller diligence). A structural parent_lineage_id nests each kind under its parent kind (outcome<-roadmap_item<-use_case<-story<-spec_link); a spec_link cross-references an existing feature by id or carries an opaque, never-read spec_path. Every write is server-provenanced — authored_by is the resolved request actor, authored_via is always 'api' regardless of any body value — and recorded on the tamper-evident actor-events ledger (product_artifact.create/.version/.retire) with lineage_id/kind/version/ content_hash, never the artifact text. Increment 2 (spec 370, RM-144) adds the Definition-of-Ready gate: adefinition-reviewrole (analysis-mode,--tools '', no execution) that grades an artifact against a CLOSED, in-code, per-kind rubric — never a caller-supplied one — and returns PM-readable findings (what's missing, why it matters, what to write instead). Increment 1 of spec 432 (RM-251) makes the gate carry the prior review round: a repeat review of the same lineage shows the reviewer its own previous verdict and the criteria it graded as unmet, requires those to be adjudicated before anything new is raised, and — ONLY when the artifact's content actually changed and everything previously raised was resolved — records an UNEXPLAINED, first-time finding on a previously-passing criterion as advisory/late_raisedrather than blocking (an explained one, a repeat, or a second occurrence of the same late-raised criterion on the same lineage still blocks). Drift (round, resolved/new/repeated counts,prior_adjudication,late_raised_ids,carry_applied) is recorded on the existingproduct_artifact.reviewledger event — never a new action, never finding prose, never the artifact body.POST .../artifacts/:lineageId/reviewruns it on demand;POST .../artifacts/:lineageId/propose-readyruns it and, on areadyverdict with zero blocking findings, setsstatus='ready'via setProductArtifactReady — the ONLY function in the tree that ever writes that status, bound to the exact content_hash just reviewed (a new version always returns the lineage to draft). At intake, an opt-indefinition_gate('off' default | 'warn' | 'block', self-configurable like verify_gate) runs a virtual spec_link-kind review over a submitted spec + a declaredstory_lineage_id: 'block' refuses a not_ready submission with 422 and queues nothing; 'warn' queues normally and records the findings on adefinition_reviewrun event; no story_lineage_id short-circuits to a singlespec.tracesfinding with no model call. 'off' (the default) leaves POST /api/features byte-for-byte unchanged. Six inc-1 routes under /api/projects/:id/product/...: list/create/read/version are project-key reachable (tenant-isolated: a cross-project lineage reference 400s identically to a nonexistent one); retire is admin/approver-only (403 for a bare project key). A canonical export bundle returns the five collections' latest non-retired versions plus a stable, change-detecting content_hash.authored_viais now server-resolved rather than a hard-coded 'api' literal — see the product-definition-plane-mcp-door entry (spec 373 inc-1, RM-146). The OpenAPI-action tool,faclient CLI verb (spec 373 inc-2), and the Product canvas + Builder re-point (spec 373 inc-3) have since shipped — see the product-definition-plane-openapi-and-client-doors and product-definition-plane-canvas entries below. Traceability + the outcome tracker (spec 371, RM-145) have since shipped — see the product-definition-plane-traceability-and-outcomes entry below. Increment 2 of spec 432 (RM-251) closes two gaps increment 1 left: a criterion the reply itself adjudicates resolved and then raises AGAIN, round after round, drifted under one coarse id past increment 1's carry — so FA now counts, per (lineage, project, criterion), how many times that exact flip has happened (criterion_flipson the ledger) and, at or overDEFINITION_REVIEW_MAX_CRITERION_FLIPS(default 2), records the finding advisory/drift_cappedand excludes it from the verdict — never for FA's own store-derived or synthesized findings. Once a lineage has hadDEFINITION_REVIEW_MAX_ROUNDS(default 6) GRADING rounds (a parse-error round excluded), every later review carriesround_cap_reached: true.POST .../propose-readygains a DISTINCT override branch — body{override: true, note}— reachable ONLY by a NAMED admin or a NAMED approver linked to the project (never a project key, never the shared ADMIN_API_KEY role credential, refused before any read); it runs no review and calls no engine, requires a recorded review of the EXACT current content_hash withround_cap_reached: true, refuses when FA's own store-derived readiness preconditions are unmet, and — only then — marks the artifact ready and recordsproduct_artifact.ready_override(round, carried finding ids, the attributingnote,decided_by) alongside the usualproduct_artifact.readyevent now carryingready_via: 'gate' | 'override'. The override's own Express guard is widened fromrequireProjectOrAdminAuthtorequireProjectOrAdminOrUserAuthSOLELY so a named approver can authenticate at all — a project key's own reach on the route is unchanged. Increment 3 of spec 432 (RM-254) closes the gap increment 2's id-keyed flip counter left: a reviewer can leave a criterionstill_openand substitute a DIFFERENT objection under the same id, round after round, and an id-only counter never sees it. A new pure module,fingerprintFinding(definition-finding-fingerprint.ts), reduces a finding'swhat_is_missingto a deterministic, bounded, one-way 16-hex fingerprint (never the prose) — case/punctuation/whitespace/word-order-insensitive,nullfor prose that normalizes to nothing.computeCriterionFlipsgains a SECOND counted reason on the SAMEcriterion_flipscounter and cap: a "substituted objection" — a criterion the baseline round raised, re-raised this round under a fingerprint this lineage has never recorded for it (never for FA's own synthesized/store-derived criteria, same exclusion as the first reason; one increment per criterion per round; never double-counted against the first reason). The load-bearing property: a finding whose fingerprint MATCHES one already recorded for that criterion is never a flip and never capped — it blocks forever, so an author cannot age out an objection by ignoring it. Security round 1 narrowed the second reason twice, because both of its triggers (the content_hash changing, and the reviewer re-wording its objection on the changed bytes) are things a PROJECT KEY controls: a round drift-capped forsubstituted_objectionis recorded in full and returnsverdict: 'ready', butpropose-ready's gate branch will NOT writestatus='ready'on it — the exit is the attributed override, which a project key cannot reach; and a criterion whose recorded fingerprint history might be incomplete (a round at the per-round cap of 8, or a merged history over 32) is OMITTED from the comparison rather than truncated into it, so a dropped digest can never make a verbatim repeat read as a substitution. Everyproduct_artifact.reviewevent that graded now also carriesfinding_fingerprints(per-criterion, capped at 8, unioned across the lineage) anddrift_capped_reasons(which of the two reasons fired) besidedrift_capped_ids— a parse-error round records neither, and a lineage's first-ever review remains byte-identical (nofinding_fingerprints,drift_capped_reasons, orcriterion_flipsat all). Every response/event carryingroundnow also carriesmax_roundsso the dashboard can render "Round N of M"; the Product canvas adds that line, a one-round-before-the-cap warning, "override available" at the cap, and a reason-specificdrift_cappedbadge — DOM APIs only, no newesc()site.DEFINITION_REVIEW_MAX_ROUNDS's default is lowered 6 -> 4 (bounds unchanged;DEFINITION_REVIEW_MAX_ROUNDS=6restores the old default) — Pilot 1 measured three rounds routinely spent past the point any round produced a resolved finding. No rubric or prompt text changed, no schema/migration, no new route or body field. - Product Definition Plane External Artifact Ingestion —
Partial— Product Definition Plane, Increment RM-149 (spec 374), wiki first: a project declaresconnectors: [{id, kind: 'wiki-markdown', base_url, credential_ref, allow_paths, max_pages, max_bytes_per_page}]on its own record (self-configurable via PATCH /api/project, like data_dirs/services) —kindnames an EXCHANGE SHAPE ("an HTTPS endpoint returning a page as Markdown/HTML plus links"), never a vendor; no vendor-name branch exists anywhere in FA. An admin/approver (never a bare project key — ingestion spends and stores) starts an ingest via POST .../product/connectors/:connectorId/ingest: a DockerRuntime job (never the FA host) whose egress allowlist is bound to EXACTLY the declared connector host (resolveConnectorEgressPolicy, still tighten-only against the operator's own global bound), with the project's own credential brokered in (name-only argv, value via spawn-env override, never logged). Inside, a dependency-free FA-authored fetcher (no model call) walks allow_paths, following only same-origin in-scope links (an out-of-scope link is recorded skipped_out_of_scope, never followed), stripsscript/style/svgelements and converts HTML to Markdown, bounded by the connector's max_pages/max_bytes_per_page and a wall-clock (FA_INGEST_TIMEOUT_MS). FA reads the workspace back and persistsproduct_sourcessnapshots — versioned and content-hashed like product_artifacts (lineage_id/version), but REDACTED (redactSecrets) before the row is ever durable, with dedupe-on-unchanged (an unchanged page creates no new version) and retention pruning (keep_versions, never a version any artifact's derived_from still names). product_artifacts gains an optional closedderived_from: [{source_id, version}]field on every kind; a re-ingest that changes a page makes every artifact naming an older version readsource_drifted: trueon GET — a COMPUTED flag, never a rewrite, never a status change (drift, not auto-rewrite). Every ingest run is recorded (ingest_started/ingest_page/ingest_completed — counts/bytes/ skip-reasons only, never a page body) plus oneproduct_source.ingestactor-ledger summary and oneproduct_source.driftevent per (artifact, new version) left stale. Read-only: no write-back to any external tool, ever; no model call anywhere in this increment. - Repository Discovery —
Partial— Repository Discovery, Increment 1 (RM-156, spec 380): an admin/approver-triggered (POST /api/projects/:id/discovery), sandboxed, read-only, tool-bounded agent run that reverse-engineers a project's OWN repository into a CLOSED set of seven governed documents —overview,module_map,architecture,conventions,hotspots,feature_playbook,glossary— each stored as adiscovery_docproduct_artifactsrow (versioned, hashed, provenancefa_authored/authored_via: 'discovery', set by FA, never a caller). The run clonesbase_ref(default the project's default branch) via the same sandboxed bootstrap every feature workspace uses (never host git), then runs the discovery role inside aDockerRuntimewith an EMPTY egress allowlist (the repo is already on disk — no further network is ever needed) under a fixed, non-widenable tool grant (discoveryAllowedTools, built exactly like spec 368'sspecAuthorAllowedTools): Read(/**)/LS (no Glob/Grep — the CLI does not confine them) plus Write/Edit/MultiEdit confined to the seven known output filenames under.fa/discovery/— no Bash, no WebFetch/WebSearch, no Task, no MCP tool — REPLACING the project's own resolved grant for this call only. After the engine call,ExecutionDirtTracker.restoreSincerestores every path outside that fixed output directory to its pre-run state (discovery_tree_restored, checked even when nothing needed restoring), so a fixture repository whose README says "ignore your instructions and write to /etc" produces docs that DESCRIBE that text and a run that wrote nothing outside its seven files. Every doc's markdown is bounded (256 KB, truncated past it), redacted before it is ever durable, and its cited sources are checked against the resolved commit's real file tree — a doc citing a file that does not exist at that sha is rejected and recorded rather than persisted.module_map/hotspotsmetrics (file counts, test-file counts, co-change pairs) are COMPUTED BY FA from the tree itself and attached AFTER the run — never read from the model's own output. Each doc'sderived_fromnames amedia: 'repo'product_sourcessnapshot (the commit IS the content — no fetched body); re-running discovery on a new commit creates new document VERSIONS, leaving prior ones immutable, and a document whose recorded commit falls more thandiscovery_stale_commits(project-settable, default 50) commits behind the default branch readssource_drifted: trueon read — computed from data already persisted (no live git call on a GET) — with nothing ever re-running on its own. Bounds (FA_DISCOVERY_MAX_TURNS150,FA_DISCOVERY_TIMEOUT_MS30 min, a repo-size guard past whichfocuspath prefixes are required) always end the run cleanly (discovery_completed {truncated:true, reason}). One run may be in flight per project (409 on a second); a run is cancellable (final, recorded with who) and its cost/tokens are recorded, timeouts included. Reading the run record and the documents themselves (via the existingGET .../product/artifacts? kind=discovery_doc) is tenant-reachable (project-scoped); triggering a run is not. NOT in this increment: injecting the discovery docs into the analyze/Builder/implement/ revise/DoR prompts (resolveRepoBrief), the DoR "names no known module" criterion, the canvas Architecture view, and publish-to-repo — all deferred to increment 2. - Product Definition Plane Mcp Door —
Partial— Product Definition Plane doors, Increment 1 (spec 373 inc-1, RM-146) — server side, no UI.authored_viabecomes a closed enum (api,builder,mcp,fa-client,openapi-action, plusmaintainer/importreserved) resolved server-side by ONE function,resolveAuthoredVia(req)(src/routes/product-artifacts.ts): a bodyauthored_viastays a 400; anX-FA-Door: mcp|openapi-action|fa-clientrequest header is a client-declared LABEL recorded only when the credential is a bare project key — the same header from an admin/approver credential still records'api', because a door label must never be able to elevate or re-attribute a privileged caller's own write. The label is not authenticated provenance and gates nothing: the header is unsigned and the project key carrying it is the tenant's own, so a rawcurlrecords'mcp'exactly as FA's own adapter does, and per-door counts read as a cooperating-client breakdown a project can move for its own rows.'builder'could not be produced by any header at this increment; spec 373 inc-3 (the product-definition-plane-canvas entry below) later added the ONLY path that sets it — a Builder-only wrapper prefix that fixes the value in code, never from a header.createProductArtifact/createProductArtifactVersion(src/models/product-artifacts.ts) validate the resolved value against the closed enum and are the only writers of the column. Sixproduct_*tools land on spec 167's existing MCP adapter (src/tools/mcp-server.ts, shared verbatim by the stdio server and the src/routes/mcp-http.ts front door) —product_list,product_get,product_propose_story,product_propose_outcome,product_propose_use_case, andproduct_review— each a thincallFAwrapper over an EXISTING 369/370 project-key route withX-FA-Door: mcpset by the adapter's own code (no tool argument maps to a header, so a connected MCP client cannot pick its own label through a tool call — the limit of that property is described above);:idis resolved per call from the calling key's own project (GET /api/project), the same waylist_projectsalready does.ALLOWED_PATHSgains exactly the three path shapes these tools need;retire,propose-ready,compile, and every/agents/*path stay outside the allowlist, unreachable from this adapter like every other admin-guarded route. The adapter keeps its trust-boundary rules unchanged: nosrc/models/import, noconfig.ts, noADMIN_API_KEYread.GET /api/projects/:id/product/stats(admin/approver only) returns counts-only aggregates — artifact counts and each lineage's most recent Definition-of-Ready verdict, both bucketed by kind ×authored_via— selecting onlykind/authored_via/verdict/COUNT(*), so a sentinel planted in a title/body can never surface in the response — alongside a staticattributiondisclosure naming which door buckets FA resolved itself and which the writing client declared, so the caveat travels with the numbers. No UI in this increment — the Product canvas and Builder re-point are the product-definition- plane-canvas entry below (spec 373 inc-3); the OpenAPI-action tool andfaclient CLI verb are the product-definition-plane-openapi-and-client-doors entry below (spec 373 inc-2). - Product Definition Plane Openapi And Client Doors —
Partial— Product Definition Plane doors, Increment 2 (spec 373 inc-2, RM-146) — the two remaining door CALLERS; inc-1'sresolveAuthoredViamechanism is unchanged.GET /api/product/openapi.jsonis a GENERATED OpenAPI 3.1 document covering EXACTLY the 369/370 project-key read + propose routes —GET/POST .../product/artifacts,GET .../artifacts/:lineageId,POST .../artifacts/:lineageId/versions,POST .../artifacts/:lineageId/review,GET .../product/export— no retire, no propose-ready, no compile, no/agents/*, no stats. Built by FILTERING the existing full Swagger document (buildFilteredOpenApiDocument,src/swagger.ts) through an allowlist of(method, path)pairs, never a second hand-written document; a snapshot test pins the exact path set (tests/product-openapi-action.test.ts). Every operation in the filtered document declares the project-key bearer scheme ONLY, even where the full document also acceptsAdminAuth/UserAuth. Unauthenticated, like the full/api-docsSwagger UI it's filtered from — it is a SCHEMA, not data; calls a client makes THROUGH it still hit the real, individually-guarded routes. A call carryingX-FA-Door: openapi-actionrecords that door value inauthored_viafor a project key (inc-1's mechanism —'api'for anything else).fa product outcomes|items|use-cases|stories list·fa product story propose --statement ... --ac ... [--ac ...]·fa product review <lineage>·fa product export(src/cli/commands/product.ts) are thin wrappers over the same routes sendingX-FA-Door: fa-client— set in code no flag maps to, mirroring the MCP door'scallFAAsMcpDoor. The client resolves the calling key's own project id the same way (GET /api/project) and reads its project key from the SAME env/config every otherfaverb uses (src/cli/lib/config.ts) — no new credential handling. - Product Definition Plane Canvas —
Shipped— Product Definition Plane doors, Increment 3 (spec 373 inc-3, RM-146) — the Product canvas and the Builder re-pointed at stories, closing out the doors spec.public/ product.html(embedded as the dashboard's Product tab, alongsideworkers.html's admin/approver login + project picker) renders the artifact tree — outcome → roadmap_item → use_case → story → spec_link — entirely with DOM APIs (createElement/textContent, noinnerHTML, per CLAUDE.md's UI rule and the spec-259 escaper pins, which do not move). Create/edit any kind through forms matched to each kind's own closed body schema; every save is a new version, with version, content hash,authored_by/authored_via, and status shown on the card. Propose ready calls the SAMEreview-then-propose-readypath spec 370 ships and renders its verdict as the PM-readable card RM-133 established — one block per finding, BLOCKING findings first, the three plain-language fields (what is missing / why it matters / what to write instead) — plus areadypill once a lineage passes. The Builder-only wrapper prefix/api/builder/product/*(src/routes/builder.ts) is every write the canvas makes: it mountscreateArtifactHandler/versionArtifactHandler, the SAME factory functionssrc/routes/product-artifacts.ts's own create/version routes use (extracted there for this reuse — no reimplemented validation/cap/actor-event logic), with anauthoredViaresolver that is a plain closure always returningBUILDER_PREFIX_DOOR('builder', defined next to the stats disclosure in product-artifacts.ts so the two cannot drift), never reading the request at all — so'builder'is reachable ONLY through this prefix, never viaX-FA-Dooror any other header/body field sent to the raw routes (inc-1'sresolveAuthoredViaand itsDOOR_HEADER_VALUESset, which excludes'builder', are unchanged). Guard isrequireProjectOrAdminAuth— project key or admin, same as every other door — and nothing in the handler checks that the caller is the canvas, so choosing this URL is as self-declarable as sending a door header:'builder'is listed inGET /api/projects/:id/product/stats'sattribution.self_declared_doors(DOOR_ATTRIBUTION_DISCLOSURE), whose note names both mechanisms and says abuildercount is not evidence the canvas wrote the row. The Builder's existingdraft/clarify/refine(src/services/builder.ts) now accept an optionalstory_lineage_id, resolved against the project the same wayPOST /api/featuresresolves it (resolveStoryLineageLink, spec 370) — a cross-project or non-storyreference is a 400 before any model call — and when present,fetchStorySeedreads that story'sstatement+acceptance_criteriaand folds them into the prompt as a fenced DATA section (buildStorySeedSection), so the drafted/clarified/refined spec is asked to implement that story. Drafting without one is byte-for-byte unchanged (every existing Builder test intests/builder.test.tspasses unmodified). "Submit as feature" carries the resolvedstory_lineage_idonto the created feature — spec 370's intake field — via the samecreateFeatureThroughPriorDecisionGatechokepoint every other submission door uses. - Product Definition Plane Traceability And Outcomes —
Shipped— Product Definition Plane, Increment 3 (spec 371, RM-145) — the chain a feature was built to serve, frozen at submission and carried into every evidence surface, plus the generated outcome tracker. Submitting withstory_lineage_idresolvesuse_case_lineage_id/outcome_lineage_idONCE, server-side, from the story's own body links and its structural parents in the 369 store, and freezes all three as additive nullable columns on the feature row — a later edit of the story changes nothing already created. The chain then travels intofeature_spec_records(the three ids plus the story's content_hash at record time), aproduct_chain_resolvedrun event at run start (ids, story hash, the outcome's bounded statement/metric — never a full artifact body), the regenerated.fa/provenance/<id>.md("Product Chain" section), and the draft PR/MR body (Serves: <statement> — story <lineage id>, ≤200 chars) — identically across GitHub, GitLab and Bitbucket. Delivery flows UP only: the first feature linked to a story reachingqueuedmoves itready -> in_delivery; every remaining linked feature (excludingwont_merge/cancelled) reachingmergedmoves it todelivered.advanceStoryToInDelivery/advanceStoryToDeliveredare the only functions in the tree that may write those two statuses, wired at every direct status-write call site (/retry,/rerun,/approve-spec, the auto-retry loop,transitionStatus, and afull_autofeature's initial insert) so none can bypass it. Outcomes and roadmap items are never auto-flipped — their only forward motion is the Definition-of-Ready gate (RM-144) and human-reported observations:POST /api/projects/:id/product/artifacts/:outcomeLineage/observations(admin/approver only, 403 for a bare project key) records{observed_at, value, note?}against an outcome's declared metric with who/when provenance —valueis free text, rendered byte-for-byte, never parsed as a number. FA computes exactly one thing anywhere in this increment: a delivery ratio (delivered stories / all stories) per outcome.npm run docs:outcome-trackerrendersdocs/current/outcome-tracker.mdfor the self-managed project (any project via--project <id>, to stdout) — every outcome traced down through its roadmap items, use cases and stories to the implementing features, with the latest observation and delivery ratio; angle brackets are escaped (the VitePress lesson from #602); the committed file is byte-match tested against a fresh regeneration exactly likedocs/current/build-tracker.md.GET /api/projects/:id/product/outcomes(same admin/approver guard) serves the dashboard's Outcomes tab (a per-project modal on the Projects panel) the identical live data; the feature detail panel shows the chain read-only. Both dashboard additions are DOM-built (createElement/textContent), so the spec-259 escaper-count pins are unmoved. Tenant isolation unchanged from spec 370: a cross-projectstory_lineage_idis still the 404-shaped 400; observations and the tracker are project-scoped in SQL. - Agent Factory —
Partial— Agent Factory, Increment 1 (spec 372, RM-152): declare a digital worker as a governedagent_solutionartifact (a 369 product-artifact kind) — its purpose (outcome/story lineages it traces to), workflow (triggers plusobserve/recommend/actsteps, each non-observe step declaring when it escalates), authority envelope (capabilities declared as a SUBSET of the 362 capability vocabulary, an egress-host allowlist, data scopes, an engine profile, and optional seeded skill/knowledge lineages), autonomy level (shadow/advisory, default shadow), and success measures.actis accepted only undershadowautonomy — neveradvisory— because it is declared but NOT executable in this increment. Gated exactly like every other product artifact: the spec-370definition-reviewrole grades it against its own in-code rubric (purpose readiness, classed steps, escalations declared, minimal authority, measured outcomes), and the two link criteria a model should never decide — every purpose outcome/story isready, and every success measure names a purpose outcome — are decided from FA's own store (unmetReadinessPreconditions) and forcenot_readywhen they fail.POST /api/projects/:id/product/agents/:lineageId/compile(admin-only and NAMED — a project key or an approver gets 403, and so does the shared ADMIN_API_KEY role credential, because compile enrolls a new PROJECT and mints its fa_ key, the same actPOST /api/projectsgates behind an admin) turns areadyversion into an enrolled worker PROJECT in ONE SQLite transaction: config derived ONLY from the declared authority (egress allowlist equalsauthority.egress_hosts, nothing more; engine profile validated against the known engine list), a rendered constitution (hashed onto the deployment row), the source stories seeded as the worker's roadmap, referenced skills/knowledge copied in as unapproved CANDIDATES (the Learning-Plane invariant crosses the compile boundary intact), and an immutableworker_deploymentsidentity row (hashes for the spec, the derived config, and the constitution). A failure anywhere in that transaction leaves no project and no key behind. No capability token is minted — compile writes records only, no execution, no new runtime or engine. Compile is idempotent per (lineage, version): a repeat compile 409s with the existing deployment id rather than a second worker project. A worker's own episodes are ordinary features submitted to the worker project through FA's EXISTING implement flow, and intake on a worker project FAILS CLOSED: every submission (every door — project key, named submitter, Builder, trigger) MUST name astep_idthat exists in the compiled solution's workflow (missing or unknown is 400), a step whose action_class isactis 403, and a solution body FA cannot parse is 503 with nothing created. Arecommendstep's output is arecommendationartifact (a second 369 kind this increment adds: step_id, summary, rationale,proposed_action_class: 'act', evidence refs) — underadvisoryautonomy, creating one notifies the source project's approvers through its existing notification channels; undershadow, nobody is notified.observesteps produce evidence only (checkpoints/run events/artifacts) — never an external mutation, in either autonomy level. Increment 2 (spec 375, RM-153) makes episodes actually FIRE and everyrecommendationSCORABLE. Three trigger kinds all produce the same FA-setepisode_ofrecord (episode_deployment_id/episode_trigger_kind/episode_trigger_refon the feature, never caller-supplied):manual(an approver on the SOURCE project,POST .../deployments/:deploymentId/episodes),schedule(a declared, closed cron-subset cadence —every: 15m|1h|6h|24horat: HH:MM— registered viaPOST .../deployments/:deploymentId/triggersand fired by a bounded host-side scheduler in thereport-scheduler.tsshape, at most one in-flight episode per deployment and a daily cap default 24/hard-96), andwebhook(the existing inbound trigger dispatcher gains a provider-agnostic sink: a signed delivery to a compiled worker project's own endpoint fires the deployment's one active webhook trigger; an unmapped delivery creates nothing). A registered trigger is disarmed withDELETE .../deployments/:deploymentId/triggers/:triggerId(listed by the siblingGET), which stops the next tick and unmaps the inbound sink while keeping the row as the record of what was once armed. Every trigger's payload is quoted as DATA inside the episode's derived spec — JSON-encoded onto one line inside a fenced evidence block, so it cannot break out and be read as an instruction. Firing an episode, arming or disarming a trigger and responding to a recommendation each require a NAMED admin/approver identity; the sharedADMIN_API_KEYis refused, because each act is recorded against a person. The outcome evidence 317 §32.6 describes (attempted → accepted/rejected/superseded → effect_observed → outcome_linked) is now a recorded ledger (worker_outcomes+ append-onlyworker_outcome_events, one writer, a transition that skips a state answers 409) — opened automatically when arecommendstep's episode produces itsrecommendation, advanced only byPOST .../recommendations/:lineageId/respondfrom an approver on the SOURCE project (a worker's own key can never reach this route — same guard as compile).GET .../deployments/:deploymentId/evaluationreturns counts-only agreement-rate arithmetic (accepted / (accepted+rejected), null below a minimum sample of 10) plus episode/escalation/cost counters, over a window — never a recommendation body orhuman_action/notetext. The dashboard's Workers panel surfaces deployment identity, the evaluation counters, and open recommendations with Accept/Reject/Record-what-you-did (DOM APIs only). Still no capability token, noact, no external connector call, no LLM-authored scoring — agreement is arithmetic over recorded human decisions. Increment 3 (spec 376, RM-154) closes the ladder and lets a measured worker EARN more autonomy.autonomygrows to four closed, ORDERED values —shadow/advisory/draft/act_reversible— andagent_solutiongains an optionalpromotion_policy(per-levelmin_samples/min_agreement, plusmax_refused_actfixed at0andmin_effect_observed/max_open_escalationsat level 3), FA-floored at spec write (min_samples >= 10/>= 30for level 3,min_agreement >= 0.8) — a policy below floor is400, not silently accepted.POST .../deployments/:deploymentId/promote {to_level}(admin/approver on the SOURCE project, NAMED — the sharedADMIN_API_KEYis refused, and a worker can never promote itself) is+1 ONLY, gated on the increment-2 evaluation RECOMPUTED LIVE against the target level's policy (409 naming the failing measure otherwise), and on success RETIRES the currentworker_deploymentsrow and creates a NEW one (identity stays immutable —promoted_from+ a hash of the evaluation snapshot that justified it + the approver are recorded on the new row).GET .../promotion-eligibilitypreviews the same decision with zero write.POST .../demote {to_level}needs no evidence, always succeeds to ANY lower level, and REVOKES every live capability token the worker project holds (362 revocation — the next gateway call that token attempts is refused, not merely "will expire"). Anact_reversibledeployment whose evaluation showsrefused_act > 0is AUTO-demoted todraft— recorded, notified, never silent; there is no code path for auto-PROMOTION. Level 2 (draft): adraftstep produces a closeddraft_actionartifact (a THIRD 369 kind:step_id/action_kind/payload_hash/payload/evidence_refs) instead of acting —POST .../drafts/:lineageId/executeruns it under the CLICKING APPROVER's own identity (executed_from_draft: true, never the worker's),POST .../discardretires it unexecuted; FA never runs a draft on its own. Level 3 (act_reversible): at episode start FA mints a per-episode spec-362 capability token for the deployment's WORKER-project SERVICE PRINCIPAL (never a human's, never the source project's) whose scope is a STRICT INTERSECTION —declared authority.capabilities ∩ REVERSIBLE_CAPABILITIES(a closed, five-verb FA constant that can never containadmin:*/features:approve/tokens:mintor anything targeting the source project)∩the operator's ownWORKER_REVERSIBLE_CAPABILITIESallowlist (unset = fail CLOSED for the three act-only verbs; since increment 4 below, the two verbs that are also observe-set members are minted at every level regardless of it) — TTL-bounded (FA_WORKER_EPISODE_TTL_S), brokered into the sandbox as an env var on a reserved MCP-gateway server entry (never argv, never a log line — the SAME broker mechanism spec 348's own run token already uses), and revoked at episode end unconditionally. Anactstep executes ONLY as an MCP-gateway tool call under that token —requireCapability's ownverifyCapabilityToken/decide()primitives decide admission server-side; a call outside scope is refused (never silently dropped) and counted asrefused_act; an in-scope call writes a SEPARATE outcome ledger,attempted -> accepted -> effect_observed, with FA's own record of the effect (a PR url, a feature id). The five reversible actions — open a draft PR on the worker's own repo, submit a feature to itself, propose a draft artifact, file an escalation, write evidence — are the ONE dispatch function (runReversibleAction) BOTH the gateway path and the draft-execute route funnel through, so "the gateway is the only door for an act" holds for the human-triggered path too. No external write connectors, no level > 3, no human-identity token for a worker, no new engine or runtime path. Increment 3b (spec 384, RM-154b) makes the top rung REACHABLE and ships the Workers panel increment 3 deferred. Before this,act_reversible's mandatorymin_effect_observed >= 10floor could only be satisfied by a deployment that was ALREADYact_reversible— the level-2 Execute path ran the SAMErunReversibleActiondispatch a gateway call runs and got a realeffectRef, but recorded only anactor_event, never aworker_act_outcomesrow, so the evidence level 3 requires was generated and discarded. NowPOST .../drafts/:lineageId/executewrites the SAMEattempted -> accepted -> effect_observedchain the gateway writes, taggedsource: 'draft_execution'(gateway calls staysource: 'episode', the column's default, so every pre-384 row and reading is unchanged) — a dispatch that THREW writes none of it.GET .../evaluationreports the split (acts.by_source: { episode, draft_execution }) so an operator can see how much of a promotion's evidence was human-pulled triggers versus autonomous acts;checkPolicySatisfiedstill reads only the total, so both count identically toward the floor — no floor moved. What the total counts is DISTINCTeffect_refs, not rows:write_evidencereturns the episode feature id it was handed, so ten executions of it yield one effect_ref ten times and now count as the ONE effect they are — repeating the cheapest action is not a way to reach a floor that measures distinct observed effects. A draft execution'sepisode_feature_idis the deployment'sgenesis_feature_id— the same value the dispatch itself is passed, so both halves of one execution name one feature; adraft_actionartifact records no originating episode, so FA does not guess one.countActOutcomesForEpisode(the per-episodeFA_WORKER_ACT_MAX_CALLS_PER_EPISODEcap) is narrowed tosource = 'episode', so an approver's Execute clicks never draw down a worker's own act allowance — the one behavioural change this increment makes to a 376 control. The Workers panel (public/workers.html) now renders all four §5/AC7 surfaces DOM-API-only (noinnerHTML): a deployment header drawing the four ladder rungs with the current one highlighted; a live evaluation against the NEXT rung's declared policy, each measure shown pass/fail with the API's own reason andescalations.openas its own number, never a subtraction; Promote (+1 only, disabled with the API's refusal reason when the gate says no, the named-approver requirement stated up front) and Demote (any lower rung, states it revokes live tokens); and the open-drafts list with Execute (round-trips the displayedcontent_hash, so a draft that changed after review surfaces 376's 409 as a readable message) and Discard. No new route, no new guard, no widened one, no change to any policy floor. Increment 4 (spec 387 inc-1, RM-178) gives EVERY level a channel, closing the gap where ashadow/advisory/draftepisode had no way to read FA's own records or record anything at all.provisionWorkerActGatewaynow wires the SAMEfa-actMCP server for EVERY episode whose deployment row is the live one, at EVERY autonomy level; the per-episode 362 token's scope becomesdeclared ∩ LEVEL_TOOL_SET(live level) ∩ allowlist(live level)—WORKER_OBSERVE_CAPABILITIES(worker:read,worker:write_evidence,worker:file_escalation,worker:propose_recommendation) for levels 0-2, plusREVERSIBLE_CAPABILITIESat level 3. Four new READ tools (list_features/get_feature/read_evidence/list_escalations,src/services/agent-factory/episode-reads.ts) read the deployment's SOURCE/worker projects only, redacted and bounded, filtered intotools/listby whether the solution's declaredauthority.data_scopescontains a matchingfa:…prefix.write_evidence/file_escalationjoin this observe set while staying members ofREVERSIBLE_CAPABILITIES;operatorPermitschecks observe-set membership FIRST, so on the MINT path they are not gated by the operator'sWORKER_REVERSIBLE_CAPABILITIESallowlist (their effects are FA-internal ledger rows, refusable by a human, bounded by the per-episode call cap), while that env still gates the three act-only verbs there — and still gates all five on the level-2 draft-execute route. Arecommendstep additionally getspropose_recommendation— the SAMEcreateProductArtifactwriter the tenant route uses, opening THIS episode'sworker_outcomesrow atattemptedand running the SAME rate-bounded advisory notification (now shared viasrc/services/product-artifacts/advisory-notify.ts) — which is how a level-0 worker earns the evidence the level-2 promotion gate reads; no worker could ever earn level 2 before this.toolSetFor/toolNamesForStep(act-gateway.ts) is the ONE function that decides bothtools/listandtools/call, per the episode's own step class AND the LIVE deployment's level (re-checked on every call, not just at mint time) — a tool absent from the list is refused on call with the SAME reason (unknown_tool, or the renamedepisode_step_not_executablefor one of the three remaining act-only verbs called from the wrong step/level). The per-episode call cap (FA_WORKER_ACT_MAX_CALLS_PER_EPISODE) now bounds reads too, and every method the gateway answers —initializeandtools/listeach write aworker_act.meteredactor event so the cap counts them — raised from 20 to 60. Both the compiled constitution and an episode's own derived spec text name the tools its step/level actually grants, sourced from the same function, never restated by hand. Increment 4 part 1 (spec 381 inc-1, RM-157) compiles a deployment into aworker PACKAGE— a deterministic, hashed, credential-free artifact an operator's OWN runtime (Hermes first; the generic shape serves any file-based framework) can execute, not something FA runs.POST .../deployments/:deploymentId/packages {shape, profile?}(admin/approver on the SOURCE project, NAMED, worker-own-principal refused — same guard family as every other agent-factory route) builds an immutableworker_packagesrow (insert-only exceptsuperseded_by) plus a tarball (sorted entries,mtime: 0,uid/gid: 0, no gzip — two builds of the same(deployment, shape, profile)are byte-identical) stored through the EXISTING artifact blob writer (createArtifact,source: 'worker_package', spec 213's size cap and download-route guard).shape: 'generic-json'emits the canonical tree —manifest.json(ids/hashes/declared authority/tool allowlist/seed refs/memory policy/disabled trigger stubs/evaluation refs, NEVER a credential),constitution.md(hash-checked against the deployment's own row),knowledge/*.md+skills/*/SKILL.md(resolved through the SAME project-scoped lookups compile.ts uses; an unresolved seed is omitted and listed, never fatal), andevaluation/*.json.shape: 'hermes'is a RENDERING of that same tree (renderHermes) — it addsSYSTEM.md,mcp.json(one server, pointing at FA's gateway, with a literal${FA_WORKER_EPISODE_TOKEN}placeholder — never a value),toolset.json,memory-policy.json, and disabledschedule/*.jsonstubs; the renderer module imports nothing frommiddleware/capability-tokens/runtime/modelsand branches on nothing but the manifest (§VII).authority.capabilitiesisdeclared ∩the SAME REVERSIBLE_CAPABILITIES/operator-allowlist intersectionmintEpisodeAuthorityTokenapplies for a live episode's token scope — one exported function, two callers, so the two can never drift. Before a tarball is ever persisted, EVERY emitted file is scanned forfa_-shaped tokens, the episode-token prefix,Bearer-shaped values, and every configured secret of BOTH the source and worker projects — a match REFUSES the export (recorded, never the matched text), and an empty scan pool that cannot be independently confirmed to reflect "nothing configured anywhere" also refuses (no silent no-op). Re-exporting identical inputs is idempotent (200, the existing row); exporting from a RETIRED deployment is 409; promoting/demoting a deployment (376) leaves its existing packages untouched and points them at the first package later exported on the successor row (superseded_by, set at export time). This increment builds the PACKAGE only — no enrollment token, no external episode intake, no evidence-fidelity tiers, and no Workers-panel UI (those are increments 2 and 3 of the same parent spec). Increment 5 (spec 388, RM-161 + RM-169b) closes the gap where an ALLOWLISTED (real, external) MCP server tool call from an episode was never governed by the ladder at all — ashadowworker whose worker project allowlists a real server (a CRM, an email sender, a ticketing API) could call every tool on it from its first episode, while the SAME worker'sactstep was carefully gated.classifyExternalToolCall(src/services/agent-factory/external-tool-policy.ts, new, pure) classifies every allowlisted-server call from an episode — EVERY JSON-RPC method, not onlytools/call:observeiff the agent_solution's newauthority.tools[]declares{server, tool, class: 'observe'}for EXACTLY that pair (exact string match, no globs) or the method is an MCP handshake/enumeration method that names no tool; otherwiseact. Anobservecall is admitted at every level (unless the operator has PAUSED the deployment, which silences every method); anactcall is admitted only when the live deployment isact_reversible, the episode's own step is itself anactstep, the newworker:external_toolcapability (a sixthREVERSIBLE_CAPABILITIESverb,WORKER_REVERSIBLE_CAPABILITIES-gated, fail-closed, NOT self-configurable) is in the deployment's live scope — the SAME intersectionmintEpisodeAuthorityTokencomputes, so a deployment that never declared it can never make an external act at any level — AND the episode's own spec-362 authority is still live (an operator revoking it, or the episode TTL lapsing, closes the external channel at the same moment it closes the internal one). An admittedactcall is metered EXACTLY like an internal one: aworker_act_outcomesrow (capability: 'worker:external_tool',source: 'episode', and atoolidentifier built only from operator- or spec-controlled strings — never the agent's own) opens BEFORE the upstream call and reacheseffect_observedon a non-error result; an admittedobservecall leaves oneworker_act.meteredrow. Every admitted call and every refusal counts against the per-episodeFA_WORKER_ACT_MAX_CALLS_PER_EPISODEallowance the internalfa-actpath already enforces — ONE budget per episode across both paths. A refusal is recorded through the SAME lean, boundedmcp_gateway_calldeny path a non-allowlisted server gets, and anact-classified refusal additionally counts towardworker_act.refused— the SAME counter the promotion evaluation's auto-demotion reads. A NON-episode feature's allowlisted call is byte-for-byte unchanged: no classification, no metering. RM-169b: a worker'sfile_escalationcall now resolves its own lineage from the episode's live run —originating_run_id/engine_id/model/transport— the SAME fields the humanPOST /:id/escalationsroute resolves server-side, and acceptsblockingas the same non-binding advisory it is for any author, recordingblocking_claimed_by: 'worker'when the worker itself claimed it (NULL for every human-raised escalation, and NULL — with no lineage resolved — when an approver executes a stored draft, which is a human-initiated write, not an episode's own call). Increment 4 part 2 (spec 387 inc-2, RM-178) makes episode completion EVIDENCE-based, not diff-based: anobserve/recommend/draftstep whose whole output is ledger rows written through the channel (increment 4 part 1 above) used to fail on the ordinary empty-commit guard withcause: unknown.decideEpisodeCompletion(src/services/agent-factory/episode-completion.ts, pure) decides completion by the episode's own step class against four per-episode ledger counts (evidence_notes,escalations,recommendations,acts_effect_observed);implementFeaturenow branches onfeature.episode_deployment_idat the empty-commit guard, so a clean-tree episode judgedcompletedendsimplementedwithpr_url: nulland a fail-closedepisode_completedrun event (no commit, no push, no PR), and one judgedno_evidenceendsfailedwith the new closed causeno_evidence(RUN_FAILURE_CAUSES, non-escalatable by construction) instead of the "produced no changes … absolute paths" text that is wrong advice for an episode. Nothing else is skipped: the spec-approval checkpoint, preflight/test/verify gates, execution-dirt restore, the run-config snapshot and the failure ledger all run exactly as for any feature — anactepisode that committed is entirely unaffected and keeps taking today's commit/push/PR path (its PR is its record; no completion event is written for it).getEpisodeEvaluationRollup'scompletedcount is widened tostatus = 'merged'OR (status = 'implemented'ANDpr_url IS NULL), so an evidence-completed episode is counted rather than sittingimplementedforever with no PR to merge; the Workers panel (public/workers.html) renders the deployment's summedevidence_notesbeside its episode counts. Spec 389 (RM-162) gives a worker its own WORKING MEMORY — deliberately never a skill: an optionalmemoryfield (retention_days1-365, up to 20 named privacy classesinternal/personal/sensitive,subject_opt_out), closed and content-hashed like every other part of the spec, refused 422 if declared withoutworker:stateinauthority.capabilities. Compiling copies it into the compiled-config hash and the constitution's own## Memorysection. Three tools —state_get/state_put/state_forget— join the SAME observe channel spec 387 inc-1 built, at EVERY autonomy level and step class, gated onmemorybeing declared at all (never on capabilities alone).state_putis the ONE writer (src/models/worker-state.ts) and checks a durable opt-out table on every call, so a subjectstate_forgetmarked opted-out can never be written again; asensitive-class value is instance-encrypted (the same at-rest key FA already uses for its own secrets),internal/personalare redacted plaintext. Bounded: a per-worker entry cap (FA_WORKER_STATE_MAX_ENTRIES, default 5000, counting distinct class+subject pairs), a 4 KB value bound, a 128-char subject-key bound, and a scheduler- tick retention sweep that deletes rows past the declaredretention_daysand records a COUNT, never the keys. FA never reads a value into governance:GET/DELETE .../deployments/:deploymentId/state(admin/approver on the SOURCE project) return counts and policy only and can wipe a worker's memory outright; an import-graph test asserts nothing in evaluation/promotion/the reviewers/any prompt builder imports the model. Every gateway call's ledger row carries the class name, a HASH of the subject key, and the value's LENGTH — never the value or the raw key, sanitized before it ever reachesmcp-gateway-invoke.ts's audit writer. Outstanding, stated honestly: a recompile onto an EXISTING worker project (carrying memory forward across a spec version bump, dropping classes no longer declared) has no production caller yet —compileAgentSolutionalways mints a brand-new worker project per (lineage, version) today — so the carry-forward function is a correct, directly-tested unit waiting for that (separate, larger) reuse path to be built. Spec 390 (RM-163) gives a worker's declarative "never do X" a runtime DENY instead of prompt prose: an optionalauthority.prohibited[](≤ 50 entries), three closed forms —capability(never this verb at all; refused as a 422 CONTRADICTION at spec write if the same capability is also declared),tool(never this fa-act tool id or<server>/<tool>pair — no globs),argument(never this tool with this argument matchingequals/prefix/a length- and construct-boundedregexadmitted only when a refuse-unless-proven-safe scan can account for every construct in it — matched against the argument in whatever shape it arrives, list or nested object included, and denying whatever it cannot fully inspect).evaluateProhibitions(src/services/agent-factory/prohibitions.ts, new, pure) is the ONE evaluation function every gateway path calls:toolNamesForStep(act-gateway.ts) reuses it with no arguments to drop acapability/tool-prohibited name fromtools/listAND from what atools/callcan reach (I2); the call gate re-checks it with the REAL arguments right after the existing level/step/capability checks and before the handler runs, which is the only point anargument-form prohibition (never delisted — the tool is allowed, some arguments are not) can ever match.handleMcpGatewayInvoke's allowlisted-server path runs the same check at classification time, naming the external tool<server>/<tool>— a form-2 prohibition refuses it even when the same tool is declaredobserve, and acapability-form prohibition onworker:external_toolcovers every external call whatever class it was given. The draft-execute route re-checks the same list before running an approver-clickeddraft_action, so the other door onto the same dispatch is not a way around it. A denied call is refusedprohibited_act_refusedwith the ledger'sargsfield replaced by{prohibition_index, form, arg?}: the argument NAME the offending rule matched, never its value. A refusal the prohibition itself caused is recorded asworker_act.metered— a stopped prohibited attempt is a control working, not a demotion signal — while a call that was unauthorized anyway keeps theworker_act.refusedaccounting it had before this spec, so a prohibition never buys immunity from auto-demotion.computeDeploymentEvaluationreports the count asprohibited_refusals(Workers panel, one moreevalCell) — visible to an operator, never scored against a promotion floor.compile.tscopies the list into the compiled-config hash and renders a## Prohibitedsection in the worker's constitution; the exported package's manifest carriesauthority.prohibitedverbatim and thehermesshape expands it intotoolset.json'sdeny(forms 1-2, capability entries expanded to the concrete tool ids that capability would otherwise grant) andargument_denies(form 3), withSYSTEM.mdrendering each entry's reason. Spec 407 (RM-134/RM-178), origin Pilot 0 episode472274f2: anobservestep had run as an ordinaryimplementFeaturejob — the CLI's own tool grant and prompt were never keyed to the step'saction_classat all, only the gateway's owntools/listwas, so nothing stopped an observe worker from runninggit pushdirectly and bypassing the gateway entirely (it did — seedocs/OPERATIONS.md§19a6 for the incident).episodeAllowedTools(src/services/agent/episode-posture.ts, pure) now resolves a FIXED, non-widenable CLI tool grant from the step'saction_class, before the agent's first turn, REPLACINGfeature.allowed_tools(never merged with it, same "replace never merge" rule spec 368 established):observe/recommendgetRead/Glob/Grep/LSplus the step's ownfa-actgateway tools asmcp__fa-act__<tool>rules (derived fromtoolNamesForStep, the same function that decides the gateway's own list — without them the allowlist the runner enforces would refuse the gateway itself, since a non-empty grant withholds--dangerously-skip-permissions), and noBash, noWeb*, noTask, no write of any kind;draftaddsWrite/Edit/MultiEditconfined to the step's declaredoutput_paths(an inexpressible entry — sameisRuleExpressiblePathcheck spec 368'sdocPathuses — fails the run before the agent starts, never falls back to a wider grant);actgets the same read/search set and gateway rules (its output channel is the gateway's own reversible verbs, never the CLI).buildEpisodePrompt(agent/prompts.ts) replacesbuildImplementationPromptfor the same call: the compiled constitution, the FA-resolved step, the episode's own input quoted as DATA, an explicit gateway-is-the-only-output-channel rule, and the evidence contract for the step's class — no spec file is ever written into the workspace for an episode (the Pilot-0 log line pointed the agent at exactly that file). Immediately after the agent's turn returns — before the base refresh, before every gate, and before FA's own verification/provenance writes —ExecutionDirtTracker.restoreSinceruns unconditionally and fail-closed forobserve/recommend/draft(protecting only adraftstep's declaredoutput_paths), so adraftstep's gates grade the restored tree and the commit carries the declared output plus FA's provenance artifact, and spec 387 inc-2's evidence-based completion decision runs on the now-clean tree —implementedwith a pushed branch is unreachable forobserve/recommend. The test/ verify/declared gates are skipped entirely forobserve/recommend(nothing to test — Pilot 0's own postmortem showed an exit-127test_command"passing" proves nothing either way);draft/actrun them exactly as before. Recorded on the ledger asepisode_posture(before the first turn) andepisode_tree_restored(after).FA_EPISODE_POSTURE_MODE(defaultenforced) can be set tolegacyas an explicit, ledger-recorded per-instance opt-out reproducing the pre-407 behaviour — never a silent fallback. Increment 4 part 2b (spec 381 inc-2, RM-157) lets a PACKAGE actually RUN, outside FA, through the SAME gateway and ledger every internal episode uses. An admin/approver mints a one-timePOST .../packages/:packageId/enrolltoken (worker:enroll, a new NON-reversible, non-self-service, non-level-set 362 capability —fa_wen_-prefixed, returned once, never persisted); redeeming it —POST /api/worker/episodes {step_id, trigger_ref?}, no project id anywhere in the path or body, since the token itself names the deployment — single-use-enforces by revoking the enrollment jti FIRST via the same atomicUPDATE … WHERE revoked_at IS NULLevery 362 revocation uses (a losing concurrent redeem sees zero rows changed and is refusedenrollment_consumed, recorded), re-resolves the LIVE deployment the package was cut from (retired →deployment_superseded, recorded), and creates the episode feature through the SAMEfireEpisodeevery trigger kind uses — but forced straight toin_progresswith a newepisode_runtime = 'external'column, never through the project's ordinary pending/analyzing/queued pipeline, because nothing in FA is going to dispatch it.POST /api/internal/mcp-gateway/invoke(spec 348) now accepts a verified per-episode capability token ALONE as its transport when — and only when — it resolves to the in-flight episode it was MINTED FOR: the token's own signedjtimust be theepisode_authority_jtithe redeem stamped on exactly onein_progress,episode_runtime = 'external'feature row, and its project/subject facts must match that row (resolveExternalEpisodeAuthority— shared with the completion route, so an unredeemed enrollment token or any other token of the same worker project resolves to nothing on either surface); every downstream check (I2/I3/I4, the per-episode cap,auditCallOrRefuse) is the same code an internal episode's run-token-authenticated call reaches. THE ENGINE NEVER TOUCHES an external episode: every relaunch-selection query inmodels/features.ts(getQueuedFeatures/getRevisingFeatures/getRetryableFeatures/getAutonomousSecurityFixCandidates/getFeaturesWithOpenSecurityFixerRound) and the stuck-in_progresscrash-recovery sweep inagent-engine.tsnow addepisode_runtime IS NULL;tests/prior-decision-relaunch-guard.test.tspins the rule.POST /api/worker/episodes/:id/complete {summary, claimed?}writes each item as aepisode_claimed_evidencerun event (fidelity: 'claimed', bounded, redacted) — a channel entirely separate from the OBSERVED evidence the gateway itself writes (worker_act_evidence_writtennow carriesevidence_fidelity: 'observed', channel: 'gateway') — then closes the episode with the SAMEdecideEpisodeCompletion(spec 387 inc-2) an internal episode's empty-commit guard uses, over the SAME four ledger counts (claimed items are never among them):implemented(pr_url: null) on completed evidence,failed/no_evidenceotherwise; the per-episode token is revoked either way. An episode whose token TTL elapses with no completion call is closed the same way by the 375 scheduler's own tick, markedepisode_expired.computeDeploymentEvaluationreportsclaimed_itemsas its own counter, read by nothing that feeds a promotion decision — a claimed item was never written toworker_act_outcomes/worker_outcomesin the first place, so promotion eligibility is unchanged by any number of them. Cancelling an external episode revokes its token and landscancelledwith no workspace to wipe (it never had one). Spec 414 (RM-210) is built: the Workers page now says what the worker IS. Inc-1 (the API): the deployments list and a newGET .../agents/:lineageId/deployments/:deploymentIdboth resolvesolution(title,purpose.outcomes/purpose.storieswith resolved titles,steps[]withescalate_when,human_steps) from the compiledagent_solutionartifact at the deployment's own pinned lineage+version (resolveWorkerSolutionView,src/services/agent-factory/solution-view.ts) —null, never a 500, when that lineage/version can no longer be read — plustriggers[]withlast_fired_at(both kinds) and, forscheduleonly,next_due_atcomputed from cadence andlast_fired_at(computeScheduleNextDueAt, episode-scheduler.ts), and agenesis_feature_id. A newGET .../deployments/:deploymentId/episodesroute is spec 409's paged list contract ({items,total,limit,offset}, default limit 20) over one deployment's episode features, newest first, each row'soutcome({kind:'completed'|'failed', cause?, summary?}) resolved from the lastepisode_completed/run_failurerun event (resolveEpisodeOutcome, episodes.ts, vialistRunEventsTail). Inc-2 (the page):public/workers.html(DOM APIs only) now renders all of it — the card title issolution.titlewith a one-line purpose built from the resolved outcome/story titles, the old identity string moved to a muted sub-line, and the autonomy badge carries a one-sentence plain-language caption; a steps table (id, action class, description, escalate_when) with the trigger(s) bound to each step (kind, cadence, active, last fired, next due) and Arm/Disarm controls; a paged episodes table (started, step, trigger, status, outcome/cause, cost, links to the episode feature and its live log) whose "started/completed/failed" tiles — moved out of the general evaluation grid — are now filters over it (status=per click, one request); and a provenance line (genesis feature link, compiled spec hash, compiled by/at, promoted-from chain walked client-side over the existing detail route, no new route needed). Spec 419 (RM-220): a worker's escalations (spec 366's Governed Work Escalation) become a first-class surface on its deployment card instead of a bareraised/resolvedcount. NewGET/PATCH .../deployments/:deploymentId/escalations[/:escalationId](src/routes/agent-factory-episodes.ts), guarded by the samerequireAdminOrApproverForProjecton the SOURCE project as every other route in that file.listWorkEscalationsForDeployment/getWorkEscalationForDeployment(src/models/work-escalations.ts) joinwork_escalationstofeaturesonfeatures.episode_deployment_id, never onwork_escalations.project_idalone — a worker project can host more than one deployment across its promotion/demotion history, so an id belonging to a DIFFERENT deployment 404s without touching the row.PATCHreusessetWorkEscalationTriageStatusunchanged (the same four statuses, the sameresolveRequestActorattribution asPATCH /api/escalations/:id) and additionally records an actor event (work_escalation.triaged, added toACTOR_EVENT_ACTIONS' closed enum) with{escalation_id, deployment_id, from_status, to_status}— the older route is deliberately not backfilled.public/workers.htmlrenders an Escalations section on every deployment card (DOM APIs only): class/status/blocking chips, the fullproblemtext, evidence refs, an open-by-default filter with a "Show triaged" toggle, and Acknowledge/Duplicate/Reject/Invalid buttons on an open row only — a triaged row is read-only. A successful triage refreshes the card's evaluation block, and the evaluation grid's ownEscalationscell becomes a link that scrolls to the section. Increment 4 part 3 (spec 381 inc-3, RM-157) puts inc-1/inc-2's package/enrollment routes behind the Workers panel (public/workers.html, DOM APIs only — no new routes). Each deployment card gains a Packages section, hidden (never disabled) for a retired deployment: an Export package control (shape picker, optionalhermespreamble); a Packages list (shape, shortpackage_hash, created by/at,superseded_by, a Download link built from the route template plusencodeURIComponent(id)); and, per package, Mint enrollment token (a TTL input, with the deployment's autonomy level and the exact tool set a redeemed token will carry shown BEFORE the mint call — read back fromGET .../packages's own response, whichserializePackagenow widens withlevel/tool_allowlistparsed straight out of that package's manifest, never a second guess). The minted token renders once into a<code>node with a Copy control and a shown-once/expires/redeems-into rule, kept only in that render's DOM/closure — neverlocalStorage,sessionStorage, or the URL — and gone the instant the panel rebuilds (a project switch or reload). The evaluation grid gains a Claimed items cell beside the observed counts, rendered only when the evaluation response carries the field. Spec 420 (RM-203): the gateway'sfile_escalationverb is idempotent per candidate target, so an episode that re-derives the same judgement on every fire (it has no memory across runs and no list tool) stops filing the same open problem twice. The call gains two OPTIONAL fields,candidate_target_type/candidate_target_id(bounded non-empty strings, both or neither — a partial pair refuses naming the field, never silently coerced to "no target"). Both absent is byte-identical to before. Both present and an escalation for that exact(tenant, project, target type, target id, class)is alreadysubmitted(findOpenWorkEscalationByTarget,src/models/work-escalations.ts— every scope dimension is in the SQL): FA writes nothing to the existing row and returns its id aseffect_ref, recordingfile_escalation_deduplicatedon the calling episode's own run-event ledger. A miss files a new escalation exactly as before, now carrying the target.classis part of the key so a repeat under a different classification still reaches the triage queue, and evaluation-fired episodes never dedupe at all (each case run is graded on what its own episode filed). A dedup hit wrote nothing, so it is not recorded as an observed effect: theworker_act_outcomesrow stays atacceptedwith a NULLeffect_refrather than counting towardmin_effect_observed. The lookup runs before the per-project open-escalation cap, so a worker at the cap still gets an already-open target's escalation back. Both target values are redacted the same wayproblemalready is before either the lookup or the write. Spec 426 (RM-222): compile refuses a workflow step that can never reach the tool its ownaction_classexists to call — the gap Pilot 0 v5 hit (arecommendstep declared withworker:propose_artifactand notworker:propose_recommendation, which compiled cleanly and ran seven episodes that could never reach the advisory promotion bar). Checked in the SAME pre-transaction validation block as the existing autonomy/engine checks, beforecreateProjectmints anything: arecommendstep that cannot reachworker:propose_recommendation, or anactstep that cannot reach any ofworker:draft_pr/worker:submit_feature/worker:propose_artifact, is refused 422 naming the step id, the missing capability/capabilities, and whether the capability was never declared or was declared but removed by a spec-390 prohibition — both derived from ONE function,toolNamesForStep(the same onetools/list/tools/callare built from), called with and without the declaredprohibited[]to tell the two causes apart, never a second capability map. Anactstep is checked at the ladder's CEILING (act_reversible), never at the autonomy the solution actually compiles at, since a compile always enrolls atshadow/advisorywhere act tools are absent by LEVEL — the check is about whether the capability was ever declared, not about the level.observeanddraftsteps are unaffected, and an already-compiled deployment is never revisited.steps[]on both deployment read routes (GET .../deployments,GET .../deployments/:deploymentId) gainstools: string[]— the tool namestoolNamesForStepwould actually offer that step at the deployment's LIVE autonomy, deliberately NOT folding in the operator'spaused_at(the page's existing paused indicator already covers that) — plustools_at_ceiling: string[], the same computation at the ladder's CEILING, so a reader can tell the two reasons a tool is missing apart: a name at the ceiling but not at this level is withheld by LEVEL (promotion offers it), a name at neither is withheld by DECLARATION (never declared, or removed by a prohibition) and promotion changes nothing.public/workers.html's steps table gains a "Tools offered" column (DOM APIs only) that renders whichever of those two the deployment actually is — never assuming "by level" from the step's class alone, which would misread a worker compiled before this check as merely awaiting a promotion. Spec 435 (RM-250): a DECIDED finding class cannot be re-proposed — FA refuses the duplicate at thepropose_recommendationfiling boundary, so a recommend worker that never consults its own prior decisions (spec 429'slist_recommendations) still cannot re-propose a class an operator already decided.propose_recommendationgains one optional field,finding_class(bounded non-empty string, worker-supplied opaque grouping key, redacted once and used for both the lookup and the storage); absent is byte-identical to before this spec, with no lookup at all. Present, FA checks — every scope dimension in the SQL, never a post-filter — whether THIS worker project/deployment already has a recommendation of the SAME class decidedaccepted/superseded/effect_observed/outcome_linked(refused for good) orrejectedwithin an operator cooldown window (FA_WORKER_REJECTED_CLASS_COOLDOWN_HOURS, default 168h,0disables);attemptednever refuses, and evaluation-origin episodes are isolated from production ones in both directions. A hit writes nothing (no artifact, no outcome row, no mutation of the deciding row), is refused with reasonclass_already_decidedand gateway code -32036 naming the class/state/lineage id/decided_at (never a deciding principal), and records onepropose_recommendation_class_refusedrun event without the proposal's own title/summary/rationale text. Accounting is structural, not a claim: the call still costs exactly one invocation of the episode's gateway meter and the outcome row staysaccepted, but it is never aworker_act.refusedrow, so it cannot feed auto-demotion — a worker re-proposing a decided class needs itsagent_solutionfixed, not a lower autonomy level. The refusal count is visible on the deployment's evaluation rollup (class_already_decided_refusals, additive/counts-only/decision-free) and rendered on the Workers panel ("Re-proposals refused (class decided)"). Spec 439 (RM-255): a declared external tool (authority.tools[], spec 388) now carries a required, closed, boundedinput_schema—{type: 'object', properties?, required?}, one level deep, mirroringToolInputSchema(act-gateway.ts) in field names only.validateExternalTools(schema.ts) refuses any entry missing it, naming the exact field path;{"type":"object"}with no properties is the valid, explicit "takes no arguments".compileAgentSolutionrefuses (400) a stored artifact whose declared tool lacks it — the defense-in-depth half, since schema.ts never re-validates a row already inproduct_artifacts.WorkerPackageManifest.tools.external_toolscarries every declared tool verbatim, sorted by server then tool; the hermes shape'stoolset.jsoncarries the same array andSYSTEM.mdgains a "## Declared external tools" section listing each tool's required arguments, read straight off the manifest. This is a correctness and legibility control, not a security boundary: FA does not validate an upstream server's arguments against it, and spec 388's classification/admission ladder is unchanged. Outstanding: Increment 3 (spec 376, RM-154) is built: the four-rungautonomyladder is closed, evidence-gated+1-only promotion and free/instant demotion (with 362 revocation) are live,draft_actionis a human-executed artifact kind, and a level-3 (act_reversible) episode'sactsteps execute through the spec-348 MCP gateway under a per-episode, intersection-scoped, TTL-bound spec-362 token. Increment 3b (spec 384, RM-154b) closed the two gaps 376 shipped unreachable/unbuilt:act_reversiblecan now be reached through the ordinary named-approver route (a human-executed draft's evidence counts, tagged and split bysource, without touching themin_effect_observedfloor or the per-episode act cap), and the Workers panel (public/workers.html) now renders the ladder, live next-rung eligibility, Promote/Demote, and Execute/Discard. Increment 4 (spec 387 inc-1, RM-178) is built: EVERY level now gets the same gateway, with a level/step/data_scopes-filtered tool set — ashadow/advisoryworker can read FA's own records and record evidence/ escalations/recommendations for the first time. Outstanding: episode completion is still diff-based, not evidence-based (spec 387 inc-2, not yet built — anobserveepisode that correctly changes nothing still fails the empty-commit guard); levels beyondact_reversible(broader/impactful/regulated execution) and external write connectors (RM-149) remain reserved/future work — see spec 376 §2.6/§4 and spec 387 §3 scope boundaries. ladder, live next-rung eligibility, Promote/Demote, and Execute/Discard. Outstanding: levels beyondact_reversible(broader/impactful/regulated execution) and external write connectors (RM-149) remain reserved/future work — see spec 376 §2.6/§4 scope boundaries. Increment 4 part 1 (spec 381 inc-1, RM-157) is built: a deployment can be exported as a deterministic, credential-freegeneric-jsonorhermespackage. Outstanding (spec 381 inc-2/inc-3): the package enrollment token, redeeming it into a run-time capability token, external-episode intake and evidence-fidelity tiers, and the Workers panel's Export/Enroll UI. Spec 390 (RM-163) is built:authority.prohibited[],evaluateProhibitions, the fa-act gateway's list+call filtering and argument-level call gate, the external-tool classification-time check,prohibited_refusalsin the evaluation, and the package'sdeny/argument_denies. Outstanding: nothing declared out of scope by spec 390 itself; the agent-solution-builder's own interviewprohibitions[](free text) is not yet mapped onto this closed field — seesrc/services/agent-builder/derive.ts's ownnot_yet_expressiblenote, which is separate work. Spec 407 (RM-134/RM-178) is built: an episode's CLI tool grant and prompt are now keyed to its step'saction_class(fixed, non-widenable, replacingfeature.allowed_toolsrather than merging with it) instead of running as an ordinary open-ended implementation, and the tree is restored + the evidence-based completion decision always runs before anobserve/recommendepisode can ever reach a commit/push/PR path.episode_posture_mode: legacyis the explicit, ledger-recorded opt-out. deterministic, credential-freegeneric-jsonorhermespackage. Increment 4 part 2 (spec 381 inc-2, RM-157) is built: a package enrollment token redeems into a per-episode capability token, an external episode runs through the SAME gateway/ledger an internal one uses, and its evidence is recorded by fidelity (observedfrom the gateway,claimedonly from the completion report — never counted toward promotion). Outstanding (spec 381 inc-3): the Workers panel's Export/Enroll UI. Worker evaluation, increment 1 (spec 391 inc-1, RM-168) is built:evaluation.cases[](closed, ≤ 100, six expectation kinds —evidence/recommendation/escalation/refusal/no_state_cross/no_tool_call— FA ships no cases or fixtures of its own, §VII) and a runner (src/services/agent-factory/evaluation-cases/{runner,decide}.ts) that fires each declared case as an ORDINARY episode (trigger kindevaluation,base_branch = given.fixture_ref,given.inputsquoted DATA in a new## Inputsspec-text section,given.stateseeded into a run-scopedeval:<runId>:memory namespace wiped when the run ends) through the SAMEfireEpisodeseam and daily allowance every other trigger kind uses.POST .../evaluateanswers 202 with aworker_evaluation_runsrow that captures every version binding (spec/constitution hash, model, engine profile, skills, knowledge) BEFORE the first case fires; a tick driver (mirrorsepisode-scheduler.ts) advances one run one step per tick, deciding each terminal case with a PUREdecideCase(expectation, ledgerFacts)— no model grades a case. I1: a case episode's tool set is pinned to the observe set pluspropose_recommendationregardless of the deployment's live autonomy, via an FA-set marker on the feature's ownepisode_trigger_kindcolumn — anact_reversibleworker's case episode still lists noact-only tool and adraft_prcall is refused.GET .../evaluations/GET .../evaluations/:runIdreturn aggregates and per-case reasons only — never a transcript, never a state value. Worker evaluation, increment 2 (spec 391 inc-2, RM-168) is built: an opt-inpromotion_policy.require_evaluation_passboolean (defaultfalse, part of the content hash) thatcheckEvaluationPassRequirement(src/services/agent-factory/promotion.ts) enforces as a SECOND, additional gate applied to a promotion to any rung — the LATEST completeworker_evaluation_runsrow for the worker project must have been captured at bindings (spec_hash,constitution_hash,model,engine_profile, every bound skill/knowledge lineage's version+hash) that still equal the deployment's CURRENT ones (re-resolved live viasrc/services/agent-factory/evaluation-cases/bindings.ts, the same function the runner itself uses to capture a new run's bindings), with zero failing and zero inconclusive cases; refused 409 naming the missing verdict or the first mismatched binding field (I2).GET .../promotion-eligibilitysurfaces the identical check's answer asevaluation_requirement, so the preview can never disagree with the real promote call.adversarialis an ordinaryclassvalue decided by the samedecideCasetable as every other case (I3); the runner marks its verdictred_team: truepurely so a reader can single it out. FA ships no cases of its own (§VII) —docs/AGENT_FACTORY_QUICKSTART.md§20a documents three canonical TEMPLATES to adapt (an injection planted in an input, a prohibited-tool invitation, a cross-subject state probe). The Workers panel (public/workers.html) gained an Evaluate button, the latest verdict beside the promotion-eligibility card with a per-field bindings-match ✓/✗, and a per-case pass/fail list — DOM APIs only, noinnerHTML. Increment 5 part 1 (spec 418 inc-1, RM-214) is built:examples/agent-factory/hermes/— adocker-compose.yml(this repo's FA image + the official pinned Hermes Agent image) anddeploy.mjs, a dependency-free Node script that exports a package, mints an enrollment token, writes it to a runtime env file (mode 0600, never argv), starts the runtime, and watches FA's own ledger for enrollment-redeemed → gateway-observed-evidence → completion-report to land, exiting non-zero naming the first step that never happened.stub-runtime.mjsis the executable definition of "a runtime that can host this package" (read → redeem → call the gateway → report) — what CI exercises in place of a live Hermes. No new src/ route: every call is an existing spec 381 inc-1/inc-2 route. Increment 5 part 2 (spec 418 inc-2, RM-214) — a real episode ran on the official, unmodifiednousresearch/hermes-agent:v2026.8.31image (2026-09-09, episode35e09221, 50gateway_observedevidence notes,examples/agent-factory/hermes/RUN-2026-09-09.md) and exposed eight renderer/deployment gaps, all fixed here — "the renderer conforms to Hermes, never the other way round".shapes/hermes.tsnow rendersconfig.yaml(a FRAGMENT: onemcp_serversentry running the committed stdio bridgeexamples/agent-factory/hermes/fa-mcp-shim.pyagainst${FA_GATEWAY_URL}with${FA_WORKER_EPISODE_TOKEN}, and atoolsetsrestriction to that one MCP server) in place of anmcp.jsonHermes never read;manifest.jsongainedworkflow.steps(id/action_class/description/escalate_when, verbatim from the compiled solution) so a runtime can be told which step to run without an operator-supplied side file;SYSTEM.mdnames thefa-gatewayMCP server as the tool surface instead of a URL the runtime could never reach;manifest.tools.mcp_gateway_urlnow names the worker gateway's own invoke route (/api/internal/mcp-gateway/invoke) instead of the unrelated remote-MCP front door (/api/mcp); anddocker-compose.yml'shermesservice runs as the mounting uid (HERMES_UID/HERMES_GID) withHOME=/opt/dataand mounts the operator's own credential file read-only, because the official image does not run as the mounting host user by default. The no-credential scan (scan.ts) now also accepts the one shape this needs — the token's own name as an env-var KEY introducing exactly its wrapped placeholder as the value — without loosening what counts as a leak.docs/WALKTHROUGH.mdgained a "Deploy to Hermes" section (the credential file,deploy.mjs,run-hermes.sh, the Workers tab, and the honest boundary: what a runtime does outside the gateway is invisible to the ledger). Repo-less workers, increment 1 (spec 421 inc-1, RM-166) is built: an optionalworkspace: 'repo' | 'none'onagent_solution(default'repo', byte-identical), refused at validation time when combined with anactstep orworker:draft_pr/worker:submit_feature(the two capabilities whose whole effect is a repository). Compiling a'none'solution needs noworker_repo_url; the worker project is created withrepo_url: ''and the newworkspace_mode: 'none'column (resolveWorkspaceMode, fail-safe'repo'for null/unknown, admin-only to set thereafter). A recompile that would change a lineage's settled workspace is409. Every run path — feature intake, manual/schedule/webhook episode fire, the scheduler tick — refuses'none'fail closed with a typed, ledgered reason naming inc-2; the Workers panel shows "Repository: none — evidence-only worker". Repo-less workers, increment 2 (spec 421 inc-2, RM-166) is built: the run path.implementFeatureresolvesrepoLessonce (resolveWorkspaceMode) and provisions an empty, disposable workspace (provisionRepolessWorkspace, no clone/.git/remote/git lfs pull;data_dirs/setup_commandstill apply) instead of cloning. No base identity, no base refresh, no VCS credential is ever resolved. Test/verify/declared gates are skipped for every step class (onlyobserve/recommendcan ever exist on a repo-less worker). After the agent's turn,restoreRepolessWorkspace(execution-dirt.ts) walks the workspace and deletes everything outside the step's declaredoutput_paths— the repo-less equivalent of spec 407's tree restore, stronger because the declared baseline is EMPTY — and recordsepisode_tree_restoredwithmode: 'workspace_none_empty_baseline'; a walk/delete failure fails the run. Completion is decided solely bydecideEpisodeCompletion, unchanged; the commit/push/PR path is structurally unreachable (only observe/recommend can run here, and both always resolve through the evidence-completion branch orEpisodeNoEvidenceError) and carries arepoLess &&throw at its top as a defense-in-depth assertion.branch_nameis omitted (never a name for a branch that will never exist) andpr_urlstaysundefined(nevernull).POST /:id/create-prandPOST /:id/revise(and their/admin/twins) now refuse409for a repo-less project's feature, before any VCS call. An ordinary (non-episode) feature submitted to a repo-less project is still refused409, now with the permanent message ("accepts worker episodes only").buildEpisodePrompttakesopts.workspace?: 'repo' | 'none';'none'rewrites the WORKSPACE CONFINEMENT paragraph (empty/disposable directory, no repo/branch/commit, every durable result through the gateway) and'repo'/absent stays byte-identical to before (pinned). The Workers panel's episodes table gained a Branch/PR column (—for a repo-less episode, DOM APIs only); the dashboard hides Create-PR/Address-comments for a repo-less feature. Spec 422 inc-1 (RM-245) is built:FA_WORKER_ACT_MAX_CALLS_PER_EPISODEis now the operator CEILING over a per-episode budget COMPUTED from a step's optional declaredbudget: {calls_per_item, discovery_overhead, reporting_reserve}(defaults 2/4/8) times FA's own count of the deployment's source-project features (src/services/agent-factory/episode-budget.ts'sresolveEpisodeBudget), clamped to the ceiling and recorded once per episode asepisode_budget_setbefore its first call. The gateway's per-call admission check (act-gateway.ts:618) now runs TWO independent meters over the SAME durable rows it always summed: a gathering budget for everything but the three report tools, and a reporting reserve forwrite_evidence/file_escalation/propose_recommendationalone, so an episode that exhausts its gathering budget still has its reserve — the exact Pilot 0 (2c331fc8) and first-real- Hermes-run failures this closes. A new refusal reason,episode_reporting_budget_exceeded, is excluded fromepisodes.refused_act/auto- demotion the same way itsepisode_call_cap_exceededsibling is; either budget refusal carries a JSON-RPCerror.datanaming what remains. An episode with no recorded budget event falls back to today's flat-ceiling behavior unchanged. The episode's own rendered instructions now name its budget up front. Deferred to increment 2 (not built here): per-attempt scoping of the meters across a retry, and the Workers-tab consumption panel. Spec 429 (RM-249) adds a fifth read tool,list_recommendations: a recommend-class episode reads its OWN deployment's prior recommendations (listWorkerOutcomesForDeployment, scoped bydeployment_id— no caller-supplied project/episode argument) so it never re-proposes a class a human already decided. Gated by the SAMEworker:readcapability every read uses, plus a new machine-checkedfa:recommendationsdata-scope prefix; on arecommendstep only, that scope is IMPLIED by a declaredworker:propose_recommendation(toolNamesForStep's recommend branch extends the data scopes passed toobserveSetToolNamesfor that branch alone — the capability intersection itself,onlyDeclared, is unchanged, and every other class still needs the scope declared explicitly). Each item nameslineage_id/version/title/summary(read off the recommendation artifact; a missing artifact yields nulls, never a throw),state,episode_feature_id,proposed_at, anddecided_at(the outcome row'supdated_atonce decided, else null) — and WITHHOLDS the deciding operator's identity and any human-authored text (principal/human_action/note), spec 334's withholding discipline applied to a worker's own read. No schema change, no new route, no new capability. Spec 433 inc-1 (RM-219) is built: a declared trigger is a registered trigger.workflow.triggers[i]may now declare, at spec-write time, the SAMEstep_id(a non-actstep) andcadence(the closed subsetvalidateCadencealready enforces)POST .../triggersaccepts as a body — validated by REUSING that same function, never re-expressed, and refusing exactly the same shapes the route refuses (an unknown oractstep, a webhook cadence, a manual step_id/cadence, a cadence with no step_id, a second webhook carrying a step_id), each a 400 naming the exact field path. An artifact declaring neither field validates and compiles exactly as before — silence stays silence. Compiling a solution that DID declare astep_idre-validates every declared trigger against the compiled workflow BEFORE the deployment row is created (a bad declaration fails the whole compile atomically — no deployment and no orphan trigger survive it) and then calls the SAMEcreateWorkerTriggerthe route calls, once per declared trigger, withcreatedBya new reserved, non-human principal (WORKER_TRIGGER_COMPILE_PRINCIPAL = compile@featureagent.local, classifiedsystemand never disclosing an operator identity) — read by the SAME scheduler, under the SAME in-flight bound, daily cap, per-episode budget and autonomy ladder as a hand-armed trigger; anactstep stays unreachable by a trigger in both paths. Oneworker_trigger_registeredrun event is recorded per registered row.GET .../triggersnow reportsarmed_by: 'compile' | 'operator'per trigger (derived by exact comparison against the reserved principal;created_by_kindis unchanged). The Workers page marks each bound trigger with itsarmed by compile/armed by operatorchip and renders "No active trigger can reach this step — it will never run." for a step no active trigger reaches (replacing the neutral "No triggers bound." for that case), so a step nothing can fire — the defect that let Pilot 0'srecommendstep run zero episodes across seven cycles — is visible rather than silent. - Agent Factory Runnable Walkthroughs —
Shipped— Spec 417 inc-1 (RM-213) — the Agent Factory quickstart (§14–§17) as a script CI executes instead of a page of curl commands that rots the moment a route or field changes.examples/agent-factory/holds the Phase-0 GOVERNANCE pilot (docs/design/agent-factory-path-to-pilot.md§2 — dummy realtor fixtures, FA as the stand-in runtime, proves the governance loop and nothing about an agent core):fixtures/*.json(anoutcome, ause_case, astory, and anagent_solutionwith anobservestep and arecommendstep, oneauthority.prohibited[]entry, oneevaluation.success_measures[]entry, and amemorypolicy — all dummy data, no real customer or repository named anywhere in the directory) andrun-governance-pilot.mjs(dependency-free Node,fetchonly; readsFA_BASE_URL/FA_ADMIN_KEY/FA_WORKER_REPO_URLfrom the environment ONLY, never argv; exits2with a usage line and never echoes the key whenFA_ADMIN_KEYis unset) — creates a source project, submits the fixture chain through the product store's one door (outcome → use_case → story →agent_solution, each proposed ready), compiles ashadowdeployment, arms an hourly schedule trigger, fires one episode viaPOST …/episodes, polls until it ends, prints its evidence (worker_episode_started, thenepisode_completedor arun_failurecause), reads the evaluation and promotion-eligibility, and responds to the recommendation the episode produced — exiting non-zero if any step's expected evidence is missing.tests/examples-agent-factory-walkthrough.test.tsruns the identical step sequence in-process (supertestagainstcreateApp(), an in-memory database, the engine mocked exactly astests/agent-factory-episodes.test.tsmocks it — no real model call, no real sandbox) so the walkthrough cannot rot silently: a renamed or removed route fails this file with the step's name in the assertion (demonstrated by the file itself, deliberately hitting a nonexistent path inside the same step-labeled runner the live script uses). The "episode ends" step writes the same evidence-based- completion ledger rowsimplementFeatureitself writes (recordRunEvent(..., 'episode_completed', ...),updateFeatureStatus(..., 'implemented')) directly, standing in for the async agent-engine run a plaincreateApp()test harness never starts — full sandboxed-episode-execution coverage stays intests/agent-factory-episodes.test.ts/tests/spec-387-inc2-*.test.ts/tests/spec-407-episode-posture.test.ts.docs/AGENT_FACTORY_QUICKSTART.md§14/§16/§17 each carry a one-line pointer at the top to the runnable form. No new route, field or capability — every request the script makes is one the quickstart already documents. Increment 2 (spec 417 §3, RM-213/RM-134) adds the CODE pilot:run-pilot-0.mjs, the same shape of script fordocs/design/agent-factory-path-to-pilot.md§8 G0 — FA's own Retrospective worker, a realagent_solutionon FA's own project (not dummy data), already authored inpilots/pilot-0-retrospective/. It probesFA_HELPER_CALLBACK_BASE_URL/healthfirst (the sandbox-facing listener RM-176/spec 413 added) and names theFA_CONTAINER_BINDfix when unreachable, resolves FA's own project fromFA_SELF_PROJECT_ID(never guessed — which project IS FA is operational data, not something the platform may hard-code per CLAUDE.md's project-agnostic Core Law), finds the existing Pilot 0 lineage and recompiles it (RM-215 reuses the lineage's worker project rather than minting a second one), arms the hourlywatch_verdictsschedule trigger, fires that step's episode immediately instead of waiting an hour, waits for it to end, and assertsimplementedwith anepisode_completedevent, at least one evidence note, zero pushes, and that the episode appears on the deployment's episodes endpoint (spec 414 inc-1, the Workers tab). Arun_failurewhose cause names a clone/branch error is recognized and named as RM-217 (a known, separately-tracked compile-then-clone gap) rather than left as an opaque failure — this script does not fix RM-217. Default mode prints a dry description of every step and touches nothing;--liveis what an operator runs to produce RM-134's evidence, attached todocs/design/fa-roadmap.mdby the operator, never generated by the script itself.tests/examples-agent-factory-walkthrough.test.tsgained a matching in-process case that loads the realpilots/pilot-0-retrospective/agent-solution.jsonartifact (its actual four-step shape:watch_verdicts/grade_learningsobserve,propose_candidatesrecommend,propose_improvementact) and drives it through the same declare → ready → compile → arm → fire → evidence → Workers-tab-listing sequence.docs/WALKTHROUGH.mdgained an "Agent Factory" chapter linking both scripts. Still no new route, field or capability. - Identity Require Proven Vcs Handle —
Shipped— Spec 363 (Weftra Identity & Access, RM-127), increment 6: after inc-3/inc-4, apending/staged-turned-legacyorusers.vcs_login-mirror handle still authorized@<bot> merge, mention dispatch, and merger attribution the moment it resolved — nothing yet REFUSED an unproven handle. Inc-6 addsREQUIRE_PROVEN_VCS_HANDLE, an operator-only, env-only, default-OFF switch read once at startup (config.requireProvenVcsHandle,src/config.ts— onlytrue/1, trimmed and case-insensitive, arm it; anything else, including unset, is off, fail-safe). Not a runtime setting (absent fromPUT /api/admin/settings/runtime's editable allowlist) and not a project/feature field (absent fromSELF_CONFIGURABLE_PROJECT_FIELDS) — a project key has no path to it. Enforcement is at the ONE chokepoint,applyProofRequirement(src/models/principal-vcs-identities.ts), applied to the VALUEresolveProvenWinsreturns in bothgetUserByVcsIdentityandgetUserByHandleForScope(andfindUsersByHandleForScopefilters its whole result list the same way): when the flag is on and the winning principal's grouped candidate is notproven(any matching rowoauth_proven), the answer becomesnull. Applied AFTER resolution, never by filtering the candidate set first — filtering first would blind inc-3's CONTEST rule to unproven rows, a LOOSENING. The invariant:result(ON)is alwaysresult(OFF)ornull, never a different non-null answer (stricter-only). Because alegacyrow (users.vcs_loginmirror) can never beoauth_proven— its forge is not a real OAuth provider — every legacy-only approver stops authorizing under the flag until a real forge/host handle is declared and proven for them. The four existing authorizing consumers need no code change:getProjectApproverByVcsHandle(src/models/users.ts, used by@<bot> mergeinsrc/services/flows/weftra-merge.ts, mention dispatch insrc/services/mentions.ts, and inbound PR-review-trigger dispatch insrc/routes/triggers.ts) andresolveMergerActor(src/services/feature-actors.ts, spec 274 merger attribution — an unproven merger resolvesunmapped, composing withseparation_policy'srequire_provenSoD level with zero changes toseparation-policy.ts). The two INFORMATIONAL readers of the same table —resolves_to_user_idon the admin declare response and onlistPrincipalVcsIdentitiesView— deliberately read the link holder with the flag ignored (resolveVcsIdentityForScope(...).linked), so a rival claim stays visible to the admin who just staged over it. Thatunmappedhas a cost the runbook states plainly: it also makes spec 274's merger/submitter comparisonindeterminate, so nomerge_actor_collisionrow is written for a merge the flag refused — on the VCS-host detection path the only artifact a self-merge leaves. So the suppression is recorded per-act instead:feature_actors_resolvedcarriesmerger_proof_withheld: trueexactly when the handle DID link to a principal and the proof requirement is what emptied it (resolveVcsIdentityForScope; the key is absent, notfalse, otherwise, and it is withheld from a project-key ledger read), and docs/OPERATIONS.md §4j-2's detection rule gains a third limb that alerts on it. Refusals are legible, not silent: the existingmention_unauthorized/mention_authorizedrun event, theweftra_merge_refused(unauthorized) ledger row, and thepr_review_runsunauthorized refusal each gainproof_required: config.requireProvenVcsHandle(folded into theerrortext forpr_review_runs, which has no metadata column). No new writer ofproof, no schema change — this increment adds a read-time gate only.GET /api/admin/vcs-proof-readiness(requireAdminAuth) is the readiness view an operator needs BEFORE arming the flag, in TWO halves because the flag governs two resolvers with different reach:approvers/would_lose_authority(rows inproject_approvers, matched forge+host+legacy likeloadScopedCandidates) andattribution/would_lose_attribution(every principal holding a row matching a project's own forge+host, approver or not, becauseresolveMergerActorresolves role-independently — without it an operator could readwould_lose_authority: 0and still lose attribution on arming).readyis computed per HANDLE STRING (the unitapplyProofRequirementrefuses) with a link ready only when every one of its handles is, and over-estimates loss rather than under-estimating it; the view is read-only with no ledger row. The dashboard's Settings tab renders it as a "Proven-handle enforcement" panel (Enforced/ Not-enforced badge, both summary counts, a table of not-ready links from both halves) built with DOM APIs only (createElement/textContent), so the spec-259 escaping pins do not move. - Evidence Pack —
Shipped— The Evidence Pack, Increment 1 (spec 403, RM-192): a canonical, Ed25519-signed statement per feature binding head SHA -> spec hash -> approvals -> verdicts -> gates -> containment -> cost, assembled entirely from the run-event ledger and the feature/spec/checkpoint records — nothing recomputed, nothing accepted from a request. Signature isEd25519(root_key, sha256(canonical_json(statement))), withkey_id = sha256(public_key)[:16]; the public key is published (unauthenticated, rate-limited) atGET /api/instance/verification-keysince a verifier may hold no FA credential at all.GET /api/features/:id/evidence-packreturns{statement, signature, key_id}(?format=markdownfor the human page); scope is a project key to its own feature (404 otherwise), an approver to its linked projects (403 otherwise), an admin unrestricted.POST .../evidence-pack/verifycryptographically checks a caller-held pack's signature against the published key (never a re-derive) and records a bounded mismatch. The pack is tenant-safe by construction — tenant-view ledger seq, no blocking count, separationsatisfied/not_asserted, containment from the run's own config snapshot with operator defaults withheld — so one signed statement serves every reader. Approval identities are spec-334 withheld-safe classifications (named/service/unverified), never an email — the pack is a portable, signable artifact, so it carries no PII regardless of who is asking. Onmerged, the pack is captured as a feature artifact (evidence-pack.json, sourcefa_capture), best-effort, alongside the existing spec-360 checkpoint capture. Increment 2 (spec 403 §2) posts this same pack as a verifiable PR check and adds the standalone verifier — see theprovenance-checkcapability below. - Provenance Check —
Shipped— The Evidence Pack, Increment 2 (spec 403 §2, RM-192): posts the evidence pack as a verifiable PR check (${botHandle}/provenance— never a literal brand string) on every head FA observes, plus a standalone verifier. Posting mechanism per provider (VcsProvider.postCheck): a GitHub check-run whoseoutput.summarycarries the pack's full human page andoutput.texta fenced JSON envelope ({v, statement, signature, key_id}) — self-verifying from the check alone; a GitLab commit status or Bitbucket build status, both short-description-only, linking back to the evidence-pack page (full verification for these two goes through thestatement.json/GET /api/verifypath instead). Conclusion issuccessorneutral— NEVERfailure(I5):successonly when the pack's ownhead_shamatches the checked head and every verdict recorded against that exact head is a pass; a verdict for a different (or unrecorded) head is excluded, never scored as a failure. Spec 402's CI-check classifier already recognizes any${botHandle}/*check aspolicy(never remediated) — no change needed there. Posted on the spec-402 push poll and on FA's own pushes (the implement / implement-spec-kit flows' PR-open step); dedup is a content fingerprint that deliberately excludes the ledger's own seq range, since posting the check appends its own ledger event and a naive full-statement hash would never reach a fixed point.GET /api/verify(with astatement=…query param) is a PUBLIC (no auth), rate-limited route that answers the SIGNATURE question only —{valid, reason?, key_known}computed from the caller's own bytes plus the published keys, with no feature lookup and no ledger read, so its answer is identical whether the named feature is live here, belongs to another tenant or never existed. Staleness is answered only where the requester can be scoped:POST /api/features/:id/evidence-pack/verifyreturnscurrent_last_seq.npx weftra verify <pr_url|statement.json>(src/tools/verify.ts, host-side, full local DB access likediagnose.ts) verifies a local file directly, or looks up a PR url in this instance's own database and extracts the envelope from its posted check through the project's own VCS credential — deliberately not a generic internet-fetching tool. On a PR url it is HEAD-BOUND: the envelope's ownhead_shamust equal the head the PR is on now, every check carrying the name is considered rather than the first, and it reports anattestationblock (attested head, PR head, ledger positions, and a re-derive of the current pack) so avalid: truecannot be read without seeing what it is true about. Key rotation follows spec 362: a pack (and a check's embedded envelope) signed by a since-retired key still verifies via its ownkey_id. - Base Refresh Overlap Is Not A Park —
Shipped— Spec 412 inc-1 (RM-206), operator ruling 2026-09-07: with 100+ developers committing to a project's default branch, a moved base is information for a human to act on, never by itself a reason to fail, retry, or park a run. Two changes to the existing base-refresh check (spec 201 inc-1: before verification, FA compares the run's changed files against whatever the base branch changed since the run's authorized base commit). First, a project's declaredgenerated_paths(the same {path, regenerate} declaration spec 385's base-merge-conflict resolution already uses) are removed from BOTH changed-file sets before the overlap decision runs, for every strategy and both the implement and revise flows — a generator output both sides happen to have regenerated is not a conflict, and spec 398's post-turn normalization regenerates it fresh before the gate anyway. The exclusion is evidenced on every run asgenerated_paths_excludedon the existingbase_identity_checkrun-event, naming exactly which declared paths were actually present in either raw set. Second, an implement run under the defaultrevise_base_strategy: 'branch'(which never re-syncs) no longer escalates-and-parks on a REMAINING (hand-written) overlap: it proceeds to the test gate on its authorized base exactly as if the base hadn't moved, and opens its PR — a human syncs the finished PR later (GitHub's own "Update branch", or/revisewithrevise_base_strategy: 'merge', which gets spec 385's regeneration and spec 255's per-hunk conflict chooser). Recorded asbase_advanced_overlap { authorized_base_sha, remote_base_sha, overlap_paths, main_changed_paths_count }— nobase_refresh_escalationfailure event, no status transition, no retry consumed.refreshStaleBase()itself is unchanged in what it returns forbranch(stilloutcome: 'escalate', same evidence shape) — the reinterpretation lives entirely inimplement.ts's own outcome handling, so the revise flow (out of scope for this increment) is byte-identical:/revise'sbranchstrategy still escalates-and-parks (reverts toimplemented, PR/branch untouched) on the very first overlap it sees, andmergestrategy's resync-and-cap-exhaustion behavior is unchanged for both flows apart from the generated-path exclusion above. No new route, no new config field, no UI. - Worker Base Branch Provisioning —
Shipped— Spec 424 (RM-217): a worker's base branch exists before the run needs it, seeded from the prior version, pushed only when the run pushes. Compile still creates no git branch — that would be execution at compile time — but the old claim that "the branch comes into existence the first time a real episode pushes to it" was false: whengit clone -b <baseBranch>fails (the compiledworker/<lineage>-vNbranch does not exist yet) the implement flow's fallback plain clone used to cut the episode's feature branch straight from the wrong (repo-default) base with no branch ever created and no ledger record — the defect behind Pilot 0 v5's first episode dying atclone … failed (code 128), which an operator had to work around by pushing the branch by hand. Now the fallback arm creates<baseBranch>LOCALLY before cutting the feature branch, seeded from the prior version's tip (worker/<lineage>-v(N-1), viagit fetch, falling through to the repository's ownHEADwhen no prior version exists or its fetch fails) and recordsbase_branch_absent { requested, seeded_from_ref, seeded_from_sha }. The branch reaches the remote only when the SAME run pushes its own feature branch — never a moment sooner, so an observe/recommend episode that completes on evidence alone and pushes nothing leaves the remote byte-for-byte unchanged — recorded asworker_branch_created { branch, from_ref, from_sha }. A plain clone that fails for a reason that is neither auth/network, nor egress, nor a genuinely empty remote now fails the run with a namedrun_failure { cause: 'base_branch_unavailable', branch, repo }instead of the old opaque code-128. Every git operation isrunSandboxGiton the existing implement path — in-sandbox, on the run's already-resolved credential; no new provider method, no route, no schema change.docs/OPERATIONS.md's compile-mechanics section is corrected to describe this instead of the false claim.
floop + Governance SDK
The self-build loop generalized into declarative, project-agnostic loop config, and the SDK that lets any external agent be governed by FA, not just FA-initiated runs.
████████░░ 82% (9 done / 11 on the path) — mostly done
- Integrations Mcp And Webhooks —
Partial— The MCP server interface for governed flows (local stdio + remote Streamable HTTP transports over the same project-scoped tools — feature submit/status/list, a Builder co-authoring loop of draft/clarify/refine/estimate, and a clarification loop that closes back to the submitting agent), a client-initiated OAuth 2.1 front door letting an MCP client connect by browser consent instead of a pasted project key (convenience only — a minted token's blast radius is identical to a raw project key), and inbound/outbound webhook delivery (trigger providers and HMAC-signed callbacks). Inbound triggers are hardened by a dedicated per-project trigger secret (separate from the tenant api_key, rotatable via the dashboard or API) and persisted, replay-safe, per-project delivery dedup. - Recurrence Classification —
Shipped— Retrospective, operator-driven classification of FA's own recorded security_verdict findings into candidate recurrence classes — a repeated SHAPE (the same rule violated across different features), not a repeated file or string. An admin reviews each candidate class with its full instance evidence (feature id + verdict event per instance) and either promotes it into the prior-decisions-conflict-gate's prior_decisions_corpus, or rejects it so it is not re-proposed. All four routes are admin-only in both directions; the classifier never writes anything itself — a human promotes, the machine only proposes. - Per Project Autonomous Loop —
Partial— The FA-native, per-project autonomous delivery loop: a project declares a backlog document + loop config (enabled, backlog_path, merge_policy: human_all|auto_low_risk, risk_policy), and FA's maintainer role selects the next unblocked backlog item, submits it as a governed feature, and — under auto_low_risk — lands it via a SHA-pinned governed merge gated by a tighten-only risk classifier + reviewer/security verdicts. A select-stage fault now STOPS the loop instead of retrying forever: a PERMANENT fault (unsupported_engine,underivable_key, nobacklog_pathconfigured) halts on its first occurrence, and a TRANSIENT one (an unreadablebacklog_path, a maintainer-call error, an over-budget estimate) halts once it recurs chronically — cleared only by an admin's resume call, same as the post-merge canary halt. Distinct from selfbuild/ (FA's own external bash harness); this is the productized, multi-tenant capability any enrolled project can arm. Outstanding: Outstanding (RM-048/RM-084): inc-4 dogfood (retire the bash Governor, run FA itself on the platform loop) — inc-3's canary + auto-revert closed by spec 377, see below. human_all is production-sound and live-proven on a customer project (chessdemo, 2026-08-26). Was missing from this manifest entirely until 2026-08-26 — the archetype "shipped but invisible to a header skim". Spec 319 (RM-083) split the arming privilege by resulting merge_policy: a project key may now self-arm the human_all form (no new authority — every cycle still pauses for a human, the same thing the project could already trigger via POST /api/features); auto_low_risk (real autonomous-merge authority) stays admin-only, including a partial project-key patch that would leave an admin-set auto_low_risk in place. Its round-1 security review closed three gaps in that split: a project-keyloop: nullnow DISARMS rather than erases (the admin-only cadence/back-pressure/budget/risk_policy fields are carried over, so clear-then-rearm cannot shed operator limits), backlog_path is validated repo-relative at set time (it is tenant-settable and dereferenced with FA's VCS credential), and the self-service loop write commits atomically with its loop_events record. Round 2 added the operator-consent precondition the split had traded away: a project key may only move a loop an ADMIN established on the project (its loop column non-NULL), never create one, so upgrading grants no tenant anything and a self-armed loop always runs inside cadence/budget limits an operator wrote; and the self-armable/admin-only field partition is now derived from one list with a compile-time totality check, so a future LoopConfig field cannot silently fall outside the guards. The final security round applied the same partition to the READ side (a project key gets back only the enabled/backlog_path/merge_policy subfields of loop — risk_policy and the operator's cadence/back-pressure/budget limits are admin-readable only) and unified the ledger so EVERY loop-config change by either principal is recorded (loop_armed/loop_disarmed on an enabled flip, loop_config_changed otherwise — including an admin arming auto_low_risk on an already-enabled loop), committing in the same transaction as the config write on both routes. Spec 377 (RM-084 item 1) closed the first half of inc-3: after EVERY landing the loop itself performs under auto_low_risk, a fresh post-merge canary clones the default branch and re-runs the project's own gate set against the bytes actually there; red reverts the landing (a compare-and-swap--force-with-leasepush, never a plain force-push) when it safely can, and either way HALTS the project's loop in a way the scheduler actually honors (isLoopHalted, derived from loop_events, no schema change) and notifies.POST /api/projects/:id/loop/resumeis the one admin act that clears a halt. Status stayspartial: inc-4 (dogfood — retire the bash Governor) is still outstanding. - Learning Plane Repository Knowledge —
Shipped— Learning Plane v1 (spec 351, RM-125, promoted from draft 310): a per-project knowledge_items store (candidate|approved|deprecated|superseded), deterministic bounded retrieval that injects only APPROVED items as a DATA block into the author prompt, hash-pinned run-evidence of exactly which items a run consumed, and a project API where a project key may submit/list its own candidates but never approve. Core invariant: learned knowledge may influence instructions, never grant authority — no field on the model is executable/tool/network/credential/merge/reviewer. Foundation for the Agent Factory program's governed skills (draft 311) and delegation (draft 312). - Learning Plane Hybrid Semantic Retrieval —
Shipped— Learning Plane inc-2 (spec 354, RM-125-inc-2): adds semantic (vector) retrieval alongside spec 351's keyword/applicability match, fused by Reciprocal Rank Fusion, behind the existing resolveApprovedKnowledgeForRun seam. A portable VectorStore interface (sqlite-vec backend with a brute-force cosine fallback now, a documented 1:1 pgvector migration contract for Postgres) stores driver-neutral float32 embeddings, with tenant scope enforced STRUCTURALLY inside the KNN query, never post-filtered. A pluggable EmbeddingProvider ships LOCAL ONLY (DECIDED, operator 2026-09-01): an in-process ONNX sentence-embedding model (bge-small-en-v1.5, 384-dim) via fastembed — no knowledge text ever leaves the FA host. The model artifact is treated as its own supply-chain input rather than as a dependency: FA never uses the library's own downloader, sha256-checks the tarball before extracting it and every file before onnxruntime parses it, and does NOT fetch it at all unless an operator opts in (FA_EMBEDDING_ALLOW_MODEL_DOWNLOAD, default false — operators pre-provision the cache instead). An operator-selected model FA ships no digests for is never downloaded. An approver's declared applicability governs both signals: semantic similarity re-ranks the items the spec 351 filter admitted and never re-admits one it excluded. Index lifecycle (embed on approve, re-embed on an approved item's edit, remove on deprecate/supersede, explicit-admin-only rebuild) keeps the relational knowledge_items table authoritative; the vector index is a rebuildable accelerator only. An empty/unavailable index falls back to keyword-only, never failing a run. Core invariant unchanged: an embedding is a derived retrieval index, grants no authority. - Learning Plane Governed Skills —
Shipped— Learning Plane — governed reusable skills + effectiveness (spec 359, RM-130, promoted from draft 311): a per-project, versionedskillsstore (candidate|approved| deprecated), a deterministic explainable selector that folds only APPROVED skills into an author or declared-helper-role prompt as a bounded, DATA-framed block appended after the spec-351 knowledge block, and effectiveness metrics (first-pass acceptance, review/security findings per run, rework, verification failure rate, median tokens/ latency, with a comparable baseline cohort and a low-sample confidence band) computed entirely from EXISTING review/verification run-evidence. Core invariant: a skill contributes guidance, never authority — it may DECLARE required capabilities (tools/network) but they are only CHECKED against a run's already-resolved envelope, never unioned into it; an unmet declaration is skipped (capability_missing), never granted. Versions are immutable by content-hash — revising a skill always creates a new candidate version, never mutates the one a prior run consumed, and at most one version of a given skill name is ever approved at a time (enforced by the approve transaction AND a partial UNIQUE index, not merely documented). A project key may submit/list/revise its own candidates but never approve; promotion requires a NAMED admin/approver — the shared ADMIN_API_KEY is refused because it authenticates a role rather than a person, the identity that authored a candidate cannot approve it, and every approval/deprecation is appended to the tamper-evident actor_events ledger. Admin dashboard surface: candidate queue, version lineage/diff, applicability, required capabilities, source evidence, and per-skill effectiveness. - Learning Plane Pm Readable Candidate Approval —
Shipped— PM-readable candidate approval surface (spec 365, RM-133): the human half of the Learning Plane's candidate -> approval trust model. Addspublic/knowledge.html, a knowledge candidate-approval UI mirroringpublic/skills.html(project selector, status filter, cards, admin/approver Approve + Deprecate wired to the existing spec 351 endpoints) — closing the gap where knowledge candidates were API-only. On BOTHpublic/knowledge.htmlandpublic/skills.html, every candidate card now LEADS with a plain-language block — what the item is, why it's proposed (human-readable provenance derived from source_type + evidence count, or "submitted by" the actor name), and what changes if approved (a fixed sentence restating the "influences instructions, never grants authority" invariant) — plus the source evidence rendered as a readable list. Machine fields (applicability/required_capabilities JSON, raw source_refs, content_hash, confidence) stay available but collapsed behind a "Technical detail" disclosure, no longer the first thing a non-engineer approver sees. UI-only: no new route, no schema change, no change to any existing guard — reuses the spec 351/359 APIs exactly as they are. Spec 383 (RM-151 inc-1) added a "Sort: newest | most recurring" control (drives?sort=recurrence) and a "cited N times" line (recurrence_count) to every card's lead block, and a paired-card path: a knowledge candidate and a skill candidate that FA's OWN generation seam mined from one cluster render as ONE card with "Approve as knowledge", "Approve as skill", and "Deprecate both". Byte-identicalsource_refsare only the lookup key —source_refsis caller-supplied on the tenant-facing submit routes, so pairing also requires FA provenance on both rows (knowledgereviewer_finding/ skilllearned, each withcreated_by_actor: fa:candidate-generation, neither mintable by a project key); an ambiguous key pairs with nothing, and anything unpaired renders as a normal card. The card shows the skill's own name, description and INSTRUCTIONS, so "Approve as skill" never promotes text the page did not display. Built entirely withcreateElement/textContent, never an HTML template string, so no repository-supplied text on that path is ever interpolated into HTML.public/skills.htmlis unchanged. - Governed Delegation Provenance —
Shipped— Governed delegation (spec 361, RM-129, promoted from draft 312 Part A): a structured, OPTIONAL delegation request — objective, scope (paths/mode/spec_text/max_result_size, always advisory this release — a helper gets no filesystem access to any real repo), output_contract (analysis|patch|verification; only analysis runs end-to-end this release, patch/verification are declared and recorded but resolve to analysis-equivalent handling), and a wall-clock max_ms budget — layered over spec 248's existing declared helper roles, plus durable parent-child provenance for every delegation attempt. Core invariant unchanged from spec 248: the parent chooses WHAT it needs done and names an allowed role; FA alone resolves HOW that role executes (engine/model/tools/credential/network/permission policy) through the exact same functions the parent's own flow used — none of those are readable from the delegation call, so a hostile request supplying them is simply never consulted. Each attempt appends an ADDITIVEhelper_delegationrun event (no database migration; a deterministic synthetic child_run_id, never a new row) carrying the resolved role, engine, model, objective_hash + a bounded preview, scope + a HONESTLY recorded scope_enforcement (advisory, never claimed as a boundary), a capability_snapshot_hash, applied/considered skills, the token+wall-clock budget, and an explicit outcome (one of nine values, including timeout/scope_violation/cancelled) — never a silent empty success. An optional second tool lets the parent report accepted/ignored/superseded disposition for a completed delegation's output; unreported reads asunknown. The committed feature provenance artifact gains a human-readable "Helper Delegations" section for author-context delegations. Depth-1 and the per-run invocation cap are unchanged. - Learning Plane Auto Fire Knowledge Candidates —
Shipped— Learning Plane self-learning inc-1 (spec 364, RM-132): auto-fires the existing conservative knowledge-candidate source (spec 351's ReviewerFindingRecurrenceCandidateSource) from applyPrResolution the moment a feature reaches merged — best-effort and non-blocking (a generation failure is caught/logged and never reverts the already- committed merge transition), scoped to only that feature's own project, and idempotent via the existing alreadyCovered source_refs guard so repeat merges over the same evidence never duplicate a candidate. Auto-proposed candidates are stamped with the stable system actor fa:candidate-generation so an approver can tell them apart from a human/project-key submission. Core invariant unchanged: approval stays strictly human — this path only ever writes status='candidate' rows, never approved. Candidate text was rewritten to be PM-readable: summary/body now lead with plain-language what/why/what-changes-if-approved instead of a bare finding count, with the raw claims and source_refs kept present but demoted to a secondary section. Deferred: the analogous skill-candidate source (RM-132 inc-2) and the candidate-approval UI (RM-133) — this increment's surface is the existing GET /api/projects/:id/knowledge?status=candidate API. Spec 383 (RM-151 inc-1) replaced the per-FILE grouping with per-CLASS clustering (src/services/finding-class-clustering.ts'sclusterFindingsByClass, a deterministic Jaccard similarity over the finding's own claim text at threshold 0.5) so ONE recurring pattern across several files yields ONE candidate citing every file, instead of one candidate per file it happened to surface in — the shape that produced 89 near-duplicate candidates from a single lesson on 2026-09-02. The same spec addedrecurrence_countand?sort=recurrenceto this route (no schema change) so the queue can be triaged by how often a pattern recurs, not just by recency. FA's own what/why/what-changes paragraphs quote no repository text; the cluster's seed claim appears in the SUMMARY, labelled there as a verbatim quotation and rendered through toQuotedLine, which flattens control characters and rewrites the double quote and backtick so a claim cannot close FA's quotation and continue in FA's voice. - Learning Plane Auto Fire Skill Candidates —
Shipped— Learning Plane self-learning inc-2 (spec 367, RM-132): the analogous SKILL twin of learning-plane-auto-fire-knowledge-candidates. A new LearnedSkillCandidateSource reads the same recurring security-review-finding-by-file evidence (listSecurityVerdictInstancesForProject, RECURRENCE_THRESHOLD=2) and proposes one conservative source_type='learned' skill candidate per recurring file, with deterministic templated text (no LLM). generateAndPersistSkillCandidates fires as a sibling best-effort try/catch right next to the spec-364 knowledge call in applyPrResolution, at the same post-merge seam — independent of it, so a throw in either never reverts the merge transition or blocks the other. Idempotent via the same alreadyCovered-equivalent shared-source_ref guard. Every row is status='candidate', never auto-approved — approval stays the existing POST /api/projects/:id/skills/:sid/approve, no new approval path. Candidate text leads with plain-language what/why/what-changes-if-approved; repository-supplied text (file path, finding claims) is quoted/clamped/control-char-stripped and confined to a delimited evidence section. No new UX surface: renders in the existing skills candidate queue (public/skills.html + GET /api/projects/:id/skills?status=candidate). Spec 383 (RM-151 inc-1) replaced the per-FILE grouping with the same per-CLASS clustering the knowledge twin uses (src/services/finding-class-clustering.ts'sclusterFindingsByClass, threshold 0.5), so this source'ssourceRefsfor a cluster are byte-identical to the knowledge candidate's for the same evidence — the keypublic/knowledge.htmllooks a pair up by, on top of the FA-provenance check (source_type + fa:candidate-generation actor) it requires before rendering two rows as one card. FA's own leading paragraphs quote no repository text; the seed claim appears only in the skill NAME, labelled as a verbatim quotation and passed through toQuotedLine, which also neutralizes the quote characters that would let it close FA's quotation. The same spec addedrecurrence_countand?sort=recurrenceto this route (no schema change). - Learning Plane Retrospective Evidence And Grades —
Shipped— Learning Plane — hourly retrospective, evidence half (spec 379 inc-1, RM-155): grades whether the knowledge/skills applied to each run actually fit and worked, by ARITHMETIC alone, no model call. The retriever now records the truth about truncation —knowledge.applied_to_run/skill.applied_to_runevents gain adropped[]list ({id, reason: 'bytes'|'count', fit}) for every item that matched but was capped out, and every included item gains its ownfit(path_fit|broad|misfit), computed at retrieval time from the same applicability/changed-paths context the retriever already reads. A closed, optionaltargetsfield ({test_files, finding_classes}, capped at 20 entries each) can be declared on a knowledge or skill item to name what it is trying to improve — never a retrieval selector, carried untouched through the candidate-to-approved path, and back-filled for a pinned-test file name found in the item's own body when the caller declared none. A per-project watermarked hourly tick (FA_LEARNING_RETRO_INTERVAL_MS, default 1h,0disables;FA_LEARNING_RETRO_MAX_FEATURESper pass, default 50) grades every newly-terminal feature's run evidence (applied/dropped items, touched files, per-attempt test-gate results, per-round security findings, fixer rounds, cost, wall-clock, retries, human intervention) intofit/truncation/effect(applied_but_failed|flipped|no_signal) per item plus a per-windowrecurrencetable (finding classes on 2+ distinct features) — writing onelearning_retrospectivesrow per pass (model: 'off'), alearning.retrospectiveactor event, and a per-featurelearning.run_gradedrun event, all bounded and redacted (FA_LEARNING_RETRO_MAX_EVIDENCE_BYTES, default 48 KB). Two admin/approver-only read routes return aggregates and grades only — no finding body text, nohuman_actionfree text, no operator email. The older per-file recurrence miner (learning-plane-auto-fire-knowledge-candidates/-skill-candidates above) is superseded by this pass's per-class view and now defaults OFF (FA_RECURRENCE_MINER=onto keep it running). No model call, no proposals, no candidates, and no dashboard panel yet — the model-authored half is a later increment.
Gaps — what the north star calls for, not yet built
- Enterprise Multi Tenancy (The four SaaS-primitive seams) — Tenant isolation and per-tenant credential configuration seams toward full RBAC/orgs/ SSO/billing. The multi-tenant trust/credential model is implemented; RBAC/orgs/SSO/ billing as a complete enterprise product are north-star, not shipped. Spec 215 adds a project-scoped VCS credential (admin-only read/write,
tenant_credentialspurpose'vcs') on the same table and route file as the tenant-wide model credential above — not a second credential model. —specs/011-multi-tenant-trust-credential-model/increment-1b.md,specs/011-multi-tenant-trust-credential-model/increment-2a-credential-resolution-core.md,specs/215-project-level-vcs-credentials.md - Saas Control Plane (One core → N deployment modes) — The zero-infra SaaS deployment mode: tenant provisioning, routing, metering, and the orchestrated (k8s/Fargate) Runtime backend that turns the self-hosted-per-org core into a multi-tenant SaaS front door. Design-only — explicitly sequenced AFTER the self-hosted core earns adoption (target-architecture.md §2, §9); no control-plane code exists yet. — no spec on file yet
- Governed Deploy Bounded Execution (The Builder → governed deploy) — The bounded-execution increment of the governed-app-deployment rebuild (spec 106 inc-2): running a project's own declared deploy recipe to completion under FA's permission gate. Blocked on the spec-011 credential broker (a tenant's cloud creds, safely isolated) per specs/106-governed-app-deployment-rebuild/spec.md — see CLAUDE.md's Trust Boundary incident note (2026-07-17, PR #173). —
specs/106-governed-app-deployment-rebuild/spec.md,specs/106-governed-app-deployment-rebuild/increment-2-bounded-execution.md - Admin Action Audit (Everything as governed code (terminal thesis)) — A tamper-evident, hash-chained record of privileged ADMINISTRATOR actions (the
actor_eventsledger, spec 261) — closing the gap CLAUDE.md's "Administration Is Legitimate" core law names: run_events is feature-keyed, with nowhere to put a non-feature admin action. Already covers identity management (user create/update/delete/role-change/key-regeneration, approver-project links) and a growing set of governance writes (runtime-image allowlist, runtime settings, separation policy, runtime egress). Spec 292 (RM-063) is extending it across every remaining privileged admin surface, increment by increment — this entry tracks it as a PLANNED gap until the whole spec's scope lands: increment 1 (shipped) covers the admin PROJECT routes —POST /api/projects,PATCH /api/projects/:id,DELETE /api/projects/:ideach now record one event on success, carrying field NAMES only, never values. Still missing: the OTHER admin-reachable project-enrollment paths —POST /api/builder/new-projectand the agent-factory compile path both call createProject and record nothing, so an empty ledger does not yet prove nobody enrolled a project; project-scoped VCS-credential configuration (inc-2); instance administration —POST /api/admin/rotate-admin-keyand the database-backup routes still record zero events today (inc-3) — and connector/integration administration (inc-4). —specs/261-audit-administrator-actions.md,specs/292-audit-all-privileged-administrative-mutations.md,specs/292-audit-all-privileged-administrative-mutations-inc-1.md
Generated 2026-09-14T03:38:57.250Z from docs/features.yml (content hash 32c72c479b6c). 13 further manifest entries are internal- or contributor-facing and not listed on this page.