From 9f2652a34bbe66faf64bf1db1f9644f44c6cf902 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:07:01 -0400 Subject: [PATCH 01/32] docs: add MacBrains operating contract --- MACBRAINS.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 MACBRAINS.md diff --git a/MACBRAINS.md b/MACBRAINS.md new file mode 100644 index 000000000000..5fd5bb0f604e --- /dev/null +++ b/MACBRAINS.md @@ -0,0 +1,153 @@ +# MacBrains T3 Code Operating Contract + +This fork is the user-facing control plane for Nayte's coding agents, repositories, Mac, and Linux compute node. It is not a generic chat launcher. + +## Authority + +Apply this precedence whenever instructions conflict: + +1. current repository state and repository-scoped instructions; +2. canonical project records, manifests, databases, CI, and experiment ledgers; +3. current command output and durable receipts; +4. approved handoff documents; +5. conversation narrative. + +Never use remembered state as a substitute for reading the real repository. Every state-changing run starts with a read-only source-truth preflight. + +## Required run lifecycle + +Every substantial run MUST visibly pass through these durable states: + +`queued -> preflight -> oriented -> planned -> executing -> testing -> reviewing -> deliverable-ready -> completed` + +Additional terminal states: + +`blocked`, `failed`, `cancelled`, `superseded`, `recovery-required`. + +A provider saying it is done does not complete a run. Completion is decided by the orchestration layer only after required receipts exist. + +## Mandatory receipts + +A run cannot enter `completed` without all applicable receipts: + +- repository identity, branch, HEAD, worktree, dirty state, and instruction files; +- ownership and overlap assessment; +- plan and acceptance criteria; +- files changed and final diff summary; +- commands executed with exit status; +- tests, lint, typecheck, build, and UI validation results; +- staged file list or explicit statement that nothing is staged; +- user-visible launch or access path; +- screenshots or recordings for UI changes; +- unresolved risks and missing evidence; +- final verdict using `PROVEN`, `INFERRED`, `SUSPECTED`, and `UNKNOWN`. + +No spinner, assistant message, or provider process state may impersonate these receipts. + +## Worktree isolation + +Substantial repository work uses one isolated Git worktree per implementation lane. The UI must show: + +- repository; +- base branch and base SHA; +- lane branch; +- worktree path; +- owner agent; +- changed files; +- ahead/behind and conflict status; +- whether another live lane overlaps the same files. + +Never silently edit the primary checkout when an isolated lane is available. Never clean, reset, switch, delete, or repurpose another lane without explicit evidence that it is abandoned and safe. + +## Agent topology + +Default substantial topology: + +1. **Executor** performs inspection, edits, and focused validation. +2. **Advisor** reviews the approach after orientation and before irreversible or broad edits. +3. **Verifier** independently reviews the finished diff, evidence, and acceptance criteria. + +Do not fan out multiple editors into overlapping files. Parallel agents require explicit ownership boundaries and merge contracts. + +Preserve productive explorer context, but transfer claims to an independent canonical verifier before treating them as fact. + +## Model routing + +Use the smallest sufficient model and reasoning level. + +- routine bounded work: lower-cost executor; +- normal implementation and research: medium reasoning; +- architecture, migrations, concurrency, security, performance, and provenance: executor + stronger advisor + independent verifier; +- escalate only after task-specific failure evidence. + +The UI must store the selected role, provider instance, model, reasoning level, and escalation cause for every agent lane. + +## Mac and Linux node + +The Mac is the interactive control and integration surface. The Linux node is a first-class execution environment for heavy builds, tests, benchmarks, analysis, and long-running jobs. + +Node rules: + +- no Python implementation or Python-based worker prompts; +- prefer C, C++, Rust, Swift where supported, shell, or another native compiled implementation; +- show the exact environment and filesystem owning every command; +- preserve artifacts and return them through typed receipts; +- do not imply that node output is integrated until the Mac repository has received and verified it. + +For ANVIL, always write `ANVIL` uppercase except in case-sensitive identifiers, commands, and paths. + +## Visible completion + +Backend implementation is not completion. Every durable feature must have: + +- an obvious UI surface; +- a launch or access path; +- persistent visibility after restart; +- notifications when attention is required; +- a demonstrated end-to-end acceptance check that Nayte can see and use. + +A hidden daemon, database row, log file, or command without a discoverable UI does not count as delivered. + +## Remote and mobile + +Web, desktop, iPhone, and iPad are first-class control surfaces. A workflow that can start on desktop must remain observable and controllable remotely unless the operation is inherently local-only. + +Required remote capabilities include: + +- add and manage projects on remote environments; +- create and inspect worktrees; +- start, interrupt, steer, queue, resume, and recover runs; +- view live logs, diff, tests, receipts, blockers, and agent topology; +- receive actionable notifications; +- open the exact repository, branch, PR, artifact, or dashboard involved. + +Never expose a public backend without authenticated access. Prefer Tailscale endpoints and preserve the existing environment/access-endpoint boundary. + +## User interaction + +Do not force repeated gates or ceremonial confirmations. Ask only when missing information changes the correct action or an irreversible boundary requires explicit consent. + +For authorized repository work, proceed through the full lifecycle and stop only at a real blocker or completed acceptance contract. Surface blockers with exact evidence and the smallest safe next action. + +## Completion report + +Every completed or blocked run must close with these fields: + +- `proven` +- `missing evidence` +- `possibly wrong or overstated` +- `exact next action` +- `what does not count as completion` +- `safe to continue here or start a fresh context` + +## Implementation map + +The complete product and engineering specification is in: + +- `docs/macbrains/PRODUCT_SPEC.md` +- `docs/macbrains/DOMAIN_MODEL.md` +- `docs/macbrains/IMPLEMENTATION_LEDGER.md` +- `docs/macbrains/ACCEPTANCE_MATRIX.md` +- `docs/macbrains/AGENT_EXECUTION_PROMPT.md` + +Agents must read all five before changing behavior governed by this fork. \ No newline at end of file From dd6e8dbc7eb41004f959e93092f5b0e5ba9f2fb3 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:07:50 -0400 Subject: [PATCH 02/32] docs: define MacBrains product specification --- docs/macbrains/PRODUCT_SPEC.md | 422 +++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 docs/macbrains/PRODUCT_SPEC.md diff --git a/docs/macbrains/PRODUCT_SPEC.md b/docs/macbrains/PRODUCT_SPEC.md new file mode 100644 index 000000000000..652e25db0d5f --- /dev/null +++ b/docs/macbrains/PRODUCT_SPEC.md @@ -0,0 +1,422 @@ +# MacBrains T3 Code Product Specification + +## Product definition + +MacBrains T3 Code is the persistent, multi-environment operating surface for repository-grounded agent work. It coordinates human direction, source-truth inspection, isolated implementation lanes, provider sessions, Linux-node execution, verification, and visible delivery across desktop, web, iPhone, and iPad. + +The product must answer, at all times: + +1. What work exists? +2. Which repository, branch, worktree, environment, and source of truth own it? +3. Which agent is doing what, with which model and tools? +4. What has actually happened? +5. What evidence proves the result? +6. What needs Nayte's attention? +7. How does the finished work become visible and usable? + +## Design principles + +### Evidence before narrative + +Repository state, GitHub, CI, canonical databases, manifests, ledgers, and command receipts outrank conversation history. The UI presents evidence next to claims and never collapses `INFERRED` into `PROVEN`. + +### One obvious surface + +The default home is a work command center, not an empty chat view. It shows projects, active lanes, blockers, completed deliverables, node jobs, review status, and attention items. + +### Durable by default + +Runs, plans, receipts, artifacts, checkpoints, agent relationships, and user decisions survive process restarts and device changes. Recovery is an ordinary state transition, not an exceptional manual ritual. + +### Isolation without invisibility + +Every substantial editing lane gets a worktree, but all lanes remain visible together. Worktree isolation must reduce collisions without fragmenting awareness. + +### Advisors at leverage points + +Independent advice is inserted after orientation, when evidence conflicts or strategy changes, and before completion. Advisors do not become hidden parallel implementers. + +### Completion means usable + +A backend result without a visible surface, launch path, notification path, and end-to-end acceptance receipt is incomplete. + +### Remote control is first-class + +Mobile and web are not read-only dashboards. They can create projects, start and steer runs, approve actions, inspect evidence, recover work, and open outputs. + +## Primary navigation + +### Command Center + +Default landing surface. Sections: + +- **Needs attention**: approvals, blockers, failed checks, evidence conflicts, stale lanes, merge conflicts, missing deliverables. +- **Active work**: every executing or waiting lane across environments. +- **Ready for review**: finished implementation awaiting verifier or human review. +- **Ready to use**: validated outputs with launch/access actions. +- **Recent evidence**: tests, CI, benchmarks, receipts, and source-truth changes. +- **Node activity**: Linux-node jobs, resource status, artifacts, and return-to-Mac integration status. + +Each card must show project, lane, environment, agent role, provider/model, current lifecycle state, elapsed active time, last durable receipt, and next action. + +### Projects + +Project pages aggregate all environments and clones belonging to one logical repository identity. + +Required tabs: + +- Overview +- Work lanes +- Source truth +- Plans +- Changes +- Checks +- Pull requests +- Experiments +- Artifacts +- Decisions +- Activity +- Settings + +A project may have local Mac and remote Linux clones. They are distinct environment-local workspaces linked by repository identity. + +### Work + +Cross-project lane browser with filters for state, role, model, provider, environment, branch, worktree, owner, and attention status. + +### Environments + +Shows Mac, Linux node, and future hosts. Each environment page exposes: + +- reachability and authentication; +- Tailscale/LAN/SSH/tunnel endpoints; +- server and client version compatibility; +- provider instances and authentication state; +- repositories and worktrees; +- CPU, memory, disk, thermal, and active process telemetry; +- background-service status; +- recent failures and recovery controls. + +### Deliverables + +A persistent library of finished outputs: applications, dashboards, builds, DMGs, AppImages, reports, benchmark bundles, PRs, patches, datasets, and launch URLs. Each deliverable includes provenance and acceptance receipts. + +## Work lane creation + +The new-work composer must support: + +- project and environment selection; +- existing branch or new branch; +- isolated worktree by default for substantial work; +- task contract and acceptance criteria; +- executor provider/model/reasoning; +- advisor provider/model/reasoning; +- verifier provider/model/reasoning; +- tool permissions; +- runtime access policy; +- node-offload policy; +- notification policy; +- explicit ownership paths when parallel work is enabled. + +Provide templates: + +- Implement feature +- Fix bug +- Review PR +- Repository audit +- Research and design +- Experiment/benchmark +- Recovery/resume +- Documentation/artifact +- ANVIL package execution + +Templates preconfigure contracts but do not hide them. + +## Lifecycle UX + +### Queued + +The task is persisted but not executing. Show environment availability, dependencies, and estimated scheduling order without inventing duration estimates. + +### Preflight + +Display repository root, current branch/HEAD, status, worktrees, remotes, instruction files, manifests, relevant tests, and overlap risks. The lane may not edit until the preflight receipt is accepted by orchestration. + +### Oriented + +The executor has read the relevant implementation and tests. Show source files inspected, symbols identified, and unknowns that change action. + +### Planned + +Show an executable plan tied to acceptance criteria and file ownership. Plans are versioned. Strategy changes create a new plan revision with rationale. + +### Executing + +Show live agent transcript, tool calls, changed files, resource usage, child agents, node jobs, and durable progress receipts. Provide `steer`, `queue`, `interrupt`, `pause`, and `handoff` controls. + +### Testing + +Checks render as structured entities, not transcript text. Each check records environment, command, scope, start/end timestamps, exit status, log artifact, and related commit/worktree state. + +### Reviewing + +The verifier receives the task contract, final diff, receipts, test results, and known risks. It must independently inspect current source rather than trust the executor summary. + +### Deliverable ready + +The implementation passed technical checks but completion waits for a visible launch/access path and end-to-end acceptance evidence. + +### Completed + +The UI shows the final completion report, deliverable actions, exact commit/PR, verified checks, unresolved caveats, and where the work appears in the user's daily workflow. + +### Blocked and recovery required + +Blockers are typed: + +- missing access/authentication; +- source conflict; +- dirty or overlapping ownership; +- dependency unavailable; +- failed invariant; +- test/build failure; +- environment offline; +- provider failure; +- corrupted persisted state; +- context exhaustion; +- user decision genuinely required. + +Recovery offers evidence-based actions: resume same provider, switch provider while preserving state, create recovery agent, restore checkpoint, recreate session, retry node job, or fork a clean lane. + +## Agent topology visualization + +Represent agents as a directed graph: + +- executor owns the lane; +- advisor attaches to a plan revision or escalation point; +- verifier attaches to a candidate completion; +- child workers attach to bounded tasks and declared ownership; +- recovery agents supersede failed contexts without erasing history. + +Every node shows provider instance, model, reasoning level, context status, tool access, environment, timestamps, and outcome. No invisible subagents. + +## Source-truth panel + +For every lane, expose a persistent panel containing: + +- repository and canonical root; +- branch, HEAD, base SHA, upstream divergence; +- worktree path and status; +- instruction hierarchy; +- canonical project records and connected sources; +- relevant files and symbols; +- generated/vendored boundaries; +- active operations such as merge/rebase/cherry-pick; +- ownership collisions; +- staleness warnings. + +Users can pin sources as authoritative. The system records conflicts rather than silently choosing. + +## Evidence and receipts + +Receipts are typed, queryable, exportable, and linked to the exact run revision. Minimum receipt classes: + +- SourceTruthPreflightReceipt +- OrientationReceipt +- PlanReceipt +- AdvisorReceipt +- EditReceipt +- CommandReceipt +- CheckReceipt +- DiffReceipt +- CheckpointReceipt +- NodeJobReceipt +- ArtifactReceipt +- ReviewReceipt +- UIAcceptanceReceipt +- CompletionReceipt +- RecoveryReceipt + +Each receipt includes stable ID, lane ID, turn/run ID, environment ID, timestamps, producer, claim labels, payload schema version, and content hash where applicable. + +## Checks dashboard + +Checks are grouped by acceptance criterion and severity. Required status vocabulary: + +- not-run +- running +- passed +- failed +- skipped-with-reason +- blocked +- stale +- superseded + +A check becomes stale when repository HEAD, relevant files, environment configuration, or dependency lock state changes after it ran. + +The product must never display "all checks passed" when required checks were skipped, not run, or became stale. + +## Diff and ownership experience + +The diff view must support: + +- lane-only diff; +- staged/unstaged split; +- base-to-head diff; +- commit-by-commit view; +- cross-lane overlap detection; +- generated/vendor filtering; +- test-to-change traceability; +- reviewer annotations; +- checkpoint comparison and restore. + +Before merge, show exact files that overlap other active lanes and whether those lanes have uncommitted changes. + +## Linux node integration + +The Linux node is modeled as an ExecutionEnvironment with dedicated job orchestration. + +A node job declares: + +- source repository identity and source commit; +- input artifacts; +- exact command and toolchain; +- resource class; +- expected outputs; +- timeout/cancellation policy; +- return path; +- verification required on Mac. + +Node output is immutable and content-addressed. Returning an artifact creates a receipt; integrating it into a Mac worktree is a separate explicit event and verification step. + +No Python is allowed in node implementation plans or generated worker scripts. Validate this before dispatch. + +## Notifications + +Notifications are event-driven and actionable. Supported triggers: + +- approval or user input required; +- run blocked or failed; +- advisor found a strategy conflict; +- verifier rejected completion; +- tests/build/CI changed state; +- node job completed or failed; +- PR review/check changed; +- deliverable became ready; +- stale lane or environment disconnected; +- recovery required. + +A notification opens the exact lane, receipt, check, diff, or action. Avoid generic "agent finished" notifications without evidence state. + +## Provider and model management + +Provider instances remain separate from provider drivers. Support multiple accounts and endpoints per driver. + +Add role presets: + +- Executor +- Advisor +- Verifier +- Explorer +- Recovery +- Fast mechanical worker + +Role presets select provider instance, model, reasoning, permissions, and context policy. Project-level overrides are allowed and visible. + +Model availability is dynamic. Store stable intent (role/capability tier) separately from the concrete current model name so obsolete names do not brick saved workflows. + +## Context management + +Expose context health per agent: + +- provider-reported usage; +- compaction events; +- estimated retained task state; +- durable receipts already externalized; +- risk of context loss. + +Compaction must reset/adjust usage honestly. Before context exhaustion, persist a structured handoff containing task contract, source truth, plan revision, edits, checks, blockers, and next action. + +## GitHub integration + +GitHub is a first-class evidence and delivery surface: + +- repository and branch discovery; +- issue/PR linkage; +- PR diff, checks, reviews, and threads; +- branch protection and mergeability; +- comment/review actions; +- artifact and release linkage. + +Do not create a PR automatically unless the work contract requests it. When requested, rebase/update from the actual target branch, verify current state, then open a focused PR with evidence. + +## Project instructions and reusable skills + +Projects can register instruction bundles and reusable workflow skills. The UI must show which instructions were loaded for a lane and their precedence. + +Skills are versioned assets with: + +- trigger description; +- required connectors/tools; +- input/output contract; +- scripts and references; +- validation status; +- package hash; +- usage history. + +A skill cannot silently override repository instructions or safety boundaries. + +## Performance requirements + +- No continuously repainting progress animations. +- Virtualize large lane, receipt, activity, log, and diff lists. +- Use incremental subscriptions and projection deltas. +- Avoid repeated VCS polling; invalidate from repository events and bounded refresh policies. +- Heavy log parsing and diff summarization run outside the UI thread. +- Mobile defaults to summarized live updates with on-demand detail. +- Persist writes atomically with backup/recovery for critical catalogs and state. + +## Reliability requirements + +- Event-sourced orchestration remains canonical. +- Commands are idempotent. +- Reactors emit typed receipts. +- Completion decisions are deterministic from state plus receipts. +- Provider replay is deduplicated. +- Background work keeps sessions alive using explicit activity leases, not foreground-turn assumptions. +- Persisted catalogs use atomic replace, fsync where appropriate, validation, and last-known-good recovery. +- A corrupt nonessential projection must be rebuildable from canonical events. + +## Security requirements + +- Pairing and remote access require explicit authentication. +- Sensitive provider values stay server-side and are never re-sent after storage. +- Every tool permission and full-access selection is visible on the lane. +- Destructive actions require a typed intent and scoped target. +- No pattern-based process killing. +- Never expose live user data to development worktrees. +- Audit all cross-environment artifact transfers. + +## Non-goals + +- Replacing provider-native reasoning or protocols with one proprietary agent runtime. +- Hiding all complexity behind an opaque "magic" button. +- Automatically merging unreviewed work. +- Treating chat transcript as canonical project state. +- Creating an independent transport protocol for the Linux node. +- Maintaining feature parity with upstream when it conflicts with this fork's evidence and workflow contract. + +## Definition of product completion + +The fork is fit for daily use only when Nayte can, from desktop and mobile: + +1. add both local and remote projects; +2. create an isolated lane with explicit executor/advisor/verifier roles; +3. see preflight source truth before edits; +4. observe and steer all agents and node jobs; +5. inspect structured changes and checks; +6. recover interrupted work without reconstructing context manually; +7. receive actionable attention notifications; +8. open a verified user-visible deliverable; +9. see the exact evidence behind completion; +10. restart the app and retain all of the above. \ No newline at end of file From 213f22ac6d6b1d9b054e57cf6123446c286307d0 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:08:42 -0400 Subject: [PATCH 03/32] docs: define agent workflow domain model --- docs/macbrains/DOMAIN_MODEL.md | 623 +++++++++++++++++++++++++++++++++ 1 file changed, 623 insertions(+) create mode 100644 docs/macbrains/DOMAIN_MODEL.md diff --git a/docs/macbrains/DOMAIN_MODEL.md b/docs/macbrains/DOMAIN_MODEL.md new file mode 100644 index 000000000000..96291de8a3bb --- /dev/null +++ b/docs/macbrains/DOMAIN_MODEL.md @@ -0,0 +1,623 @@ +# MacBrains Agent Workflow Domain Model + +## Scope + +This document defines the canonical domain additions required to turn T3 Code into the MacBrains agent operating surface while preserving the existing command -> event -> projection -> reactor -> receipt architecture. + +Do not encode these concepts as UI-only state. Anything required for recovery, evidence, multi-device control, or completion must cross typed contracts and persist as events or canonical records. + +## Aggregate boundaries + +### Project + +Existing environment-local workspace aggregate. Extend with policy references, logical project identity, and defaults. + +```ts +interface ProjectPolicy { + sourceTruthPolicyId: string + workflowTemplateId: string | null + defaultAgentTopologyId: string | null + defaultNodePolicyId: string | null + completionPolicyId: string + instructionBundleIds: readonly string[] +} +``` + +The project remains bound to one execution environment and one workspace root. Cross-environment grouping belongs to `RepositoryIdentity` / `LogicalProject`, not to the Project aggregate. + +### WorkLane + +A durable unit of substantial work. It may contain multiple provider threads over its lifetime, but owns one task contract, source-truth snapshot lineage, worktree, plan revisions, acceptance criteria, and completion decision. + +```ts +interface WorkLane { + id: WorkLaneId + projectId: ProjectId + title: string + taskContract: TaskContract + state: WorkLaneState + priority: WorkPriority + environmentId: EnvironmentId + repositoryIdentity: RepositoryIdentity | null + baseRef: GitRef | null + branch: string | null + worktreePath: string | null + ownerAssignmentId: AgentAssignmentId | null + advisorAssignmentIds: readonly AgentAssignmentId[] + verifierAssignmentIds: readonly AgentAssignmentId[] + sourceTruthRevisionId: SourceTruthRevisionId | null + activePlanRevisionId: PlanRevisionId | null + acceptanceCriterionIds: readonly AcceptanceCriterionId[] + requiredReceiptKinds: readonly ReceiptKind[] + deliverableIds: readonly DeliverableId[] + blockerIds: readonly BlockerId[] + createdAt: IsoDateTime + updatedAt: IsoDateTime + completedAt: IsoDateTime | null +} +``` + +### AgentAssignment + +A role-specific durable assignment of one provider instance/model configuration to a lane. + +```ts +interface AgentAssignment { + id: AgentAssignmentId + laneId: WorkLaneId + role: AgentRole + providerInstanceId: ProviderInstanceId + modelIntent: ModelIntent + resolvedModel: string + reasoningLevel: ReasoningLevel + toolPolicyId: string + environmentId: EnvironmentId + threadId: ThreadId | null + parentAssignmentId: AgentAssignmentId | null + ownership: OwnershipBoundary | null + status: AgentAssignmentStatus + contextHealth: ContextHealth + supersedesAssignmentId: AgentAssignmentId | null +} +``` + +### NodeJob + +A bounded execution unit dispatched to a remote environment, especially the Linux node. + +```ts +interface NodeJob { + id: NodeJobId + laneId: WorkLaneId + environmentId: EnvironmentId + sourceRepository: RepositoryIdentity + sourceCommit: string + commandSpec: CommandSpec + inputArtifacts: readonly ArtifactReference[] + expectedOutputs: readonly ExpectedArtifact[] + resourceClass: NodeResourceClass + languagePolicy: NodeLanguagePolicy + status: NodeJobStatus + outputArtifactIds: readonly ArtifactId[] + dispatchedAt: IsoDateTime | null + completedAt: IsoDateTime | null +} +``` + +### Deliverable + +A user-visible output. A lane cannot complete without at least one deliverable unless the task contract explicitly declares `deliverableRequirement: none` and the verifier accepts that declaration. + +```ts +interface Deliverable { + id: DeliverableId + laneId: WorkLaneId + kind: DeliverableKind + title: string + location: DeliverableLocation + launchAction: LaunchAction | null + provenanceReceiptIds: readonly ReceiptId[] + acceptanceReceiptId: ReceiptId | null + visibleOnSurfaces: readonly ClientSurface[] + status: DeliverableStatus +} +``` + +## Value objects + +### TaskContract + +```ts +interface TaskContract { + objective: string + constraints: readonly Constraint[] + nonGoals: readonly string[] + deliverableRequirement: "required" | "none" + requiresPullRequest: boolean + requiresUserVisibleSurface: boolean + authorizedActions: readonly AuthorizedAction[] + prohibitedActions: readonly ProhibitedAction[] + completionReportRequired: true +} +``` + +### AcceptanceCriterion + +```ts +interface AcceptanceCriterion { + id: AcceptanceCriterionId + laneId: WorkLaneId + description: string + category: "foundation" | "correctness" | "reproducibility" | "test" | "delivery" | "performance" | "security" + required: boolean + evidenceRule: EvidenceRule + status: CriterionStatus + supportingReceiptIds: readonly ReceiptId[] +} +``` + +### ModelIntent + +Concrete model names are unstable. Persist intent separately. + +```ts +interface ModelIntent { + capabilityTier: "mechanical" | "standard" | "advanced" | "frontier" + latencyPreference: "fast" | "balanced" | "quality" + costPreference: "free-flat-local-first" | "balanced" | "quality-first" + continuityRequired: boolean + independentVerificationRequired: boolean +} +``` + +### OwnershipBoundary + +```ts +interface OwnershipBoundary { + includePaths: readonly string[] + excludePaths: readonly string[] + symbols: readonly string[] + sharedFiles: readonly SharedFileRule[] + mergeContract: string +} +``` + +### ClaimLabel + +All machine-generated conclusions use: + +```ts +type ClaimLabel = "PROVEN" | "INFERRED" | "SUSPECTED" | "UNKNOWN" +``` + +No additional synonym such as `confirmed`, `likely`, or `validated` may replace the canonical label in persisted evidence. + +## State machines + +### WorkLaneState + +```ts +type WorkLaneState = + | "queued" + | "preflight" + | "oriented" + | "planned" + | "executing" + | "testing" + | "reviewing" + | "deliverable-ready" + | "completed" + | "blocked" + | "failed" + | "cancelled" + | "superseded" + | "recovery-required" +``` + +Allowed normal transitions: + +- `queued -> preflight` +- `preflight -> oriented | blocked | cancelled` +- `oriented -> planned | blocked | cancelled` +- `planned -> executing | blocked | cancelled` +- `executing -> testing | blocked | failed | recovery-required | cancelled` +- `testing -> executing | reviewing | blocked | failed | recovery-required` +- `reviewing -> executing | testing | deliverable-ready | blocked | failed | recovery-required` +- `deliverable-ready -> completed | executing | testing | reviewing | blocked` + +`completed` is terminal except for an explicit `completion.invalidated` event, which moves the lane to `recovery-required` when evidence becomes stale or false. + +### AgentAssignmentStatus + +```ts +type AgentAssignmentStatus = + | "pending" + | "starting" + | "active" + | "waiting" + | "paused" + | "completed" + | "failed" + | "cancelled" + | "superseded" +``` + +### NodeJobStatus + +```ts +type NodeJobStatus = + | "draft" + | "validating" + | "queued" + | "dispatching" + | "running" + | "collecting" + | "completed" + | "failed" + | "cancelled" + | "stale" +``` + +## Commands + +Add commands to `packages/contracts` and enforce invariants in the pure decider. + +### Lane commands + +- `lane.create` +- `lane.task-contract.update` +- `lane.preflight.request` +- `lane.orientation.record` +- `lane.plan.propose` +- `lane.plan.activate` +- `lane.execution.start` +- `lane.testing.start` +- `lane.review.request` +- `lane.deliverable.register` +- `lane.completion.request` +- `lane.block` +- `lane.unblock` +- `lane.cancel` +- `lane.supersede` +- `lane.recovery.request` +- `lane.completion.invalidate` + +### Assignment commands + +- `agent-assignment.create` +- `agent-assignment.start` +- `agent-assignment.steer` +- `agent-assignment.queue-input` +- `agent-assignment.pause` +- `agent-assignment.resume` +- `agent-assignment.interrupt` +- `agent-assignment.complete` +- `agent-assignment.fail` +- `agent-assignment.supersede` + +### Source-truth commands + +- `source-truth.preflight.record` +- `source-truth.conflict.record` +- `source-truth.authority.pin` +- `source-truth.refresh.request` + +### Evidence commands + +- `receipt.record` +- `receipt.supersede` +- `check.define` +- `check.start` +- `check.finish` +- `check.mark-stale` +- `claim.record` + +### Node commands + +- `node-job.create` +- `node-job.validate` +- `node-job.dispatch` +- `node-job.cancel` +- `node-job.artifact.record` +- `node-job.complete` +- `node-job.fail` +- `node-job.integrate.request` +- `node-job.integrate.record` + +### Deliverable commands + +- `deliverable.register` +- `deliverable.launch-action.set` +- `deliverable.acceptance.request` +- `deliverable.acceptance.record` +- `deliverable.invalidate` + +## Events + +Every accepted command emits immutable domain events. Use past tense and preserve intent/result separation. + +Examples: + +- `lane.created` +- `lane.preflight-requested` +- `source-truth.preflight-recorded` +- `lane.oriented` +- `lane.plan-proposed` +- `lane.plan-activated` +- `agent-assignment-created` +- `agent-assignment-started` +- `agent-assignment-steered` +- `check-started` +- `check-finished` +- `check-became-stale` +- `node-job-dispatched` +- `node-job-artifact-recorded` +- `review-recorded` +- `deliverable-registered` +- `ui-acceptance-recorded` +- `completion-requested` +- `completion-rejected` +- `lane-completed` +- `lane-recovery-required` + +## Receipt envelope + +```ts +interface ReceiptEnvelope { + id: ReceiptId + kind: Kind + schemaVersion: number + laneId: WorkLaneId + turnId: TurnId | null + assignmentId: AgentAssignmentId | null + environmentId: EnvironmentId + producedBy: ReceiptProducer + producedAt: IsoDateTime + sourceRevision: SourceRevision + claimLabels: readonly ClaimLabel[] + payload: Payload + contentHash: string + supersedesReceiptId: ReceiptId | null +} +``` + +### SourceRevision + +```ts +interface SourceRevision { + repositoryIdentity: RepositoryIdentity | null + workspaceRoot: string | null + branch: string | null + headSha: string | null + worktreePath: string | null + dirtyFingerprint: string | null + dependencyFingerprint: string | null +} +``` + +A receipt is stale when the source revision no longer matches the evidence rule governing it. + +## Required receipt payloads + +### SourceTruthPreflightReceipt + +- requested path +- repository root +- Git common dir +- branch/detached state +- HEAD +- status porcelain +- staged/unstaged/untracked summaries +- worktree list +- operation state +- remotes with credentials redacted +- instruction files in precedence order +- manifests and build/test candidates +- relevant files and tests +- generated/vendor boundaries +- ownership collision assessment +- unknowns that change action +- safe next action + +### PlanReceipt + +- objective +- plan revision +- files/symbols expected to change +- ownership boundary +- checks mapped to acceptance criteria +- advisor requirement and status +- rollback/recovery strategy +- risks and evidence gaps + +### CommandReceipt + +- exact command argv or structured invocation +- environment and cwd +- redacted environment variables +- start/end timestamps +- exit status/signal +- stdout/stderr artifact references +- source revision before and after + +### CheckReceipt + +- check definition ID +- category and required flag +- command receipt IDs +- status +- assertion summary +- relevant file fingerprint +- logs/artifacts +- skip/block reason + +### ReviewReceipt + +- reviewer assignment +- source truth independently refreshed +- diff reviewed +- checks reviewed +- acceptance criteria verdicts +- findings with severity +- rejected claims +- final verdict + +### UIAcceptanceReceipt + +- client surface +- build/version +- scenario steps +- visible result +- screenshot/video artifact IDs +- launch/access action tested +- result status + +### CompletionReceipt + +- final commit/branch/PR +- final diff summary +- required receipt checklist +- deliverable IDs +- claim ledger +- missing evidence +- possibly wrong or overstated +- exact next action +- what does not count as completion +- safe continuation context + +## Completion invariant + +`lane.completion.request` is accepted only when: + +1. lane state is `deliverable-ready`; +2. active source-truth revision matches final diff/check receipts; +3. all required acceptance criteria are passed; +4. no required check is `not-run`, `failed`, `blocked`, `stale`, or `skipped-with-reason` unless the criterion explicitly permits it; +5. verifier receipt exists and does not reject completion; +6. required deliverables exist; +7. user-visible surface tasks have a passing UI acceptance receipt; +8. no unresolved blocker exists; +9. completion report fields are populated. + +The decider, not the provider adapter or UI, enforces this invariant. + +## Advisor invariant + +An advisor receipt is required before broad or irreversible execution when any applies: + +- architecture or domain model change; +- persistence migration; +- concurrency or lifecycle change; +- security boundary change; +- performance-sensitive hot path; +- destructive repository operation; +- source authorities conflict; +- plan changes after failed implementation; +- task risk is marked high. + +The advisor may approve, approve with constraints, or reject. Rejection blocks execution until a revised plan is activated. + +## Worktree invariant + +A lane marked `substantial` cannot enter `executing` unless: + +- a worktree path is recorded; +- the worktree points to the expected branch and base lineage; +- source-truth preflight covers that path; +- no other active lane owns that same worktree; +- overlap analysis is current. + +A task explicitly classified as `bounded-readonly` or `tiny-reversible` may use the primary checkout if the preflight proves it is safe. + +## Node language invariant + +A Linux-node job is rejected before dispatch when its command, generated script, task prompt, or declared implementation language requires Python. This policy applies to implementation and worker automation; existing project test tooling that invokes Python must be surfaced as a blocker or explicitly waived by project policy, never silently used. + +## Provider integration + +Provider adapters remain responsible for protocol translation only. They emit normalized runtime events including: + +- session lifecycle; +- turn lifecycle; +- text/message deltas; +- tool call lifecycle; +- approval/user-input requests; +- token/context telemetry; +- child-agent lifecycle; +- background activity lease; +- provider-native errors. + +The lane orchestrator maps these events to assignments, receipts, and lane lifecycle. Do not put completion policy inside individual adapters. + +## Background activity lease + +Add a provider-neutral activity lease: + +```ts +interface BackgroundActivityLease { + assignmentId: AgentAssignmentId + source: "provider" | "child-agent" | "node-job" | "reactor" + lastActivityAt: IsoDateTime + expiresAt: IsoDateTime + description: string +} +``` + +The session reaper must not stop a session while a valid lease exists. Leases are refreshed by real provider/child/job activity, not polling the foreground turn state. + +## Projection strategy + +Create separate projections optimized for: + +- command center shell; +- lane detail; +- source-truth panel; +- agent topology; +- checks dashboard; +- deliverables library; +- environment/node jobs; +- notifications; +- audit/evidence export. + +Use incremental sequence-based subscriptions. Do not append full logs or full receipt payloads to the shell projection. + +## Persistence and recovery + +- Canonical domain events remain append-only. +- Projection state is rebuildable. +- Critical JSON catalogs use atomic temp-write + fsync + rename + last-known-good backup. +- Receipt artifacts are content-addressed and immutable. +- Provider replay events are deduplicated by stable provider item identity and session/resume epoch. +- Corrupt optional caches degrade gracefully and surface a recovery action. +- Recovery agents receive a generated handoff derived from canonical state, not the previous agent's prose summary alone. + +## Notification derivation + +Notifications are projections from events and attention rules. They are not directly emitted ad hoc by providers. + +Each notification includes: + +- severity; +- lane/project/environment; +- source event and receipt; +- exact action route; +- deduplication key; +- resolved state. + +## Migration + +Introduce schema versions and compatibility transforms for existing projects and threads. + +Migration defaults: + +- existing threads without lanes become one imported lane per thread; +- imported lanes receive `taskContract.objective` from the first user message or thread title and are marked `UNKNOWN` quality; +- existing worktree metadata is preserved; +- existing checkpoints remain attached to the imported lane; +- existing provider sessions become executor assignments; +- no historical thread is falsely marked completed under the new invariant; +- imported lanes default to `recovery-required` or `queued` depending on current session state. + +## Naming + +Use `lane` for the durable work package, `thread` for provider conversation history, `assignment` for a role-bound agent instance, `job` for bounded remote execution, `receipt` for durable evidence, and `deliverable` for a user-visible output. Do not overload these terms. \ No newline at end of file From a1f431fb0a43a6e1b1a4fc58c19ff2393206aba4 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:09:43 -0400 Subject: [PATCH 04/32] docs: add dependency-ordered implementation ledger --- docs/macbrains/IMPLEMENTATION_LEDGER.md | 652 ++++++++++++++++++++++++ 1 file changed, 652 insertions(+) create mode 100644 docs/macbrains/IMPLEMENTATION_LEDGER.md diff --git a/docs/macbrains/IMPLEMENTATION_LEDGER.md b/docs/macbrains/IMPLEMENTATION_LEDGER.md new file mode 100644 index 000000000000..a88d78617c11 --- /dev/null +++ b/docs/macbrains/IMPLEMENTATION_LEDGER.md @@ -0,0 +1,652 @@ +# MacBrains T3 Code Implementation Ledger + +## How to use this ledger + +This is the authoritative dependency order for implementation. It is not a phased roadmap that permits partial product claims. Agents may execute independent packages in parallel only when ownership boundaries do not overlap. The fork is not complete until every required package and the end-to-end acceptance matrix pass. + +For every package: + +1. run source-truth preflight in an isolated worktree; +2. read repository-scoped instructions; +3. inspect current implementation and focused tests; +4. create a plan receipt; +5. request advisor review when required; +6. implement the smallest coherent change; +7. run focused tests, lint, typecheck, build, and UI acceptance as applicable; +8. produce diff, check, review, and completion receipts; +9. update this ledger with evidence links, not prose confidence. + +Status vocabulary: `NOT_STARTED`, `IN_PROGRESS`, `BLOCKED`, `IMPLEMENTED_UNVERIFIED`, `PROVEN`, `SUPERSEDED`. + +## Foundation dependency graph + +```text +F0 source truth + lane contracts + -> F1 receipt/evidence substrate + -> F2 completion/check invariants + -> F3 assignment/topology + -> F4 worktree ownership + -> F5 command-center projections + -> F6 client surfaces + +F0 -> N0 environment/node contracts -> N1 node job runtime -> N2 artifact return/integration +F3 -> P0 provider normalization -> P1 background leases -> P2 context/recovery +F1 -> R0 persistence hardening -> R1 corruption recovery +F2 -> D0 deliverables -> D1 UI acceptance -> D2 notifications +F4 -> G0 GitHub/change-request integration +All foundations -> E0 end-to-end acceptance +``` + +--- + +## F0 — Work lane and source-truth contracts + +**Status:** NOT_STARTED + +**Objective:** Add the durable `WorkLane`, `TaskContract`, `AcceptanceCriterion`, `SourceTruthRevision`, lifecycle state, commands, events, decider invariants, projector support, persistence schema, and compatibility migration. + +**Primary areas:** + +- `packages/contracts/src/orchestration.ts` or dedicated explicit subpaths; +- `apps/server/src/orchestration/decider.ts`; +- `apps/server/src/orchestration/commandInvariants.ts`; +- `apps/server/src/orchestration/projector.ts`; +- projection persistence/services; +- `packages/client-runtime` shell/detail models; +- focused orchestration tests. + +**Required behavior:** + +- lanes persist independently of provider sessions; +- existing threads migrate without false completion; +- lane lifecycle transitions are pure and deterministic; +- source-truth revisions can be recorded and superseded; +- substantial work cannot enter execution without current preflight and worktree evidence; +- command metadata is idempotent; +- web/mobile can decode old and new server payloads during migration. + +**Advisor required:** yes — domain and persistence architecture. + +**Proof:** contract decode tests, decider transition matrix tests, projection replay tests, migration tests, focused typecheck. + +--- + +## F1 — Typed receipt and evidence substrate + +**Status:** NOT_STARTED + +**Objective:** Replace transcript-only proof with persisted typed receipts and content-addressed log/artifact references. + +**Primary areas:** + +- `packages/contracts/src/receipts.ts` (new explicit subpath); +- server receipt store and query service; +- `RuntimeReceiptBus` integration; +- projection and RPC methods; +- artifact storage service; +- web/mobile evidence components. + +**Required behavior:** + +- receipt envelopes carry lane, assignment, environment, source revision, producer, timestamps, schema version, labels, payload, and hash; +- large logs live as artifacts, not shell projection payloads; +- receipts can supersede earlier receipts without mutation; +- queries filter by lane, kind, source revision, criterion, check, assignment, and time; +- exported evidence has deterministic ordering and hashes; +- every async reactor emits a completion or failure receipt. + +**Proof:** hashing tests, serialization/migration tests, artifact immutability tests, query tests, replay tests, web/mobile render tests. + +--- + +## F2 — Checks, acceptance criteria, and completion gate + +**Status:** NOT_STARTED + +**Objective:** Make completion a deterministic orchestration decision based on current evidence. + +**Primary areas:** + +- acceptance/check contracts; +- decider completion invariants; +- check staleness service; +- check command reactor; +- completion reactor; +- checks UI and criterion mapping. + +**Required behavior:** + +- checks have canonical status values; +- required skipped or stale checks prevent completion unless criterion policy explicitly permits them; +- source/dependency changes invalidate relevant checks; +- provider text cannot directly mark a lane complete; +- completion receipt contains the mandated closeout fields; +- invalid evidence can reopen a completed lane as `recovery-required`. + +**Proof:** full transition truth table, stale fingerprint tests, false-completion regression tests, completion invalidation tests, UI status tests. + +--- + +## F3 — Agent assignments and topology + +**Status:** NOT_STARTED + +**Objective:** Model executor, advisor, verifier, explorer, child worker, and recovery roles as visible durable assignments. + +**Primary areas:** + +- assignment contracts/events/projectors; +- provider session directory integration; +- role preset settings; +- assignment-to-thread binding; +- agent topology projection; +- desktop/web/mobile topology UI. + +**Required behavior:** + +- each assignment records provider instance, model intent, resolved model, reasoning, tools, environment, and ownership; +- executor/advisor/verifier are distinct roles; +- child agents are visible and attached to explicit parents; +- multiple editors cannot own overlapping paths without an explicit shared-file merge contract; +- recovery assignments supersede but do not erase prior contexts; +- model changes and escalation causes are recorded. + +**Proof:** assignment lifecycle tests, ownership conflict tests, provider restart/resume tests, topology rendering tests, multi-device sync tests. + +--- + +## F4 — Worktree and ownership manager + +**Status:** NOT_STARTED + +**Objective:** Promote worktrees from optional thread metadata to managed lane isolation with overlap protection. + +**Primary areas:** + +- Git contracts/services; +- worktree creation/bootstrap/removal; +- lane bootstrap command; +- ownership index; +- VCS event invalidation; +- UI creation and status surfaces. + +**Required behavior:** + +- substantial lanes default to a new worktree; +- base branch/SHA and lane branch are explicit; +- bootstrap scripts run with structured receipts; +- worktree ownership is exclusive; +- path overlap is computed across active lanes; +- destructive removal requires a clean/safe proof or explicit force intent; +- remote environments support project/worktree creation from GUI; +- no pattern-based process killing. + +**Proof:** real temporary-repository integration tests, branch-with-slash tests, dirty worktree refusal tests, remote RPC tests, collision tests, restart recovery tests. + +--- + +## F5 — Command Center and projections + +**Status:** NOT_STARTED + +**Objective:** Replace chat-first landing with an evidence-first command center using compact incremental projections. + +**Primary areas:** + +- server command-center shell projection; +- client-runtime atoms/services; +- web layout/navigation; +- mobile home/navigation; +- desktop wrapper integration. + +**Required behavior:** + +- sections for attention, active work, review, ready-to-use, recent evidence, and node activity; +- cards show project/lane/environment/role/model/state/last receipt/next action; +- no full receipt/log payloads in shell data; +- sequence-based incremental updates and cache hydration; +- all cards deep-link to exact evidence/action; +- large lists are virtualized; +- no repainting animations. + +**Proof:** projection delta tests, reconnect/cache tests, performance measurement with large seeded data, web/mobile integrated screenshots. + +--- + +## F6 — Lane detail and cross-surface controls + +**Status:** NOT_STARTED + +**Objective:** Deliver the complete lane workspace on web, desktop, iPhone, and iPad. + +**Required tabs/panels:** + +- Overview/lifecycle +- Agent topology +- Source truth +- Plan revisions +- Transcript/live activity +- Changes/diff +- Checks +- Receipts/evidence +- Node jobs +- Deliverables +- Recovery/history + +**Required controls:** + +- start, steer, queue, pause, resume, interrupt; +- request advisor/verifier; +- retry check/node job; +- resolve blocker; +- refresh source truth; +- checkpoint restore; +- open editor/terminal/repository/PR/artifact; +- create remote projects and worktrees. + +**Proof:** shared runtime tests, web/mobile parity checklist, real-client acceptance with persisted restart and remote connection. + +--- + +## P0 — Provider event normalization + +**Status:** NOT_STARTED + +**Objective:** Normalize all supported provider adapters into assignment-aware lifecycle, tool, child-agent, context, and background activity events. + +**Providers requiring an explicit implementation decision:** + +- Codex +- Claude Code +- Cursor +- Grok Build +- OpenCode + +**Required behavior:** + +- provider-instance routing remains intact; +- replay/resume events are deduplicated; +- child agents emit visible lifecycle events; +- model/reasoning/options are reported when available; +- unsupported features emit capability records rather than silently disappearing; +- provider errors preserve structured cause and recovery advice; +- adapter complexity does not leak into pure orchestration. + +**Proof:** contract tests per adapter, replay/resume regressions, child-agent tests, capability matrix snapshots. + +--- + +## P1 — Background activity leases and session reaper correctness + +**Status:** NOT_STARTED + +**Objective:** Prevent legitimate background workflows and subagents from being killed after foreground turns settle. + +**Primary areas:** + +- `ProviderSessionReaper`; +- provider runtime ingestion; +- child-agent/job event handling; +- activity lease store/projection. + +**Required behavior:** + +- active background work refreshes an explicit lease; +- reaper refuses to stop sessions with valid leases; +- abandoned leases expire deterministically; +- UI shows why a session remains alive; +- lease recovery works after server restart when provider state proves activity; +- no polling-based fake activity. + +**Proof:** deterministic clock tests, long-running subagent simulation, restart tests, stale lease cleanup. + +--- + +## P2 — Context health, compaction, and recovery handoff + +**Status:** NOT_STARTED + +**Objective:** Make context usage honest and recovery automatic before or after provider context failure. + +**Required behavior:** + +- track provider-reported usage with event epochs; +- compaction reduces/reset usage instead of monotonic `Math.max` ratcheting; +- create structured handoff from canonical lane state; +- recovery agent can resume with source truth, plan, diff, checks, blockers, and exact next action; +- preserve productive explorer context while verifying claims elsewhere; +- context exhaustion cannot erase running node/child work. + +**Proof:** compaction regression tests, handoff schema tests, provider switch recovery, context-limit simulation. + +--- + +## N0 — First-class environment and node policy + +**Status:** NOT_STARTED + +**Objective:** Extend existing execution environments with node capabilities, policies, and project management parity. + +**Required behavior:** + +- Mac and Linux node are explicit environments; +- environment capability snapshot includes OS, architecture, tools, provider availability, service status, resources, and reachable endpoints; +- remote GUI can add projects and worktrees; +- node policy records no-Python constraint; +- Tailscale remains an endpoint provider, not a new environment type; +- environment setup/version drift is visible. + +**Proof:** environment capability tests, remote project creation integration, Tailscale/direct/SSH connection tests, policy validation tests. + +--- + +## N1 — Node job runtime + +**Status:** NOT_STARTED + +**Objective:** Dispatch bounded heavy work to the Linux node with immutable inputs and typed execution receipts. + +**Required behavior:** + +- job source commit and input hashes fixed before dispatch; +- validate command/toolchain/language policy; +- queue, cancel, reconnect, and recover jobs; +- stream bounded summaries while preserving complete logs as artifacts; +- collect outputs content-addressably; +- resource telemetry and failure cause visible; +- no claim of integration on completion. + +**Proof:** local fake-node tests, real remote environment test, cancellation, disconnect/reconnect, output hash validation, Python rejection tests. + +--- + +## N2 — Artifact return and Mac integration + +**Status:** NOT_STARTED + +**Objective:** Separate node computation completion from repository integration and Mac verification. + +**Required behavior:** + +- artifacts have provenance and hash; +- return transfer is resumable and authenticated; +- integrating an artifact creates a distinct event and diff; +- Mac-side checks must rerun where applicable; +- stale source commit blocks blind integration; +- final receipt traces node inputs -> outputs -> integrated files -> Mac verification. + +**Proof:** interrupted transfer, hash mismatch, stale source, clean integration, conflict path, end-to-end benchmark bundle. + +--- + +## R0 — Atomic persistence and last-known-good recovery + +**Status:** NOT_STARTED + +**Objective:** Eliminate single-write corruption paths for critical connection/settings/catalog state. + +**Required behavior:** + +- temp file in same filesystem; +- write and flush; +- fsync file and parent directory where supported; +- atomic rename; +- schema validation before adoption; +- rotate last-known-good backup; +- never rewrite unchanged content on a fixed cadence; +- expose corruption diagnostics and recovery action. + +**Targets:** connection catalog, settings, credentials metadata, environment registrations, any non-SQLite critical JSON state. + +**Proof:** fault-injection tests at each write boundary, NUL/truncated/corrupt file recovery, unchanged-write suppression. + +--- + +## R1 — Event/projection and heavy-thread resilience + +**Status:** NOT_STARTED + +**Objective:** Prevent large threads, logs, and diffs from crashing or bricking a workspace. + +**Required behavior:** + +- paginate/stream thread detail; +- bounded message and activity rendering; +- large payload artifacts instead of inline state; +- projection rebuild path; +- isolate one corrupt thread rather than failing the environment; +- diagnostic recovery UI without deleting `.t3`; +- memory/CPU budgets measured. + +**Proof:** seeded heavy-thread soak, corrupt event/projection simulation, restart recovery, mobile constrained-memory pass. + +--- + +## D0 — Deliverable registry and launch actions + +**Status:** NOT_STARTED + +**Objective:** Make finished work persistently visible and launchable. + +**Deliverable kinds:** application, dashboard, build, installer, PR, patch, report, benchmark, dataset, artifact bundle, URL, service. + +**Required behavior:** + +- provenance receipts required; +- launch action tested and scoped by surface/environment; +- invalidated or missing outputs cannot remain "ready"; +- completed lane links to deliverable; +- deliverables appear in command center and library after restart; +- support local file, remote file, URL, repository/branch/PR, app launch, and service open actions. + +**Proof:** kind-specific contract tests, missing-path invalidation, cross-device presentation, restart persistence. + +--- + +## D1 — User-visible UI acceptance + +**Status:** NOT_STARTED + +**Objective:** Require integrated proof that user-facing changes can be seen and used. + +**Required behavior:** + +- scenario definitions tied to acceptance criteria; +- web, desktop, and mobile applicability decision; +- screenshot/video artifacts; +- exact build/version/environment; +- launch route and observed result; +- failure blocks completion. + +**Proof:** integrated `test-t3-app`, `test-t3-mobile`, desktop smoke/packaged app path as applicable; artifacts attached to receipt. + +--- + +## D2 — Actionable notifications + +**Status:** NOT_STARTED + +**Objective:** Deliver persistent attention signals with exact actions. + +**Required triggers:** approvals/input, blocker/failure, advisor conflict, verifier rejection, check/CI transition, node completion/failure, PR review, deliverable ready, stale lane, environment disconnect, recovery required. + +**Required behavior:** + +- event-derived, deduplicated, resolveable; +- desktop/mobile push where platform permits; +- clicking opens exact lane/receipt/action; +- user-configurable severity and quiet rules; +- no generic completion notification before completion invariant passes. + +**Proof:** notification projection tests, dedupe/resolution, deep-link tests, mobile/desktop integrated pass. + +--- + +## G0 — GitHub evidence and delivery integration + +**Status:** NOT_STARTED + +**Objective:** Connect lanes to current GitHub repository, issue, PR, checks, reviews, and merge state. + +**Required behavior:** + +- detect repository identity robustly for HTTPS/SSH/self-hosted remotes; +- link issue/PR to lane; +- show current PR diff/checks/reviews/threads; +- refresh current holder/state rather than trust cached narrative; +- request/create PR only when task contract authorizes it; +- verify target branch and rebase/update before PR; +- keep PR review findings as receipts and blockers; +- support GitHub from remote environments. + +**Proof:** public/private repo cases, SSH user variants, stale PR head, review-thread lifecycle, checks update, auth failure. + +--- + +## S0 — Project instruction and skill registry + +**Status:** NOT_STARTED + +**Objective:** Make reusable workflows discoverable, versioned, visible, and subordinate to repository authority. + +**Required behavior:** + +- register instruction bundles and skills per project/environment; +- show exactly which were loaded and precedence; +- validate skill structure/hash/version; +- declare required connectors/tools and input/output contract; +- prevent skill from silently overriding repository instructions; +- support export/import and usage history; +- expose a workflow template for ANVIL package execution and source-truth preflight. + +**Proof:** precedence tests, incompatible/invalid skill handling, version migration, UI visibility. + +--- + +## U0 — MacBrains branding and fork identity + +**Status:** NOT_STARTED + +**Objective:** Make the fork unmistakably MacBrains without damaging maintainability. + +**Required behavior:** + +- product name and official MacBrains logo assets; +- app IDs, package names, data directories, update channels, and signing identifiers do not collide with upstream T3 Code; +- migration/import from upstream profile is explicit and reversible; +- About screen shows upstream commit and fork commit; +- upstream remote/sync documentation exists; +- no accidental calls to upstream hosted services unless intentionally configured. + +**Proof:** package metadata inspection, side-by-side install, clean profile, import test, DMG and mobile identity checks. + +--- + +## U1 — Settings and policy editor + +**Status:** NOT_STARTED + +**Objective:** Expose project, role, model, node, completion, notification, and source-authority policies without burying them. + +**Required behavior:** + +- global defaults with environment/project/lane overrides; +- effective-policy view explains origin and precedence; +- role presets and fallback model intent; +- no-Python node policy locked for Linux node unless Nayte explicitly changes contract; +- runtime access is per lane/project, not only global binary switch; +- exportable policy JSON with schema version; +- reverse action for every setting mutation. + +**Proof:** precedence and migration tests, web/mobile settings parity, secret redaction. + +--- + +## V0 — Performance and telemetry hardening + +**Status:** NOT_STARTED + +**Objective:** Keep the control plane responsive under many projects, lanes, receipts, VCS refs, and logs. + +**Required behavior:** + +- no idle VCS request storms; +- event-driven invalidation and bounded refresh; +- slow request diagnostics; +- per-projection payload metrics; +- CPU/memory thresholds in seeded soak tests; +- mobile summarized subscriptions; +- UI thread avoids log parsing/diff computation; +- resource monitor covers server and provider child processes. + +**Proof:** clean-profile idle soak, 100-project/1000-lane seeded benchmark, heavy thread, mobile scrolling, reconnect under load. + +--- + +## E0 — End-to-end daily workflow acceptance + +**Status:** NOT_STARTED + +**Objective:** Prove the entire system as one usable workflow rather than isolated feature demonstrations. + +**Canonical scenario:** + +1. Open MacBrains T3 Code on the Mac. +2. Pair iPhone/iPad over Tailscale-authenticated endpoint. +3. Add a Mac project and corresponding Linux-node clone from GUI. +4. Create a substantial feature lane from mobile. +5. System creates isolated worktree and records source-truth preflight. +6. Executor begins with medium reasoning. +7. Advisor reviews the oriented plan before broad edits. +8. Executor edits in declared ownership boundary. +9. Heavy build/test job dispatches to Linux node without Python. +10. Node returns content-addressed artifacts. +11. Mac integrates and reruns applicable checks. +12. Verifier independently refreshes source truth and reviews diff/evidence. +13. User-visible app/dashboard change is launched and captured. +14. Completion gate produces final receipt and deliverable. +15. Mobile receives actionable ready-to-use notification. +16. Restart Mac app and Linux service. +17. All lane state, evidence, deliverables, and controls remain available. +18. Simulate provider failure; recovery assignment resumes from canonical handoff. +19. Simulate stale source/check; completion invalidates and returns to recovery. +20. Restore proof and re-complete. + +**Proof:** one timestamped acceptance bundle containing video/screenshots, event/receipt export, exact commits, command receipts, checks, environment snapshots, and restart/recovery evidence. + +--- + +## Implementation ownership recommendation + +Use non-overlapping lanes: + +- Contracts/domain lane: F0, F1 schemas only. +- Server orchestration lane: F0/F1/F2 decider/projector/reactors. +- Worktree/Git lane: F4, G0. +- Provider lane: P0, P1, P2. +- Environment/node lane: N0, N1, N2. +- Persistence/reliability lane: R0, R1. +- Client-runtime lane: shared services/projections. +- Web/desktop lane: F5/F6/D0/D1/D2/U0/U1. +- Mobile lane: F5/F6/D1/D2/U0/U1 mobile-specific. +- Performance/verifier lane: V0 and independent review across all packages. + +Shared contract files require a designated single owner and serialized integration. Do not let parallel agents edit `packages/contracts/src/orchestration.ts` simultaneously; split new schemas into explicit subpath files and integrate through one owner. + +## Merge order + +1. F0 contracts and migration +2. F1 receipts +3. F2 completion/checks +4. F3 assignments +5. F4 worktrees +6. P0/P1/P2 providers +7. N0/N1/N2 environments/node +8. R0/R1 reliability +9. client-runtime projections +10. F5/F6/D0/D1/D2 UI +11. G0/S0/U0/U1 integrations and identity +12. V0 performance +13. E0 acceptance + +Every integration branch must rebase on current fork main, run focused tests for its surface, and be independently reviewed before merge. \ No newline at end of file From e0131360ae8e29380a650438d3d057b151628bf5 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:10:30 -0400 Subject: [PATCH 05/32] docs: add full acceptance matrix --- docs/macbrains/ACCEPTANCE_MATRIX.md | 297 ++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 docs/macbrains/ACCEPTANCE_MATRIX.md diff --git a/docs/macbrains/ACCEPTANCE_MATRIX.md b/docs/macbrains/ACCEPTANCE_MATRIX.md new file mode 100644 index 000000000000..5d05ded4e8f8 --- /dev/null +++ b/docs/macbrains/ACCEPTANCE_MATRIX.md @@ -0,0 +1,297 @@ +# MacBrains T3 Code Acceptance Matrix + +## Rules + +- `PROVEN` requires observed tool output or an attached artifact from the exact tested revision. +- `INFERRED` cannot satisfy a required acceptance criterion. +- A passing test from an older HEAD is stale. +- A skipped required check is not a pass. +- UI functionality requires integrated evidence on every applicable surface. +- The final verifier must independently inspect current repository state and receipts. + +Each row must be updated with: + +- status; +- commit SHA; +- environment/build version; +- receipt IDs; +- artifact links; +- tester/verifier assignment; +- exact failures or caveats. + +## A. Repository and source truth + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| A01 | Every substantial lane records repository root, branch, HEAD, worktree, dirty state, remotes, operations, instructions, manifests, relevant files, tests, and ownership risks before edits | Preflight receipt from exact worktree | NOT_STARTED | +| A02 | Narrative memory cannot override contradictory repository or canonical-source evidence | Conflict test with persisted authorities and blocked transition | NOT_STARTED | +| A03 | Source truth can be refreshed and revisions are preserved | Event/projection test and UI revision history | NOT_STARTED | +| A04 | Active merge/rebase/cherry-pick/bisect/revert state blocks unsafe execution | Integration tests in temporary repos | NOT_STARTED | +| A05 | Generated and vendored boundaries are visible and respected | Preflight fixture and ownership test | NOT_STARTED | +| A06 | Unknowns that change action become typed blockers; irrelevant unknowns do not halt work | Decider tests | NOT_STARTED | + +## B. Work lanes and lifecycle + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| B01 | Lane persists independently of provider session/thread | Restart and provider replacement test | NOT_STARTED | +| B02 | Lifecycle allows only documented transitions | Exhaustive transition matrix | NOT_STARTED | +| B03 | Substantial lane cannot execute without current preflight/worktree | Negative decider tests | NOT_STARTED | +| B04 | Provider message cannot mark lane completed | Regression test | NOT_STARTED | +| B05 | Completion can be invalidated by stale/false evidence | End-to-end invalidation test | NOT_STARTED | +| B06 | Existing threads migrate without false completion | Migration fixtures | NOT_STARTED | +| B07 | Cancel, supersede, block, unblock, and recovery states have reverse/continuation paths | Command/event/projection tests and UI controls | NOT_STARTED | + +## C. Worktrees and ownership + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| C01 | Substantial work creates or attaches to an isolated worktree by default | Real Git integration test | NOT_STARTED | +| C02 | UI shows base SHA, branch, worktree path, status, and owner | Web/mobile screenshots | NOT_STARTED | +| C03 | Same worktree cannot be owned by two active lanes | Invariant test | NOT_STARTED | +| C04 | Overlapping file/path ownership is detected before execution and merge | Multi-lane fixture | NOT_STARTED | +| C05 | Shared-file work requires an explicit merge contract | Negative/positive tests | NOT_STARTED | +| C06 | Dirty worktree deletion/refactoring is refused without typed force intent | Real Git test | NOT_STARTED | +| C07 | Remote GUI creates project and worktree on Linux node | Desktop/mobile remote integration | NOT_STARTED | +| C08 | Worktree setup scripts emit structured receipts and do not touch live profile | Integration test | NOT_STARTED | + +## D. Agent topology and routing + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| D01 | Executor, advisor, verifier, explorer, child, and recovery roles are durable and visible | Contract/projector/UI tests | NOT_STARTED | +| D02 | Assignment records provider instance, model intent, resolved model, reasoning, tools, environment, and ownership | Schema roundtrip and UI | NOT_STARTED | +| D03 | Architecture/high-risk plan requires advisor before execution | Invariant tests | NOT_STARTED | +| D04 | Verifier independently refreshes source truth | Review receipt validation test | NOT_STARTED | +| D05 | Parallel editing is blocked without disjoint ownership | Assignment conflict tests | NOT_STARTED | +| D06 | Child agents are visible with parent, task, ownership, status, and output | Provider simulation and UI | NOT_STARTED | +| D07 | Recovery agent supersedes failed context without erasing history | Recovery scenario | NOT_STARTED | +| D08 | Model escalation records task-specific cause | Routing event test | NOT_STARTED | +| D09 | Saved workflows survive model name removal by resolving model intent | Capability-resolution migration test | NOT_STARTED | + +## E. Provider behavior + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| E01 | Codex adapter maps normalized lifecycle/tool/child/context events | Adapter test suite | NOT_STARTED | +| E02 | Claude Code adapter maps normalized events | Adapter test suite | NOT_STARTED | +| E03 | Cursor adapter maps normalized events and does not replay duplicate history on resume | Regression test | NOT_STARTED | +| E04 | Grok Build adapter has explicit capability matrix | Contract snapshot | NOT_STARTED | +| E05 | OpenCode adapter has explicit capability matrix | Contract snapshot | NOT_STARTED | +| E06 | Unsupported features are visible, not silently absent | Capability UI test | NOT_STARTED | +| E07 | Provider instance/account routing remains correct across restart | Multi-instance integration | NOT_STARTED | +| E08 | Context meter reflects compaction rather than monotonically increasing | Compaction regression | NOT_STARTED | +| E09 | Provider replay is deduplicated across session/resume epochs | Replay fixtures | NOT_STARTED | + +## F. Background work and context recovery + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| F01 | Background subagent/job activity keeps session alive through explicit lease | Deterministic reaper test | NOT_STARTED | +| F02 | Foreground turn completion alone does not kill active background work | Regression scenario > threshold virtual clock | NOT_STARTED | +| F03 | Abandoned leases expire and session is reaped | Virtual clock test | NOT_STARTED | +| F04 | Context handoff contains task, source truth, plan, edits, checks, blockers, and next action | Schema and snapshot test | NOT_STARTED | +| F05 | Provider switch/recovery resumes from canonical handoff | End-to-end recovery | NOT_STARTED | +| F06 | Running node work survives provider context replacement | Cross-runtime test | NOT_STARTED | + +## G. Receipts and evidence + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| G01 | Every receipt has stable ID, schema version, lane, environment, producer, timestamp, source revision, labels, payload, and hash | Schema tests | NOT_STARTED | +| G02 | Content hash detects mutation | Tamper test | NOT_STARTED | +| G03 | Large logs are artifact references, not shell projection payloads | Payload-size test | NOT_STARTED | +| G04 | Superseded receipts remain auditable | Projection/query test | NOT_STARTED | +| G05 | Evidence export is deterministic and hash-verifiable | Repeated export comparison | NOT_STARTED | +| G06 | Claims use only PROVEN/INFERRED/SUSPECTED/UNKNOWN | Validation tests | NOT_STARTED | +| G07 | Every reactor emits success or failure receipt | Reactor test coverage | NOT_STARTED | +| G08 | Receipt query supports lane/kind/criterion/check/assignment/revision filters | Query tests | NOT_STARTED | + +## H. Checks and completion + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| H01 | Check statuses exactly match canonical vocabulary | Schema test | NOT_STARTED | +| H02 | Required failed, blocked, stale, not-run, or unpermitted skipped check blocks completion | Truth-table tests | NOT_STARTED | +| H03 | Relevant source or dependency change marks check stale | Fingerprint integration | NOT_STARTED | +| H04 | Completion requires verifier receipt | Negative invariant test | NOT_STARTED | +| H05 | UI change requires passing UI acceptance receipt | Negative invariant test | NOT_STARTED | +| H06 | Completion report includes six mandated closeout fields | Schema and UI test | NOT_STARTED | +| H07 | No unresolved blocker at completion | Invariant test | NOT_STARTED | +| H08 | Completion receipt links final commit/branch/PR, diff, checks, deliverables, and claims | End-to-end receipt | NOT_STARTED | + +## I. Linux node + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| I01 | Linux node is a first-class authenticated environment | Environment UI and API test | NOT_STARTED | +| I02 | Node capability snapshot includes OS/arch/toolchain/resources/service/providers | Snapshot test | NOT_STARTED | +| I03 | Node job pins source commit and input hashes | Contract/invariant test | NOT_STARTED | +| I04 | Python implementation, worker prompt, and generated script are rejected | Validation fixtures | NOT_STARTED | +| I05 | Job supports queue, cancel, disconnect/reconnect, recovery | Integration tests | NOT_STARTED | +| I06 | Full logs preserved as artifacts; UI receives bounded summaries | Payload and artifact test | NOT_STARTED | +| I07 | Outputs are content-addressed and hash-verified | Tamper/transfer tests | NOT_STARTED | +| I08 | Node completion is distinct from Mac integration | State-machine test | NOT_STARTED | +| I09 | Stale source blocks blind integration | Integration negative test | NOT_STARTED | +| I10 | Mac reruns applicable verification after integration | End-to-end receipt chain | NOT_STARTED | +| I11 | ANVIL is uppercase except exact case-sensitive values | Policy validation tests | NOT_STARTED | + +## J. Remote and mobile + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| J01 | iPhone/iPad can pair over authenticated Tailscale-compatible endpoint | Real-device evidence | NOT_STARTED | +| J02 | Mobile can add remote project | Real-device evidence | NOT_STARTED | +| J03 | Mobile can create isolated lane/worktree | Real-device evidence | NOT_STARTED | +| J04 | Mobile can start, steer, queue, pause, resume, interrupt, and recover | Scenario recordings | NOT_STARTED | +| J05 | Mobile shows source truth, agents, diff, checks, receipts, node jobs, blockers, and deliverables | Screenshots and navigation test | NOT_STARTED | +| J06 | Offline cache is clearly distinguished from live state | Connection-state tests | NOT_STARTED | +| J07 | Reconnect does not allow stale cache to overwrite newer live data | Deterministic runtime test | NOT_STARTED | +| J08 | Hosted HTTPS app refuses insecure mixed-content backend path with clear guidance | Browser test | NOT_STARTED | +| J09 | Remote project/worktree management no longer requires server-side CLI workaround | End-to-end proof | NOT_STARTED | + +## K. Command Center and UX + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| K01 | Command Center is default landing surface | Web/desktop/mobile screenshot | NOT_STARTED | +| K02 | Needs-attention section is event-derived and actionable | Projection/deep-link tests | NOT_STARTED | +| K03 | Active work shows all environments and role/model state | Seeded data test | NOT_STARTED | +| K04 | Ready-for-review and ready-to-use are distinct states | Projection/UI tests | NOT_STARTED | +| K05 | Node activity visible without opening transcript | UI test | NOT_STARTED | +| K06 | Every card deep-links to exact lane/evidence/action | Navigation tests | NOT_STARTED | +| K07 | No continuously repainting animation | Code review and performance capture | NOT_STARTED | +| K08 | Large lists are virtualized and responsive | Seeded benchmark | NOT_STARTED | +| K09 | Chat remains available but is not the sole project state surface | UX acceptance | NOT_STARTED | + +## L. Diff, plans, and review + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| L01 | Plans are versioned and strategy changes record rationale | Event/projection tests | NOT_STARTED | +| L02 | Plan maps files/symbols/checks to acceptance criteria | UI and schema test | NOT_STARTED | +| L03 | Diff supports lane, staged/unstaged, base-to-head, and commits | Integration UI | NOT_STARTED | +| L04 | Cross-lane overlaps visible before merge | Multi-lane UI | NOT_STARTED | +| L05 | Generated/vendor filtering available | Diff fixture | NOT_STARTED | +| L06 | Tests trace to changed behavior/acceptance criteria | Check mapping UI | NOT_STARTED | +| L07 | Verifier findings carry severity, evidence, status, and resolution | Review workflow test | NOT_STARTED | +| L08 | Restore checkpoint does not erase audit history | Checkpoint/recovery test | NOT_STARTED | + +## M. Deliverables and visibility + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| M01 | Finished lane has persistent deliverable or explicit approved none | Completion invariant | NOT_STARTED | +| M02 | Deliverable records provenance and acceptance receipts | Schema test | NOT_STARTED | +| M03 | Launch/open action is tested | UI acceptance receipt | NOT_STARTED | +| M04 | Missing/invalid output becomes invalidated, not ready | Filesystem/URL test | NOT_STARTED | +| M05 | Deliverables appear after restart on desktop/mobile | Persistence scenario | NOT_STARTED | +| M06 | App/dashboard changes include obvious access path | End-to-end user acceptance | NOT_STARTED | +| M07 | Hidden daemon/log/database-only result cannot satisfy visible delivery | Negative completion test | NOT_STARTED | + +## N. Notifications + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| N01 | Approval/input notification opens exact request | Mobile/desktop deep link | NOT_STARTED | +| N02 | Failure/blocker notification opens exact evidence and action | Scenario test | NOT_STARTED | +| N03 | Advisor conflict and verifier rejection are distinct | Projection tests | NOT_STARTED | +| N04 | Check/CI/PR state changes deduplicate | Notification tests | NOT_STARTED | +| N05 | Node completion/failure notification distinguishes integration status | Scenario test | NOT_STARTED | +| N06 | Ready-to-use notification only after completion invariant | Negative/positive tests | NOT_STARTED | +| N07 | Notifications persist and resolve across devices | Multi-device test | NOT_STARTED | + +## O. GitHub integration + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| O01 | Detect GitHub remotes for HTTPS and arbitrary SCP-style SSH user | Parser tests | NOT_STARTED | +| O02 | Private repository access failure is actionable | Integration error test | NOT_STARTED | +| O03 | Lane links issue/PR/current head/checks/reviews/threads | UI/API test | NOT_STARTED | +| O04 | Stale PR head is detected | GitHub fixture | NOT_STARTED | +| O05 | PR only created when authorized | Invariant test | NOT_STARTED | +| O06 | Target branch updated/rebased before PR as policy requires | Workflow receipt | NOT_STARTED | +| O07 | Bot/reviewer findings are verified against source before action | Review workflow test | NOT_STARTED | + +## P. Persistence and resilience + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| P01 | Critical JSON uses atomic same-filesystem replace and backup | Fault injection | NOT_STARTED | +| P02 | Truncated/NUL/corrupt catalog recovers last-known-good or opens repair UI | Corruption tests | NOT_STARTED | +| P03 | Unchanged catalog is not rewritten every few seconds | File-write instrumentation | NOT_STARTED | +| P04 | One corrupt thread does not brick environment | Isolation test | NOT_STARTED | +| P05 | Projections can rebuild from canonical events | Rebuild test | NOT_STARTED | +| P06 | Heavy thread does not crash app or require deleting `.t3` | Soak test | NOT_STARTED | +| P07 | Live userdata is never opened read-write by dev worktree | Integration guard test | NOT_STARTED | + +## Q. Performance + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| Q01 | Idle client produces no VCS request storm | 30-minute clean-profile trace | NOT_STARTED | +| Q02 | Automatic fetch disabled means no fetch-derived refresh | Trace test | NOT_STARTED | +| Q03 | 100 projects/1000 lanes remain navigable | Seeded benchmark | NOT_STARTED | +| Q04 | Shell projection omits full transcripts/logs/receipts | Payload measurement | NOT_STARTED | +| Q05 | Heavy log parsing/diff work stays off UI thread | Profile capture | NOT_STARTED | +| Q06 | Mobile scrolling remains responsive under large data | Real-device profile | NOT_STARTED | +| Q07 | Reconnect under server load succeeds without false offline state | Load scenario | NOT_STARTED | + +## R. Security and permissions + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| R01 | Remote-capable endpoint requires authentication | Integration test | NOT_STARTED | +| R02 | Pairing token never appears in hosted query parameter | URL test | NOT_STARTED | +| R03 | Sensitive provider values never return after save | API test | NOT_STARTED | +| R04 | Lane displays effective access/tool policy | UI test | NOT_STARTED | +| R05 | Destructive commands are scoped and typed | Contract/invariant tests | NOT_STARTED | +| R06 | No pattern-based process killing | Static search and runtime test | NOT_STARTED | +| R07 | Cross-environment artifact transfer is audited and hash-checked | Transfer receipts | NOT_STARTED | +| R08 | Dev/test cannot mutate live profile | Guard test | NOT_STARTED | + +## S. Fork identity and maintainability + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| S01 | App/package/data IDs do not collide with upstream | Artifact inspection | NOT_STARTED | +| S02 | MacBrains branding appears consistently on web/desktop/mobile | Screenshots | NOT_STARTED | +| S03 | About shows upstream base and fork commit | UI test | NOT_STARTED | +| S04 | Upstream sync procedure is documented and tested | Dry-run receipt | NOT_STARTED | +| S05 | Upstream profile import is explicit, reversible, and does not overwrite | Migration test | NOT_STARTED | +| S06 | Hosted upstream services are not used accidentally | Network/config audit | NOT_STARTED | +| S07 | New fork-specific code is isolated in explicit modules/subpaths where practical | Diff review | NOT_STARTED | + +## T. Project instructions and skills + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| T01 | UI shows loaded instruction files/bundles and precedence | UI test | NOT_STARTED | +| T02 | Repository instructions outrank reusable skill instructions | Conflict test | NOT_STARTED | +| T03 | Skill version/hash/validation/requirements are persisted | Schema test | NOT_STARTED | +| T04 | Invalid skill cannot execute | Negative test | NOT_STARTED | +| T05 | ANVIL/source-truth templates are selectable | UI and generated contract | NOT_STARTED | +| T06 | Skill usage history is auditable | Projection test | NOT_STARTED | + +## U. End-to-end acceptance + +| ID | Requirement | Required proof | Status | +|---|---|---|---| +| U01 | Canonical Mac + Linux node + mobile scenario completes | Full acceptance bundle | NOT_STARTED | +| U02 | Restart preserves state and control | Restart video + receipt export | NOT_STARTED | +| U03 | Provider failure recovers through new assignment | Recovery evidence | NOT_STARTED | +| U04 | Stale evidence invalidates completion and can be repaired | Invalidation scenario | NOT_STARTED | +| U05 | User can open and use final deliverable | Recorded acceptance | NOT_STARTED | +| U06 | Final completion report contains exact proof and caveats | Completion receipt | NOT_STARTED | + +## Final release gate + +Release is blocked unless: + +- every required row is `PROVEN`; +- no `PROVEN` row references stale commits or missing artifacts; +- all known high/critical verifier findings are resolved; +- desktop, web, iPhone, and iPad applicable paths pass; +- Mac and Linux-node canonical scenario passes after clean restart; +- fork identity does not collide with upstream; +- the final evidence bundle can be independently verified from hashes and repository state. \ No newline at end of file From 465bfe17cb999a5535c2d9732c91adcb30471f9c Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:11:05 -0400 Subject: [PATCH 06/32] docs: add implementation agent prompt --- docs/macbrains/AGENT_EXECUTION_PROMPT.md | 221 +++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 docs/macbrains/AGENT_EXECUTION_PROMPT.md diff --git a/docs/macbrains/AGENT_EXECUTION_PROMPT.md b/docs/macbrains/AGENT_EXECUTION_PROMPT.md new file mode 100644 index 000000000000..1d332e0e607c --- /dev/null +++ b/docs/macbrains/AGENT_EXECUTION_PROMPT.md @@ -0,0 +1,221 @@ +# MacBrains T3 Code Implementation Agent Prompt + +Use this prompt as the root instruction for the implementation campaign. + +--- + +You are implementing the MacBrains workflow overhaul in `naytewilson/t3code`. + +## Mission + +Transform this fork from a generic coding-agent chat/control surface into Nayte's complete, persistent, evidence-driven agent operating system across Mac, Linux node, web, iPhone, and iPad. + +The authoritative contract is: + +1. `MACBRAINS.md` +2. `docs/macbrains/PRODUCT_SPEC.md` +3. `docs/macbrains/DOMAIN_MODEL.md` +4. `docs/macbrains/IMPLEMENTATION_LEDGER.md` +5. `docs/macbrains/ACCEPTANCE_MATRIX.md` +6. current repository state and scoped instruction files + +Do not reinterpret the mission as a cosmetic redesign, a prototype, a mock UI, or a documentation-only exercise. Implement working contracts, server behavior, persistence, clients, tests, packaging, and end-to-end acceptance. + +## Operating mode + +Proceed continuously. Do not stop for routine confirmations, phase approvals, or preference questions. Ask only when a missing answer changes the correct action and cannot be resolved from repository evidence. Otherwise choose the safest reversible action and continue. + +Do not claim partial foundations are the finished product. Maintain the dependency order in the implementation ledger, but treat the work as one continuous campaign whose completion gate is the full acceptance matrix. + +## Source-truth preflight + +Before planning or editing: + +- identify repository root, Git common directory, current branch, exact HEAD, remotes, worktrees, status, staged/unstaged/untracked files, and active Git operations; +- read `AGENTS.md`, `MACBRAINS.md`, all referenced MacBrains specifications, and path-scoped instructions; +- inspect manifests, package manager, toolchain requirements, build/test commands, CI, generated/vendor boundaries, and relevant implementation/tests; +- verify current upstream/fork divergence; +- identify active lanes or overlapping ownership; +- record unknowns only when they change action. + +Never use conversation memory or this prompt as proof of current repository state. + +## Worktree and branch + +Create an isolated worktree and focused branch for the package you own. Do not edit the primary checkout. Do not switch, reset, clean, delete, or repurpose another worktree. Never kill processes by pattern. + +Use one concern per branch/PR. If a package requires shared contract changes, assign one integration owner and serialize those edits. Parallel agents must have disjoint file/symbol ownership and an explicit merge contract. + +## Package selection + +Read the implementation ledger and select the earliest executable package whose dependencies are already integrated and whose ownership does not collide with active work. + +Priority: + +1. foundation blockers; +2. correctness and recovery; +3. reproducibility and evidence; +4. focused tests; +5. product behavior; +6. polish. + +Do not skip a foundation dependency to build a disconnected UI facade. + +## Agent topology + +For architecture, persistence, concurrency, security, provider lifecycle, performance, or broad multi-file work: + +- executor: performs inspection, edits, and focused checks; +- advisor: independently reviews the oriented plan before broad or irreversible edits; +- verifier: independently refreshes source truth and reviews final diff/evidence before completion. + +Do not use hidden subagents. Every child assignment must have a bounded task, explicit inputs/outputs, path ownership, and visible result. Do not allow multiple editing agents to touch overlapping files. + +Use the smallest sufficient model. Record provider instance, model, reasoning level, role, and escalation reason. Escalate only after task-specific failure evidence. + +## Architecture constraints + +Preserve T3 Code's core architecture: + +- typed contracts at the wire boundary; +- pure command decider; +- persisted domain events; +- deterministic projectors; +- queue-backed reactors; +- typed receipts; +- incremental projections; +- provider protocol complexity at adapter boundaries; +- shared connection runtime for web/mobile; +- one execution environment per running T3 server; +- access endpoints separate from launch methods. + +Do not move canonical workflow state into React component state, transcript text, provider-specific code, or ad hoc JSON files. + +Use Effect conventions already present in the repository. Read the local Effect guidance before editing Effect-heavy server code. Avoid `any` except an existing intentional boundary with an explicit rationale. + +## Product constraints + +The default product surface is the Command Center, not an empty chat. + +Every substantial run must persist and display: + +- task contract; +- source-truth revision; +- worktree and ownership; +- executor/advisor/verifier assignments; +- lifecycle state; +- plan revisions; +- changes; +- checks; +- receipts; +- blockers; +- node jobs; +- deliverables; +- recovery history. + +Web, desktop, iPhone, and iPad are first-class. Make an explicit applicability decision for every feature and implement every applicable surface. Remote GUI project/worktree creation is required. + +## Linux node constraints + +The Linux node is a first-class execution environment for heavy work. + +- Do not implement or generate Python. +- Do not write Python-based worker prompts or automation. +- Prefer C, C++, Rust, Swift where supported, shell, or another native compiled implementation. +- Validate source commit and inputs before dispatch. +- Preserve logs and outputs as immutable, content-addressed artifacts. +- Treat node completion, artifact return, Mac integration, and Mac verification as distinct states. +- Never claim node output is integrated until the Mac worktree received it and applicable checks passed. + +Write `ANVIL` uppercase except exact case-sensitive identifiers, commands, and paths. + +## Persistence and reliability constraints + +Critical persisted JSON/catalog state must use atomic same-filesystem replacement, validation, backup, and recovery. Avoid fixed-cadence rewrites of unchanged data. + +One corrupt thread, projection, cache, or catalog must not brick the environment or require deletion of `.t3`. Canonical events must permit projection rebuild. Provider replay must be deduplicated. Background activity must be protected with explicit leases rather than foreground-turn assumptions. + +## Performance constraints + +- no continuous repaint animations; +- virtualize large lists; +- do not put full transcripts/logs/receipt payloads in shell projections; +- use sequence-based deltas; +- avoid repeated idle VCS polling; +- move heavy parsing/diff work off the UI thread; +- measure seeded large-data and idle behavior; +- mobile receives summarized streams with on-demand detail. + +## Security constraints + +- preserve authenticated remote access; +- sensitive provider values remain server-side; +- display effective lane tool/access policy; +- use typed, scoped destructive intent; +- never touch live `~/.t3/userdata` read-write during development; +- never expose pairing credentials in hosted query parameters; +- audit cross-environment transfers. + +## Implementation method + +For your selected package: + +1. Inspect exact existing source, callers, tests, similar patterns, and migrations. +2. Produce a concise executable plan tied to acceptance rows. +3. Obtain advisor review when required; revise before editing if rejected. +4. Implement the smallest coherent vertical slice that preserves architecture. +5. Add focused deterministic tests with receipts/worker drains, never sleeps/polling. +6. Run targeted formatting, lint, typecheck, tests, build, and integrated UI checks applicable to touched surfaces. +7. Inspect final diff and staged files. +8. Have verifier refresh source truth and review current code/evidence. +9. Fix all real findings; document false positives with source evidence. +10. Update implementation ledger and acceptance matrix only with real receipt/artifact references. +11. Commit with a focused conventional message. +12. Rebase/update from current target branch and rerun stale checks. +13. Open a focused PR only when the task contract authorizes it. +14. Continue to the next executable package unless blocked by a genuine external dependency. + +## Testing rules + +Use the repository's actual supported commands. Do not invent conventional commands. Start with focused tests and checks for the touched surface. Run broader checks when integration risk requires them or before the final release gate. + +Every command receipt must record exact command, cwd/environment, exit status, and relevant output artifact. Do not say a test passed unless you ran it on the exact reported revision. + +Backend behavior requires focused tests. UI changes require before/after evidence and an integrated real-client acceptance pass. Motion/timing changes require a recording. Remote/mobile behavior requires actual remote/mobile evidence, not desktop inference. + +## Completion rules + +A package is not complete because code compiles or an agent says it is done. It is complete only when its acceptance rows are `PROVEN` on the current revision and an independent verifier accepts the evidence. + +The entire fork is not complete until the final release gate and canonical end-to-end scenario pass. + +Every package closeout must state: + +- `proven` +- `missing evidence` +- `possibly wrong or overstated` +- `exact next action` +- `what does not count as completion` +- `safe to continue here or start a fresh context` + +## Prohibited shortcuts + +Do not: + +- build only mock screens; +- store critical state only in transcript text; +- use one provider/model for every role by default; +- spawn overlapping editing agents; +- bypass worktrees for substantial work; +- mark skipped checks as passed; +- declare UI delivery from backend logs; +- trust stale tests after source changes; +- rewrite the architecture around a new custom transport; +- use Python on the Linux node; +- delete user state as a recovery mechanism; +- claim completion without a visible deliverable and acceptance receipt; +- stop after writing a plan when implementation is authorized. + +Begin by running the source-truth preflight and reporting the exact package selected, dependencies proven, ownership boundary, and acceptance rows it will close. Then execute it fully. + +--- \ No newline at end of file From 74ef33122aca001eb938ba4f431cc900752942dc Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:11:31 -0400 Subject: [PATCH 07/32] docs: record fork baseline and known limitations --- docs/macbrains/FORK_BASELINE.md | 188 ++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/macbrains/FORK_BASELINE.md diff --git a/docs/macbrains/FORK_BASELINE.md b/docs/macbrains/FORK_BASELINE.md new file mode 100644 index 000000000000..29061d4ae50f --- /dev/null +++ b/docs/macbrains/FORK_BASELINE.md @@ -0,0 +1,188 @@ +# MacBrains T3 Code Fork Baseline + +## Baseline identity + +- Fork: `naytewilson/t3code` +- Upstream: `pingdotgg/t3code` +- Fork default branch at analysis time: `main` +- Baseline commit: `b125b7635170ec0c33f8ddf39299155a21f8c9b9` +- Fork and upstream were identical at that commit. +- Specification branch: `macbrains/agent-workflow-overhaul` + +Agents must re-verify all of this before implementation. This document records the initial analysis, not perpetual truth. + +## Existing architecture worth preserving + +### Event-sourced orchestration + +The server already uses typed WebSocket requests, a pure decider, persisted domain events, projectors, queue-backed reactors, and typed runtime receipts. Each turn ends with Git checkpointing. This is the correct foundation for durable work lanes and evidence-driven completion. + +Relevant sources: + +- `AGENTS.md` +- `docs/architecture/overview.md` +- `docs/reference/encyclopedia.md` +- `packages/contracts/src/orchestration.ts` +- `apps/server/src/orchestration/` +- `apps/server/src/checkpointing/` + +### Multi-surface connection runtime + +Web and mobile share an environment-scoped connection runtime with persistent catalogs, retry ownership, cache hydration, and sequence-based subscriptions. Desktop wraps the web app and manages a local backend. Remote environments remain one T3 server reached through access endpoints. + +Relevant sources: + +- `docs/architecture/connection-runtime.md` +- `docs/architecture/remote-environments.md` +- `packages/client-runtime/` +- `apps/web/` +- `apps/mobile/` +- `apps/desktop/` + +### Provider instance architecture + +Providers are no longer hard-coded singletons. The repository has provider drivers, multiple instances, continuation identity, adapter registry, session directory, and instance-aware routing. This is compatible with role-specific executor/advisor/verifier presets and multiple accounts. + +Relevant sources: + +- `apps/server/src/provider/ProviderDriver.ts` +- `apps/server/src/provider/Services/ProviderAdapterRegistry.ts` +- `apps/server/src/provider/Layers/ProviderService.ts` +- `docs/providers/codex.md` + +### Worktree and checkpoint primitives + +Threads already record branch/worktree metadata, turn bootstrap can prepare a worktree, project scripts can run on worktree creation, and checkpoints use hidden Git refs. These primitives should be promoted into managed lane isolation rather than replaced. + +Relevant sources: + +- `packages/contracts/src/orchestration.ts` +- `docs/git-integration-plan.md` +- `t3.json` +- `apps/server/src/git/` +- `apps/server/src/checkpointing/` + +### Remote/Tailscale/SSH primitives + +The remote architecture treats Tailscale as an endpoint provider and keeps launch methods separate from WebSocket access. Desktop-managed SSH launch, direct endpoints, pairing, and Linux systemd service support already exist. + +Relevant sources: + +- `docs/architecture/remote-environments.md` +- `docs/user/remote-access.md` +- `docs/user/background-service.md` + +## Current limitations that directly conflict with the MacBrains contract + +### Chat/thread is the dominant durable unit + +The current model persists projects, threads, turns, activities, sessions, plans, and checkpoints, but not a higher-level durable work lane with task contract, role topology, source-truth lineage, acceptance criteria, checks, deliverables, and deterministic completion. + +Required response: F0-F3 in the implementation ledger. + +### Runtime policy is too coarse + +Current runtime modes are provider/session access modes. User-facing documentation primarily presents a global full-access versus supervised choice. The MacBrains workflow needs effective policies at global, environment, project, lane, role, and assignment levels. + +Required response: U1 plus lane policy contracts. + +### Remote GUI project creation is documented as unsupported + +The remote-access guide states that GUIs cannot currently add projects on remote environments and recommends server-side CLI as a workaround. This blocks first-class Linux-node and mobile workflows. + +Required response: F4, F6, N0. + +### Completion is not evidence-gated + +Current turn quiescence and checkpoints are useful runtime milestones, but there is no complete acceptance-criterion/check/deliverable gate preventing an assistant message or settled foreground turn from appearing finished while tests, background workers, or user-visible delivery are incomplete. + +Required response: F1, F2, D0, D1. + +### Background session reaping can conflict with subagent work + +Upstream issue `#4198` reports the idle session reaper can terminate sessions while dynamic workflows or subagents continue after the foreground turn settles. The current busy guard relies on foreground projection state rather than an explicit background activity lease. + +Required response: P1. + +### Context telemetry can become dishonest after compaction + +Upstream issue `#4650` reports context usage ratcheting upward and not reflecting `/compact`, caused by cumulative progress values merged with monotonic maximum behavior. + +Required response: P2. + +### Heavy threads can make a workspace unusable + +Upstream issue `#996` reports crashes and a workspace that only recovers after deleting `.t3`. The fork must isolate corrupt/heavy state and provide in-app recovery without data deletion. + +Required response: R1. + +### Critical catalog writes may be corruption-prone + +Upstream issue `#4750` reports non-atomic repeated writes to `connection-catalog.json`, where an interrupted write can permanently brick the app state without fallback. + +Required response: R0. + +### Idle VCS refresh can become CPU-bound + +Upstream issue `#4773` reports repeated `vcs.listRefs` activity that can make the local backend unresponsive and trigger reconnect behavior while idle. + +Required response: V0. + +### Active-run interaction modes are incomplete + +Upstream issue `#231` requests explicit steer and queue modes alongside default/plan. MacBrains requires start, steer, queue, pause, resume, interrupt, and recovery as durable actions. + +Required response: F3, F6, P0. + +### Named custom-agent selection is incomplete + +Upstream issue `#3875` describes child-agent tracking but no deterministic selection boundary for named Codex custom agents. MacBrains requires visible role/assignment selection before child creation. + +Required response: F3, P0. + +### Private repository and remote parsing edges exist + +Upstream issue `#3664` reports private GitHub repository add/clone failure. Issue `#3648` reports SCP-like SSH remotes with users other than `git@` not being detected. + +Required response: G0. + +### Lifecycle hooks/notifications are not a stable public integration surface + +Upstream issue `#376` requests a clean lifecycle hook boundary for external notification tools. MacBrains requires event-derived actionable notifications and can expose a stable hook/export boundary after canonical notification events exist. + +Required response: D2. + +## Build and toolchain baseline + +The root manifest currently declares: + +- Node engine `^24.13.1`; +- pnpm package manager metadata; +- Vite+ (`vp`) tasks; +- TypeScript/Effect application stack; +- Rust resource monitor; +- Electron desktop, React/Vite web, React Native mobile; +- build, focused test, lint, format, typecheck, desktop smoke, and packaging scripts. + +Repository instructions prohibit casual repo-wide checks during ordinary focused changes and require targeted proof, while final integration/release work will necessarily need an explicitly authorized broader gate. + +Agents must use commands proven by current manifests/instructions and must not assume stale quick-start examples are authoritative when they conflict with root package scripts or `AGENTS.md`. + +## Development-state safety baseline + +Existing repository instructions already require: + +- never kill by process-name/path pattern; +- never start development work against live `~/.t3/userdata`; +- never bake localhost origins into the web bundle; +- use isolated worktree `.t3` state; +- wait on receipts and worker drains rather than sleeps; +- consider web, desktop, mobile, providers, contracts, connection modes, reverse states, and documentation for every feature. + +These remain mandatory and are extended, not weakened, by `MACBRAINS.md`. + +## Initial implementation conclusion + +The fork does not need a ground-up rewrite. It needs a new durable work-lane/evidence layer integrated with existing event sourcing, provider instances, worktrees, checkpoints, connection runtime, and remote environment model. The largest architectural risks are schema migration, projection size/performance, provider lifecycle normalization, background leases, and cross-surface feature completeness. + +No implementation claim should rely on this baseline alone. Re-run source-truth preflight against the selected worktree and current branch before every package. \ No newline at end of file From 4862fcf8d39022a4a5962699c8c821c07c2dd3d6 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:11:59 -0400 Subject: [PATCH 08/32] docs: bind agents to MacBrains workflow contract --- AGENTS.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b7786ba84b59..297af41d15b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,13 @@ +# MacBrains Fork Directive + +This branch belongs to the MacBrains fork. Before planning or editing, read `MACBRAINS.md` and every file in `docs/macbrains/` referenced by it. Those files define the fork's product contract, domain model, implementation ledger, acceptance matrix, and execution prompt. + +For fork work, the MacBrains contract overrides upstream product preferences where they conflict. Existing safety rules, architecture boundaries, performance discipline, multi-surface requirements, and development-state protections below remain mandatory. + +Do not stop after producing a plan when implementation is authorized. Use an isolated worktree, preserve source truth, keep agents visible, produce exact receipts, and do not claim completion without current tests, independent verification, and a user-visible deliverable. + +--- + # T3 Code T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients. @@ -136,4 +146,4 @@ Full glossary with file links: `docs/reference/encyclopedia.md` ## Additional tips - Don't verify with browsers or computer use unless the user explicitly agrees or requests it. -- Security is important, but should not be over-indexed on, especially for dev mode/maintainer-only features. +- Security is important, but should not be over-indexed on, especially for dev mode/maintainer-only features. \ No newline at end of file From baa70a6f992cd148faefdfe617fb534bd2345e06 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:12:14 -0400 Subject: [PATCH 09/32] docs: add MacBrains specification index --- docs/macbrains/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/macbrains/README.md diff --git a/docs/macbrains/README.md b/docs/macbrains/README.md new file mode 100644 index 000000000000..41f728956230 --- /dev/null +++ b/docs/macbrains/README.md @@ -0,0 +1,31 @@ +# MacBrains T3 Code Specification Index + +Read in this order: + +1. [`../../MACBRAINS.md`](../../MACBRAINS.md) — non-negotiable operating contract. +2. [`FORK_BASELINE.md`](./FORK_BASELINE.md) — verified initial architecture and limitation inventory; re-verify before acting. +3. [`PRODUCT_SPEC.md`](./PRODUCT_SPEC.md) — complete product behavior and UX. +4. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. +5. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. +6. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. +7. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. +8. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. + +## Canonical status + +The repository and current tool output remain authoritative. These documents define intent and acceptance but do not prove current implementation state. + +## First implementation target + +Start with `F0 — Work lane and source-truth contracts`. Do not begin disconnected UI work. The first agent must: + +- create an isolated worktree from the current fork target branch; +- run source-truth preflight; +- inspect existing orchestration contracts, decider, invariants, projector, projection persistence, migrations, client-runtime models, and focused tests; +- define an ownership boundary that avoids parallel edits to shared contract files; +- obtain independent architecture advice before changing persistence/domain schemas; +- implement the full F0 vertical slice with migration and focused proof. + +## Campaign completion + +The campaign is complete only after every required acceptance row is `PROVEN` on current revisions and the E0 canonical Mac + Linux node + mobile scenario passes with a restart/recovery evidence bundle. \ No newline at end of file From a4c5040ede382a51b4477588e48fa9d1a67dd1b4 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:12:34 -0400 Subject: [PATCH 10/32] docs: add machine-readable workflow defaults --- docs/macbrains/DEFAULT_POLICIES.json | 187 +++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 docs/macbrains/DEFAULT_POLICIES.json diff --git a/docs/macbrains/DEFAULT_POLICIES.json b/docs/macbrains/DEFAULT_POLICIES.json new file mode 100644 index 000000000000..ace55df8de0d --- /dev/null +++ b/docs/macbrains/DEFAULT_POLICIES.json @@ -0,0 +1,187 @@ +{ + "$schemaVersion": 1, + "product": { + "name": "MacBrains T3 Code", + "defaultLandingSurface": "command-center", + "completionRequiresVisibleDeliverable": true, + "claimLabels": [ + "PROVEN", + "INFERRED", + "SUSPECTED", + "UNKNOWN" + ] + }, + "sourceTruth": { + "authorityOrder": [ + "repository", + "canonical-operational-source", + "current-receipts", + "durable-handoff", + "narrative" + ], + "preflightRequiredForSubstantialWork": true, + "refreshBeforeVerification": true, + "conflictBehavior": "block-or-safest-reversible-action" + }, + "work": { + "substantialWorkUsesWorktree": true, + "defaultLifecycle": [ + "queued", + "preflight", + "oriented", + "planned", + "executing", + "testing", + "reviewing", + "deliverable-ready", + "completed" + ], + "allowOverlappingEditors": false, + "requireMergeContractForSharedFiles": true, + "continueWithoutRoutineConfirmations": true + }, + "agents": { + "defaultTopology": "executor-advisor-verifier", + "roles": { + "executor": { + "capabilityTier": "standard", + "reasoning": "medium", + "costPreference": "free-flat-local-first" + }, + "advisor": { + "capabilityTier": "advanced", + "reasoning": "high", + "costPreference": "balanced" + }, + "verifier": { + "capabilityTier": "advanced", + "reasoning": "high", + "costPreference": "balanced", + "mustRefreshSourceTruth": true + }, + "mechanical-worker": { + "capabilityTier": "mechanical", + "reasoning": "light", + "costPreference": "free-flat-local-first" + }, + "recovery": { + "capabilityTier": "standard", + "reasoning": "medium", + "requiresCanonicalHandoff": true + } + }, + "advisorRequiredFor": [ + "architecture", + "persistence-migration", + "concurrency", + "security-boundary", + "performance-hot-path", + "destructive-operation", + "conflicting-authorities", + "strategy-change-after-failure", + "high-risk" + ], + "visibleChildAgents": true, + "recordEscalationCause": true + }, + "linuxNode": { + "firstClassEnvironment": true, + "pythonAllowed": false, + "preferredLanguages": [ + "C", + "C++", + "Rust", + "Swift", + "shell", + "native-compiled-other" + ], + "pinSourceCommit": true, + "contentAddressedArtifacts": true, + "separateStates": [ + "job-completed", + "artifact-returned", + "integrated-on-mac", + "verified-on-mac" + ], + "anvilUppercaseRequired": true + }, + "completion": { + "requiredReceipts": [ + "source-truth-preflight", + "orientation", + "plan", + "diff", + "checks", + "review", + "deliverable", + "ui-acceptance-when-applicable", + "completion" + ], + "requiredCloseoutFields": [ + "proven", + "missing evidence", + "possibly wrong or overstated", + "exact next action", + "what does not count as completion", + "safe to continue here or start a fresh context" + ], + "blockOnStaleCheck": true, + "blockOnSkippedRequiredCheck": true, + "blockOnUnresolvedFinding": true, + "providerMessageMayCompleteLane": false + }, + "surfaces": { + "desktop": "first-class", + "web": "first-class", + "iphone": "first-class", + "ipad": "first-class", + "remoteProjectManagementRequired": true, + "remoteWorktreeManagementRequired": true + }, + "remote": { + "preferredPrivateNetwork": "tailscale", + "authenticatedAccessRequired": true, + "keepEnvironmentAccessAndLaunchSeparate": true, + "hostedPairTokenLocation": "url-fragment" + }, + "notifications": { + "actionableOnly": true, + "triggers": [ + "approval-required", + "user-input-required", + "blocked", + "failed", + "advisor-conflict", + "verifier-rejection", + "check-state-changed", + "ci-state-changed", + "node-job-completed", + "node-job-failed", + "pull-request-state-changed", + "deliverable-ready", + "lane-stale", + "environment-disconnected", + "recovery-required" + ], + "doNotNotifyFinishedBeforeCompletionGate": true + }, + "performance": { + "continuousRepaintAnimationsAllowed": false, + "virtualizeLargeLists": true, + "incrementalSubscriptions": true, + "avoidIdleVcsPolling": true, + "largeLogsStoredAsArtifacts": true, + "heavyParsingOffUiThread": true + }, + "persistence": { + "criticalJsonWriteMode": "atomic-replace-with-last-known-good", + "projectionRebuildRequired": true, + "deleteUserStateAsRecovery": false, + "liveProfileWritableFromDev": false + }, + "github": { + "createPullRequestOnlyWhenAuthorized": true, + "refreshBeforeReporting": true, + "verifyFindingsAgainstSource": true + } +} \ No newline at end of file From 58502014b2c9d207e60186e064336f67dbaf5dea Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:14:23 -0400 Subject: [PATCH 11/32] docs: audit upstream documentation and contradictions --- docs/macbrains/DOCUMENTATION_AUDIT.md | 309 ++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 docs/macbrains/DOCUMENTATION_AUDIT.md diff --git a/docs/macbrains/DOCUMENTATION_AUDIT.md b/docs/macbrains/DOCUMENTATION_AUDIT.md new file mode 100644 index 000000000000..f8b5c73ee88d --- /dev/null +++ b/docs/macbrains/DOCUMENTATION_AUDIT.md @@ -0,0 +1,309 @@ +# MacBrains T3 Code Documentation Audit + +## Purpose + +This audit records documentation, architecture, operational constraints, and contradictions discovered while preparing the MacBrains fork contract. It is a navigation aid and risk register, not a substitute for inspecting current source. + +Agents must re-check every referenced file and current implementation before editing because this upstream project changes rapidly and several documents are already stale relative to source. + +## Documentation areas inspected + +### Repository governance and contribution + +- `README.md` +- `AGENTS.md` +- `CONTRIBUTING.md` +- root `package.json` +- `t3.json` + +### Architecture + +- `docs/architecture/overview.md` +- `docs/architecture/providers.md` +- `docs/architecture/connection-runtime.md` +- `docs/architecture/remote-environments.md` +- `docs/architecture/server-updates.md` +- `docs/reference/encyclopedia.md` +- `docs/reference/workspace-layout.md` +- `docs/reference/scripts.md` +- `docs/git-integration-plan.md` + +### User and environment operation + +- `docs/getting-started/quick-start.md` +- `docs/user/remote-access.md` +- `docs/user/background-service.md` +- `docs/user/runtime-modes.md` +- `docs/providers/codex.md` +- `docs/providers/claude.md` +- `docs/cloud/environment-auth.md` +- `docs/integrations/source-control-providers.md` + +### Development, observability, CI, and release + +- `docs/operations/ci.md` +- `docs/operations/observability.md` +- `docs/operations/release.md` +- root package scripts +- repository issue tracker for current limitations and regressions + +## Confirmed architectural strengths + +### Typed event-sourced orchestration + +The current server already has the correct foundation for durable MacBrains workflow state: + +- typed commands and wire contracts; +- pure decision logic; +- persisted domain events; +- projections/read models; +- queue-backed side-effect reactors; +- typed runtime receipts; +- Git checkpoints and turn diffs. + +The MacBrains work should add lane, assignment, evidence, checks, deliverables, and recovery concepts inside this architecture. It should not create a second orchestration database or use transcript text as canonical workflow state. + +### Provider driver and instance separation + +Current source supports provider drivers, multiple configured instances, per-instance routing, continuation identity, and session recovery. This is a strong base for executor/advisor/verifier role presets and multiple accounts or routing configurations. + +### Environment and connection separation + +The remote design correctly distinguishes: + +- execution environment; +- saved known environment; +- access endpoint; +- launch method; +- endpoint provider such as Tailscale. + +Keep this model. The Linux node should be another execution environment with node-job capabilities, not a special transport bolted into the renderer. + +### Shared client connection runtime + +Web and mobile already share a durable connection runtime with environment-scoped services, reconnect policy, cached state, and sequence-based subscriptions. MacBrains projections should extend that runtime rather than create client-owned sockets or retry loops. + +### Worktree and checkpoint primitives + +Branch/worktree fields, worktree bootstrap, project setup scripts, Git services, and hidden checkpoint refs already exist. The work is to make isolation and ownership mandatory/visible for substantial lanes, not to recreate Git worktree support. + +## Documentation contradictions and stale statements + +### Provider implementation contradiction + +`docs/architecture/providers.md` states that Codex is the only implemented provider and Claude is only reserved in contracts/UI. That conflicts with: + +- the root README listing Codex, Claude, Cursor, Grok Build, and OpenCode; +- current provider driver/instance source; +- provider-specific documentation; +- recent provider behavior issues. + +**Rule:** treat current source and tested provider capability snapshots as authority. Update or retire the stale architecture page as part of P0. + +### Development command contradiction + +`docs/getting-started/quick-start.md` uses `bun run ...`, while root scripts and current repository instructions center on Vite+ `vp`, Node scripts, and worktree-aware dev runner behavior. + +**Rule:** agents must derive commands from current root/package manifests and `AGENTS.md`. Do not copy quick-start commands into receipts until proven on the selected revision. + +### Queueing contradiction + +`docs/project/todo.md` still lists queueing messages as a larger TODO. The issue tracker also requests Steer/Queue behavior, while current interaction/runtime contracts and provider behavior have evolved. + +**Rule:** inspect current source before assuming queueing is absent or complete. MacBrains requires explicit durable `steer` and `queue-input` commands regardless of provider-specific partial behavior. + +### Contribution policy mismatch + +Upstream contribution guidance rejects broad opinionated changes and 1,000+ line feature PRs. This is appropriate for upstream but not the product scope of a private fork campaign. + +**Rule:** preserve small focused branches and reviews for maintainability, but do not limit the fork to upstream's contribution appetite. Never open the MacBrains overhaul as one giant upstream PR. + +## Product limitations confirmed by documentation + +### Remote project management gap + +The remote access guide explicitly says the GUIs cannot add projects on remote environments and recommends server-side CLI as a workaround. + +This is a foundation blocker for the Linux node and mobile-first control. F4/F6/N0 must add authenticated remote project and worktree operations through existing environment RPC boundaries. + +### Runtime policy presentation is too coarse + +The user runtime guide presents a global full-access versus supervised switch. Current contracts contain more provider modes, but the user model is still too broad for project/lane/role-specific policy. + +MacBrains requires effective policy resolution at: + +1. global default; +2. environment; +3. project; +4. lane; +5. role preset; +6. assignment override. + +The UI must show the effective result and its origin. + +### No complete evidence-gated completion model + +Existing quiescence, turn completion, checkpoints, and settlement are useful but do not prove: + +- current source truth; +- tests/checks against current HEAD; +- independent review; +- node artifact integration; +- user-visible delivery; +- durable launch path. + +F1/F2/D0/D1 must make completion a decider invariant rather than a provider/UI convention. + +## Reliability limitations from current issue evidence + +### Background session reaping + +Issue `#4198` describes active background workflows/subagents being terminated because the session reaper only recognizes an active foreground turn. + +Required correction: explicit provider-neutral background activity leases, deterministic expiry, and visible session-retention reason. + +### Context compaction accounting + +Issue `#4650` describes context usage remaining monotonic after compaction. + +Required correction: usage epochs and provider-compaction events; never merge all usage values with an unconditional maximum. + +### Heavy thread failure and destructive workaround + +Issue `#996` reports heavy-thread crashes and recovery by deleting `.t3`. + +Required correction: bounded projections/rendering, artifact-backed large payloads, corrupt-thread isolation, projection rebuild, and in-app recovery. Deleting user state is prohibited as normal recovery. + +### Non-atomic connection catalog persistence + +Issue `#4750` reports repeated in-place catalog writes and unrecoverable corruption after interruption. + +Required correction: atomic same-filesystem write/flush/rename, last-known-good backup, schema validation, unchanged-write suppression, and repair UI. + +### Idle VCS request storm + +Issue `#4773` reports idle `vcs.listRefs` request storms causing CPU saturation, timeouts, and false reconnect behavior. + +Required correction: event-driven invalidation, deduplicated/bounded refresh, per-repository request ownership, and idle soak acceptance. + +### Provider replay duplication + +Issue `#3149` reports Cursor ACP resume replaying prior updates into an existing thread. + +Required correction: provider event epoch/item identity and deduplication before orchestration append. + +### Git remote parsing and private clone edges + +Issues `#3648` and `#3664` show gaps in SCP-style remote parsing and private GitHub project onboarding. + +Required correction: robust remote parser, authenticated clone path, explicit credential diagnostics, and private-repository integration tests. + +## Observability implications + +Current observability documentation says: + +- stdout logs are human-facing and not persisted; +- completed spans are persisted to local NDJSON; +- metrics are in-process or remote OTLP only; +- provider event NDJSON is separate. + +MacBrains receipts must not pretend stdout is durable evidence. Required command/check logs need explicit artifact capture and hashes. Traces remain diagnostic telemetry, not substitutes for completion receipts. + +The UI should correlate lane/assignment/check/node-job IDs into spans while keeping metric labels low-cardinality. + +## Authentication and remote security implications + +Current environment authentication is capability-based and uses: + +- one-time bootstrap credentials; +- browser session cookies; +- bearer token exchange; +- short-lived WebSocket tickets; +- per-RPC scopes. + +MacBrains should extend scopes deliberately for new operations if required, rather than treating pairing as blanket authority. Remote project creation, worktree mutation, node-job dispatch, artifact transfer, evidence export, and administrative recovery need explicit authorization decisions. + +The existing hard-cutover auth migration deleted old sessions and required re-pairing. Future migration work must expose planned re-pairing/recovery behavior and must never silently strand mobile devices. + +## Server update implications + +The current update architecture: + +- targets the client's exact server version; +- supports boot-service, foreground respawn, desktop-managed, and manual paths; +- verifies the replacement before handoff; +- has no separate progress stream; +- depends on exact package publication before client release. + +MacBrains fork identity changes must supply a separate server package/update source or disable upstream self-update paths until configured. The lane UI should model update work as structured progress/receipts rather than an indefinitely pending button. + +## Release and hosted-service collision risks + +The upstream release process assumes: + +- npm package `t3`; +- app/bundle identity associated with T3 Code; +- GitHub release updater metadata; +- upstream relay deployment and production credentials; +- Clerk configuration; +- Vercel domains under `app.t3.codes`; +- T3 Connect production resources; +- Apple app ID `com.t3tools.t3code`. + +A fork that changes only visible branding could still: + +- overwrite or share upstream user data; +- check upstream releases; +- publish to an unavailable npm name; +- connect to upstream hosted services; +- use incompatible passkey/associated-domain configuration; +- collide with the upstream desktop install. + +U0 must split package names, bundle IDs, data directories, updater repository/channels, hosted domains, relay/Clerk configuration, and release credentials before distributing a branded build. + +## Source-control integration scope + +Source-control documentation covers GitHub, GitLab, Bitbucket, and Azure DevOps. MacBrains workflow should keep the generic source-control provider boundary, while GitHub receives the deepest initial lane/PR/check/review integration because the user's active repositories are there. + +Do not hard-code GitHub assumptions into work-lane domain contracts. Store generic change-request identity and provider-specific payloads at the adapter boundary. + +## CI and verification implications + +Current CI runs broad quality gates and release builds multiple desktop platforms. Repository instructions require focused checks during ordinary changes and discourage repeatedly running the entire monorepo suite locally. + +MacBrains policy: + +- focused checks per package during implementation; +- broader integration checks when shared contracts/projections change; +- full CI/release gate before declaring the fork ready; +- every result tied to exact HEAD and invalidated by relevant changes; +- UI acceptance on all applicable surfaces, not inferred from TypeScript success. + +## Documents requiring eventual upstream/fork repair + +The implementation campaign should update these as behavior lands: + +- `README.md` — MacBrains fork identity and current supported workflow. +- `docs/architecture/providers.md` — current provider architecture and capability matrix. +- `docs/getting-started/quick-start.md` — authoritative toolchain commands. +- `docs/project/todo.md` — remove stale queueing status or replace with current ledger. +- `docs/user/remote-access.md` — remove remote-project CLI workaround after F4/F6/N0. +- `docs/user/runtime-modes.md` — effective project/lane/role policy. +- `docs/reference/encyclopedia.md` — lane, assignment, receipt, check, blocker, deliverable, node job. +- `docs/architecture/overview.md` — lane/evidence orchestration. +- `docs/architecture/server-updates.md` — fork package/update identity and progress receipts. +- `docs/operations/observability.md` — correlation with lane/check/node receipt IDs. +- `docs/operations/release.md` — MacBrains release channels and no upstream collisions. +- provider guides — capability and role preset behavior. + +Documentation updates must ship with the corresponding implementation package, not as promises detached from behavior. + +## Final audit verdict + +**PROVEN:** The repository has the architectural primitives needed for the MacBrains control plane without a ground-up rewrite. + +**PROVEN:** Several documented limitations and current regressions directly block the requested workflow and are represented as foundation/correctness packages in the implementation ledger. + +**INFERRED:** Isolating fork-specific modules and adding durable lane/evidence projections will preserve upstream sync better than rewriting thread/provider/environment foundations. + +**UNKNOWN until implementation preflight:** exact current file ownership, migration numbers, adapter capability details, UI component boundaries, active upstream fixes, and command/test behavior on the agent's selected revision. \ No newline at end of file From 058d355f6fa053fe5080fb5be67f3c431202dd6b Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:14:55 -0400 Subject: [PATCH 12/32] docs: define fork identity and release isolation --- docs/macbrains/FORK_IDENTITY_AND_RELEASE.md | 256 ++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/macbrains/FORK_IDENTITY_AND_RELEASE.md diff --git a/docs/macbrains/FORK_IDENTITY_AND_RELEASE.md b/docs/macbrains/FORK_IDENTITY_AND_RELEASE.md new file mode 100644 index 000000000000..730624bb559c --- /dev/null +++ b/docs/macbrains/FORK_IDENTITY_AND_RELEASE.md @@ -0,0 +1,256 @@ +# MacBrains Fork Identity and Release Isolation + +## Objective + +Ship MacBrains T3 Code beside upstream T3 Code without sharing application identity, user data, update channels, hosted control planes, package publication, credentials, or release artifacts by accident. + +This document defines required isolation. Concrete public domains, Apple team configuration, package registry names, and signing credentials must be resolved from current MacBrains-owned resources during U0 implementation. Do not invent or publish them merely from examples below. + +## Non-negotiable isolation boundaries + +The fork must have independent values for: + +- product display name; +- desktop executable and application name; +- macOS bundle identifier; +- Windows application/product identifiers; +- Linux desktop/AppImage identifiers; +- mobile iOS/Android bundle/package identifiers; +- desktop protocol/custom URL scheme; +- data/config/cache/log directories; +- Keychain/credential-service identifiers; +- IPC/socket/service names; +- npm/registry package names; +- GitHub release repository and updater metadata; +- stable/nightly/dev update channels; +- hosted web domains; +- relay/control-plane resources; +- authentication tenant and passkey relying-party domains; +- APNS/FCM application identifiers; +- analytics/OTLP service identity; +- service/unit/launch-agent names; +- signing/notarization credentials. + +A visible logo or window-title change is not fork isolation. + +## Recommended identity intent + +The implementation should use a central typed identity manifest rather than scattered literals. + +```ts +interface ForkIdentity { + productName: string + productSlug: string + desktopAppName: string + desktopBundleId: string + mobileBundleId: string + androidApplicationId: string + urlScheme: string + dataDirectoryName: string + serviceName: string + cliPackageName: string + updateRepository: string | null + hostedWebOrigin: string | null + relayOrigin: string | null + authTenant: string | null + telemetryServiceName: string +} +``` + +Initial intent, subject to resource validation: + +- product name: `MacBrains T3 Code`; +- product slug: `macbrains-t3code`; +- data root: distinct from `~/.t3`, for example a MacBrains-specific directory; +- application IDs: MacBrains-owned reverse-DNS namespace; +- updater repository: `naytewilson/t3code` or a later dedicated release repository; +- hosted/relay/auth origins: disabled unless explicitly configured with MacBrains-owned resources. + +Do not hard-code placeholder domains or bundle IDs into production release artifacts. Fail closed or clearly mark local-only builds when required identity values are not configured. + +## Data-directory isolation + +### Requirements + +- Never open upstream `~/.t3/userdata` read-write by default. +- A MacBrains install starts with its own empty data root. +- Import from upstream is an explicit one-time or repeatable operation. +- Import is copy-based, schema-validated, logged, and reversible. +- Never symlink active upstream state into the fork. +- Preserve original files and provide an import receipt. +- Detect when both apps are running and prevent shared database access. + +### Import flow + +1. Inspect source profile read-only. +2. Verify no upstream process has the SQLite source open when a consistent copy is required. +3. Copy SQLite database plus WAL/SHM consistently or use SQLite backup API. +4. Copy only supported secrets/settings through typed migration adapters. +5. Rewrite fork-specific endpoint/update/identity fields. +6. Validate and open the imported profile in isolation. +7. Produce a receipt listing imported, skipped, transformed, and failed records. +8. Preserve a rollback snapshot of the fork profile. + +## CLI and package publication + +The upstream release graph publishes npm package `t3` and relies on exact package versions for remote server self-update. The fork must not assume permission to publish that package. + +Choose and verify a distinct package identity before enabling release self-update. Until then: + +- development/local source launches are allowed; +- packaged desktop can supervise its bundled backend; +- remote self-update must be disabled or offer a MacBrains-specific manual command; +- no client should request `t3@` from upstream npm by accident. + +The release invariant remains: a client must never advertise an automatic exact-version server update unless the matching fork server artifact is already available and verified. + +## Desktop update channels + +Use MacBrains-owned GitHub releases and updater metadata. Required separation: + +- repository slug; +- stable/nightly channel names if upstream metadata names would collide; +- artifact names; +- update cache directory; +- public key/signature identity where supported; +- current installed-app identity. + +The About screen must show: + +- MacBrains product version; +- fork commit SHA; +- upstream base commit SHA; +- update channel; +- server version per environment; +- whether the build is local/dev, unsigned, signed, or notarized. + +Do not silently fall back to upstream release feeds. + +## Hosted web, relay, and authentication + +The upstream release process refers to T3-owned Vercel domains, relay infrastructure, Clerk configuration, passkey domains, and production credentials. The fork must not use these implicitly. + +Supported safe modes: + +1. **Local/private-only mode** — desktop/local web plus direct Tailscale/LAN/SSH endpoints; hosted MacBrains control plane disabled. +2. **Self-hosted web mode** — MacBrains-owned HTTPS web origin connecting directly to authenticated environments. +3. **MacBrains relay mode** — enabled only after separate relay, auth tenant, DNS, secrets, notifications, and operational ownership are configured. + +Default the fork to local/private-only mode until MacBrains-owned hosted resources are proven. + +Any UI referencing upstream T3 Connect must be hidden, disabled with a clear reason, or explicitly configurable. Never send environment metadata or user credentials to upstream services as a side effect of branding the fork. + +## Mobile identity + +Mobile release requires independent: + +- iOS bundle ID and provisioning profile; +- associated domains/passkey configuration; +- APNS topic/key/team ownership; +- Android application ID and signing key; +- deep-link schemes and universal/app links; +- push notification environment; +- app store/TestFlight identity. + +Local development may use a clearly marked dev identity. Do not claim mobile distribution complete from simulator-only or upstream TestFlight behavior. + +## Service identity + +Linux systemd and macOS launch/service integration must use fork-specific unit names, paths, logs, and runtime directories. Service management must not stop or rewrite an upstream T3 service. + +Before install/update/uninstall: + +- verify the exact service owner/path; +- preserve existing unrelated units; +- use captured PID/service identity, never name-pattern process killing; +- record a command receipt. + +## Telemetry and privacy + +Use a separate OTLP service name and endpoint configuration. No telemetry endpoint is enabled by default unless the user configured it. Persisted local traces must live under the fork data root. + +The evidence/receipt subsystem is not analytics. Receipts remain user-owned local project evidence unless explicitly exported. + +## Branding assets + +Use the official MacBrains logo and wordmark supplied by the project owner. Do not invent substitutes in implementation branches. Generate required platform sizes from one canonical source asset and verify icon export scripts. + +Required surfaces: + +- desktop app icon/window/About; +- web favicon/PWA/metadata; +- mobile icons/splash/About; +- installer/release artifacts; +- command center empty/loading/error surfaces; +- notification identity. + +Branding must not regress accessibility or performance. + +## Upstream synchronization strategy + +Maintain remotes: + +- `origin` -> Nayte's fork; +- `upstream` -> `pingdotgg/t3code`. + +Recommended durable branches: + +- fork `main`: integrated, proven MacBrains baseline; +- `upstream-sync/`: temporary import branch from current upstream; +- package branches/worktrees: focused implementation; +- release branches/tags: only after full gate. + +Sync procedure: + +1. Fetch origin and upstream. +2. Record current fork main, upstream main, merge base, and divergence. +3. Create isolated upstream-sync worktree/branch. +4. Integrate upstream without rewriting proven fork history. +5. Resolve conflicts by preserving MacBrains contracts and current upstream correctness. +6. Run schema migrations and package-focused checks. +7. Run provider, connection, persistence, command-center, and E0 regression gates as affected. +8. Independent verifier reviews the source diff and acceptance impact. +9. Merge only with a sync receipt that lists upstream commits, conflicts, superseded fork patches, tests, and unresolved gaps. + +Do not routinely rebase long-lived integrated fork main onto upstream. Focused feature branches may rebase before integration when safe. + +## Release channels + +Define at least: + +- `dev`: local source builds, isolated data root, no updater; +- `preview`: internal MacBrains desktop/mobile artifacts, explicit warnings, private update feed if configured; +- `stable`: only after full acceptance matrix and signing/notarization requirements. + +Nightly is optional. Do not create an automated nightly channel until tests, artifact retention, update rollback, and remote server version coordination are proven. + +## Release gate additions + +In addition to the acceptance matrix, release requires: + +- side-by-side upstream/fork install test; +- data-directory collision test; +- protocol/deep-link collision test; +- service collision test; +- clean uninstall preserving upstream and user projects; +- updater repository/feed verification; +- hosted network audit showing no unintended upstream endpoints; +- signed/notarized identity inspection when distributing signed builds; +- mobile bundle/push/deep-link verification; +- rollback from new release to last-known-good where schema permits; +- exact matching remote server package/artifact available before client update action. + +## Failure behavior + +When fork identity configuration is incomplete: + +- build local/dev artifacts with explicit local-only labeling, or fail the release job; +- do not substitute upstream values; +- do not publish; +- do not enable self-update; +- do not enable relay/auth features; +- show the missing configuration in release receipts. + +## Completion criterion + +U0 is `PROVEN` only when a MacBrains build and upstream T3 Code can coexist on the same Mac without sharing application identity, mutable state, services, update feeds, credentials, protocols, or hosted control-plane traffic, and the MacBrains build can be identified and updated through its own verified release path. \ No newline at end of file From 8dfd7fb928953b3b45871f9da25de7dd79f33be9 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:15:18 -0400 Subject: [PATCH 13/32] docs: add safe Mac pull and worktree instructions --- docs/macbrains/PULL_TO_MAC.md | 196 ++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/macbrains/PULL_TO_MAC.md diff --git a/docs/macbrains/PULL_TO_MAC.md b/docs/macbrains/PULL_TO_MAC.md new file mode 100644 index 000000000000..387d48781688 --- /dev/null +++ b/docs/macbrains/PULL_TO_MAC.md @@ -0,0 +1,196 @@ +# Pull the MacBrains T3 Code Work to the Mac + +## What exists now + +The fork contains the specification branch: + +```text +macbrains/agent-workflow-overhaul +``` + +Do not implement directly in an unrelated local checkout without first confirming its remotes, branch, HEAD, dirty state, and worktrees. + +## Existing local clone + +Run from the existing local repository root: + +```sh +git remote -v +git status --short --branch +git worktree list --porcelain +git fetch origin --prune +git rev-parse origin/macbrains/agent-workflow-overhaul +``` + +If `origin` is not `naytewilson/t3code`, do not continue until the correct fork remote is added or selected. + +To inspect the specification without switching the primary checkout, create a separate worktree: + +```sh +mkdir -p ../t3code-worktrees +git worktree add ../t3code-worktrees/macbrains-spec origin/macbrains/agent-workflow-overhaul +cd ../t3code-worktrees/macbrains-spec +git status --short --branch +git rev-parse HEAD +``` + +Read: + +```sh +sed -n '1,240p' MACBRAINS.md +sed -n '1,220p' docs/macbrains/README.md +``` + +Then read every linked specification file before implementation. + +## Fresh clone + +Choose an explicit local parent directory and run: + +```sh +git clone https://github.com/naytewilson/t3code.git +cd t3code +git fetch origin --prune +git switch --track origin/macbrains/agent-workflow-overhaul +git status --short --branch +git rev-parse HEAD +``` + +Do not assume the directory created by `git clone` is the desired permanent project location. Move or clone it into the intended projects directory before agents create long-lived worktrees. + +## Start the first implementation lane + +From a clean fork checkout: + +```sh +git fetch origin --prune +git switch main +git pull --ff-only origin main +mkdir -p ../t3code-worktrees +git worktree add -b macbrains/f0-work-lane-contracts \ + ../t3code-worktrees/f0-work-lane-contracts \ + origin/macbrains/agent-workflow-overhaul +cd ../t3code-worktrees/f0-work-lane-contracts +``` + +The first agent must run source-truth preflight before installing dependencies or editing. It must verify whether implementation should branch from the specification branch or whether that branch has already been integrated into fork `main`. + +Do not reuse the example branch/worktree when another active agent already owns F0. Create a new focused branch and worktree with a unique package identifier. + +## Sync an agent branch onto the Mac + +Given an agent branch named ``: + +```sh +git fetch origin --prune +git show-ref --verify "refs/remotes/origin/" +git worktree add "../t3code-worktrees/" \ + "origin/" +cd "../t3code-worktrees/" +git status --short --branch +git rev-parse HEAD +``` + +Replace placeholders explicitly. Do not paste the angle-bracket values literally. + +If the branch already has a local tracking branch: + +```sh +git switch +git pull --ff-only origin +``` + +Never use `git reset --hard`, `git clean`, or a forced branch switch merely to make the pull succeed. Inspect and preserve local changes first. + +## Verify what was pulled + +```sh +git log --oneline --decorate -n 20 +git diff --stat origin/main...HEAD +git diff --name-status origin/main...HEAD +git status --short --branch +``` + +For the specification branch, verify these files exist: + +```sh +for path in \ + MACBRAINS.md \ + docs/macbrains/README.md \ + docs/macbrains/FORK_BASELINE.md \ + docs/macbrains/DOCUMENTATION_AUDIT.md \ + docs/macbrains/PRODUCT_SPEC.md \ + docs/macbrains/DOMAIN_MODEL.md \ + docs/macbrains/IMPLEMENTATION_LEDGER.md \ + docs/macbrains/ACCEPTANCE_MATRIX.md \ + docs/macbrains/DEFAULT_POLICIES.json \ + docs/macbrains/FORK_IDENTITY_AND_RELEASE.md \ + docs/macbrains/AGENT_EXECUTION_PROMPT.md; do + test -f "$path" || { printf 'missing: %s\n' "$path" >&2; exit 1; } +done +``` + +## Dependency setup + +Use only commands proven by the current checkout's `AGENTS.md`, root `package.json`, lockfiles, and toolchain files. The analyzed baseline uses Vite+ `vp` and a worktree setup script, but the agent must re-verify current requirements before installation. + +Never point a development server at live `~/.t3/userdata`. Worktree development should use its isolated `.t3` state or an explicit safe home directory. + +## Pulling completed implementation to the integration lane + +Do not merge from the primary checkout. Use a clean integration worktree: + +```sh +git fetch origin --prune +git worktree add -b macbrains/integration- \ + ../t3code-worktrees/macbrains-integration- \ + origin/main +cd ../t3code-worktrees/macbrains-integration- +``` + +Before integrating a package branch: + +```sh +git status --short --branch +git log --oneline --decorate origin/main..origin/ +git diff --stat origin/main...origin/ +git diff --name-status origin/main...origin/ +``` + +Use the repository's chosen merge/rebase policy and rerun all checks invalidated by integration. Do not claim that an agent branch is integrated merely because it exists on GitHub. + +## Safe cleanup + +Only remove a worktree after proving it is not active and has no uncommitted work: + +```sh +git -C status --short --branch +git worktree list --porcelain +``` + +Then, using the exact inspected path: + +```sh +git worktree remove +git worktree prune --dry-run +``` + +Do not use process-name killing, wildcard deletion, `git clean`, or forced worktree removal as routine cleanup. + +## Pull completion receipt + +Record: + +- local repository root; +- origin URL; +- branch; +- exact HEAD; +- worktree path; +- clean/dirty state; +- commits pulled; +- changed files; +- dependency setup result; +- checks run on the Mac; +- whether the branch is only present, reviewed, or actually integrated. + +A successful fetch/pull does not count as implementation, integration, testing, or release completion. \ No newline at end of file From d6fc3e44acc5579b107d7c6d126147ccb382e96d Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:15:43 -0400 Subject: [PATCH 14/32] docs: add implementation campaign handoff --- docs/macbrains/IMPLEMENTATION_HANDOFF.md | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/macbrains/IMPLEMENTATION_HANDOFF.md diff --git a/docs/macbrains/IMPLEMENTATION_HANDOFF.md b/docs/macbrains/IMPLEMENTATION_HANDOFF.md new file mode 100644 index 000000000000..ed1d4358f202 --- /dev/null +++ b/docs/macbrains/IMPLEMENTATION_HANDOFF.md @@ -0,0 +1,153 @@ +# MacBrains T3 Code Implementation Handoff + +## Current source-truth receipt + +This handoff was created from GitHub-visible repository state. A local clone was not available in the working container because outbound DNS resolution failed. Therefore no local build, dependency installation, test, lint, or rendered-client validation was performed. + +### Proven repository identity at specification start + +- Fork: `naytewilson/t3code` +- Upstream: `pingdotgg/t3code` +- Baseline fork/upstream commit: `b125b7635170ec0c33f8ddf39299155a21f8c9b9` +- Baseline comparison: identical +- Specification branch: `macbrains/agent-workflow-overhaul` + +Re-verify current HEAD and divergence before implementation; this branch has advanced through documentation commits after the baseline. + +## Durable specification artifacts + +- `MACBRAINS.md` +- `AGENTS.md` MacBrains fork directive +- `docs/macbrains/README.md` +- `docs/macbrains/FORK_BASELINE.md` +- `docs/macbrains/DOCUMENTATION_AUDIT.md` +- `docs/macbrains/PRODUCT_SPEC.md` +- `docs/macbrains/DOMAIN_MODEL.md` +- `docs/macbrains/IMPLEMENTATION_LEDGER.md` +- `docs/macbrains/ACCEPTANCE_MATRIX.md` +- `docs/macbrains/DEFAULT_POLICIES.json` +- `docs/macbrains/FORK_IDENTITY_AND_RELEASE.md` +- `docs/macbrains/PULL_TO_MAC.md` +- `docs/macbrains/AGENT_EXECUTION_PROMPT.md` + +## Specification branch commit sequence + +The branch was built as a sequence of GitHub commits. Agents must use the branch's current HEAD, not assume an intermediate SHA is complete. + +Known commits in creation order: + +- `9f2652a34bbe66faf64bf1db1f9644f44c6cf902` — operating contract +- `dd6e8dbc7eb41004f959e93092f5b0e5ba9f2fb3` — product specification +- `213f22ac6d6b1d9b054e57cf6123446c286307d0` — domain model +- `a1f431fb0a43a6e1b1a4fc58c19ff2393206aba4` — implementation ledger +- `e0131360ae8e29380a650438d3d057b151628bf5` — acceptance matrix +- `465bfe17cb999a5535c2d9732c91adcb30471f9c` — agent execution prompt +- `74ef33122aca001eb938ba4f431cc900752942dc` — fork baseline +- `4862fcf8d39022a4a5962699c8c821c07c2dd3d6` — AGENTS.md fork directive +- `baa70a6f992cd148faefdfe617fb534bd2345e06` — specification index +- `a4c5040ede382a51b4477588e48fa9d1a67dd1b4` — machine-readable defaults +- `58502014b2c9d207e60186e064336f67dbaf5dea` — documentation audit +- `058d355f6fa053fe5080fb5be67f3c431202dd6b` — fork identity/release isolation +- `8dfd7fb928953b3b45871f9da25de7dd79f33be9` — Mac pull/worktree instructions + +This handoff commit and later issue/document changes are not included in the list above. Read branch HEAD directly. + +## First implementation package + +Start with `F0 — Work lane and source-truth contracts`. + +### Required dependency state + +- Specification branch exists and is readable. +- Current event-sourced orchestration must remain canonical. +- Current provider/session/thread schemas and migration system must be inspected. +- No other implementation package may assume lane IDs or completion evidence until F0 contracts are integrated. + +### Ownership boundary + +The F0 owner controls: + +- new lane/source-truth contract modules; +- required orchestration contract integration points; +- lane/source-truth decider commands/events/invariants; +- lane/source-truth projectors and persistence migration; +- client-runtime decode/projection changes required for compatibility; +- focused tests and documentation for F0. + +The owner must avoid unrelated UI, provider adapter, node-job, notification, branding, release, and broad performance work. + +Parallel agents must not edit shared orchestration contracts, decider, projector, or migration registry until the F0 owner publishes a merge contract or integrates the foundation. + +### F0 acceptance rows + +Primary rows: + +- A01-A06 +- B01-B07 +- the F0-relevant portion of C01/C03 +- the migration compatibility portion of P05 +- source-truth visibility scaffolding required for later K/L rows + +Do not mark UI rows proven during F0 unless a real applicable client surface is implemented and tested. + +## First-agent mandatory start output + +The first implementation agent must record: + +```text +PROVEN TARGET +PROVEN REPOSITORY STATE +PROVEN INSTRUCTIONS AND AUTHORITIES +PROVEN BUILD AND TEST SURFACE +PROVEN RELEVANT FILES +OWNERSHIP OR OVERLAP RISKS +INFERRED +UNKNOWN THAT CHANGES ACTION +SAFE NEXT ACTION +``` + +It must then produce: + +- plan revision; +- advisor review because this is domain/persistence work; +- exact schema/migration compatibility strategy; +- focused implementation; +- deterministic tests; +- final diff and staged list; +- verifier review; +- package completion receipt. + +## Architectural warnings + +- `packages/contracts/src/orchestration.ts` is already large. Prefer explicit new subpath modules and one integration owner rather than increasing parallel contention. +- Shell projections must stay compact. Do not put complete lane evidence/log payloads into the existing project/thread shell. +- Existing threads require a migration/import rule that does not falsely mark history complete. +- Completion policy belongs in the decider, not provider adapters or React. +- WorkLane is not a replacement name for Thread. A lane may own/recover across multiple provider threads. +- SourceTruthRevision is durable evidence lineage, not a cached `git status` widget. +- Do not introduce sleeps/polling in tests; use receipts and worker drains. +- Do not run development against live `~/.t3/userdata`. + +## Known evidence gaps + +- No local tree/worktree/status receipt was possible from the execution environment used to write the specification. +- No test command was executed. +- No TypeScript/JSON formatter was run locally. +- No rendered markdown or client UI was inspected. +- No package implementation exists yet. +- No PR was opened because the repository instructions prohibit PR creation without explicit authorization. + +Agents must not convert the existence of this specification into an implementation claim. + +## Campaign stop conditions + +The campaign stops only when: + +- every required implementation package is integrated; +- every required acceptance row is `PROVEN` on current revisions; +- the canonical Mac + Linux node + mobile E0 scenario passes; +- fork identity/release paths are isolated and verified; +- current tests/CI and user-visible acceptance are attached; +- the final completion receipt is independently verified. + +A plan, documentation branch, build success, passing unit tests, or uploaded installer by itself does not satisfy the campaign. \ No newline at end of file From 741fd22009f8df6bc7d77ce84f3732dfc74b9a2d Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:16:16 -0400 Subject: [PATCH 15/32] docs: complete MacBrains specification index --- docs/macbrains/README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/macbrains/README.md b/docs/macbrains/README.md index 41f728956230..ecc240896dbf 100644 --- a/docs/macbrains/README.md +++ b/docs/macbrains/README.md @@ -4,12 +4,16 @@ Read in this order: 1. [`../../MACBRAINS.md`](../../MACBRAINS.md) — non-negotiable operating contract. 2. [`FORK_BASELINE.md`](./FORK_BASELINE.md) — verified initial architecture and limitation inventory; re-verify before acting. -3. [`PRODUCT_SPEC.md`](./PRODUCT_SPEC.md) — complete product behavior and UX. -4. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. -5. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. -6. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. -7. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. -8. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. +3. [`DOCUMENTATION_AUDIT.md`](./DOCUMENTATION_AUDIT.md) — documentation coverage, contradictions, operational constraints, and issue-backed risk register. +4. [`PRODUCT_SPEC.md`](./PRODUCT_SPEC.md) — complete product behavior and UX. +5. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. +6. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. +7. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. +8. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. +9. [`FORK_IDENTITY_AND_RELEASE.md`](./FORK_IDENTITY_AND_RELEASE.md) — application, data, service, hosted-control-plane, and updater isolation from upstream. +10. [`PULL_TO_MAC.md`](./PULL_TO_MAC.md) — safe fetch, worktree, verification, integration, and cleanup instructions for the Mac. +11. [`IMPLEMENTATION_HANDOFF.md`](./IMPLEMENTATION_HANDOFF.md) — exact campaign handoff, evidence gaps, first package ownership, and stop conditions. +12. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. ## Canonical status @@ -26,6 +30,10 @@ Start with `F0 — Work lane and source-truth contracts`. Do not begin disconnec - obtain independent architecture advice before changing persistence/domain schemas; - implement the full F0 vertical slice with migration and focused proof. +## Campaign tracking + +GitHub Issues are disabled in this fork, so the authoritative implementation tracker is `IMPLEMENTATION_LEDGER.md` plus the evidence fields in `ACCEPTANCE_MATRIX.md`. Do not maintain a conflicting private checklist. + ## Campaign completion The campaign is complete only after every required acceptance row is `PROVEN` on current revisions and the E0 canonical Mac + Linux node + mobile scenario passes with a restart/recovery evidence bundle. \ No newline at end of file From 7db2f253a998ef1c55b03f94afd929c150ad4af4 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:17:09 -0400 Subject: [PATCH 16/32] docs: add project-specific workflow profiles --- docs/macbrains/PROJECT_PROFILES.md | 304 +++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 docs/macbrains/PROJECT_PROFILES.md diff --git a/docs/macbrains/PROJECT_PROFILES.md b/docs/macbrains/PROJECT_PROFILES.md new file mode 100644 index 000000000000..4db3b6d46365 --- /dev/null +++ b/docs/macbrains/PROJECT_PROFILES.md @@ -0,0 +1,304 @@ +# MacBrains Project Profiles + +## Purpose + +These profiles adapt the generic lane model to Nayte's actual repositories and recurring work. They are policy templates, not proof that a path, branch, service, database, or experiment is current. Project onboarding must verify the real checkout, Git state, canonical records, and connected environments before applying a profile. + +The UI should let a project inherit one profile and then show every effective override. + +## Common rules for all profiles + +- Run read-only source-truth preflight before planning or editing. +- Use an isolated worktree for substantial changes. +- Keep executor, advisor, verifier, child, and recovery assignments visible. +- Use the smallest sufficient model; normal implementation defaults to medium reasoning. +- Require independent advice for architecture, persistence, concurrency, security, performance, migrations, destructive operations, or conflicting evidence. +- Do not use a passing build as a substitute for acceptance. +- Do not mark backend work complete without a visible launch/access path when the project has a user-facing surface. +- Record exact commands, outputs, diffs, checks, staged files, and current commit. +- No Python implementation or worker automation on the Linux node. + +## Profile: ANE-RE + +### Identity hints + +- Logical project: `ANE-RE` +- Known Mac checkout hint: `/Users/nayte/Projects/ane-re` +- Known GitHub identity: `naytewilson/ane-re` +- A GitLab mirror may exist. + +Treat every path/remote as unstable until preflight verifies it. + +### Mission + +Research, measure, and implement real Apple Neural Engine placement, compiler, runtime, emitter, token-generation, and performance advances without converting hypotheses into claims. + +### Canonical authority order + +1. Current repository and scoped instructions. +2. Current experiment ledger/manifests and designated canonical campaign records. +3. Raw measurement artifacts, environment captures, generated models, traces, and independent recomputation receipts. +4. Current CI/build/test output. +5. Durable handoffs. +6. Conversation narrative. + +### Required lane types + +- Experiment design +- Harness implementation +- Compiler/emitter implementation +- Runtime investigation +- Placement verification +- Performance confirmation +- Independent recomputation +- Integration/review +- Recovery of partial campaign + +### Experiment contract + +Every experiment lane records: + +- exact hypothesis and falsification condition; +- baseline and candidate commits; +- hardware, OS/build, frameworks, power/thermal state, and relevant environment; +- generated model/artifact hashes; +- command lines and repetitions; +- correctness gates before performance claims; +- ANE placement/residency evidence method; +- warmup, sample count, variance, and outlier policy; +- raw and summarized results; +- independent recomputation environment; +- verdict vocabulary and superseded prior claims. + +No percentage, token rate, speedup, placement, residency, or correctness claim is `PROVEN` without raw artifacts tied to the exact revision. + +### Environment policy + +- Apple-hardware-dependent ANE compilation/runtime validation runs on the Mac or another explicitly supported Apple environment. +- Linux node may perform native compiled CPU work, statistics implemented without Python, data validation, artifact hashing, and independent recomputation where semantically valid. +- Cloud compute follows the project's current canonical provider and manifest; historical providers are not assumed current. +- Node results are not ANE placement proof. + +### Ownership policy + +Parallel experiment lanes may share immutable inputs but must not edit the same harness, generator, manifest, or canonical verdict registry concurrently. Designate one integration owner for shared experiment schemas and ledgers. + +### Completion + +A campaign closes only with: + +- correctness result; +- placement/residency result when claimed; +- performance result with measurement floor respected; +- raw artifact bundle; +- independent review/recomputation; +- canonical ledger update; +- exact integration commit or explicit falsified/no-change verdict. + +## Profile: ANVIL + +### Identity hints + +- Logical project: `ANVIL` +- Known Mac checkout hint: `/Users/Nayte/ANVIL` +- Primary implementation language: Swift, with native compiled helpers where needed. + +Always write `ANVIL` uppercase except exact case-sensitive identifiers, commands, and paths. + +### Mission + +Operate ANVIL as an OS-like durable orchestration system with source-truth-first package execution, crash recovery, worker isolation, resource-aware scheduling, event-driven dispatch, verification, and visible user control. + +### Canonical authority order + +1. Current repository and `AGENTS.md`/project instructions. +2. Canonical Postgres records, package ledger, manifests, receipts, and designated decision tables. +3. Current Git/worktree state. +4. Current tests/build output. +5. Durable handoffs. +6. Conversation narrative. + +### Default package execution flow + +1. Read repository instructions. +2. Read the command center/current authoritative runbook. +3. Load the current completion/package manifest. +4. Select the next executable package from canonical state. +5. Emit a source-truth preflight record. +6. Execute only the selected package in an isolated worktree. +7. Produce receipts and focused verification. +8. Integrate or open the authorized PR. +9. Update canonical state. +10. Continue to the next executable package only when ownership/dependencies permit. + +Do not select work from remembered conversation when canonical package state is available. + +### Linux node policy + +- No Python implementation, scripts, or worker prompts. +- Prefer C, C++, Rust, Swift where supported, shell, or another native compiled language. +- Pin source commit and input hashes. +- Return content-addressed artifacts and logs. +- Mac integration and verification are separate events. +- Postgres access follows current network, role, and read/write policy; never assume broad database authority. + +### Agent topology + +- Executor: normal package implementation. +- Advisor: architecture/state-machine/persistence/concurrency/security decisions. +- Verifier: current source, tests, receipts, DB/ledger consistency, and final diff. +- Recovery agent: stale or contradicted executor context. +- Child workers: bounded math, tests, or independent validation with explicit ownership. + +### Completion + +ANVIL package completion requires: + +- canonical package state updated; +- exact source-truth receipt; +- tests and build on current commit; +- final diff/staged list; +- database/migration verification where applicable; +- visible control/status surface when user-facing; +- no unresolved ownership or package dependency conflict. + +## Profile: MacBrains native product + +### Mission + +Build persistent local intelligence and user-facing Mac software that is obvious to launch, continuously useful, resource-aware, and visible across Mac and mobile control surfaces. + +### Technical preference + +- Prefer native Swift/SwiftUI/AppKit/Foundation and native compiled helpers for long-running Mac services. +- Electron/React may remain where inherited by T3 Code, but fork additions must respect performance and avoid duplicating native OS responsibilities unnecessarily. +- Use launchd/FSEvents/AXUIElement and platform APIs only with explicit lifecycle, sandbox, entitlement, accessibility, and privacy review. + +### Delivery invariant + +Backend implementation is never enough. Every feature needs: + +- obvious navigation or launch action; +- persistent status after restart; +- real attention notifications where applicable; +- exact access path from desktop and mobile when applicable; +- end-to-end acceptance evidence that Nayte can see and use. + +### Completion + +Include packaged/local launch path, persistence/restart test, accessibility review, resource-use evidence, and visible acceptance receipt. + +## Profile: NeoDSP and native macOS system components + +### Mission + +Develop native macOS audio, driver, HAL, service, and system integration safely and with platform-specific review. + +### Required advisor/verifier specialties + +- Swift/macOS concurrency and lifecycle; +- Core Audio/HAL behavior; +- sandbox, entitlements, signing, notarization; +- memory/resource ownership; +- real-time thread safety; +- accessibility and user control; +- installation/uninstallation rollback. + +### Hard gates + +- No allocation, blocking, logging, or unsafe synchronization on real-time audio paths unless explicitly proven safe. +- No entitlement or signing assumptions. +- System installation has exact rollback and coexistence tests. +- Packaged behavior must be tested outside the IDE. + +## Profile: Sieve and security-sensitive tooling + +### Mission + +Build observable local security/network tooling without hiding interception, trust, credentials, or ownership boundaries. + +### Required gates + +- Advisor and verifier required. +- Explicit threat model and data-handling contract. +- No silent certificate, proxy, trust-store, traffic, or credential changes. +- Reversible setup/uninstall. +- Secrets never enter receipts or UI payloads. +- End-to-end test proves the visible dashboard matches real runtime ownership. + +## Profile: Frontier Atlas and web dashboards + +### Mission + +Deliver a fast, evidence-backed visual dashboard rather than a disconnected static frontend. + +### Required behavior + +- data provenance visible; +- stale/current state explicit; +- large lists virtualized; +- no continuously repainting animation; +- mobile-responsive control where useful; +- backend/API and frontend contract tested together; +- deployment or local launch path included. + +## Profile: IFAR and research architecture initiatives + +### Mission + +Explore high-upside ANE/model architecture ideas while keeping explorer creativity separate from canonical proof. + +### Topology + +- Explorer: preserves continuity and generates hypotheses/designs. +- Canonical verifier: independently validates math, source constraints, measurements, and implementability. +- Executor: implements only accepted bounded candidates. +- Performance verifier: tests correctness, placement, and measurement reliability. + +Do not interrupt the explorer with constant source policing, but do not promote explorer claims into project truth without independent verification. + +## Profile: CPU/GPU/benchmark campaigns + +### Required contract + +- baseline commit and environment; +- workload definition and inputs; +- correctness oracle; +- warmup/sample/variance plan; +- thermal/power/resource telemetry; +- cancellation and partial-output semantics; +- raw results preserved throughout; +- final verdict distinguishes partial, falsified, inconclusive, and proven. + +A stopped soak with partial CSVs is not a finished campaign and must remain visibly partial. + +## Profile: Imported or unfamiliar repository + +Use this when no named profile applies. + +1. Inspect repository and instructions. +2. Detect language/build/test surfaces from actual files. +3. Identify user-visible product surface. +4. Establish source of truth and ownership. +5. Select the smallest applicable workflow template. +6. Persist a project-specific profile proposal as `INFERRED`. +7. Advisor reviews before broad architecture changes. + +Do not force ANE-RE or ANVIL conventions onto unrelated repositories. + +## UI implementation requirements + +Project settings must display: + +- selected profile and version; +- path/remote hints versus verified current values; +- source-authority order; +- default lane templates; +- default topology/model intent; +- environment/node policy; +- required receipts and checks; +- delivery invariant; +- project-specific prohibited actions; +- effective overrides and origin. + +A lane creation form should recommend a profile-derived template but allow explicit override. Every override is recorded in the task contract. \ No newline at end of file From bbaf42ebbe612f47d4f6e5eab885ce943bc82ab8 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:17:39 -0400 Subject: [PATCH 17/32] docs: add machine-readable campaign manifest --- docs/macbrains/CAMPAIGN_MANIFEST.json | 305 ++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 docs/macbrains/CAMPAIGN_MANIFEST.json diff --git a/docs/macbrains/CAMPAIGN_MANIFEST.json b/docs/macbrains/CAMPAIGN_MANIFEST.json new file mode 100644 index 000000000000..42274876f128 --- /dev/null +++ b/docs/macbrains/CAMPAIGN_MANIFEST.json @@ -0,0 +1,305 @@ +{ + "$schemaVersion": 1, + "campaignId": "macbrains-t3code-agent-workflow-overhaul", + "title": "MacBrains T3 Code Agent Workflow Overhaul", + "specificationBranch": "macbrains/agent-workflow-overhaul", + "canonicalDocuments": [ + "MACBRAINS.md", + "docs/macbrains/README.md", + "docs/macbrains/FORK_BASELINE.md", + "docs/macbrains/DOCUMENTATION_AUDIT.md", + "docs/macbrains/PRODUCT_SPEC.md", + "docs/macbrains/PROJECT_PROFILES.md", + "docs/macbrains/DOMAIN_MODEL.md", + "docs/macbrains/IMPLEMENTATION_LEDGER.md", + "docs/macbrains/ACCEPTANCE_MATRIX.md", + "docs/macbrains/DEFAULT_POLICIES.json", + "docs/macbrains/FORK_IDENTITY_AND_RELEASE.md", + "docs/macbrains/PULL_TO_MAC.md", + "docs/macbrains/IMPLEMENTATION_HANDOFF.md", + "docs/macbrains/AGENT_EXECUTION_PROMPT.md" + ], + "statusVocabulary": [ + "NOT_STARTED", + "IN_PROGRESS", + "BLOCKED", + "IMPLEMENTED_UNVERIFIED", + "PROVEN", + "SUPERSEDED" + ], + "selectionRules": { + "chooseEarliestExecutableDependency": true, + "sourceTruthPreflightRequired": true, + "isolatedWorktreeRequired": true, + "disjointParallelOwnershipRequired": true, + "advisorForHighRisk": true, + "independentVerifierRequired": true, + "continueWithoutRoutineConfirmation": true, + "pullRequestRequiresTaskAuthorization": true + }, + "packages": [ + { + "id": "F0", + "title": "Work lane and source-truth contracts", + "status": "NOT_STARTED", + "dependencies": [], + "ownershipGroup": "contracts-orchestration-foundation", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["A", "B"], + "blocks": ["F1", "F3", "F4", "F5", "N0"] + }, + { + "id": "F1", + "title": "Typed receipt and evidence substrate", + "status": "NOT_STARTED", + "dependencies": ["F0"], + "ownershipGroup": "contracts-evidence-server", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["G"], + "blocks": ["F2", "D0", "R0"] + }, + { + "id": "F2", + "title": "Checks, acceptance criteria, and completion gate", + "status": "NOT_STARTED", + "dependencies": ["F0", "F1"], + "ownershipGroup": "orchestration-completion", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["H"], + "blocks": ["D0", "D1", "E0"] + }, + { + "id": "F3", + "title": "Agent assignments and topology", + "status": "NOT_STARTED", + "dependencies": ["F0"], + "ownershipGroup": "assignments-topology", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["D"], + "blocks": ["P0", "F6"] + }, + { + "id": "F4", + "title": "Worktree and ownership manager", + "status": "NOT_STARTED", + "dependencies": ["F0"], + "ownershipGroup": "git-worktrees", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["C"], + "blocks": ["G0", "F6"] + }, + { + "id": "F5", + "title": "Command Center and projections", + "status": "NOT_STARTED", + "dependencies": ["F0", "F1", "F2", "F3"], + "ownershipGroup": "client-runtime-command-center", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["K"], + "blocks": ["F6"] + }, + { + "id": "F6", + "title": "Lane detail and cross-surface controls", + "status": "NOT_STARTED", + "dependencies": ["F2", "F3", "F4", "F5"], + "ownershipGroup": "web-desktop-mobile-lane-ui", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["J", "K", "L"], + "blocks": ["E0"] + }, + { + "id": "P0", + "title": "Provider event normalization", + "status": "NOT_STARTED", + "dependencies": ["F3"], + "ownershipGroup": "provider-adapters", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["E"], + "blocks": ["P1", "P2"] + }, + { + "id": "P1", + "title": "Background activity leases and session reaper correctness", + "status": "NOT_STARTED", + "dependencies": ["P0"], + "ownershipGroup": "provider-lifecycle", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["F"], + "blocks": ["E0"] + }, + { + "id": "P2", + "title": "Context health, compaction, and recovery handoff", + "status": "NOT_STARTED", + "dependencies": ["F1", "F3", "P0"], + "ownershipGroup": "provider-context-recovery", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["E", "F"], + "blocks": ["E0"] + }, + { + "id": "N0", + "title": "First-class environment and node policy", + "status": "NOT_STARTED", + "dependencies": ["F0"], + "ownershipGroup": "environment-node-contracts", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["I", "J"], + "blocks": ["N1"] + }, + { + "id": "N1", + "title": "Node job runtime", + "status": "NOT_STARTED", + "dependencies": ["F1", "N0"], + "ownershipGroup": "node-runtime", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["I"], + "blocks": ["N2"] + }, + { + "id": "N2", + "title": "Artifact return and Mac integration", + "status": "NOT_STARTED", + "dependencies": ["F1", "F2", "N1"], + "ownershipGroup": "artifact-transfer-integration", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["I"], + "blocks": ["E0"] + }, + { + "id": "R0", + "title": "Atomic persistence and last-known-good recovery", + "status": "NOT_STARTED", + "dependencies": ["F1"], + "ownershipGroup": "persistence-reliability", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["P"], + "blocks": ["R1"] + }, + { + "id": "R1", + "title": "Event/projection and heavy-thread resilience", + "status": "NOT_STARTED", + "dependencies": ["F1", "R0"], + "ownershipGroup": "projection-resilience", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["P", "Q"], + "blocks": ["E0"] + }, + { + "id": "D0", + "title": "Deliverable registry and launch actions", + "status": "NOT_STARTED", + "dependencies": ["F1", "F2"], + "ownershipGroup": "deliverables", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["M"], + "blocks": ["D1", "D2"] + }, + { + "id": "D1", + "title": "User-visible UI acceptance", + "status": "NOT_STARTED", + "dependencies": ["D0", "F6"], + "ownershipGroup": "ui-acceptance", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["M", "U"], + "blocks": ["E0"] + }, + { + "id": "D2", + "title": "Actionable notifications", + "status": "NOT_STARTED", + "dependencies": ["F1", "D0"], + "ownershipGroup": "notifications", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["N"], + "blocks": ["E0"] + }, + { + "id": "G0", + "title": "GitHub and source-control evidence integration", + "status": "NOT_STARTED", + "dependencies": ["F1", "F4"], + "ownershipGroup": "source-control-providers", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["O"], + "blocks": ["E0"] + }, + { + "id": "S0", + "title": "Project instruction and skill registry", + "status": "NOT_STARTED", + "dependencies": ["F0", "F1"], + "ownershipGroup": "instructions-skills", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["T"], + "blocks": ["E0"] + }, + { + "id": "U0", + "title": "MacBrains branding and fork identity", + "status": "NOT_STARTED", + "dependencies": [], + "ownershipGroup": "fork-identity-release", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["S"], + "blocks": ["E0"] + }, + { + "id": "U1", + "title": "Settings and policy editor", + "status": "NOT_STARTED", + "dependencies": ["F0", "F3", "N0"], + "ownershipGroup": "settings-policy-ui", + "advisorRequired": false, + "primaryAcceptancePrefixes": ["R", "S", "T"], + "blocks": ["E0"] + }, + { + "id": "V0", + "title": "Performance and telemetry hardening", + "status": "NOT_STARTED", + "dependencies": ["F5", "R1"], + "ownershipGroup": "performance-observability", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["Q"], + "blocks": ["E0"] + }, + { + "id": "E0", + "title": "End-to-end daily workflow acceptance", + "status": "NOT_STARTED", + "dependencies": [ + "F2", + "F6", + "P1", + "P2", + "N2", + "R1", + "D1", + "D2", + "G0", + "S0", + "U0", + "U1", + "V0" + ], + "ownershipGroup": "integration-verification", + "advisorRequired": true, + "primaryAcceptancePrefixes": ["U"], + "blocks": [] + } + ], + "releaseGate": { + "allRequiredAcceptanceRowsProven": true, + "canonicalE0ScenarioProven": true, + "currentRevisionEvidenceOnly": true, + "independentVerificationRequired": true, + "forkIdentityIsolationProven": true, + "desktopWebIphoneIpadApplicabilityCovered": true, + "macAndLinuxRestartRecoveryProven": true + } +} \ No newline at end of file From b913b430377e39fb07099a429b97394072f0748c Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:18:05 -0400 Subject: [PATCH 18/32] docs: add project-aware workflow templates --- docs/macbrains/WORKFLOW_TEMPLATES.json | 332 +++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 docs/macbrains/WORKFLOW_TEMPLATES.json diff --git a/docs/macbrains/WORKFLOW_TEMPLATES.json b/docs/macbrains/WORKFLOW_TEMPLATES.json new file mode 100644 index 000000000000..f1c583824a9f --- /dev/null +++ b/docs/macbrains/WORKFLOW_TEMPLATES.json @@ -0,0 +1,332 @@ +{ + "$schemaVersion": 1, + "templates": [ + { + "id": "implement-feature", + "title": "Implement feature", + "classification": "substantial", + "lifecycle": [ + "preflight", + "orientation", + "plan", + "advisor-when-risk-triggered", + "implementation", + "focused-checks", + "independent-review", + "ui-acceptance-when-applicable", + "deliverable", + "completion" + ], + "topology": ["executor", "advisor", "verifier"], + "worktreeRequired": true, + "requiredReceipts": [ + "source-truth-preflight", + "orientation", + "plan", + "diff", + "checks", + "review", + "deliverable", + "completion" + ] + }, + { + "id": "fix-bug", + "title": "Fix bug", + "classification": "substantial", + "lifecycle": [ + "preflight", + "reproduction", + "root-cause", + "plan", + "implementation", + "regression-test", + "affected-checks", + "independent-review", + "visible-acceptance-when-applicable", + "completion" + ], + "topology": ["executor", "verifier"], + "advisorTriggers": [ + "architecture", + "persistence", + "concurrency", + "security", + "performance-hot-path", + "failed-first-fix" + ], + "worktreeRequired": true, + "requiredReceipts": [ + "source-truth-preflight", + "reproduction", + "plan", + "diff", + "regression-check", + "review", + "completion" + ] + }, + { + "id": "review-pull-request", + "title": "Review pull request", + "classification": "readonly-or-bounded", + "lifecycle": [ + "preflight", + "fetch-current-pr", + "inspect-diff-and-source", + "run-focused-checks", + "classify-findings", + "verify-findings", + "submit-authorized-review", + "completion" + ], + "topology": ["verifier"], + "worktreeRequired": "when-running-or-editing", + "requiredReceipts": [ + "source-truth-preflight", + "pr-state", + "review", + "checks", + "completion" + ] + }, + { + "id": "repository-audit", + "title": "Repository audit", + "classification": "readonly", + "lifecycle": [ + "preflight", + "instruction-and-architecture-read", + "source-and-test-inspection", + "risk-classification", + "evidence-report", + "completion" + ], + "topology": ["executor", "verifier"], + "worktreeRequired": false, + "requiredReceipts": [ + "source-truth-preflight", + "orientation", + "review", + "completion" + ] + }, + { + "id": "research-and-design", + "title": "Research and design", + "classification": "substantial-readonly", + "lifecycle": [ + "source-collection", + "exploration", + "candidate-designs", + "canonical-verification", + "architecture-decision", + "implementation-contract", + "completion" + ], + "topology": ["explorer", "advisor", "verifier"], + "worktreeRequired": "when-writing-canonical-design", + "requiredReceipts": [ + "source-truth-preflight", + "research-source-ledger", + "advisor", + "review", + "decision", + "completion" + ] + }, + { + "id": "experiment-benchmark", + "title": "Experiment or benchmark", + "classification": "substantial", + "lifecycle": [ + "preflight", + "hypothesis-and-falsification", + "harness-orientation", + "measurement-plan", + "correctness-gate", + "execution", + "raw-artifact-capture", + "independent-recomputation", + "verdict", + "ledger-update", + "completion" + ], + "topology": ["executor", "advisor", "verifier"], + "worktreeRequired": true, + "requiredReceipts": [ + "source-truth-preflight", + "experiment-contract", + "environment", + "commands", + "correctness-check", + "raw-artifacts", + "statistics", + "review", + "verdict", + "completion" + ] + }, + { + "id": "recovery-resume", + "title": "Recover or resume interrupted work", + "classification": "substantial", + "lifecycle": [ + "source-truth-preflight", + "recover-canonical-handoff", + "compare-provider-and-repository-state", + "classify-safe-resume-point", + "restore-or-supersede-assignment", + "rerun-stale-checks", + "continue-execution", + "review", + "completion" + ], + "topology": ["recovery", "verifier"], + "worktreeRequired": true, + "requiredReceipts": [ + "source-truth-preflight", + "recovery", + "diff", + "checks", + "review", + "completion" + ] + }, + { + "id": "anvil-package-execution", + "title": "ANVIL package execution", + "projectProfile": "ANVIL", + "classification": "substantial", + "lifecycle": [ + "read-project-instructions", + "read-command-center", + "load-canonical-manifest", + "select-next-executable-package", + "source-truth-preflight-event", + "isolated-package-execution", + "focused-verification", + "receipt-production", + "canonical-state-update", + "authorized-integration-or-pr", + "completion" + ], + "topology": ["executor", "advisor", "verifier"], + "worktreeRequired": true, + "linuxNode": { + "pythonAllowed": false, + "pinSourceCommit": true, + "contentAddressedArtifacts": true, + "macIntegrationSeparate": true + }, + "requiredReceipts": [ + "source-truth-preflight", + "canonical-package-selection", + "plan", + "commands", + "checks", + "diff", + "review", + "canonical-state-update", + "completion" + ] + }, + { + "id": "ane-re-experiment", + "title": "ANE-RE experiment campaign", + "projectProfile": "ANE-RE", + "classification": "substantial", + "lifecycle": [ + "repository-and-ledger-preflight", + "hypothesis-and-falsification", + "baseline-artifact-pin", + "harness-correctness", + "placement-evidence-plan", + "measurement-floor-plan", + "execution", + "raw-artifact-preservation", + "independent-recomputation", + "canonical-verdict", + "ledger-update", + "integration-or-falsified-closeout" + ], + "topology": ["explorer", "executor", "advisor", "verifier"], + "worktreeRequired": true, + "linuxNode": { + "pythonAllowed": false, + "allowedFor": [ + "native-cpu-work", + "compiled-statistics", + "artifact-validation", + "independent-recomputation" + ], + "notProofOf": ["ANE-placement", "ANE-residency"] + }, + "requiredReceipts": [ + "source-truth-preflight", + "experiment-contract", + "environment", + "correctness", + "placement-when-claimed", + "measurement", + "raw-artifacts", + "independent-review", + "verdict", + "ledger-update", + "completion" + ] + }, + { + "id": "native-macos-system-feature", + "title": "Native macOS system feature", + "projectProfiles": ["MacBrains", "NeoDSP"], + "classification": "high-risk", + "lifecycle": [ + "preflight", + "platform-and-entitlement-orientation", + "architecture-advisor", + "implementation", + "focused-unit-and-integration-checks", + "lifecycle-and-resource-review", + "packaged-app-test", + "accessibility-and-visible-acceptance", + "rollback-uninstall-test", + "completion" + ], + "topology": ["executor", "advisor", "verifier"], + "worktreeRequired": true, + "requiredReceipts": [ + "source-truth-preflight", + "advisor", + "platform-review", + "checks", + "packaging", + "ui-acceptance", + "rollback", + "completion" + ] + }, + { + "id": "documentation-artifact", + "title": "Documentation or reusable artifact", + "classification": "bounded", + "lifecycle": [ + "source-read", + "draft", + "format-or-render-validation", + "fact-and-link-verification", + "deliverable", + "completion" + ], + "topology": ["executor", "verifier"], + "worktreeRequired": "when-repository-tracked", + "requiredReceipts": [ + "source-truth-preflight-when-repository-tracked", + "artifact", + "validation", + "review", + "completion" + ] + } + ] +} \ No newline at end of file From 79dc2e18c8986522b36a697779fb6f9a9a46442b Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:18:28 -0400 Subject: [PATCH 19/32] docs: bind operating contract to complete campaign artifacts --- MACBRAINS.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MACBRAINS.md b/MACBRAINS.md index 5fd5bb0f604e..03d3fcdaa43d 100644 --- a/MACBRAINS.md +++ b/MACBRAINS.md @@ -142,12 +142,21 @@ Every completed or blocked run must close with these fields: ## Implementation map -The complete product and engineering specification is in: +The complete product and engineering specification is indexed by `docs/macbrains/README.md`. The required campaign artifacts are: +- `docs/macbrains/FORK_BASELINE.md` +- `docs/macbrains/DOCUMENTATION_AUDIT.md` - `docs/macbrains/PRODUCT_SPEC.md` +- `docs/macbrains/PROJECT_PROFILES.md` - `docs/macbrains/DOMAIN_MODEL.md` - `docs/macbrains/IMPLEMENTATION_LEDGER.md` +- `docs/macbrains/CAMPAIGN_MANIFEST.json` +- `docs/macbrains/WORKFLOW_TEMPLATES.json` - `docs/macbrains/ACCEPTANCE_MATRIX.md` +- `docs/macbrains/DEFAULT_POLICIES.json` +- `docs/macbrains/FORK_IDENTITY_AND_RELEASE.md` +- `docs/macbrains/PULL_TO_MAC.md` +- `docs/macbrains/IMPLEMENTATION_HANDOFF.md` - `docs/macbrains/AGENT_EXECUTION_PROMPT.md` -Agents must read all five before changing behavior governed by this fork. \ No newline at end of file +Agents must read the index, contract, project profile, selected workflow template, current package definition, and applicable acceptance rows before changing behavior governed by this fork. \ No newline at end of file From a0fb7597ebdeddc5d0c86ef86ada24482a9b60b2 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:18:41 -0400 Subject: [PATCH 20/32] docs: index project profiles and campaign manifests --- docs/macbrains/README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/macbrains/README.md b/docs/macbrains/README.md index ecc240896dbf..0ccc5af17864 100644 --- a/docs/macbrains/README.md +++ b/docs/macbrains/README.md @@ -6,14 +6,17 @@ Read in this order: 2. [`FORK_BASELINE.md`](./FORK_BASELINE.md) — verified initial architecture and limitation inventory; re-verify before acting. 3. [`DOCUMENTATION_AUDIT.md`](./DOCUMENTATION_AUDIT.md) — documentation coverage, contradictions, operational constraints, and issue-backed risk register. 4. [`PRODUCT_SPEC.md`](./PRODUCT_SPEC.md) — complete product behavior and UX. -5. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. -6. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. -7. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. -8. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. -9. [`FORK_IDENTITY_AND_RELEASE.md`](./FORK_IDENTITY_AND_RELEASE.md) — application, data, service, hosted-control-plane, and updater isolation from upstream. -10. [`PULL_TO_MAC.md`](./PULL_TO_MAC.md) — safe fetch, worktree, verification, integration, and cleanup instructions for the Mac. -11. [`IMPLEMENTATION_HANDOFF.md`](./IMPLEMENTATION_HANDOFF.md) — exact campaign handoff, evidence gaps, first package ownership, and stop conditions. -12. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. +5. [`PROJECT_PROFILES.md`](./PROJECT_PROFILES.md) — ANE-RE, ANVIL, MacBrains, NeoDSP, Sieve, Frontier Atlas, IFAR, and campaign-specific policy profiles. +6. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. +7. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. +8. [`CAMPAIGN_MANIFEST.json`](./CAMPAIGN_MANIFEST.json) — machine-readable dependencies, ownership groups, status, and release gate. +9. [`WORKFLOW_TEMPLATES.json`](./WORKFLOW_TEMPLATES.json) — reusable feature, bug, review, research, experiment, recovery, ANVIL, ANE-RE, native macOS, and artifact workflows. +10. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. +11. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. +12. [`FORK_IDENTITY_AND_RELEASE.md`](./FORK_IDENTITY_AND_RELEASE.md) — application, data, service, hosted-control-plane, and updater isolation from upstream. +13. [`PULL_TO_MAC.md`](./PULL_TO_MAC.md) — safe fetch, worktree, verification, integration, and cleanup instructions for the Mac. +14. [`IMPLEMENTATION_HANDOFF.md`](./IMPLEMENTATION_HANDOFF.md) — exact campaign handoff, evidence gaps, first package ownership, and stop conditions. +15. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. ## Canonical status @@ -32,7 +35,7 @@ Start with `F0 — Work lane and source-truth contracts`. Do not begin disconnec ## Campaign tracking -GitHub Issues are disabled in this fork, so the authoritative implementation tracker is `IMPLEMENTATION_LEDGER.md` plus the evidence fields in `ACCEPTANCE_MATRIX.md`. Do not maintain a conflicting private checklist. +GitHub Issues are disabled in this fork, so the authoritative implementation tracker is `CAMPAIGN_MANIFEST.json`, the detailed package contract in `IMPLEMENTATION_LEDGER.md`, and the evidence fields in `ACCEPTANCE_MATRIX.md`. Do not maintain a conflicting private checklist. ## Campaign completion From 68b11f25d82b0feaeba8b3b8c1eb8dad8df64d49 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:19:22 -0400 Subject: [PATCH 21/32] docs: add continuous multi-agent implementation topology --- docs/macbrains/ORCHESTRATION_PLAN.md | 350 +++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 docs/macbrains/ORCHESTRATION_PLAN.md diff --git a/docs/macbrains/ORCHESTRATION_PLAN.md b/docs/macbrains/ORCHESTRATION_PLAN.md new file mode 100644 index 000000000000..c73ac7662c4b --- /dev/null +++ b/docs/macbrains/ORCHESTRATION_PLAN.md @@ -0,0 +1,350 @@ +# MacBrains T3 Code Continuous Implementation Topology + +## Goal + +Run the entire overhaul as one continuous dependency graph without repeated human phase approvals, while preventing overlapping edits, hidden subagents, stale integration, and false completion. + +This plan does not authorize implementation in the specification branch. Each editing lane uses its own isolated worktree and focused branch. The integration owner advances a dedicated integration branch and only merges current, verified package results. + +## Roles + +### Campaign integrator + +Owns: + +- current integration branch; +- package dependency/status reconciliation; +- shared contract integration order; +- cross-lane conflict resolution; +- invalidation of stale checks; +- acceptance matrix evidence links; +- final E0 acceptance bundle. + +The integrator does not casually implement package code. It protects source truth and merge order. + +### Package executor + +Owns one bounded package or a declared sequence within one ownership group. It runs preflight, implements, tests, and produces receipts. + +### Architecture advisor + +Reviews the oriented plan before broad/high-risk edits. It has read access to current source and package contract but does not share the executor's editing ownership. + +### Package verifier + +Independently refreshes source truth after implementation, reviews current diff/checks/receipts, and accepts or rejects package completion. + +### Recovery executor + +Replaces a failed or anchored executor context using a canonical handoff. It does not erase the prior assignment or continue from unverified prose alone. + +## Branch and worktree naming + +Use stable package identifiers: + +```text +macbrains/f0-work-lane-contracts +macbrains/f1-receipt-evidence +macbrains/f2-completion-gate +macbrains/f3-agent-topology +macbrains/f4-worktree-ownership +macbrains/f5-command-center +macbrains/f6-lane-surfaces +macbrains/p0-provider-normalization +macbrains/p1-background-leases +macbrains/p2-context-recovery +macbrains/n0-environment-node-policy +macbrains/n1-node-runtime +macbrains/n2-artifact-integration +macbrains/r0-atomic-persistence +macbrains/r1-heavy-thread-resilience +macbrains/d0-deliverables +macbrains/d1-ui-acceptance +macbrains/d2-notifications +macbrains/g0-source-control +macbrains/s0-instruction-skill-registry +macbrains/u0-fork-identity +macbrains/u1-policy-settings +macbrains/v0-performance +macbrains/e0-acceptance +``` + +Append a date or agent identifier only when necessary to avoid a real collision. Do not create multiple branches for the same package without recording which supersedes which. + +## Continuous execution lanes + +### Lane A — Domain foundation + +Packages: + +- F0 Work lane and source-truth contracts +- F1 Typed receipt/evidence substrate +- F2 Checks and completion gate + +Ownership: + +- lane/source-truth/check/receipt contracts; +- decider/invariants/projectors for those aggregates; +- persistence migrations; +- focused client-runtime compatibility types; +- tests for lifecycle, migration, receipt, and completion. + +Serialization rule: F0 -> F1 -> F2. Do not split these shared orchestration files among simultaneous editors. + +Advisor checkpoints: + +- F0 schema/migration design; +- F1 artifact/receipt storage boundary; +- F2 completion/staleness invariant. + +### Lane B — Agent and provider lifecycle + +Packages: + +- F3 Agent assignments/topology +- P0 Provider normalization +- P1 Background leases/session reaper +- P2 Context health/recovery + +Starts after F0 contracts are integrated. It may inspect earlier but must not commit against invented lane APIs. + +Ownership: + +- assignment contracts/projectors; +- provider-normalized lifecycle/capabilities; +- child-agent visibility; +- activity leases; +- compaction/context epochs; +- recovery handoff and provider switch. + +Provider sub-specialists may review individual adapters, but one provider-lane owner integrates shared adapter contracts. + +### Lane C — Git, worktrees, and source control + +Packages: + +- F4 Worktree/ownership manager +- G0 GitHub/source-control integration + +Starts after F0. + +Ownership: + +- Git/worktree services and contracts; +- lane bootstrap; +- ownership/overlap index; +- remote project/worktree operations at the server boundary; +- change-request adapters and evidence; +- authenticated private clone/onboarding; +- remote parser fixes. + +Must coordinate with Lane D on remote environment RPC and Lane F on UI, without sharing files casually. + +### Lane D — Environments and Linux node + +Packages: + +- N0 Environment/node capability and policy +- N1 Node job runtime +- N2 Artifact return and Mac integration + +Starts N0 after F0; N1 after F1 and N0; N2 after F2 and N1. + +Ownership: + +- environment capability schemas; +- node policy validation; +- job state machine; +- native-command dispatch; +- immutable inputs/outputs; +- authenticated/resumable artifact transfer; +- Mac integration receipts. + +Hard rule: no Python implementation, generated worker script, or node prompt. + +### Lane E — Persistence and resilience + +Packages: + +- R0 Atomic persistence/last-known-good recovery +- R1 Projection/heavy-thread resilience + +R0 starts after F1 receipt/artifact storage boundaries are stable. R1 follows R0 and coordinates with Lane A projection changes. + +Ownership: + +- critical catalog persistence; +- corruption diagnostics/recovery; +- projection rebuild/isolation; +- large thread/log/diff storage boundaries; +- memory/CPU resilience tests. + +### Lane F — Shared client runtime and Command Center + +Packages: + +- F5 Command Center/projections + +Starts after F0-F3 provide stable shell contracts. + +Ownership: + +- compact command-center projection contracts; +- client-runtime services/atoms/presentation; +- cache/reconnect behavior; +- cross-environment lane summaries; +- list virtualization primitives shared by clients. + +Do not add provider or Git logic to React components. + +### Lane G — Web, desktop, mobile, delivery, settings + +Packages: + +- F6 Lane detail/cross-surface controls +- D0 Deliverables +- D1 UI acceptance +- D2 Notifications +- U1 Settings/policy editor + +Starts incrementally after its contract dependencies integrate. Split platform-specific sub-lanes only with explicit ownership: + +- shared/client-runtime owner; +- web/desktop owner; +- mobile owner; +- notification platform owner. + +All applicable surfaces must converge on the same domain state and action semantics. + +### Lane H — Fork identity and release isolation + +Packages: + +- U0 Branding/fork identity + +May begin early because it has few domain dependencies, but it must not alter shared release/config files concurrently with Lane G or final integration without coordination. + +Ownership: + +- identity manifest; +- product/package/app/service/data IDs; +- update feed/package publication isolation; +- hosted service defaults; +- import/migration from upstream profile; +- branding assets and release documentation. + +Default to local/private-only until MacBrains-owned hosted resources are proven. + +### Lane I — Performance and final verification + +Packages: + +- V0 Performance/telemetry hardening +- E0 End-to-end acceptance + +V0 starts after F5 and R1. E0 waits on every declared dependency. + +Ownership: + +- seeded performance/idle soaks; +- payload and UI-thread profiling; +- telemetry correlation; +- full Mac + Linux node + mobile scenario; +- restart, provider failure, stale evidence, and recovery scenarios; +- final release evidence bundle. + +This lane is an independent verifier, not the author of every performance fix. It reports regressions back to the owning lane. + +## Parallelism rules + +Parallel execution is allowed only when: + +- package dependencies are satisfied in the integration branch; +- source paths/symbols are disjoint or a shared-file merge contract exists; +- each branch has a unique worktree; +- each lane records the current integration base SHA; +- package checks are rerun after rebasing/integration; +- no lane treats another lane's unmerged contract as canonical. + +Pause a lane automatically when: + +- its base is superseded by a breaking schema change; +- another active lane acquires overlapping ownership; +- source authorities conflict; +- required advisor rejects the plan; +- required environment/provider is unavailable; +- a migration or destructive operation cannot be proven safe. + +Do not pause for routine stylistic uncertainty or because a provider asks for confirmation that the task contract already grants. + +## Integration loop + +The integrator repeats: + +1. Refresh fork main/spec/integration/upstream refs. +2. Read `CAMPAIGN_MANIFEST.json` and current receipts. +3. Identify the earliest ready candidate package. +4. Verify package branch base, ownership, current source truth, and checks. +5. Require package verifier acceptance. +6. Integrate using the repository's current safe strategy. +7. Rerun checks invalidated by integration. +8. Update package status and acceptance evidence. +9. Notify newly unblocked lanes. +10. Continue immediately. + +No human phase approval is required between packages unless an irreversible decision is outside the existing contract. + +## Model routing intent + +Use capability intent rather than hard-coded model names: + +- mechanical worker: bounded deterministic edits/checks, light reasoning; +- executor: normal implementation, medium reasoning; +- advisor: architecture/risk review, stronger independent reasoning; +- verifier: independent source/diff/evidence review, stronger independent reasoning; +- explorer: continuity for open-ended research; +- recovery: fresh medium-or-strong context selected from failure evidence. + +Prefer free, flat-rate, subscription, or local capacity before pay-per-token when capability is sufficient. Record every concrete resolution and escalation cause. + +## Agent launch packet + +Every launched agent receives: + +- exact fork/repository and target branch; +- isolated worktree path; +- current integration base SHA; +- package ID and ownership boundary; +- applicable project profile and workflow template; +- authoritative document list; +- acceptance row IDs; +- allowed/prohibited actions; +- required advisor/verifier assignment; +- exact output/receipt contract; +- known active neighboring lanes and shared files. + +Do not send the entire campaign as an unbounded prompt to every child agent. Give each agent enough context to execute its package and link to the canonical documents. + +## Campaign state updates + +Because GitHub Issues are disabled, update: + +- `CAMPAIGN_MANIFEST.json` package status; +- `IMPLEMENTATION_LEDGER.md` package evidence; +- `ACCEPTANCE_MATRIX.md` row evidence; +- package receipts/artifacts in the product's evidence store when implemented; +- `IMPLEMENTATION_HANDOFF.md` only when the campaign-level handoff materially changes. + +All status edits require proof on current revisions. Do not mark `PROVEN` manually from an executor summary. + +## Final completion + +The campaign integrator may request final completion only when: + +- all required packages are `PROVEN`; +- no acceptance evidence is stale; +- fork identity and release isolation are proven; +- E0 passes on Mac, Linux node, desktop/web/iPhone/iPad as applicable; +- restart, provider failure, node disconnect, corrupt optional state, and stale evidence recovery are demonstrated; +- the final deliverable is visible and launchable; +- an independent verifier accepts the complete evidence bundle. \ No newline at end of file From 0708298a546e248d5f5ba340fce1e1e5d8b94bc0 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:19:55 -0400 Subject: [PATCH 22/32] docs: add exact first-package execution prompt --- docs/macbrains/F0_EXECUTION_PROMPT.md | 298 ++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/macbrains/F0_EXECUTION_PROMPT.md diff --git a/docs/macbrains/F0_EXECUTION_PROMPT.md b/docs/macbrains/F0_EXECUTION_PROMPT.md new file mode 100644 index 000000000000..faaceb15cbf5 --- /dev/null +++ b/docs/macbrains/F0_EXECUTION_PROMPT.md @@ -0,0 +1,298 @@ +# F0 Execution Prompt — Work Lane and Source-Truth Foundation + +Copy this entire prompt to the first implementation agent after pulling the specification branch to the Mac. + +--- + +Implement package `F0 — Work lane and source-truth contracts` in `naytewilson/t3code`. + +## Authority + +Read in order: + +1. current repository/worktree state and scoped instruction files; +2. `AGENTS.md`; +3. `MACBRAINS.md`; +4. `docs/macbrains/README.md`; +5. `docs/macbrains/PROJECT_PROFILES.md` for the active project profile; +6. `docs/macbrains/DOMAIN_MODEL.md`; +7. F0 in `docs/macbrains/IMPLEMENTATION_LEDGER.md`; +8. `docs/macbrains/CAMPAIGN_MANIFEST.json`; +9. A/B/C/P rows applicable to F0 in `docs/macbrains/ACCEPTANCE_MATRIX.md`; +10. `docs/macbrains/AGENT_EXECUTION_PROMPT.md`. + +Do not treat these documents as proof of current code. Inspect the real source and current branch. + +## Continuous execution instruction + +Proceed through inspection, plan, advisor review, implementation, tests, independent verification, commit, and package handoff without asking for routine confirmation. Stop only at a genuine blocker where a missing answer changes the correct action and cannot be resolved from repository/canonical evidence. + +Do not open a pull request unless the task contract or repository owner explicitly authorizes it. + +## Source-truth preflight + +Before installing or editing, record: + +```text +PROVEN TARGET +PROVEN REPOSITORY STATE +PROVEN INSTRUCTIONS AND AUTHORITIES +PROVEN BUILD AND TEST SURFACE +PROVEN RELEVANT FILES +OWNERSHIP OR OVERLAP RISKS +INFERRED +UNKNOWN THAT CHANGES ACTION +SAFE NEXT ACTION +``` + +At minimum verify: + +- repository root/common Git dir; +- exact branch and HEAD; +- origin/upstream remotes without credentials; +- staged, unstaged, and untracked state; +- all worktrees and checked-out branches; +- active merge/rebase/cherry-pick/revert/bisect state; +- instruction hierarchy; +- package manager/toolchain/lockfiles; +- orchestration contract, decider, invariant, event, projector, persistence, migration, RPC, client-runtime, and test files; +- generated/vendored boundaries; +- any active agent ownership. + +Use an isolated worktree. Never touch live `~/.t3/userdata` read-write. Never kill a process by pattern. + +## F0 objective + +Add a durable `WorkLane` and `SourceTruthRevision` foundation above existing projects/threads/provider sessions while preserving the event-sourced architecture. + +The implementation must provide: + +- lane/task-contract/acceptance/source-truth IDs and schemas; +- lane lifecycle state and validated transitions; +- commands and immutable events; +- pure decider invariants; +- deterministic projectors/read models; +- persistence and migration/replay support; +- compact shell/detail query boundaries; +- client-runtime decode/compatibility support where required; +- migration of existing threads without false completion; +- focused tests and documentation. + +F0 does not implement the complete receipt store, checks engine, provider topology, command center, node runtime, or full UI. It must create stable integration boundaries for those packages. + +## Domain requirements + +### WorkLane + +At minimum represent: + +- stable lane ID; +- project ID; +- title; +- task contract; +- lifecycle state; +- priority/classification; +- environment association available from current architecture; +- repository identity; +- base ref/SHA where known; +- branch and worktree path; +- source-truth revision ID; +- active plan revision reference or reserved forward-compatible field only if current schema rules support it cleanly; +- acceptance criterion references or an F0-owned minimal representation; +- blocker references or a future-compatible boundary; +- created/updated/completed timestamps. + +Do not store provider-specific runtime data inside WorkLane. + +### TaskContract + +At minimum represent: + +- objective; +- constraints; +- non-goals; +- deliverable requirement; +- pull-request authorization; +- visible-surface requirement; +- authorized/prohibited action categories; +- completion-report requirement. + +Use stable typed values. Do not persist arbitrary executable permission logic as unvalidated text. + +### SourceTruthRevision + +At minimum represent: + +- stable revision ID and lane ID; +- repository identity/root; +- branch/detached state; +- HEAD/base SHA; +- worktree path; +- dirty fingerprint/status summary; +- instruction files in precedence order; +- manifests/build/test candidates; +- relevant file/test references; +- active Git operation state; +- worktree/ownership overlap result; +- canonical external source references where the existing architecture supports them; +- unknowns that change action; +- safe next action; +- produced timestamp and producer assignment/thread when available. + +Large raw command output must be stored through an existing suitable artifact/log boundary or a forward-compatible reference. Do not bloat the shell projection. + +## Lifecycle + +Implement canonical states: + +```text +queued +preflight +oriented +planned +executing +testing +reviewing +deliverable-ready +completed +blocked +failed +cancelled +superseded +recovery-required +``` + +F0 must enforce the documented transition matrix even before later packages fill all evidence requirements. + +Temporary F0 transition policy: + +- execution cannot start without a current source-truth revision; +- substantial lanes cannot execute without a worktree reference unless explicitly classified as a permitted exception; +- completed is not accepted until F2 supplies the full evidence gate; F0 should either reserve completion commands behind an explicit invariant or support imported historical state without pretending it meets the new completion contract; +- invalid states/events fail decoding or decision with actionable errors; +- commands are idempotent by command ID according to existing architecture. + +Do not weaken later completion policy for convenience. + +## Existing-thread migration + +Migration must: + +- preserve existing project/thread/message/activity/checkpoint/session data; +- create one imported lane per existing thread or another explicitly justified deterministic mapping; +- derive an objective from the earliest useful user message or title, labeling derivation quality as unknown/inferred where the domain supports it; +- preserve branch/worktree/checkpoint relationships; +- attach active provider session as future executor association only through a clean compatibility boundary; +- never mark historical threads completed under the new contract; +- place active/ambiguous imports into `queued` or `recovery-required` according to current durable state; +- be deterministic and replay-safe; +- include downgrade/rollback analysis even if automatic downgrade is not supported. + +Do not mutate historical event facts merely to make the new projection easy. + +## Contract organization + +`packages/contracts/src/orchestration.ts` is already large. First inspect current explicit subpath conventions and package exports. Prefer focused new contract modules when consistent with current architecture: + +- lane contracts; +- source-truth contracts; +- shared IDs/base schemas; +- RPC/query contracts. + +One owner integrates shared export/wire changes. Do not create a second barrel if the package prohibits barrels. + +## Persistence and projection + +- Keep canonical events append-only. +- Make projections rebuildable. +- Preserve sequence semantics and reconnect behavior. +- Keep command-center/shell records compact; do not embed full task contracts/source-truth payloads unless justified by measured payload needs. +- Provide detail query/subscription for full lane state. +- Add schema versioning/compatibility transforms following current repository patterns. +- Do not invent JSON sidecars when the current event/SQLite architecture is the correct authority. + +## Advisor requirement + +Before broad edits, launch an independent architecture advisor with read-only access. Give it: + +- current preflight; +- inspected source map; +- proposed aggregate boundaries; +- migration strategy; +- compatibility/query strategy; +- planned files and tests; +- risks and alternatives. + +The advisor must explicitly approve, constrain, or reject. Record the response. Revise before implementation if rejected. + +## Focused tests + +At minimum cover: + +- schema roundtrip and invalid decoding; +- every allowed and disallowed lane transition; +- command idempotency; +- no-execution-without-preflight; +- substantial-no-worktree refusal; +- project/thread/lane relationship invariants; +- source-truth revision append/supersession behavior; +- projection replay determinism; +- shell/detail projection shape and payload boundary; +- migration of empty, normal, archived, active, failed/interrupted, worktree, checkpointed, and malformed legacy fixtures; +- reconnect/subscription compatibility where touched; +- no false historical completion. + +Use receipts/worker drains/deterministic clocks following repository conventions. Do not use arbitrary sleeps or polling. + +## Verification + +Run only commands proven by current manifests/instructions. Include: + +- focused formatter/lint for touched files; +- focused contract/server/client-runtime typecheck; +- focused tests; +- migration/replay tests; +- build of affected packages if required by current project scripts. + +After implementation: + +1. inspect staged and unstaged diff; +2. inspect generated files and package exports; +3. rerun checks after any fix; +4. launch an independent verifier; +5. verifier refreshes source truth and reviews current source, tests, migration, diff, and F0 acceptance rows; +6. fix all real findings; +7. commit with a focused conventional message; +8. provide branch/commit and package handoff. + +Do not mark a check passed if it was skipped, stale, or run before the final change. + +## Required final output + +```text +PACKAGE: F0 +BRANCH: +WORKTREE: +BASE SHA: +FINAL SHA: +FILES CHANGED: +MIGRATION: +COMMAND RECEIPTS: +CHECK RECEIPTS: +ADVISOR RECEIPT: +VERIFIER RECEIPT: +ACCEPTANCE ROWS PROVEN: +ACCEPTANCE ROWS STILL MISSING: +DEPENDENCIES UNBLOCKED: + +proven: +missing evidence: +possibly wrong or overstated: +exact next action: +what does not count as completion: +safe to continue here or start a fresh context: +``` + +A documentation update, schema-only stub, mock UI, or tests without integrated persistence/projection behavior does not complete F0. + +--- \ No newline at end of file From 5df37d77230321363984f7b39c3ec3b980faf00a Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:20:37 -0400 Subject: [PATCH 23/32] chore: add MacBrains specification validator --- scripts/validate-macbrains-spec.mjs | 246 ++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 scripts/validate-macbrains-spec.mjs diff --git a/scripts/validate-macbrains-spec.mjs b/scripts/validate-macbrains-spec.mjs new file mode 100644 index 000000000000..4202ab35ab04 --- /dev/null +++ b/scripts/validate-macbrains-spec.mjs @@ -0,0 +1,246 @@ +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const root = process.cwd(); +const errors = []; +const notices = []; + +const relative = (filePath) => path.relative(root, filePath) || "."; +const resolve = (filePath) => path.resolve(root, filePath); + +const requireFile = (filePath) => { + const absolute = resolve(filePath); + if (!fs.existsSync(absolute)) { + errors.push(`missing required file: ${filePath}`); + return null; + } + const stat = fs.statSync(absolute); + if (!stat.isFile()) { + errors.push(`required path is not a file: ${filePath}`); + return null; + } + return absolute; +}; + +const readText = (filePath) => { + const absolute = requireFile(filePath); + if (absolute === null) return null; + return fs.readFileSync(absolute, "utf8"); +}; + +const readJson = (filePath) => { + const text = readText(filePath); + if (text === null) return null; + try { + return JSON.parse(text); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + errors.push(`invalid JSON in ${filePath}: ${message}`); + return null; + } +}; + +const expect = (condition, message) => { + if (!condition) errors.push(message); +}; + +const expectUniqueIds = (items, label) => { + const seen = new Set(); + for (const item of items) { + const id = item?.id; + if (typeof id !== "string" || id.trim().length === 0) { + errors.push(`${label} contains an item without a non-empty string id`); + continue; + } + if (seen.has(id)) errors.push(`${label} contains duplicate id: ${id}`); + seen.add(id); + } + return seen; +}; + +const validateDependencyGraph = (packages) => { + const byId = new Map(packages.map((item) => [item.id, item])); + + for (const item of packages) { + expect(Array.isArray(item.dependencies), `package ${item.id} dependencies must be an array`); + for (const dependency of item.dependencies ?? []) { + expect(byId.has(dependency), `package ${item.id} has unknown dependency: ${dependency}`); + expect(dependency !== item.id, `package ${item.id} depends on itself`); + } + } + + const visiting = new Set(); + const visited = new Set(); + + const visit = (id, trail) => { + if (visited.has(id)) return; + if (visiting.has(id)) { + errors.push(`package dependency cycle: ${[...trail, id].join(" -> ")}`); + return; + } + visiting.add(id); + const item = byId.get(id); + for (const dependency of item?.dependencies ?? []) visit(dependency, [...trail, id]); + visiting.delete(id); + visited.add(id); + }; + + for (const id of byId.keys()) visit(id, []); +}; + +const requiredDocuments = [ + "AGENTS.md", + "MACBRAINS.md", + "docs/macbrains/README.md", + "docs/macbrains/FORK_BASELINE.md", + "docs/macbrains/DOCUMENTATION_AUDIT.md", + "docs/macbrains/PRODUCT_SPEC.md", + "docs/macbrains/PROJECT_PROFILES.md", + "docs/macbrains/DOMAIN_MODEL.md", + "docs/macbrains/IMPLEMENTATION_LEDGER.md", + "docs/macbrains/CAMPAIGN_MANIFEST.json", + "docs/macbrains/WORKFLOW_TEMPLATES.json", + "docs/macbrains/ACCEPTANCE_MATRIX.md", + "docs/macbrains/DEFAULT_POLICIES.json", + "docs/macbrains/FORK_IDENTITY_AND_RELEASE.md", + "docs/macbrains/PULL_TO_MAC.md", + "docs/macbrains/IMPLEMENTATION_HANDOFF.md", + "docs/macbrains/ORCHESTRATION_PLAN.md", + "docs/macbrains/F0_EXECUTION_PROMPT.md", + "docs/macbrains/AGENT_EXECUTION_PROMPT.md", +]; + +for (const document of requiredDocuments) requireFile(document); + +const manifest = readJson("docs/macbrains/CAMPAIGN_MANIFEST.json"); +if (manifest !== null) { + expect(manifest.$schemaVersion === 1, "campaign manifest schema version must be 1"); + expect( + manifest.campaignId === "macbrains-t3code-agent-workflow-overhaul", + "campaign manifest has unexpected campaignId", + ); + expect(Array.isArray(manifest.canonicalDocuments), "campaign canonicalDocuments must be an array"); + for (const document of manifest.canonicalDocuments ?? []) requireFile(document); + + const packages = Array.isArray(manifest.packages) ? manifest.packages : []; + expect(packages.length > 0, "campaign manifest must contain packages"); + const packageIds = expectUniqueIds(packages, "campaign packages"); + const statuses = new Set(manifest.statusVocabulary ?? []); + expect(statuses.size > 0, "campaign statusVocabulary must not be empty"); + + for (const item of packages) { + expect(statuses.has(item.status), `package ${item.id} has invalid status: ${item.status}`); + expect( + typeof item.ownershipGroup === "string" && item.ownershipGroup.length > 0, + `package ${item.id} must define ownershipGroup`, + ); + expect(Array.isArray(item.primaryAcceptancePrefixes), `package ${item.id} acceptance prefixes must be an array`); + } + + validateDependencyGraph(packages); + expect(packageIds.has("F0"), "campaign manifest must contain F0"); + expect(packageIds.has("E0"), "campaign manifest must contain E0"); + const e0 = packages.find((item) => item.id === "E0"); + expect((e0?.dependencies?.length ?? 0) > 0, "E0 must depend on implementation packages"); + expect( + manifest.releaseGate?.allRequiredAcceptanceRowsProven === true, + "release gate must require all acceptance rows proven", + ); + expect( + manifest.releaseGate?.canonicalE0ScenarioProven === true, + "release gate must require the canonical E0 scenario", + ); +} + +const templates = readJson("docs/macbrains/WORKFLOW_TEMPLATES.json"); +if (templates !== null) { + expect(templates.$schemaVersion === 1, "workflow templates schema version must be 1"); + const items = Array.isArray(templates.templates) ? templates.templates : []; + const ids = expectUniqueIds(items, "workflow templates"); + for (const required of [ + "implement-feature", + "fix-bug", + "review-pull-request", + "experiment-benchmark", + "recovery-resume", + "anvil-package-execution", + "ane-re-experiment", + ]) { + expect(ids.has(required), `missing required workflow template: ${required}`); + } + for (const item of items) { + expect(Array.isArray(item.lifecycle) && item.lifecycle.length > 0, `workflow ${item.id} needs lifecycle steps`); + expect(Array.isArray(item.topology) && item.topology.length > 0, `workflow ${item.id} needs topology`); + expect( + Array.isArray(item.requiredReceipts) && item.requiredReceipts.length > 0, + `workflow ${item.id} needs required receipts`, + ); + } +} + +const policies = readJson("docs/macbrains/DEFAULT_POLICIES.json"); +if (policies !== null) { + expect(policies.$schemaVersion === 1, "default policy schema version must be 1"); + const expectedClaims = ["PROVEN", "INFERRED", "SUSPECTED", "UNKNOWN"]; + expect( + JSON.stringify(policies.product?.claimLabels) === JSON.stringify(expectedClaims), + "default policies must use the canonical claim labels in canonical order", + ); + expect(policies.linuxNode?.pythonAllowed === false, "Linux node policy must prohibit Python"); + expect( + policies.completion?.providerMessageMayCompleteLane === false, + "provider messages must not complete lanes", + ); + expect( + policies.work?.substantialWorkUsesWorktree === true, + "substantial work must use worktrees by default", + ); +} + +const contract = readText("MACBRAINS.md"); +if (contract !== null) { + for (const required of [ + "PROVEN", + "INFERRED", + "SUSPECTED", + "UNKNOWN", + "no Python implementation", + "ANVIL", + "deliverable-ready", + "safe to continue here or start a fresh context", + ]) { + expect(contract.includes(required), `MACBRAINS.md is missing required contract text: ${required}`); + } +} + +const acceptance = readText("docs/macbrains/ACCEPTANCE_MATRIX.md"); +if (acceptance !== null) { + for (const section of "ABCDEFGHIJKLMNOPQRSTU") { + expect( + acceptance.includes(`## ${section}.`), + `acceptance matrix is missing section ${section}`, + ); + } + expect(acceptance.includes("## Final release gate"), "acceptance matrix is missing final release gate"); + expect(acceptance.includes("U01"), "acceptance matrix is missing canonical end-to-end row U01"); +} + +const index = readText("docs/macbrains/README.md"); +if (index !== null) { + for (const document of requiredDocuments.filter((item) => item.startsWith("docs/macbrains/"))) { + const basename = path.basename(document); + expect(index.includes(basename), `specification index does not reference ${basename}`); + } +} + +if (errors.length > 0) { + console.error(`MacBrains specification validation failed with ${errors.length} error(s):`); + for (const error of errors) console.error(`- ${error}`); + process.exitCode = 1; +} else { + notices.push(`validated ${requiredDocuments.length} required files`); + notices.push(`repository root: ${relative(root)}`); + console.log("MacBrains specification validation passed."); + for (const notice of notices) console.log(`- ${notice}`); +} From 70f4afaae85f8407199ae763f9c6563e6f5a5669 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:20:53 -0400 Subject: [PATCH 24/32] docs: index orchestration and F0 launch prompts --- docs/macbrains/README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/macbrains/README.md b/docs/macbrains/README.md index 0ccc5af17864..7b9bd71f05be 100644 --- a/docs/macbrains/README.md +++ b/docs/macbrains/README.md @@ -10,13 +10,15 @@ Read in this order: 6. [`DOMAIN_MODEL.md`](./DOMAIN_MODEL.md) — commands, events, aggregates, receipts, and invariants. 7. [`IMPLEMENTATION_LEDGER.md`](./IMPLEMENTATION_LEDGER.md) — dependency-ordered implementation packages. 8. [`CAMPAIGN_MANIFEST.json`](./CAMPAIGN_MANIFEST.json) — machine-readable dependencies, ownership groups, status, and release gate. -9. [`WORKFLOW_TEMPLATES.json`](./WORKFLOW_TEMPLATES.json) — reusable feature, bug, review, research, experiment, recovery, ANVIL, ANE-RE, native macOS, and artifact workflows. -10. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. -11. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. -12. [`FORK_IDENTITY_AND_RELEASE.md`](./FORK_IDENTITY_AND_RELEASE.md) — application, data, service, hosted-control-plane, and updater isolation from upstream. -13. [`PULL_TO_MAC.md`](./PULL_TO_MAC.md) — safe fetch, worktree, verification, integration, and cleanup instructions for the Mac. -14. [`IMPLEMENTATION_HANDOFF.md`](./IMPLEMENTATION_HANDOFF.md) — exact campaign handoff, evidence gaps, first package ownership, and stop conditions. -15. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for implementation agents. +9. [`ORCHESTRATION_PLAN.md`](./ORCHESTRATION_PLAN.md) — continuous multi-agent topology, ownership lanes, integration loop, and no-routine-confirmation execution model. +10. [`WORKFLOW_TEMPLATES.json`](./WORKFLOW_TEMPLATES.json) — reusable feature, bug, review, research, experiment, recovery, ANVIL, ANE-RE, native macOS, and artifact workflows. +11. [`ACCEPTANCE_MATRIX.md`](./ACCEPTANCE_MATRIX.md) — proof required before any package or release is complete. +12. [`DEFAULT_POLICIES.json`](./DEFAULT_POLICIES.json) — machine-readable initial policy intent. +13. [`FORK_IDENTITY_AND_RELEASE.md`](./FORK_IDENTITY_AND_RELEASE.md) — application, data, service, hosted-control-plane, and updater isolation from upstream. +14. [`PULL_TO_MAC.md`](./PULL_TO_MAC.md) — safe fetch, worktree, verification, integration, and cleanup instructions for the Mac. +15. [`IMPLEMENTATION_HANDOFF.md`](./IMPLEMENTATION_HANDOFF.md) — exact campaign handoff, evidence gaps, first package ownership, and stop conditions. +16. [`F0_EXECUTION_PROMPT.md`](./F0_EXECUTION_PROMPT.md) — complete launch prompt for the first implementation package. +17. [`AGENT_EXECUTION_PROMPT.md`](./AGENT_EXECUTION_PROMPT.md) — root prompt for all implementation agents. ## Canonical status @@ -33,6 +35,18 @@ Start with `F0 — Work lane and source-truth contracts`. Do not begin disconnec - obtain independent architecture advice before changing persistence/domain schemas; - implement the full F0 vertical slice with migration and focused proof. +Use `F0_EXECUTION_PROMPT.md` as the launch packet, with the actual repository/worktree/branch/base SHA filled from current preflight. + +## Specification validation + +Run from the repository root: + +```sh +node scripts/validate-macbrains-spec.mjs +``` + +This validates required files, JSON syntax, package IDs/status/dependencies/cycles, workflow templates, default policy invariants, contract markers, acceptance sections, and index coverage. It does not validate product implementation. + ## Campaign tracking GitHub Issues are disabled in this fork, so the authoritative implementation tracker is `CAMPAIGN_MANIFEST.json`, the detailed package contract in `IMPLEMENTATION_LEDGER.md`, and the evidence fields in `ACCEPTANCE_MATRIX.md`. Do not maintain a conflicting private checklist. From bbf6968e026dbfc7e422335891cb6b13a86ede91 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:21:02 -0400 Subject: [PATCH 25/32] ci: validate MacBrains campaign specification --- .github/workflows/macbrains-spec.yml | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/macbrains-spec.yml diff --git a/.github/workflows/macbrains-spec.yml b/.github/workflows/macbrains-spec.yml new file mode 100644 index 000000000000..66aebd1f8c06 --- /dev/null +++ b/.github/workflows/macbrains-spec.yml @@ -0,0 +1,40 @@ +name: MacBrains specification + +on: + push: + branches: + - "macbrains/**" + paths: + - "AGENTS.md" + - "MACBRAINS.md" + - "docs/macbrains/**" + - "scripts/validate-macbrains-spec.mjs" + - ".github/workflows/macbrains-spec.yml" + pull_request: + paths: + - "AGENTS.md" + - "MACBRAINS.md" + - "docs/macbrains/**" + - "scripts/validate-macbrains-spec.mjs" + - ".github/workflows/macbrains-spec.yml" + +permissions: + contents: read + +jobs: + validate: + name: Validate campaign files + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "24.13.1" + package-manager-cache: false + + - name: Validate MacBrains specification + run: node scripts/validate-macbrains-spec.mjs From 05b9711c805067af05c731143127b701c016c25b Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:22:00 -0400 Subject: [PATCH 26/32] fix: skip specification index self-reference --- scripts/validate-macbrains-spec.mjs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scripts/validate-macbrains-spec.mjs b/scripts/validate-macbrains-spec.mjs index 4202ab35ab04..2f26b5cb9300 100644 --- a/scripts/validate-macbrains-spec.mjs +++ b/scripts/validate-macbrains-spec.mjs @@ -135,7 +135,10 @@ if (manifest !== null) { typeof item.ownershipGroup === "string" && item.ownershipGroup.length > 0, `package ${item.id} must define ownershipGroup`, ); - expect(Array.isArray(item.primaryAcceptancePrefixes), `package ${item.id} acceptance prefixes must be an array`); + expect( + Array.isArray(item.primaryAcceptancePrefixes), + `package ${item.id} acceptance prefixes must be an array`, + ); } validateDependencyGraph(packages); @@ -170,8 +173,14 @@ if (templates !== null) { expect(ids.has(required), `missing required workflow template: ${required}`); } for (const item of items) { - expect(Array.isArray(item.lifecycle) && item.lifecycle.length > 0, `workflow ${item.id} needs lifecycle steps`); - expect(Array.isArray(item.topology) && item.topology.length > 0, `workflow ${item.id} needs topology`); + expect( + Array.isArray(item.lifecycle) && item.lifecycle.length > 0, + `workflow ${item.id} needs lifecycle steps`, + ); + expect( + Array.isArray(item.topology) && item.topology.length > 0, + `workflow ${item.id} needs topology`, + ); expect( Array.isArray(item.requiredReceipts) && item.requiredReceipts.length > 0, `workflow ${item.id} needs required receipts`, @@ -222,13 +231,19 @@ if (acceptance !== null) { `acceptance matrix is missing section ${section}`, ); } - expect(acceptance.includes("## Final release gate"), "acceptance matrix is missing final release gate"); + expect( + acceptance.includes("## Final release gate"), + "acceptance matrix is missing final release gate", + ); expect(acceptance.includes("U01"), "acceptance matrix is missing canonical end-to-end row U01"); } const index = readText("docs/macbrains/README.md"); if (index !== null) { - for (const document of requiredDocuments.filter((item) => item.startsWith("docs/macbrains/"))) { + const indexedDocuments = requiredDocuments.filter( + (item) => item.startsWith("docs/macbrains/") && item !== "docs/macbrains/README.md", + ); + for (const document of indexedDocuments) { const basename = path.basename(document); expect(index.includes(basename), `specification index does not reference ${basename}`); } From f8673aab321f5ad5522b8a8c0b36cd98328be7b2 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:22:56 -0400 Subject: [PATCH 27/32] docs: complete canonical campaign document set --- docs/macbrains/CAMPAIGN_MANIFEST.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/macbrains/CAMPAIGN_MANIFEST.json b/docs/macbrains/CAMPAIGN_MANIFEST.json index 42274876f128..e1d5c1f26754 100644 --- a/docs/macbrains/CAMPAIGN_MANIFEST.json +++ b/docs/macbrains/CAMPAIGN_MANIFEST.json @@ -12,11 +12,15 @@ "docs/macbrains/PROJECT_PROFILES.md", "docs/macbrains/DOMAIN_MODEL.md", "docs/macbrains/IMPLEMENTATION_LEDGER.md", + "docs/macbrains/CAMPAIGN_MANIFEST.json", + "docs/macbrains/ORCHESTRATION_PLAN.md", + "docs/macbrains/WORKFLOW_TEMPLATES.json", "docs/macbrains/ACCEPTANCE_MATRIX.md", "docs/macbrains/DEFAULT_POLICIES.json", "docs/macbrains/FORK_IDENTITY_AND_RELEASE.md", "docs/macbrains/PULL_TO_MAC.md", "docs/macbrains/IMPLEMENTATION_HANDOFF.md", + "docs/macbrains/F0_EXECUTION_PROMPT.md", "docs/macbrains/AGENT_EXECUTION_PROMPT.md" ], "statusVocabulary": [ From dfd68e7b8d186a91a762eb5f731b88567d46c349 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:23:19 -0400 Subject: [PATCH 28/32] ci: allow manual MacBrains specification validation --- .github/workflows/macbrains-spec.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/macbrains-spec.yml b/.github/workflows/macbrains-spec.yml index 66aebd1f8c06..80751ed7c6bf 100644 --- a/.github/workflows/macbrains-spec.yml +++ b/.github/workflows/macbrains-spec.yml @@ -1,6 +1,7 @@ name: MacBrains specification on: + workflow_dispatch: push: branches: - "macbrains/**" From 1e03d7198ae186c48dc065e3c9dcdeaa90003086 Mon Sep 17 00:00:00 2001 From: Nayte Date: Thu, 30 Jul 2026 00:23:49 -0400 Subject: [PATCH 29/32] docs: finalize durable implementation handoff --- docs/macbrains/IMPLEMENTATION_HANDOFF.md | 173 ++++++++++++----------- 1 file changed, 90 insertions(+), 83 deletions(-) diff --git a/docs/macbrains/IMPLEMENTATION_HANDOFF.md b/docs/macbrains/IMPLEMENTATION_HANDOFF.md index ed1d4358f202..3b3fc98cbef8 100644 --- a/docs/macbrains/IMPLEMENTATION_HANDOFF.md +++ b/docs/macbrains/IMPLEMENTATION_HANDOFF.md @@ -1,99 +1,101 @@ # MacBrains T3 Code Implementation Handoff -## Current source-truth receipt +## Source-truth boundary -This handoff was created from GitHub-visible repository state. A local clone was not available in the working container because outbound DNS resolution failed. Therefore no local build, dependency installation, test, lint, or rendered-client validation was performed. +This specification was authored against GitHub-visible repository state. The execution container could not clone the repository because outbound DNS resolution failed. Therefore no local checkout, dependency installation, formatter, test, lint, build, packaged client, or rendered UI receipt exists from this specification session. -### Proven repository identity at specification start +### Proven baseline identity - Fork: `naytewilson/t3code` - Upstream: `pingdotgg/t3code` -- Baseline fork/upstream commit: `b125b7635170ec0c33f8ddf39299155a21f8c9b9` -- Baseline comparison: identical +- Initial fork/upstream merge base: `b125b7635170ec0c33f8ddf39299155a21f8c9b9` +- Initial fork/upstream comparison: identical - Specification branch: `macbrains/agent-workflow-overhaul` -Re-verify current HEAD and divergence before implementation; this branch has advanced through documentation commits after the baseline. +The specification branch has advanced beyond the baseline. Agents must fetch and record its current HEAD rather than copy any historical SHA from conversation or documentation. -## Durable specification artifacts +## Authoritative artifacts + +The complete list and reading order are in `docs/macbrains/README.md`. Core machine-readable assets: + +- `docs/macbrains/CAMPAIGN_MANIFEST.json` +- `docs/macbrains/WORKFLOW_TEMPLATES.json` +- `docs/macbrains/DEFAULT_POLICIES.json` + +Core execution assets: + +- `docs/macbrains/ORCHESTRATION_PLAN.md` +- `docs/macbrains/F0_EXECUTION_PROMPT.md` +- `docs/macbrains/AGENT_EXECUTION_PROMPT.md` +- `docs/macbrains/PULL_TO_MAC.md` + +Core proof assets: -- `MACBRAINS.md` -- `AGENTS.md` MacBrains fork directive -- `docs/macbrains/README.md` -- `docs/macbrains/FORK_BASELINE.md` -- `docs/macbrains/DOCUMENTATION_AUDIT.md` -- `docs/macbrains/PRODUCT_SPEC.md` -- `docs/macbrains/DOMAIN_MODEL.md` - `docs/macbrains/IMPLEMENTATION_LEDGER.md` - `docs/macbrains/ACCEPTANCE_MATRIX.md` -- `docs/macbrains/DEFAULT_POLICIES.json` + +Project-specific adaptation: + +- `docs/macbrains/PROJECT_PROFILES.md` + +Fork/release isolation: + - `docs/macbrains/FORK_IDENTITY_AND_RELEASE.md` -- `docs/macbrains/PULL_TO_MAC.md` -- `docs/macbrains/AGENT_EXECUTION_PROMPT.md` -## Specification branch commit sequence +## Specification validation + +A dependency-free validator exists: -The branch was built as a sequence of GitHub commits. Agents must use the branch's current HEAD, not assume an intermediate SHA is complete. +```sh +node scripts/validate-macbrains-spec.mjs +``` -Known commits in creation order: +A GitHub Actions workflow exists at `.github/workflows/macbrains-spec.yml` with push, pull-request, and manual triggers. -- `9f2652a34bbe66faf64bf1db1f9644f44c6cf902` — operating contract -- `dd6e8dbc7eb41004f959e93092f5b0e5ba9f2fb3` — product specification -- `213f22ac6d6b1d9b054e57cf6123446c286307d0` — domain model -- `a1f431fb0a43a6e1b1a4fc58c19ff2393206aba4` — implementation ledger -- `e0131360ae8e29380a650438d3d057b151628bf5` — acceptance matrix -- `465bfe17cb999a5535c2d9732c91adcb30471f9c` — agent execution prompt -- `74ef33122aca001eb938ba4f431cc900752942dc` — fork baseline -- `4862fcf8d39022a4a5962699c8c821c07c2dd3d6` — AGENTS.md fork directive -- `baa70a6f992cd148faefdfe617fb534bd2345e06` — specification index -- `a4c5040ede382a51b4477588e48fa9d1a67dd1b4` — machine-readable defaults -- `58502014b2c9d207e60186e064336f67dbaf5dea` — documentation audit -- `058d355f6fa053fe5080fb5be67f3c431202dd6b` — fork identity/release isolation -- `8dfd7fb928953b3b45871f9da25de7dd79f33be9` — Mac pull/worktree instructions +The validator checks required files, JSON syntax, package IDs/status/dependencies/cycles, required workflow templates, policy invariants, contract markers, acceptance sections, and specification-index coverage. It does not prove product implementation. -This handoff commit and later issue/document changes are not included in the list above. Read branch HEAD directly. +GitHub Issues are disabled in this fork. Campaign status must be maintained in `CAMPAIGN_MANIFEST.json`, `IMPLEMENTATION_LEDGER.md`, and `ACCEPTANCE_MATRIX.md`; do not create a hidden conflicting tracker. ## First implementation package -Start with `F0 — Work lane and source-truth contracts`. +Start with `F0 — Work lane and source-truth contracts` using `F0_EXECUTION_PROMPT.md`. -### Required dependency state +### Dependency state -- Specification branch exists and is readable. -- Current event-sourced orchestration must remain canonical. -- Current provider/session/thread schemas and migration system must be inspected. -- No other implementation package may assume lane IDs or completion evidence until F0 contracts are integrated. +- Specification branch is the implementation contract source. +- Existing event-sourced orchestration remains canonical. +- Current provider/session/thread schemas and migration system must be inspected locally. +- No later package may invent lane, source-truth, receipt, or completion APIs ahead of integrated foundations. -### Ownership boundary +### F0 ownership boundary The F0 owner controls: -- new lane/source-truth contract modules; +- lane/source-truth contract modules; - required orchestration contract integration points; -- lane/source-truth decider commands/events/invariants; -- lane/source-truth projectors and persistence migration; -- client-runtime decode/projection changes required for compatibility; -- focused tests and documentation for F0. +- lane/source-truth commands, events, decider invariants, and projectors; +- persistence migration and replay compatibility; +- client-runtime decode/projection compatibility required by F0; +- focused tests and F0 documentation. -The owner must avoid unrelated UI, provider adapter, node-job, notification, branding, release, and broad performance work. +The F0 owner must not absorb unrelated provider, node, notification, branding, release, full Command Center, or broad performance work. -Parallel agents must not edit shared orchestration contracts, decider, projector, or migration registry until the F0 owner publishes a merge contract or integrates the foundation. +Parallel agents must not edit shared orchestration contracts, decider, projector, package exports, or migration registry until the F0 owner defines a merge contract or integrates the foundation. -### F0 acceptance rows +### F0 acceptance scope Primary rows: - A01-A06 - B01-B07 -- the F0-relevant portion of C01/C03 -- the migration compatibility portion of P05 -- source-truth visibility scaffolding required for later K/L rows +- F0-relevant portions of C01/C03 +- migration/rebuild compatibility relevant to P05 +- only the source-truth shell/detail scaffolding necessary for later UI packages -Do not mark UI rows proven during F0 unless a real applicable client surface is implemented and tested. +Do not mark UI, provider, receipt-store, completion-gate, or node rows proven from F0 scaffolding. ## First-agent mandatory start output -The first implementation agent must record: - ```text PROVEN TARGET PROVEN REPOSITORY STATE @@ -106,48 +108,53 @@ UNKNOWN THAT CHANGES ACTION SAFE NEXT ACTION ``` -It must then produce: +Then produce: - plan revision; -- advisor review because this is domain/persistence work; -- exact schema/migration compatibility strategy; +- independent advisor review; +- exact schema/migration/compatibility strategy; - focused implementation; - deterministic tests; - final diff and staged list; -- verifier review; -- package completion receipt. +- independent verifier review; +- package completion receipt; +- updated campaign/acceptance evidence only after proof. ## Architectural warnings -- `packages/contracts/src/orchestration.ts` is already large. Prefer explicit new subpath modules and one integration owner rather than increasing parallel contention. -- Shell projections must stay compact. Do not put complete lane evidence/log payloads into the existing project/thread shell. -- Existing threads require a migration/import rule that does not falsely mark history complete. -- Completion policy belongs in the decider, not provider adapters or React. -- WorkLane is not a replacement name for Thread. A lane may own/recover across multiple provider threads. -- SourceTruthRevision is durable evidence lineage, not a cached `git status` widget. -- Do not introduce sleeps/polling in tests; use receipts and worker drains. -- Do not run development against live `~/.t3/userdata`. +- `packages/contracts/src/orchestration.ts` is already large; prefer current explicit-subpath conventions and one export integration owner. +- Keep shell projections compact; full evidence/log payloads require detail/artifact boundaries. +- Existing threads must migrate deterministically without false completion. +- Completion policy belongs in the decider, not provider adapters, transcripts, or React. +- WorkLane is not a renamed Thread; a lane may survive or own multiple provider threads. +- SourceTruthRevision is durable evidence lineage, not a cached status widget. +- Tests must use deterministic clocks, receipts, and worker drains rather than sleeps/polling. +- Never run development against live `~/.t3/userdata`. +- Remote project/worktree operations must respect existing environment authentication scopes and transport ownership. -## Known evidence gaps +## Known missing evidence -- No local tree/worktree/status receipt was possible from the execution environment used to write the specification. -- No test command was executed. -- No TypeScript/JSON formatter was run locally. -- No rendered markdown or client UI was inspected. -- No package implementation exists yet. -- No PR was opened because the repository instructions prohibit PR creation without explicit authorization. +- No local repository/worktree/status receipt from this specification session. +- The validator was added but not run in the unavailable local checkout. +- No GitHub Actions result was observed from this session. +- No formatter, tests, lint, typecheck, build, package, or client acceptance was run. +- No implementation package has started. +- No pull request was opened because it was not explicitly authorized. +- Concrete MacBrains bundle IDs, domains, auth tenant, updater package, signing, and hosted infrastructure remain configuration decisions to verify during U0; upstream values must not be reused automatically. -Agents must not convert the existence of this specification into an implementation claim. +Agents must not convert the existence or detail of this specification into an implementation claim. ## Campaign stop conditions The campaign stops only when: -- every required implementation package is integrated; -- every required acceptance row is `PROVEN` on current revisions; -- the canonical Mac + Linux node + mobile E0 scenario passes; -- fork identity/release paths are isolated and verified; -- current tests/CI and user-visible acceptance are attached; -- the final completion receipt is independently verified. +- every required package is integrated and `PROVEN`; +- every required acceptance row has current evidence; +- no stale check or unresolved blocker remains; +- fork identity/release isolation is proven; +- the canonical Mac + Linux node + desktop/web/iPhone/iPad E0 scenario passes; +- restart, provider failure, node disconnect, stale evidence, and recovery are demonstrated; +- the final user-visible deliverable is launchable; +- an independent verifier accepts the final evidence bundle. -A plan, documentation branch, build success, passing unit tests, or uploaded installer by itself does not satisfy the campaign. \ No newline at end of file +A plan, documentation branch, code compilation, unit-test pass, CI green state, or installer artifact alone does not complete the campaign. \ No newline at end of file From e6358ecf8036fb50ab80f40bdefc7fa828630ec4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 07:13:53 +0000 Subject: [PATCH 30/32] feat(orchestration): add work lane and source-truth foundation Introduce durable WorkLane and SourceTruthRevision contracts, lifecycle decider invariants, projections, migration of existing threads without false completion, and shell/detail query boundaries for MacBrains F0. Co-authored-by: Nayte --- apps/server/src/auth/RpcAuthorization.ts | 2 + .../checkpointing/CheckpointDiffQuery.test.ts | 10 + .../src/environment/ServerEnvironment.ts | 1 + .../Layers/OrchestrationEngine.test.ts | 5 + .../Layers/OrchestrationEngine.ts | 31 +- .../Layers/ProjectionPipeline.ts | 202 ++++++ .../Layers/ProjectionSnapshotQuery.ts | 283 +++++++- apps/server/src/orchestration/Schemas.ts | 24 + .../Services/ProjectionSnapshotQuery.ts | 17 + .../orchestration/commandInvariants.test.ts | 1 + .../src/orchestration/commandInvariants.ts | 75 ++ .../src/orchestration/decider.settled.test.ts | 1 + .../src/orchestration/decider.snoozed.test.ts | 1 + apps/server/src/orchestration/decider.ts | 683 ++++++++++++++++++ .../orchestration/decider.workLane.test.ts | 659 +++++++++++++++++ apps/server/src/orchestration/http.ts | 16 + apps/server/src/orchestration/projector.ts | 175 ++++- .../orchestration/projector.workLane.test.ts | 230 ++++++ .../src/orchestration/workLaneTransitions.ts | 45 ++ .../Layers/OrchestrationEventStore.ts | 5 +- .../ProjectionLaneAcceptanceCriteria.ts | 139 ++++ .../Layers/ProjectionSourceTruthRevisions.ts | 149 ++++ .../persistence/Layers/ProjectionWorkLanes.ts | 176 +++++ apps/server/src/persistence/Migrations.ts | 2 + .../035_WorkLanesAndSourceTruth.test.ts | 319 ++++++++ .../Migrations/035_WorkLanesAndSourceTruth.ts | 377 ++++++++++ .../Services/OrchestrationCommandReceipts.ts | 3 +- .../ProjectionLaneAcceptanceCriteria.ts | 66 ++ .../ProjectionSourceTruthRevisions.ts | 63 ++ .../Services/ProjectionWorkLanes.ts | 65 ++ .../project/ProjectSetupScriptRunner.test.ts | 2 + .../Layers/ProviderSessionReaper.test.ts | 2 + .../src/relay/AgentAwarenessRelay.test.ts | 2 + apps/server/src/server.test.ts | 9 + apps/server/src/serverRuntimeStartup.test.ts | 8 + apps/server/src/ws.ts | 151 +++- docs/macbrains/CAMPAIGN_MANIFEST.json | 318 ++++++-- docs/macbrains/IMPLEMENTATION_LEDGER.md | 2 +- docs/macbrains/receipts/F0_ADVISOR.md | 23 + docs/macbrains/receipts/F0_DOWNGRADE.md | 20 + docs/macbrains/receipts/F0_VERIFIER.md | 144 ++++ packages/client-runtime/package.json | 4 + .../src/connection/registry.test.ts | 1 + .../client-runtime/src/state/entities.test.ts | 1 + .../src/state/shell-sync.test.ts | 3 + .../client-runtime/src/state/shell.test.ts | 1 + .../src/state/shellReducer.test.ts | 41 ++ .../client-runtime/src/state/shellReducer.ts | 22 +- .../client-runtime/src/state/workLanes.ts | 66 ++ packages/contracts/package.json | 8 + packages/contracts/src/baseSchemas.ts | 23 + packages/contracts/src/environment.ts | 3 + packages/contracts/src/environmentHttp.ts | 21 +- packages/contracts/src/index.ts | 2 + packages/contracts/src/orchestration.ts | 202 +++++- packages/contracts/src/rpc.ts | 15 + packages/contracts/src/sourceTruth.ts | 133 ++++ packages/contracts/src/workLane.test.ts | 203 ++++++ packages/contracts/src/workLane.ts | 668 +++++++++++++++++ 59 files changed, 5836 insertions(+), 87 deletions(-) create mode 100644 apps/server/src/orchestration/decider.workLane.test.ts create mode 100644 apps/server/src/orchestration/projector.workLane.test.ts create mode 100644 apps/server/src/orchestration/workLaneTransitions.ts create mode 100644 apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts create mode 100644 apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts create mode 100644 apps/server/src/persistence/Layers/ProjectionWorkLanes.ts create mode 100644 apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts create mode 100644 apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts create mode 100644 apps/server/src/persistence/Services/ProjectionLaneAcceptanceCriteria.ts create mode 100644 apps/server/src/persistence/Services/ProjectionSourceTruthRevisions.ts create mode 100644 apps/server/src/persistence/Services/ProjectionWorkLanes.ts create mode 100644 docs/macbrains/receipts/F0_ADVISOR.md create mode 100644 docs/macbrains/receipts/F0_DOWNGRADE.md create mode 100644 docs/macbrains/receipts/F0_VERIFIER.md create mode 100644 packages/client-runtime/src/state/workLanes.ts create mode 100644 packages/contracts/src/sourceTruth.ts create mode 100644 packages/contracts/src/workLane.test.ts create mode 100644 packages/contracts/src/workLane.ts diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index 5655f260bc95..f64cfce1dd3a 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -27,6 +27,8 @@ export const RPC_REQUIRED_SCOPES = { [ORCHESTRATION_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope, + [ORCHESTRATION_WS_METHODS.subscribeLane]: AuthOrchestrationReadScope, + [ORCHESTRATION_WS_METHODS.getLaneDetail]: AuthOrchestrationReadScope, [WS_METHODS.serverProbe]: AuthOrchestrationReadScope, [WS_METHODS.serverGetConfig]: AuthOrchestrationReadScope, [WS_METHODS.serverRefreshProviders]: AuthOrchestrationOperateScope, diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts index 8e0e5fb74d51..66d80f6dfdc3 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts @@ -108,6 +108,8 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), ); @@ -201,6 +203,8 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), ); @@ -284,6 +288,8 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), ); @@ -352,6 +358,8 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), ); @@ -405,6 +413,8 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), ); diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 0eaf5a7c16a1..7668c219024d 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -142,6 +142,7 @@ export const make = Effect.gen(function* () { connectionProbe: true, threadSettlement: true, threadSnooze: true, + workLanes: true, ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }), }, }; diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 731002ea7830..011f8f2747e8 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -156,6 +156,7 @@ describe("OrchestrationEngine", () => { session: null, }, ], + lanes: [], }; const commandReadModel = { ...projectionSnapshot, @@ -183,6 +184,7 @@ describe("OrchestrationEngine", () => { snapshotSequence: projectionSnapshot.snapshotSequence, projects: [], threads: [], + lanes: [], updatedAt: projectionSnapshot.updatedAt, }), getArchivedShellSnapshot: () => @@ -190,6 +192,7 @@ describe("OrchestrationEngine", () => { snapshotSequence: projectionSnapshot.snapshotSequence, projects: [], threads: [], + lanes: [], updatedAt: projectionSnapshot.updatedAt, }), getSnapshotSequence: () => @@ -203,6 +206,8 @@ describe("OrchestrationEngine", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), ), Layer.provide( diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts index 19184915ac7c..58cdba9bf60a 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts @@ -3,6 +3,7 @@ import type { OrchestrationReadModel, ProjectId, ThreadId, + WorkLaneId, } from "@t3tools/contracts"; import { OrchestrationCommand } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; @@ -57,8 +58,8 @@ interface CommandEnvelope { } function commandToAggregateRef(command: OrchestrationCommand): { - readonly aggregateKind: "project" | "thread"; - readonly aggregateId: ProjectId | ThreadId; + readonly aggregateKind: "project" | "thread" | "lane"; + readonly aggregateId: ProjectId | ThreadId | WorkLaneId; } { switch (command.type) { case "project.create": @@ -68,6 +69,32 @@ function commandToAggregateRef(command: OrchestrationCommand): { aggregateKind: "project", aggregateId: command.projectId, }; + case "lane.create": + case "lane.task-contract.update": + case "lane.preflight.request": + case "lane.orientation.record": + case "lane.plan.propose": + case "lane.plan.activate": + case "lane.execution.start": + case "lane.testing.start": + case "lane.review.request": + case "lane.deliverable.register": + case "lane.completion.request": + case "lane.block": + case "lane.unblock": + case "lane.cancel": + case "lane.supersede": + case "lane.recovery.request": + case "lane.completion.invalidate": + case "lane.fail": + case "lane.meta.update": + case "source-truth.preflight.record": + case "source-truth.conflict.record": + case "source-truth.refresh.request": + return { + aggregateKind: "lane", + aggregateId: command.laneId, + }; default: return { aggregateKind: "thread", diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 1f24a4a0200c..38db9da4ac71 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -34,6 +34,9 @@ import { ProjectionTurnRepository, } from "../../persistence/Services/ProjectionTurns.ts"; import { ProjectionThreadRepository } from "../../persistence/Services/ProjectionThreads.ts"; +import { ProjectionWorkLaneRepository } from "../../persistence/Services/ProjectionWorkLanes.ts"; +import { ProjectionSourceTruthRevisionRepository } from "../../persistence/Services/ProjectionSourceTruthRevisions.ts"; +import { ProjectionLaneAcceptanceCriterionRepository } from "../../persistence/Services/ProjectionLaneAcceptanceCriteria.ts"; import { ProjectionPendingApprovalRepositoryLive } from "../../persistence/Layers/ProjectionPendingApprovals.ts"; import { ProjectionProjectRepositoryLive } from "../../persistence/Layers/ProjectionProjects.ts"; import { ProjectionStateRepositoryLive } from "../../persistence/Layers/ProjectionState.ts"; @@ -43,6 +46,9 @@ import { ProjectionThreadProposedPlanRepositoryLive } from "../../persistence/La import { ProjectionThreadSessionRepositoryLive } from "../../persistence/Layers/ProjectionThreadSessions.ts"; import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts"; import { ProjectionThreadRepositoryLive } from "../../persistence/Layers/ProjectionThreads.ts"; +import { ProjectionWorkLaneRepositoryLive } from "../../persistence/Layers/ProjectionWorkLanes.ts"; +import { ProjectionSourceTruthRevisionRepositoryLive } from "../../persistence/Layers/ProjectionSourceTruthRevisions.ts"; +import { ProjectionLaneAcceptanceCriterionRepositoryLive } from "../../persistence/Layers/ProjectionLaneAcceptanceCriteria.ts"; import { ServerConfig } from "../../config.ts"; import { OrchestrationProjectionPipeline, @@ -54,6 +60,7 @@ import { parseThreadSegmentFromAttachmentId, toSafeThreadAttachmentSegment, } from "../../attachmentStore.ts"; +import type { WorkLane } from "@t3tools/contracts"; export const ORCHESTRATION_PROJECTOR_NAMES = { projects: "projection.projects", @@ -65,8 +72,32 @@ export const ORCHESTRATION_PROJECTOR_NAMES = { threadTurns: "projection.thread-turns", checkpoints: "projection.checkpoints", pendingApprovals: "projection.pending-approvals", + workLanes: "projection.work-lanes", } as const; +function toProjectionWorkLaneRow(lane: WorkLane, lastSequence: number) { + return { + id: lane.id, + projectId: lane.projectId, + title: lane.title, + state: lane.state, + priority: lane.priority, + classification: lane.classification, + environmentId: lane.environmentId, + branch: lane.branch, + worktreePath: lane.worktreePath, + sourceTruthRevisionId: lane.sourceTruthRevisionId, + primaryThreadId: lane.primaryThreadId, + importedThreadId: lane.importedThreadId, + objectiveSummary: lane.taskContract.objective, + lane, + createdAt: lane.createdAt, + updatedAt: lane.updatedAt, + completedAt: lane.completedAt, + lastSequence, + }; +} + type ProjectorName = (typeof ORCHESTRATION_PROJECTOR_NAMES)[keyof typeof ORCHESTRATION_PROJECTOR_NAMES]; @@ -480,6 +511,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository; const projectionTurnRepository = yield* ProjectionTurnRepository; const projectionPendingApprovalRepository = yield* ProjectionPendingApprovalRepository; + const projectionWorkLaneRepository = yield* ProjectionWorkLaneRepository; + const projectionSourceTruthRevisionRepository = yield* ProjectionSourceTruthRevisionRepository; + const projectionLaneAcceptanceCriterionRepository = + yield* ProjectionLaneAcceptanceCriterionRepository; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -1536,6 +1571,166 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti } }); + const applyWorkLanesProjection: ProjectorDefinition["apply"] = Effect.fn( + "applyWorkLanesProjection", + )(function* (event, _attachmentSideEffects) { + switch (event.type) { + case "lane.created": + case "lane.imported": { + yield* projectionWorkLaneRepository.upsert( + toProjectionWorkLaneRow(event.payload.lane, event.sequence), + ); + for (const criterion of event.payload.acceptanceCriteria) { + yield* projectionLaneAcceptanceCriterionRepository.upsert({ + id: criterion.id, + laneId: criterion.laneId, + criterion, + lastSequence: event.sequence, + }); + } + return; + } + + case "lane.state-changed": + case "lane.task-contract-updated": + case "lane.meta-updated": + case "lane.plan-proposed": + case "lane.plan-activated": + case "lane.deliverable-registered": { + const existing = yield* projectionWorkLaneRepository.getById({ + id: event.payload.laneId, + }); + if (Option.isNone(existing)) { + return; + } + const lane = existing.value.lane; + let next = lane; + switch (event.type) { + case "lane.state-changed": + next = { + ...lane, + state: event.payload.toState, + resumeState: event.payload.resumeState, + updatedAt: event.payload.updatedAt, + completedAt: + event.payload.toState === "completed" + ? event.payload.updatedAt + : event.payload.fromState === "completed" + ? null + : lane.completedAt, + }; + break; + case "lane.task-contract-updated": + next = { + ...lane, + taskContract: event.payload.taskContract, + updatedAt: event.payload.updatedAt, + }; + break; + case "lane.meta-updated": + next = { + ...lane, + ...(event.payload.title !== undefined ? { title: event.payload.title } : {}), + ...(event.payload.priority !== undefined + ? { priority: event.payload.priority } + : {}), + ...(event.payload.classification !== undefined + ? { classification: event.payload.classification } + : {}), + ...(event.payload.branch !== undefined ? { branch: event.payload.branch } : {}), + ...(event.payload.worktreePath !== undefined + ? { worktreePath: event.payload.worktreePath } + : {}), + ...(event.payload.baseRef !== undefined ? { baseRef: event.payload.baseRef } : {}), + ...(event.payload.repositoryIdentity !== undefined + ? { repositoryIdentity: event.payload.repositoryIdentity } + : {}), + updatedAt: event.payload.updatedAt, + }; + break; + case "lane.plan-proposed": + next = { + ...lane, + updatedAt: event.payload.proposedAt, + }; + break; + case "lane.plan-activated": + next = { + ...lane, + activePlanRevisionId: event.payload.planRevisionId, + updatedAt: event.payload.updatedAt, + }; + break; + case "lane.deliverable-registered": + next = { + ...lane, + deliverableIds: lane.deliverableIds.includes(event.payload.deliverableId) + ? lane.deliverableIds + : [...lane.deliverableIds, event.payload.deliverableId], + updatedAt: event.payload.updatedAt, + }; + break; + } + yield* projectionWorkLaneRepository.upsert( + toProjectionWorkLaneRow(next, event.sequence), + ); + return; + } + + case "source-truth.preflight-recorded": { + const existing = yield* projectionWorkLaneRepository.getById({ + id: event.payload.laneId, + }); + if (Option.isSome(existing) && event.payload.previousRevisionId !== null) { + const previous = yield* projectionSourceTruthRevisionRepository.getById({ + id: event.payload.previousRevisionId, + }); + if (Option.isSome(previous)) { + yield* projectionSourceTruthRevisionRepository.upsert({ + ...previous.value, + supersededAt: event.payload.recordedAt, + revision: { + ...previous.value.revision, + supersededAt: event.payload.recordedAt, + }, + lastSequence: event.sequence, + }); + } + } + + yield* projectionSourceTruthRevisionRepository.upsert({ + id: event.payload.revision.id, + laneId: event.payload.laneId, + revision: event.payload.revision, + producedAt: event.payload.revision.producedAt, + supersededAt: event.payload.revision.supersededAt, + lastSequence: event.sequence, + }); + + if (Option.isSome(existing)) { + yield* projectionWorkLaneRepository.upsert( + toProjectionWorkLaneRow( + { + ...existing.value.lane, + sourceTruthRevisionId: event.payload.revision.id, + updatedAt: event.payload.recordedAt, + }, + event.sequence, + ), + ); + } + return; + } + + case "source-truth.conflict-recorded": + case "source-truth.refresh-requested": + return; + + default: + return; + } + }); + const projectors: ReadonlyArray = [ { name: ORCHESTRATION_PROJECTOR_NAMES.projects, @@ -1573,6 +1768,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti name: ORCHESTRATION_PROJECTOR_NAMES.threads, apply: applyThreadsProjection, }, + { + name: ORCHESTRATION_PROJECTOR_NAMES.workLanes, + apply: applyWorkLanesProjection, + }, ]; const runProjectorForEvent = Effect.fn("runProjectorForEvent")(function* ( @@ -1675,5 +1874,8 @@ export const OrchestrationProjectionPipelineLive = Layer.effect( Layer.provideMerge(ProjectionThreadSessionRepositoryLive), Layer.provideMerge(ProjectionTurnRepositoryLive), Layer.provideMerge(ProjectionPendingApprovalRepositoryLive), + Layer.provideMerge(ProjectionWorkLaneRepositoryLive), + Layer.provideMerge(ProjectionSourceTruthRevisionRepositoryLive), + Layer.provideMerge(ProjectionLaneAcceptanceCriterionRepositoryLive), Layer.provideMerge(ProjectionStateRepositoryLive), ); diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 3d05bef4bdf4..40eaff46c404 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -1,4 +1,5 @@ import { + AcceptanceCriterion, ChatAttachment, CheckpointRef, IsoDateTime, @@ -23,7 +24,14 @@ import { type OrchestrationThreadShell, ModelSelection, ProjectId, + SourceTruthRevision, ThreadId, + WorkLane, + WorkLaneDetailSnapshot, + WorkLaneId, + WorkLaneShell, + toWorkLaneShell, + type SourceTruthRevisionShellSummary, } from "@t3tools/contracts"; import * as Arr from "effect/Array"; import * as Effect from "effect/Effect"; @@ -139,6 +147,41 @@ const ProjectionFullThreadDiffContextRowSchema = Schema.Struct({ latestCheckpointTurnCount: Schema.NullOr(NonNegativeInt), toCheckpointRef: Schema.NullOr(CheckpointRef), }); +const ProjectionWorkLaneDbRowSchema = Schema.Struct({ + id: WorkLaneId, + lane: Schema.fromJsonString(WorkLane), + updatedAt: IsoDateTime, +}); +const ProjectionSourceTruthRevisionDbRowSchema = Schema.Struct({ + id: Schema.String, + laneId: WorkLaneId, + revision: Schema.fromJsonString(SourceTruthRevision), + producedAt: IsoDateTime, + supersededAt: Schema.NullOr(IsoDateTime), +}); +const ProjectionLaneAcceptanceCriterionDbRowSchema = Schema.Struct({ + id: Schema.String, + laneId: WorkLaneId, + criterion: Schema.fromJsonString(AcceptanceCriterion), +}); +const LaneIdLookupInput = Schema.Struct({ + laneId: WorkLaneId, +}); + +function toSourceTruthShellSummary( + revision: SourceTruthRevision, +): SourceTruthRevisionShellSummary { + return { + revisionId: revision.id, + branch: revision.branch, + headSha: revision.headSha, + worktreePath: revision.worktreePath, + isDirty: revision.dirty.isDirty, + activeGitOperation: revision.activeGitOperation, + ownershipOverlap: revision.ownershipOverlap, + producedAt: revision.producedAt, + }; +} const REQUIRED_SNAPSHOT_PROJECTORS = [ ORCHESTRATION_PROJECTOR_NAMES.projects, @@ -148,6 +191,7 @@ const REQUIRED_SNAPSHOT_PROJECTORS = [ ORCHESTRATION_PROJECTOR_NAMES.threadActivities, ORCHESTRATION_PROJECTOR_NAMES.threadSessions, ORCHESTRATION_PROJECTOR_NAMES.checkpoints, + ORCHESTRATION_PROJECTOR_NAMES.workLanes, ] as const; function maxIso(left: string | null, right: string): string { @@ -947,6 +991,82 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { `, }); + const listWorkLaneRows = SqlSchema.findAll({ + Request: Schema.Void, + Result: ProjectionWorkLaneDbRowSchema, + execute: () => + sql` + SELECT + id, + lane_json AS "lane", + updated_at AS "updatedAt" + FROM projection_work_lanes + ORDER BY created_at ASC, id ASC + `, + }); + + const listSourceTruthRevisionRows = SqlSchema.findAll({ + Request: Schema.Void, + Result: ProjectionSourceTruthRevisionDbRowSchema, + execute: () => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt" + FROM projection_source_truth_revisions + ORDER BY produced_at ASC, id ASC + `, + }); + + const getWorkLaneRow = SqlSchema.findOneOption({ + Request: LaneIdLookupInput, + Result: ProjectionWorkLaneDbRowSchema, + execute: ({ laneId }) => + sql` + SELECT + id, + lane_json AS "lane", + updated_at AS "updatedAt" + FROM projection_work_lanes + WHERE id = ${laneId} + `, + }); + + const listSourceTruthRevisionRowsByLane = SqlSchema.findAll({ + Request: LaneIdLookupInput, + Result: ProjectionSourceTruthRevisionDbRowSchema, + execute: ({ laneId }) => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt" + FROM projection_source_truth_revisions + WHERE lane_id = ${laneId} + ORDER BY produced_at ASC, id ASC + `, + }); + + const listAcceptanceCriterionRowsByLane = SqlSchema.findAll({ + Request: LaneIdLookupInput, + Result: ProjectionLaneAcceptanceCriterionDbRowSchema, + execute: ({ laneId }) => + sql` + SELECT + id, + lane_id AS "laneId", + criterion_json AS "criterion" + FROM projection_lane_acceptance_criteria + WHERE lane_id = ${laneId} + ORDER BY id ASC + `, + }); + const getSnapshot: ProjectionSnapshotQueryShape["getSnapshot"] = () => sql .withTransaction( @@ -1015,6 +1135,14 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ), ), + listWorkLaneRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getSnapshot:listWorkLanes:query", + "ProjectionSnapshotQuery.getSnapshot:listWorkLanes:decodeRows", + ), + ), + ), listProjectionStateRows(undefined).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( @@ -1036,6 +1164,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { sessionRows, checkpointRows, latestTurnRows, + workLaneRows, stateRows, ]) => Effect.gen(function* () { @@ -1214,10 +1343,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { session: sessionsByThread.get(row.threadId) ?? null, })); + for (const row of workLaneRows) { + updatedAt = maxIso(updatedAt, row.updatedAt); + } + const snapshot = { snapshotSequence: computeSnapshotSequence(stateRows), projects, threads, + lanes: workLaneRows.map((row) => row.lane), updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", }; @@ -1280,6 +1414,14 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ), ), + listWorkLaneRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getCommandReadModel:listWorkLanes:query", + "ProjectionSnapshotQuery.getCommandReadModel:listWorkLanes:decodeRows", + ), + ), + ), listProjectionStateRows(undefined).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( @@ -1292,7 +1434,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ) .pipe( Effect.flatMap( - ([projectRows, threadRows, proposedPlanRows, sessionRows, latestTurnRows, stateRows]) => + ([ + projectRows, + threadRows, + proposedPlanRows, + sessionRows, + latestTurnRows, + workLaneRows, + stateRows, + ]) => Effect.sync(() => { let updatedAt: string | null = null; const projects: OrchestrationProject[] = []; @@ -1417,10 +1567,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { }); } + for (const row of workLaneRows) { + updatedAt = maxIso(updatedAt, row.updatedAt); + } + return { snapshotSequence: computeSnapshotSequence(stateRows), projects, threads, + lanes: workLaneRows.map((row) => row.lane), updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", } satisfies OrchestrationReadModel; }), @@ -1469,6 +1624,22 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ), ), + listWorkLaneRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getShellSnapshot:listWorkLanes:query", + "ProjectionSnapshotQuery.getShellSnapshot:listWorkLanes:decodeRows", + ), + ), + ), + listSourceTruthRevisionRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getShellSnapshot:listSourceTruthRevisions:query", + "ProjectionSnapshotQuery.getShellSnapshot:listSourceTruthRevisions:decodeRows", + ), + ), + ), listProjectionStateRows(undefined).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( @@ -1480,7 +1651,16 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ]), ) .pipe( - Effect.flatMap(([projectRows, threadRows, sessionRows, latestTurnRows, stateRows]) => + Effect.flatMap( + ([ + projectRows, + threadRows, + sessionRows, + latestTurnRows, + workLaneRows, + sourceTruthRows, + stateRows, + ]) => Effect.gen(function* () { let updatedAt: string | null = null; for (const row of projectRows) { @@ -1501,6 +1681,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { updatedAt = maxIso(updatedAt, row.completedAt); } } + for (const row of workLaneRows) { + updatedAt = maxIso(updatedAt, row.updatedAt); + } for (const row of stateRows) { updatedAt = maxIso(updatedAt, row.updatedAt); } @@ -1512,6 +1695,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { const sessionByThread = new Map( sessionRows.map((row) => [row.threadId, mapSessionRow(row)] as const), ); + const sourceTruthById = new Map( + sourceTruthRows.map((row) => [row.id, row.revision] as const), + ); const snapshot = { snapshotSequence: computeSnapshotSequence(stateRows), @@ -1549,6 +1735,15 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { } satisfies OrchestrationThreadShell) : Result.failVoid, ), + lanes: workLaneRows.map((row) => { + const revisionId = row.lane.sourceTruthRevisionId; + const revision = + revisionId === null ? null : (sourceTruthById.get(revisionId) ?? null); + return toWorkLaneShell( + row.lane, + revision === null ? null : toSourceTruthShellSummary(revision), + ); + }), updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", }; @@ -1686,6 +1881,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { hasActionableProposedPlan: row.hasActionableProposedPlan > 0, }), ), + lanes: [], updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", }; @@ -2103,6 +2299,87 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ); + const getLaneShellById: ProjectionSnapshotQueryShape["getLaneShellById"] = (laneId) => + getWorkLaneRow({ laneId }).pipe( + Effect.flatMap((row) => + Effect.gen(function* () { + if (Option.isNone(row)) { + return Option.none(); + } + const revisionId = row.value.lane.sourceTruthRevisionId; + if (revisionId === null) { + return Option.some(toWorkLaneShell(row.value.lane, null)); + } + const revisions = yield* listSourceTruthRevisionRowsByLane({ laneId }); + const revision = revisions.find((entry) => entry.id === revisionId)?.revision ?? null; + return Option.some( + toWorkLaneShell( + row.value.lane, + revision === null ? null : toSourceTruthShellSummary(revision), + ), + ); + }), + ), + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getLaneShellById:query", + "ProjectionSnapshotQuery.getLaneShellById:decodeRows", + ), + ), + ); + + const getLaneDetail: ProjectionSnapshotQueryShape["getLaneDetail"] = (laneId) => + sql + .withTransaction( + Effect.gen(function* () { + const laneRow = yield* getWorkLaneRow({ laneId }).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getLaneDetail:getLane:query", + "ProjectionSnapshotQuery.getLaneDetail:getLane:decodeRow", + ), + ), + ); + if (Option.isNone(laneRow)) { + return Option.none(); + } + const [criterionRows, revisionRows, { snapshotSequence }] = yield* Effect.all([ + listAcceptanceCriterionRowsByLane({ laneId }).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getLaneDetail:listCriteria:query", + "ProjectionSnapshotQuery.getLaneDetail:listCriteria:decodeRows", + ), + ), + ), + listSourceTruthRevisionRowsByLane({ laneId }).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.getLaneDetail:listRevisions:query", + "ProjectionSnapshotQuery.getLaneDetail:listRevisions:decodeRows", + ), + ), + ), + getSnapshotSequence(), + ]); + return Option.some({ + snapshotSequence, + detail: { + lane: laneRow.value.lane, + acceptanceCriteria: criterionRows.map((row) => row.criterion), + sourceTruthRevisions: revisionRows.map((row) => row.revision), + }, + } satisfies WorkLaneDetailSnapshot); + }), + ) + .pipe( + Effect.mapError((error) => + isPersistenceError(error) + ? error + : toPersistenceSqlError("ProjectionSnapshotQuery.getLaneDetail:transaction")(error), + ), + ); + return { getCommandReadModel, getSnapshot, @@ -2118,6 +2395,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { getThreadShellById, getThreadDetailById, getThreadDetailSnapshot, + getLaneShellById, + getLaneDetail, } satisfies ProjectionSnapshotQueryShape; }); diff --git a/apps/server/src/orchestration/Schemas.ts b/apps/server/src/orchestration/Schemas.ts index 3b558d24739e..36d1ee84780f 100644 --- a/apps/server/src/orchestration/Schemas.ts +++ b/apps/server/src/orchestration/Schemas.ts @@ -24,6 +24,17 @@ import { ThreadApprovalResponseRequestedPayload as ContractsThreadApprovalResponseRequestedPayloadSchema, ThreadCheckpointRevertRequestedPayload as ContractsThreadCheckpointRevertRequestedPayloadSchema, ThreadSessionStopRequestedPayload as ContractsThreadSessionStopRequestedPayloadSchema, + LaneCreatedPayload as ContractsLaneCreatedPayloadSchema, + LaneImportedPayload as ContractsLaneImportedPayloadSchema, + LaneStateChangedPayload as ContractsLaneStateChangedPayloadSchema, + LaneTaskContractUpdatedPayload as ContractsLaneTaskContractUpdatedPayloadSchema, + LaneMetaUpdatedPayload as ContractsLaneMetaUpdatedPayloadSchema, + LanePlanProposedPayload as ContractsLanePlanProposedPayloadSchema, + LanePlanActivatedPayload as ContractsLanePlanActivatedPayloadSchema, + LaneDeliverableRegisteredPayload as ContractsLaneDeliverableRegisteredPayloadSchema, + SourceTruthPreflightRecordedPayload as ContractsSourceTruthPreflightRecordedPayloadSchema, + SourceTruthConflictRecordedPayload as ContractsSourceTruthConflictRecordedPayloadSchema, + SourceTruthRefreshRequestedPayload as ContractsSourceTruthRefreshRequestedPayloadSchema, } from "@t3tools/contracts"; // Server-internal alias surface, backed by contract schemas as the source of truth. @@ -58,3 +69,16 @@ export const ThreadApprovalResponseRequestedPayload = export const ThreadCheckpointRevertRequestedPayload = ContractsThreadCheckpointRevertRequestedPayloadSchema; export const ThreadSessionStopRequestedPayload = ContractsThreadSessionStopRequestedPayloadSchema; + +export const LaneCreatedPayload = ContractsLaneCreatedPayloadSchema; +export const LaneImportedPayload = ContractsLaneImportedPayloadSchema; +export const LaneStateChangedPayload = ContractsLaneStateChangedPayloadSchema; +export const LaneTaskContractUpdatedPayload = ContractsLaneTaskContractUpdatedPayloadSchema; +export const LaneMetaUpdatedPayload = ContractsLaneMetaUpdatedPayloadSchema; +export const LanePlanProposedPayload = ContractsLanePlanProposedPayloadSchema; +export const LanePlanActivatedPayload = ContractsLanePlanActivatedPayloadSchema; +export const LaneDeliverableRegisteredPayload = ContractsLaneDeliverableRegisteredPayloadSchema; +export const SourceTruthPreflightRecordedPayload = + ContractsSourceTruthPreflightRecordedPayloadSchema; +export const SourceTruthConflictRecordedPayload = ContractsSourceTruthConflictRecordedPayloadSchema; +export const SourceTruthRefreshRequestedPayload = ContractsSourceTruthRefreshRequestedPayloadSchema; diff --git a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts index 23b291d8778a..c5efeef3826b 100644 --- a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts @@ -18,6 +18,9 @@ import type { OrchestrationThreadShell, ProjectId, ThreadId, + WorkLaneDetailSnapshot, + WorkLaneId, + WorkLaneShell, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import type * as Option from "effect/Option"; @@ -168,6 +171,20 @@ export interface ProjectionSnapshotQueryShape { readonly getThreadDetailSnapshot: ( threadId: ThreadId, ) => Effect.Effect, ProjectionRepositoryError>; + + /** + * Read a compact work-lane shell row by id. + */ + readonly getLaneShellById: ( + laneId: WorkLaneId, + ) => Effect.Effect, ProjectionRepositoryError>; + + /** + * Read work-lane detail together with the projection snapshot sequence. + */ + readonly getLaneDetail: ( + laneId: WorkLaneId, + ) => Effect.Effect, ProjectionRepositoryError>; } /** diff --git a/apps/server/src/orchestration/commandInvariants.test.ts b/apps/server/src/orchestration/commandInvariants.test.ts index 9531cd5c3afb..19306dac4e9d 100644 --- a/apps/server/src/orchestration/commandInvariants.test.ts +++ b/apps/server/src/orchestration/commandInvariants.test.ts @@ -104,6 +104,7 @@ const readModel: OrchestrationReadModel = { deletedAt: null, }, ], + lanes: [], }; const messageSendCommand: OrchestrationCommand = { diff --git a/apps/server/src/orchestration/commandInvariants.ts b/apps/server/src/orchestration/commandInvariants.ts index b59ded77f4f4..69534b15f89e 100644 --- a/apps/server/src/orchestration/commandInvariants.ts +++ b/apps/server/src/orchestration/commandInvariants.ts @@ -5,11 +5,14 @@ import type { OrchestrationThread, ProjectId, ThreadId, + WorkLane, + WorkLaneId, } from "@t3tools/contracts"; import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; import * as Effect from "effect/Effect"; import { OrchestrationCommandInvariantError } from "./Errors.ts"; +import { isWorkLaneWorktreeOwningState } from "./workLaneTransitions.ts"; function invariantError(commandType: string, detail: string): OrchestrationCommandInvariantError { return new OrchestrationCommandInvariantError({ @@ -182,3 +185,75 @@ export function requireNonNegativeInteger(input: { ), ); } + +export function findLaneById( + readModel: OrchestrationReadModel, + laneId: WorkLaneId, +): WorkLane | undefined { + return readModel.lanes.find((lane) => lane.id === laneId); +} + +export function requireLane(input: { + readonly readModel: OrchestrationReadModel; + readonly command: OrchestrationCommand; + readonly laneId: WorkLaneId; +}): Effect.Effect { + const lane = findLaneById(input.readModel, input.laneId); + if (lane) { + return Effect.succeed(lane); + } + return Effect.fail( + invariantError( + input.command.type, + `Lane '${input.laneId}' does not exist for command '${input.command.type}'.`, + ), + ); +} + +export function requireLaneAbsent(input: { + readonly readModel: OrchestrationReadModel; + readonly command: OrchestrationCommand; + readonly laneId: WorkLaneId; +}): Effect.Effect { + if (!findLaneById(input.readModel, input.laneId)) { + return Effect.void; + } + return Effect.fail( + invariantError( + input.command.type, + `Lane '${input.laneId}' already exists and cannot be created twice.`, + ), + ); +} + +/** + * No other worktree-owning (non-terminal) lane may share the same worktree path. + * A null/empty path is treated as "no ownership claim" and always succeeds. + */ +export function requireWorktreeExclusive(input: { + readonly readModel: OrchestrationReadModel; + readonly command: OrchestrationCommand; + readonly worktreePath: string | null; + readonly exceptLaneId: WorkLaneId; +}): Effect.Effect { + if (input.worktreePath === null || input.worktreePath.trim() === "") { + return Effect.void; + } + const normalizedPath = normalizeProjectPathForComparison(input.worktreePath); + const conflictingLane = input.readModel.lanes.find( + (lane) => + lane.id !== input.exceptLaneId && + lane.worktreePath !== null && + isWorkLaneWorktreeOwningState(lane.state) && + normalizeProjectPathForComparison(lane.worktreePath) === normalizedPath, + ); + if (conflictingLane === undefined) { + return Effect.void; + } + return Effect.fail( + invariantError( + input.command.type, + `Worktree '${normalizedPath}' is already owned by lane '${conflictingLane.id}'.`, + ), + ); +} diff --git a/apps/server/src/orchestration/decider.settled.test.ts b/apps/server/src/orchestration/decider.settled.test.ts index 73f1cbf9127b..3e88be8d6971 100644 --- a/apps/server/src/orchestration/decider.settled.test.ts +++ b/apps/server/src/orchestration/decider.settled.test.ts @@ -52,6 +52,7 @@ function makeReadModel( session, }, ], + lanes: [], updatedAt: NOW, }; } diff --git a/apps/server/src/orchestration/decider.snoozed.test.ts b/apps/server/src/orchestration/decider.snoozed.test.ts index 1012240b18a0..532bde538e21 100644 --- a/apps/server/src/orchestration/decider.snoozed.test.ts +++ b/apps/server/src/orchestration/decider.snoozed.test.ts @@ -57,6 +57,7 @@ function makeReadModel(input: { session: null, }, ], + lanes: [], updatedAt: NOW, }; } diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 100369ae6e36..a5174b5ab157 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -3,6 +3,8 @@ import { type OrchestrationCommand, type OrchestrationEvent, type OrchestrationReadModel, + type WorkLane, + type WorkLaneState, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Crypto from "effect/Crypto"; @@ -13,14 +15,21 @@ import { OrchestrationCommandInvariantError } from "./Errors.ts"; import { listThreadsByProjectId, requireActiveProjectWorkspaceRootAbsent, + requireLane, + requireLaneAbsent, requireProject, requireProjectAbsent, requireThread, requireThreadArchived, requireThreadAbsent, requireThreadNotArchived, + requireWorktreeExclusive, } from "./commandInvariants.ts"; import { projectEvent } from "./projector.ts"; +import { + isAllowedWorkLaneSupersede, + requireAllowedWorkLaneTransition, +} from "./workLaneTransitions.ts"; const nowIso = Effect.map(DateTime.now, DateTime.formatIso); @@ -172,8 +181,87 @@ function withEventBase( ); } +function buildLaneCreated(input: { + readonly command: Extract; +}): WorkLane { + const primaryThreadId = input.command.primaryThreadId ?? null; + return { + id: input.command.laneId, + projectId: input.command.projectId, + title: input.command.title, + taskContract: input.command.taskContract, + state: "queued", + priority: input.command.priority, + classification: input.command.classification, + environmentId: input.command.environmentId, + repositoryIdentity: input.command.repositoryIdentity ?? null, + baseRef: input.command.baseRef ?? null, + branch: input.command.branch ?? null, + worktreePath: input.command.worktreePath ?? null, + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + activePlanRevisionId: null, + acceptanceCriterionIds: (input.command.acceptanceCriteria ?? []).map( + (criterion) => criterion.id, + ), + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId, + importedThreadId: null, + threadIds: primaryThreadId === null ? [] : [primaryThreadId], + legacyExecutorRef: null, + resumeState: null, + createdAt: input.command.createdAt, + updatedAt: input.command.createdAt, + completedAt: null, + }; +} + type PlannedOrchestrationEvent = Omit; +const emitLaneStateChanged = Effect.fn("emitLaneStateChanged")(function* ({ + command, + laneId, + fromState, + toState, + occurredAt, + resumeState = null, + reason, +}: { + readonly command: OrchestrationCommand; + readonly laneId: WorkLane["id"]; + readonly fromState: WorkLaneState; + readonly toState: WorkLaneState; + readonly occurredAt: string; + readonly resumeState?: WorkLaneState | null; + readonly reason?: string; +}): Effect.fn.Return< + PlannedOrchestrationEvent, + OrchestrationCommandInvariantError | PlatformError.PlatformError, + Crypto.Crypto +> { + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: laneId, + occurredAt, + commandId: command.commandId, + })), + type: "lane.state-changed" as const, + payload: { + laneId, + fromState, + toState, + resumeState, + ...(reason !== undefined ? { reason } : {}), + updatedAt: occurredAt, + }, + }; +}); + type DecideOrchestrationCommandResult = | PlannedOrchestrationEvent | ReadonlyArray; @@ -1168,6 +1256,601 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" return [unsettledEvent, activityAppendedEvent]; } + case "lane.create": { + yield* requireProject({ + readModel, + command, + projectId: command.projectId, + }); + yield* requireLaneAbsent({ + readModel, + command, + laneId: command.laneId, + }); + const lane = buildLaneCreated({ command }); + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.createdAt, + commandId: command.commandId, + })), + type: "lane.created" as const, + payload: { + lane, + acceptanceCriteria: command.acceptanceCriteria ?? [], + }, + }; + } + + case "lane.preflight.request": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "preflight", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "preflight", + occurredAt: command.requestedAt, + }); + } + + case "lane.orientation.record": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "oriented", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "oriented", + occurredAt: command.recordedAt, + }); + } + + case "lane.plan.propose": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "planned", + }); + const stateChanged = yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "planned", + occurredAt: command.proposedAt, + }); + const planProposed: PlannedOrchestrationEvent = { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.proposedAt, + commandId: command.commandId, + })), + type: "lane.plan-proposed" as const, + payload: { + laneId: command.laneId, + planRevisionId: command.planRevisionId, + proposedAt: command.proposedAt, + }, + }; + return [stateChanged, planProposed]; + } + + case "lane.plan.activate": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + const planActivated: PlannedOrchestrationEvent = { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.activatedAt, + commandId: command.commandId, + })), + type: "lane.plan-activated" as const, + payload: { + laneId: command.laneId, + planRevisionId: command.planRevisionId, + activatedAt: command.activatedAt, + updatedAt: command.activatedAt, + }, + }; + if (lane.state === "oriented") { + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "planned", + }); + const stateChanged = yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "planned", + occurredAt: command.activatedAt, + }); + return [planActivated, stateChanged]; + } + return planActivated; + } + + case "lane.execution.start": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (lane.state !== "planned") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' must be in 'planned' state to start execution (current: '${lane.state}').`, + }), + ); + } + if (lane.sourceTruthRevisionId === null) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' requires a source-truth revision before execution.`, + }), + ); + } + if (lane.classification === "substantial" && lane.worktreePath === null) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Substantial lane '${command.laneId}' requires a worktree path before execution.`, + }), + ); + } + yield* requireWorktreeExclusive({ + readModel, + command, + worktreePath: lane.worktreePath, + exceptLaneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "executing", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "executing", + occurredAt: command.startedAt, + }); + } + + case "lane.testing.start": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "testing", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "testing", + occurredAt: command.startedAt, + }); + } + + case "lane.review.request": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "reviewing", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "reviewing", + occurredAt: command.requestedAt, + }); + } + + case "lane.deliverable.register": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + const registered: PlannedOrchestrationEvent = { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.registeredAt, + commandId: command.commandId, + })), + type: "lane.deliverable-registered" as const, + payload: { + laneId: command.laneId, + deliverableId: command.deliverableId, + registeredAt: command.registeredAt, + updatedAt: command.registeredAt, + }, + }; + if (lane.state === "deliverable-ready") { + return registered; + } + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "deliverable-ready", + }); + const stateChanged = yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "deliverable-ready", + occurredAt: command.registeredAt, + }); + return [stateChanged, registered]; + } + + case "lane.completion.request": { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: "completion gate reserved until F2", + }), + ); + } + + case "lane.block": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "blocked", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "blocked", + occurredAt: command.blockedAt, + resumeState: lane.state, + ...(command.reason !== undefined ? { reason: command.reason } : {}), + }); + } + + case "lane.unblock": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (lane.state !== "blocked") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' is not blocked (current: '${lane.state}').`, + }), + ); + } + const toState: WorkLaneState = lane.resumeState ?? "recovery-required"; + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState, + occurredAt: command.unblockedAt, + resumeState: null, + }); + } + + case "lane.cancel": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "cancelled", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "cancelled", + occurredAt: command.cancelledAt, + resumeState: null, + }); + } + + case "lane.supersede": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (!isAllowedWorkLaneSupersede(lane.state)) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' cannot be superseded from terminal state '${lane.state}'.`, + }), + ); + } + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "superseded", + occurredAt: command.supersededAt, + resumeState: null, + }); + } + + case "lane.recovery.request": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (lane.state !== "failed" && lane.state !== "recovery-required") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' recovery is only allowed from 'failed' or 'recovery-required' (current: '${lane.state}').`, + }), + ); + } + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "preflight", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "preflight", + occurredAt: command.requestedAt, + resumeState: null, + }); + } + + case "lane.completion.invalidate": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (lane.state !== "completed") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' completion invalidation requires 'completed' state (current: '${lane.state}').`, + }), + ); + } + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "recovery-required", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "recovery-required", + occurredAt: command.invalidatedAt, + resumeState: null, + ...(command.reason !== undefined ? { reason: command.reason } : {}), + }); + } + + case "lane.fail": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if ( + lane.state !== "executing" && + lane.state !== "testing" && + lane.state !== "reviewing" + ) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' can only fail from executing|testing|reviewing (current: '${lane.state}').`, + }), + ); + } + yield* requireAllowedWorkLaneTransition({ + commandType: command.type, + from: lane.state, + to: "failed", + }); + return yield* emitLaneStateChanged({ + command, + laneId: command.laneId, + fromState: lane.state, + toState: "failed", + occurredAt: command.failedAt, + resumeState: null, + ...(command.reason !== undefined ? { reason: command.reason } : {}), + }); + } + + case "lane.task-contract.update": { + yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.updatedAt, + commandId: command.commandId, + })), + type: "lane.task-contract-updated" as const, + payload: { + laneId: command.laneId, + taskContract: command.taskContract, + updatedAt: command.updatedAt, + }, + }; + } + + case "lane.meta.update": { + yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.updatedAt, + commandId: command.commandId, + })), + type: "lane.meta-updated" as const, + payload: { + laneId: command.laneId, + ...(command.title !== undefined ? { title: command.title } : {}), + ...(command.priority !== undefined ? { priority: command.priority } : {}), + ...(command.classification !== undefined + ? { classification: command.classification } + : {}), + ...(command.branch !== undefined ? { branch: command.branch } : {}), + ...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}), + ...(command.baseRef !== undefined ? { baseRef: command.baseRef } : {}), + ...(command.repositoryIdentity !== undefined + ? { repositoryIdentity: command.repositoryIdentity } + : {}), + updatedAt: command.updatedAt, + }, + }; + } + + case "source-truth.preflight.record": { + const lane = yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + if (command.revision.laneId !== command.laneId) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Source-truth revision laneId '${command.revision.laneId}' does not match command laneId '${command.laneId}'.`, + }), + ); + } + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.recordedAt, + commandId: command.commandId, + })), + type: "source-truth.preflight-recorded" as const, + payload: { + laneId: command.laneId, + revision: { + ...command.revision, + supersedesRevisionId: lane.sourceTruthRevisionId, + }, + previousRevisionId: lane.sourceTruthRevisionId, + recordedAt: command.recordedAt, + }, + }; + } + + case "source-truth.conflict.record": { + yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.recordedAt, + commandId: command.commandId, + })), + type: "source-truth.conflict-recorded" as const, + payload: { + laneId: command.laneId, + summary: command.summary, + recordedAt: command.recordedAt, + }, + }; + } + + case "source-truth.refresh.request": { + yield* requireLane({ + readModel, + command, + laneId: command.laneId, + }); + return { + ...(yield* withEventBase({ + aggregateKind: "lane", + aggregateId: command.laneId, + occurredAt: command.requestedAt, + commandId: command.commandId, + })), + type: "source-truth.refresh-requested" as const, + payload: { + laneId: command.laneId, + requestedAt: command.requestedAt, + }, + }; + } + default: { command satisfies never; const fallback = command as never as { type: string }; diff --git a/apps/server/src/orchestration/decider.workLane.test.ts b/apps/server/src/orchestration/decider.workLane.test.ts new file mode 100644 index 000000000000..addd97b68212 --- /dev/null +++ b/apps/server/src/orchestration/decider.workLane.test.ts @@ -0,0 +1,659 @@ +import { + CommandId, + DeliverableId, + EnvironmentId, + PlanRevisionId, + ProjectId, + SourceTruthRevisionId, + WorkLaneId, + type OrchestrationCommand, + type OrchestrationEvent, + type OrchestrationReadModel, + type SourceTruthRevision, + type TaskContract, + type WorkLane, + type WorkLaneState, +} from "@t3tools/contracts"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { decideOrchestrationCommand } from "./decider.ts"; +import { createEmptyReadModel, projectEvent } from "./projector.ts"; + +const NOW = "2026-01-01T00:00:00.000Z"; +const LATER = "2026-01-01T01:00:00.000Z"; +const PROJECT_ID = ProjectId.make("project-1"); +const LANE_ID = WorkLaneId.make("lane-1"); +const ENV_ID = EnvironmentId.make("env-1"); +const WORKTREE = "/tmp/worktrees/lane-1"; + +function makeTaskContract(overrides: Partial = {}): TaskContract { + return { + objective: "Ship F0 work lanes", + constraints: [], + nonGoals: [], + deliverableRequirement: "required", + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: ["edit", "test"], + prohibitedActions: ["force-push"], + completionReportRequired: true, + objectiveDerivation: "PROVEN", + ...overrides, + }; +} + +function makeWorkLane(overrides: Partial = {}): WorkLane { + return { + id: LANE_ID, + projectId: PROJECT_ID, + title: "Lane", + taskContract: makeTaskContract(), + state: "queued", + priority: "normal", + classification: "substantial", + environmentId: ENV_ID, + repositoryIdentity: null, + baseRef: null, + branch: null, + worktreePath: WORKTREE, + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + activePlanRevisionId: null, + acceptanceCriterionIds: [], + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId: null, + importedThreadId: null, + threadIds: [], + legacyExecutorRef: null, + resumeState: null, + createdAt: NOW, + updatedAt: NOW, + completedAt: null, + ...overrides, + }; +} + +function makeSourceTruthRevision( + overrides: Partial & { readonly id?: SourceTruthRevisionId } = {}, +): SourceTruthRevision { + const { id = SourceTruthRevisionId.make("str-1"), ...rest } = overrides; + return { + id, + laneId: LANE_ID, + repositoryIdentity: null, + repositoryRoot: "/tmp/repo", + branch: "main", + detached: false, + headSha: "abc123", + baseSha: "def456", + worktreePath: WORKTREE, + dirty: { fingerprint: "fp", summary: "clean", isDirty: false }, + instructionFiles: [], + manifests: [], + buildTestCandidates: [], + relevantFiles: [], + relevantTests: [], + activeGitOperation: "none", + ownershipOverlap: "exclusive", + canonicalExternalSourceRefs: [], + unknownsThatChangeAction: [], + safeNextAction: "implement", + producedAt: NOW, + producerAssignmentId: null, + producerThreadId: null, + rawOutputArtifactRef: null, + supersededAt: null, + supersedesRevisionId: null, + ...rest, + }; +} + +function makeReadModel(lanes: ReadonlyArray = []): OrchestrationReadModel { + return { + ...createEmptyReadModel(NOW), + projects: [ + { + id: PROJECT_ID, + title: "Project", + workspaceRoot: "/tmp/repo", + defaultModelSelection: null, + scripts: [], + createdAt: NOW, + updatedAt: NOW, + deletedAt: null, + }, + ], + lanes, + }; +} + +type PlannedEvent = Omit; + +function asEvents( + decided: PlannedEvent | ReadonlyArray, +): ReadonlyArray { + return Array.isArray(decided) + ? (decided as ReadonlyArray) + : [decided as PlannedEvent]; +} + +function withSequence(event: PlannedEvent, sequence: number): OrchestrationEvent { + return { ...event, sequence } as OrchestrationEvent; +} + +function payloadOf(event: PlannedEvent | undefined, type: OrchestrationEvent["type"]): T { + expect(event?.type).toBe(type); + if (event?.type !== type) { + throw new Error(`expected ${type}, got ${event?.type}`); + } + return event.payload as T; +} + +const applyCommands = Effect.fn("applyCommands")(function* ({ + commands, + readModel, +}: { + readonly commands: ReadonlyArray; + readonly readModel: OrchestrationReadModel; +}) { + let nextReadModel = readModel; + let nextSequence = readModel.snapshotSequence; + const plannedEvents: Array = []; + + for (const command of commands) { + const decided = yield* decideOrchestrationCommand({ command, readModel: nextReadModel }); + for (const nextEvent of asEvents(decided)) { + plannedEvents.push(nextEvent); + nextSequence += 1; + nextReadModel = yield* projectEvent( + nextReadModel, + withSequence(nextEvent, nextSequence), + ).pipe(Effect.orDie); + } + } + + return { events: plannedEvents, readModel: nextReadModel }; +}); + +function expectInvariant( + error: { readonly _tag: string; readonly detail?: string; readonly commandType?: string }, + detailIncludes?: string, +) { + expect(error._tag).toBe("OrchestrationCommandInvariantError"); + if (detailIncludes !== undefined) { + expect(error.detail).toContain(detailIncludes); + } +} + +it.layer(NodeServices.layer)("work lane decider", (it) => { + it.effect("runs the full happy-path command sequence", () => + Effect.gen(function* () { + const planRevisionId = PlanRevisionId.make("plan-1"); + const deliverableId = DeliverableId.make("del-1"); + const revision = makeSourceTruthRevision(); + + const { events, readModel } = yield* applyCommands({ + readModel: makeReadModel(), + commands: [ + { + type: "lane.create", + commandId: CommandId.make("cmd-create"), + laneId: LANE_ID, + projectId: PROJECT_ID, + title: "Lane", + taskContract: makeTaskContract(), + priority: "normal", + classification: "substantial", + environmentId: ENV_ID, + worktreePath: WORKTREE, + createdAt: NOW, + }, + { + type: "lane.preflight.request", + commandId: CommandId.make("cmd-preflight"), + laneId: LANE_ID, + requestedAt: NOW, + }, + { + type: "lane.orientation.record", + commandId: CommandId.make("cmd-orient"), + laneId: LANE_ID, + recordedAt: NOW, + }, + { + type: "lane.plan.propose", + commandId: CommandId.make("cmd-plan"), + laneId: LANE_ID, + planRevisionId, + proposedAt: NOW, + }, + { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-source-truth"), + laneId: LANE_ID, + revision, + recordedAt: NOW, + }, + { + type: "lane.execution.start", + commandId: CommandId.make("cmd-exec"), + laneId: LANE_ID, + startedAt: NOW, + }, + { + type: "lane.testing.start", + commandId: CommandId.make("cmd-test"), + laneId: LANE_ID, + startedAt: NOW, + }, + { + type: "lane.review.request", + commandId: CommandId.make("cmd-review"), + laneId: LANE_ID, + requestedAt: NOW, + }, + { + type: "lane.deliverable.register", + commandId: CommandId.make("cmd-deliverable"), + laneId: LANE_ID, + deliverableId, + registeredAt: NOW, + }, + ], + }); + + const types = events.map((event) => event.type); + expect(types).toEqual([ + "lane.created", + "lane.state-changed", + "lane.state-changed", + "lane.state-changed", + "lane.plan-proposed", + "source-truth.preflight-recorded", + "lane.state-changed", + "lane.state-changed", + "lane.state-changed", + "lane.state-changed", + "lane.deliverable-registered", + ]); + + const lane = readModel.lanes[0]; + expect(lane?.state).toBe("deliverable-ready"); + expect(lane?.sourceTruthRevisionId).toBe(revision.id); + expect(lane?.deliverableIds).toEqual([deliverableId]); + }), + ); + + it.effect("rejects disallowed transitions including queued→execution.start", () => + Effect.gen(function* () { + const queuedError = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-bad-exec"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "queued", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + ]), + }).pipe(Effect.flip); + expectInvariant(queuedError, "must be in 'planned' state"); + + const orientedToTesting = yield* decideOrchestrationCommand({ + command: { + type: "lane.testing.start", + commandId: CommandId.make("cmd-bad-test"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "oriented" })]), + }).pipe(Effect.flip); + expectInvariant(orientedToTesting, "is not allowed"); + + const cancelledToPreflight = yield* decideOrchestrationCommand({ + command: { + type: "lane.preflight.request", + commandId: CommandId.make("cmd-bad-preflight"), + laneId: LANE_ID, + requestedAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "cancelled" })]), + }).pipe(Effect.flip); + expectInvariant(cancelledToPreflight, "is not allowed"); + + // completion.request always fails — covered in depth below; assert here too. + const completion = yield* decideOrchestrationCommand({ + command: { + type: "lane.completion.request", + commandId: CommandId.make("cmd-completion-disallowed"), + laneId: LANE_ID, + requestedAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "deliverable-ready" })]), + }).pipe(Effect.flip); + expectInvariant(completion, "completion gate reserved until F2"); + }), + ); + + it.effect("refuses execution.start for substantial lanes without a worktree", () => + Effect.gen(function* () { + const error = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-no-worktree"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "planned", + classification: "substantial", + worktreePath: null, + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + ]), + }).pipe(Effect.flip); + expectInvariant(error, "requires a worktree path before execution"); + }), + ); + + it.effect( + "allows bounded-readonly execution without a worktree when source-truth is present", + () => + Effect.gen(function* () { + const decided = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-readonly-exec"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "planned", + classification: "bounded-readonly", + worktreePath: null, + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + ]), + }); + const events = asEvents(decided); + expect(events).toHaveLength(1); + const stateChanged = payloadOf>(events[0], "lane.state-changed"); + expect(stateChanged.fromState).toBe("planned"); + expect(stateChanged.toState).toBe("executing"); + }), + ); + + it.effect("rejects exclusive worktree ownership for a second lane", () => + Effect.gen(function* () { + const otherLaneId = WorkLaneId.make("lane-2"); + const error = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-exclusive"), + laneId: otherLaneId, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "executing", + worktreePath: WORKTREE, + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + makeWorkLane({ + id: otherLaneId, + state: "planned", + worktreePath: WORKTREE, + sourceTruthRevisionId: SourceTruthRevisionId.make("str-2"), + }), + ]), + }).pipe(Effect.flip); + expectInvariant(error, "already owned by lane"); + }), + ); + + it.effect("always rejects completion.request with the F2 message", () => + Effect.gen(function* () { + const states: ReadonlyArray = [ + "queued", + "planned", + "executing", + "deliverable-ready", + "completed", + ]; + for (const state of states) { + const error = yield* decideOrchestrationCommand({ + command: { + type: "lane.completion.request", + commandId: CommandId.make(`cmd-completion-${state}`), + laneId: LANE_ID, + requestedAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state })]), + }).pipe(Effect.flip); + expectInvariant(error, "completion gate reserved until F2"); + } + }), + ); + + it.effect("moves constructed completed lanes to recovery-required on completion.invalidate", () => + Effect.gen(function* () { + const decided = yield* decideOrchestrationCommand({ + command: { + type: "lane.completion.invalidate", + commandId: CommandId.make("cmd-invalidate"), + laneId: LANE_ID, + reason: "false historical completion", + invalidatedAt: LATER, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "completed", + completedAt: NOW, + }), + ]), + }); + const events = asEvents(decided); + expect(events).toHaveLength(1); + const invalidated = payloadOf>(events[0], "lane.state-changed"); + expect(invalidated.fromState).toBe("completed"); + expect(invalidated.toState).toBe("recovery-required"); + expect(invalidated.resumeState).toBeNull(); + expect(invalidated.reason).toBe("false historical completion"); + }), + ); + + it.effect("block/unblock restores resumeState", () => + Effect.gen(function* () { + const blocked = yield* decideOrchestrationCommand({ + command: { + type: "lane.block", + commandId: CommandId.make("cmd-block"), + laneId: LANE_ID, + reason: "waiting on review", + blockedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "executing", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + ]), + }); + const blockEvents = asEvents(blocked); + const blockedEvent = payloadOf>(blockEvents[0], "lane.state-changed"); + expect(blockedEvent.toState).toBe("blocked"); + expect(blockedEvent.resumeState).toBe("executing"); + + const afterBlock = yield* projectEvent( + makeReadModel([ + makeWorkLane({ + state: "executing", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }), + ]), + withSequence(blockEvents[0]!, 1), + ).pipe(Effect.orDie); + expect(afterBlock.lanes[0]?.state).toBe("blocked"); + expect(afterBlock.lanes[0]?.resumeState).toBe("executing"); + + const unblocked = yield* decideOrchestrationCommand({ + command: { + type: "lane.unblock", + commandId: CommandId.make("cmd-unblock"), + laneId: LANE_ID, + unblockedAt: LATER, + }, + readModel: afterBlock, + }); + const unblockEvents = asEvents(unblocked); + const unblockedEvent = payloadOf>(unblockEvents[0], "lane.state-changed"); + expect(unblockedEvent.fromState).toBe("blocked"); + expect(unblockedEvent.toState).toBe("executing"); + expect(unblockedEvent.resumeState).toBeNull(); + + const afterUnblock = yield* projectEvent(afterBlock, withSequence(unblockEvents[0]!, 2)).pipe( + Effect.orDie, + ); + expect(afterUnblock.lanes[0]?.state).toBe("executing"); + expect(afterUnblock.lanes[0]?.resumeState).toBeNull(); + }), + ); + + it.effect("source-truth supersession updates sourceTruthRevisionId", () => + Effect.gen(function* () { + const first = makeSourceTruthRevision({ id: SourceTruthRevisionId.make("str-1") }); + const second = makeSourceTruthRevision({ + id: SourceTruthRevisionId.make("str-2"), + producedAt: LATER, + }); + + const { readModel } = yield* applyCommands({ + readModel: makeReadModel([makeWorkLane({ state: "planned" })]), + commands: [ + { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-st-1"), + laneId: LANE_ID, + revision: first, + recordedAt: NOW, + }, + { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-st-2"), + laneId: LANE_ID, + revision: second, + recordedAt: LATER, + }, + ], + }); + + expect(readModel.lanes[0]?.sourceTruthRevisionId).toBe(second.id); + + const secondDecide = yield* decideOrchestrationCommand({ + command: { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-st-2-check"), + laneId: LANE_ID, + revision: makeSourceTruthRevision({ id: SourceTruthRevisionId.make("str-3") }), + recordedAt: LATER, + }, + readModel: { + ...makeReadModel([ + makeWorkLane({ + state: "planned", + sourceTruthRevisionId: second.id, + }), + ]), + }, + }); + const events = asEvents(secondDecide); + const recorded = payloadOf>(events[0], "source-truth.preflight-recorded"); + expect(recorded.previousRevisionId).toBe(second.id); + expect(recorded.revision.supersedesRevisionId).toBe(second.id); + }), + ); + + it.effect( + "command idempotency is engine/receipt-level — pure decide re-emits events on repeat", + () => + Effect.gen(function* () { + // Pure decider has no commandId ledger. Idempotency lives in the engine + // receipt layer. Calling decide twice with the same logical command still + // produces events (re-emission), which the engine dedupes by commandId. + const readModel = makeReadModel([makeWorkLane({ state: "queued" })]); + const command = { + type: "lane.preflight.request" as const, + commandId: CommandId.make("cmd-idempotent"), + laneId: LANE_ID, + requestedAt: NOW, + }; + const first = asEvents(yield* decideOrchestrationCommand({ command, readModel })); + const second = asEvents(yield* decideOrchestrationCommand({ command, readModel })); + expect(first).toHaveLength(1); + expect(second).toHaveLength(1); + expect(first[0]?.type).toBe("lane.state-changed"); + expect(second[0]?.type).toBe("lane.state-changed"); + }), + ); + + it.effect("cancel, supersede, and recovery.request have continuation paths", () => + Effect.gen(function* () { + const cancelled = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.cancel", + commandId: CommandId.make("cmd-cancel"), + laneId: LANE_ID, + cancelledAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "executing" })]), + }), + ); + expect(payloadOf<{ toState: string }>(cancelled[0], "lane.state-changed").toState).toBe( + "cancelled", + ); + + const superseded = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.supersede", + commandId: CommandId.make("cmd-supersede"), + laneId: LANE_ID, + supersededAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "planned" })]), + }), + ); + expect(payloadOf<{ toState: string }>(superseded[0], "lane.state-changed").toState).toBe( + "superseded", + ); + + const recovered = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.recovery.request", + commandId: CommandId.make("cmd-recovery"), + laneId: LANE_ID, + requestedAt: NOW, + }, + readModel: makeReadModel([makeWorkLane({ state: "recovery-required" })]), + }), + ); + expect(payloadOf<{ toState: string }>(recovered[0], "lane.state-changed").toState).toBe( + "preflight", + ); + }), + ); +}); diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts index 659665e47b50..80fb5a87a33c 100644 --- a/apps/server/src/orchestration/http.ts +++ b/apps/server/src/orchestration/http.ts @@ -73,6 +73,22 @@ export const orchestrationHttpApiLayer = HttpApiBuilder.group( return projectThreadDetailSnapshot(snapshot.value); }), ) + .handle( + "laneSnapshot", + Effect.fn("environment.orchestration.laneSnapshot")(function* (args) { + yield* annotateEnvironmentRequest(args.endpoint.name); + yield* requireEnvironmentScope(AuthOrchestrationReadScope); + const snapshot = yield* projectionSnapshotQuery.getLaneDetail(args.params.laneId).pipe( + Effect.catch((cause) => + failEnvironmentInternal("orchestration_lane_snapshot_failed", cause), + ), + ); + if (Option.isNone(snapshot)) { + return yield* failEnvironmentNotFound("lane_not_found"); + } + return snapshot.value; + }), + ) .handle( "dispatch", Effect.fn("environment.orchestration.dispatch")(function* (args) { diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index 0504cb36f9a7..11361a8e0c7c 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -1,4 +1,10 @@ -import type { OrchestrationEvent, OrchestrationReadModel, ThreadId } from "@t3tools/contracts"; +import type { + OrchestrationEvent, + OrchestrationReadModel, + ThreadId, + WorkLane, + WorkLaneId, +} from "@t3tools/contracts"; import { OrchestrationCheckpointSummary, OrchestrationMessage, @@ -10,10 +16,19 @@ import * as Schema from "effect/Schema"; import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts"; import { + LaneCreatedPayload, + LaneDeliverableRegisteredPayload, + LaneImportedPayload, + LaneMetaUpdatedPayload, + LanePlanActivatedPayload, + LanePlanProposedPayload, + LaneStateChangedPayload, + LaneTaskContractUpdatedPayload, MessageSentPayloadSchema, ProjectCreatedPayload, ProjectDeletedPayload, ProjectMetaUpdatedPayload, + SourceTruthPreflightRecordedPayload, ThreadActivityAppendedPayload, ThreadArchivedPayload, ThreadCreatedPayload, @@ -33,6 +48,7 @@ import { } from "./Schemas.ts"; type ThreadPatch = Partial>; +type LanePatch = Partial>; const MAX_THREAD_MESSAGES = 2_000; const MAX_THREAD_CHECKPOINTS = 500; @@ -73,6 +89,14 @@ function updateThread( return threads.map((thread) => (thread.id === threadId ? { ...thread, ...patch } : thread)); } +function updateLane( + lanes: ReadonlyArray, + laneId: WorkLaneId, + patch: LanePatch, +): WorkLane[] { + return lanes.map((lane) => (lane.id === laneId ? { ...lane, ...patch } : lane)); +} + function decodeForEvent( schema: Schema.Decoder, value: unknown, @@ -187,6 +211,7 @@ export function createEmptyReadModel(nowIso: string): OrchestrationReadModel { snapshotSequence: 0, projects: [], threads: [], + lanes: [], updatedAt: nowIso, }; } @@ -748,6 +773,154 @@ export function projectEvent( }), ); + case "lane.created": + return decodeForEvent(LaneCreatedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => { + const existing = nextBase.lanes.find((entry) => entry.id === payload.lane.id); + return { + ...nextBase, + lanes: existing + ? nextBase.lanes.map((entry) => + entry.id === payload.lane.id ? payload.lane : entry, + ) + : [...nextBase.lanes, payload.lane], + }; + }), + ); + + case "lane.imported": + return decodeForEvent(LaneImportedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => { + const existing = nextBase.lanes.find((entry) => entry.id === payload.lane.id); + return { + ...nextBase, + lanes: existing + ? nextBase.lanes.map((entry) => + entry.id === payload.lane.id ? payload.lane : entry, + ) + : [...nextBase.lanes, payload.lane], + }; + }), + ); + + case "lane.state-changed": + return decodeForEvent(LaneStateChangedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + state: payload.toState, + resumeState: payload.resumeState, + updatedAt: payload.updatedAt, + completedAt: + payload.toState === "completed" + ? payload.updatedAt + : payload.fromState === "completed" + ? null + : (nextBase.lanes.find((lane) => lane.id === payload.laneId)?.completedAt ?? + null), + }), + })), + ); + + case "lane.task-contract-updated": + return decodeForEvent( + LaneTaskContractUpdatedPayload, + event.payload, + event.type, + "payload", + ).pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + taskContract: payload.taskContract, + updatedAt: payload.updatedAt, + }), + })), + ); + + case "lane.meta-updated": + return decodeForEvent(LaneMetaUpdatedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + ...(payload.title !== undefined ? { title: payload.title } : {}), + ...(payload.priority !== undefined ? { priority: payload.priority } : {}), + ...(payload.classification !== undefined + ? { classification: payload.classification } + : {}), + ...(payload.branch !== undefined ? { branch: payload.branch } : {}), + ...(payload.worktreePath !== undefined ? { worktreePath: payload.worktreePath } : {}), + ...(payload.baseRef !== undefined ? { baseRef: payload.baseRef } : {}), + ...(payload.repositoryIdentity !== undefined + ? { repositoryIdentity: payload.repositoryIdentity } + : {}), + updatedAt: payload.updatedAt, + }), + })), + ); + + case "lane.plan-proposed": + return decodeForEvent(LanePlanProposedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + updatedAt: payload.proposedAt, + }), + })), + ); + + case "lane.plan-activated": + return decodeForEvent(LanePlanActivatedPayload, event.payload, event.type, "payload").pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + activePlanRevisionId: payload.planRevisionId, + updatedAt: payload.updatedAt, + }), + })), + ); + + case "lane.deliverable-registered": + return decodeForEvent( + LaneDeliverableRegisteredPayload, + event.payload, + event.type, + "payload", + ).pipe( + Effect.map((payload) => { + const lane = nextBase.lanes.find((entry) => entry.id === payload.laneId); + if (!lane) { + return nextBase; + } + const deliverableIds = lane.deliverableIds.includes(payload.deliverableId) + ? lane.deliverableIds + : [...lane.deliverableIds, payload.deliverableId]; + return { + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + deliverableIds, + updatedAt: payload.updatedAt, + }), + }; + }), + ); + + case "source-truth.preflight-recorded": + return decodeForEvent( + SourceTruthPreflightRecordedPayload, + event.payload, + event.type, + "payload", + ).pipe( + Effect.map((payload) => ({ + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + sourceTruthRevisionId: payload.revision.id, + updatedAt: payload.recordedAt, + }), + })), + ); + default: return Effect.succeed(nextBase); } diff --git a/apps/server/src/orchestration/projector.workLane.test.ts b/apps/server/src/orchestration/projector.workLane.test.ts new file mode 100644 index 000000000000..aad35f6ce05e --- /dev/null +++ b/apps/server/src/orchestration/projector.workLane.test.ts @@ -0,0 +1,230 @@ +import { + CommandId, + EnvironmentId, + EventId, + ProjectId, + SourceTruthRevisionId, + WorkLaneId, + toWorkLaneShell, + type OrchestrationEvent, + type SourceTruthRevision, + type TaskContract, + type WorkLane, +} from "@t3tools/contracts"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { createEmptyReadModel, projectEvent } from "./projector.ts"; + +const NOW = "2026-01-01T00:00:00.000Z"; +const LATER = "2026-01-01T01:00:00.000Z"; +const PROJECT_ID = ProjectId.make("project-1"); +const LANE_ID = WorkLaneId.make("lane-1"); +const ENV_ID = EnvironmentId.make("env-1"); + +function makeTaskContract(): TaskContract { + return { + objective: "Ship F0 work lanes", + constraints: [], + nonGoals: [], + deliverableRequirement: "required", + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: ["edit", "test"], + prohibitedActions: ["force-push"], + completionReportRequired: true, + objectiveDerivation: "PROVEN", + }; +} + +function makeWorkLane(overrides: Partial = {}): WorkLane { + return { + id: LANE_ID, + projectId: PROJECT_ID, + title: "Lane", + taskContract: makeTaskContract(), + state: "queued", + priority: "normal", + classification: "substantial", + environmentId: ENV_ID, + repositoryIdentity: null, + baseRef: null, + branch: null, + worktreePath: "/tmp/worktrees/lane-1", + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + activePlanRevisionId: null, + acceptanceCriterionIds: [], + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId: null, + importedThreadId: null, + threadIds: [], + legacyExecutorRef: null, + resumeState: null, + createdAt: NOW, + updatedAt: NOW, + completedAt: null, + ...overrides, + }; +} + +function makeSourceTruthRevision( + overrides: Partial = {}, +): SourceTruthRevision { + return { + id: SourceTruthRevisionId.make("str-1"), + laneId: LANE_ID, + repositoryIdentity: null, + repositoryRoot: "/tmp/repo", + branch: "main", + detached: false, + headSha: "abc123", + baseSha: "def456", + worktreePath: "/tmp/worktrees/lane-1", + dirty: { fingerprint: "fp", summary: "clean", isDirty: false }, + instructionFiles: [], + manifests: [], + buildTestCandidates: [], + relevantFiles: [], + relevantTests: [], + activeGitOperation: "none", + ownershipOverlap: "exclusive", + canonicalExternalSourceRefs: [], + unknownsThatChangeAction: [], + safeNextAction: "implement", + producedAt: NOW, + producerAssignmentId: null, + producerThreadId: null, + rawOutputArtifactRef: null, + supersededAt: null, + supersedesRevisionId: null, + ...overrides, + }; +} + +function makeLaneEvent(input: { + readonly sequence: number; + readonly type: OrchestrationEvent["type"]; + readonly payload: unknown; + readonly occurredAt?: string; +}): OrchestrationEvent { + return { + sequence: input.sequence, + eventId: EventId.make(`event-${input.sequence}`), + type: input.type, + aggregateKind: "lane", + aggregateId: LANE_ID, + occurredAt: input.occurredAt ?? NOW, + commandId: CommandId.make(`command-${input.sequence}`), + causationEventId: null, + correlationId: null, + metadata: {}, + payload: input.payload as never, + } as OrchestrationEvent; +} + +it.effect("replays lane.created + state-changed + preflight-recorded deterministically", () => + Effect.gen(function* () { + const lane = makeWorkLane(); + const revision = makeSourceTruthRevision(); + + const events = [ + makeLaneEvent({ + sequence: 1, + type: "lane.created", + payload: { lane, acceptanceCriteria: [] }, + }), + makeLaneEvent({ + sequence: 2, + type: "lane.state-changed", + payload: { + laneId: LANE_ID, + fromState: "queued", + toState: "preflight", + resumeState: null, + updatedAt: LATER, + }, + occurredAt: LATER, + }), + makeLaneEvent({ + sequence: 3, + type: "source-truth.preflight-recorded", + payload: { + laneId: LANE_ID, + revision, + previousRevisionId: null, + recordedAt: LATER, + }, + occurredAt: LATER, + }), + ] as const; + + const replay = (initial = createEmptyReadModel(NOW)) => + Effect.gen(function* () { + let model = initial; + for (const event of events) { + model = yield* projectEvent(model, event); + } + return model; + }); + const firstPass = yield* replay(); + const secondPass = yield* replay(); + + expect(firstPass).toEqual(secondPass); + expect(firstPass.snapshotSequence).toBe(3); + expect(firstPass.lanes).toHaveLength(1); + expect(firstPass.lanes[0]?.state).toBe("preflight"); + expect(firstPass.lanes[0]?.sourceTruthRevisionId).toBe(revision.id); + expect(firstPass.lanes[0]?.updatedAt).toBe(LATER); + }), +); + +it.effect("toWorkLaneShell keeps shell compact without taskContract", () => + Effect.gen(function* () { + const lane = makeWorkLane({ + state: "preflight", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + }); + const projected = yield* projectEvent( + createEmptyReadModel(NOW), + makeLaneEvent({ + sequence: 1, + type: "lane.created", + payload: { lane, acceptanceCriteria: [] }, + }), + ); + const projectedLane = projected.lanes[0]; + expect(projectedLane).toBeDefined(); + + const shell = toWorkLaneShell(projectedLane!); + expect(shell.id).toBe(LANE_ID); + expect(shell.objectiveSummary).toBe(lane.taskContract.objective); + expect(shell.sourceTruthRevisionId).toBe(lane.sourceTruthRevisionId); + expect("taskContract" in shell).toBe(false); + expect(Object.keys(shell).sort()).toEqual( + [ + "id", + "projectId", + "title", + "state", + "priority", + "classification", + "environmentId", + "branch", + "worktreePath", + "sourceTruthRevisionId", + "sourceTruthSummary", + "primaryThreadId", + "importedThreadId", + "objectiveSummary", + "createdAt", + "updatedAt", + "completedAt", + ].sort(), + ); + }), +); diff --git a/apps/server/src/orchestration/workLaneTransitions.ts b/apps/server/src/orchestration/workLaneTransitions.ts new file mode 100644 index 000000000000..816d54c87f14 --- /dev/null +++ b/apps/server/src/orchestration/workLaneTransitions.ts @@ -0,0 +1,45 @@ +import type { OrchestrationCommand, WorkLaneState } from "@t3tools/contracts"; +import { + isAllowedWorkLaneTransition, + isWorkLaneTerminalState, + isWorkLaneWorktreeOwningState, + WORK_LANE_NORMAL_TRANSITIONS, + WORK_LANE_TERMINAL_STATES, + WORK_LANE_WORKTREE_OWNING_STATES, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import { OrchestrationCommandInvariantError } from "./Errors.ts"; + +export { + isAllowedWorkLaneTransition, + isWorkLaneTerminalState, + isWorkLaneWorktreeOwningState, + WORK_LANE_NORMAL_TRANSITIONS, + WORK_LANE_TERMINAL_STATES, + WORK_LANE_WORKTREE_OWNING_STATES, +}; + +/** + * Supersede is a terminal exit that is not listed in the normal matrix — + * allowed from any non-terminal state. + */ +export function isAllowedWorkLaneSupersede(from: WorkLaneState): boolean { + return !isWorkLaneTerminalState(from); +} + +export function requireAllowedWorkLaneTransition(input: { + readonly commandType: OrchestrationCommand["type"]; + readonly from: WorkLaneState; + readonly to: WorkLaneState; +}): Effect.Effect { + if (isAllowedWorkLaneTransition(input.from, input.to)) { + return Effect.void; + } + return Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: input.commandType, + detail: `Work lane transition from '${input.from}' to '${input.to}' is not allowed.`, + }), + ); +} diff --git a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts index 18d0e9aa578b..fd8f57147f8d 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts @@ -10,6 +10,7 @@ import { OrchestrationEventType, ProjectId, ThreadId, + WorkLaneId, } from "@t3tools/contracts"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; @@ -35,7 +36,7 @@ const EventMetadataFromJsonString = Schema.fromJsonString(OrchestrationEventMeta const AppendEventRequestSchema = Schema.Struct({ eventId: EventId, aggregateKind: OrchestrationAggregateKind, - streamId: Schema.Union([ProjectId, ThreadId]), + streamId: Schema.Union([ProjectId, ThreadId, WorkLaneId]), type: OrchestrationEventType, causationEventId: Schema.NullOr(EventId), correlationId: Schema.NullOr(CommandId), @@ -51,7 +52,7 @@ const OrchestrationEventPersistedRowSchema = Schema.Struct({ eventId: EventId, type: OrchestrationEventType, aggregateKind: OrchestrationAggregateKind, - aggregateId: Schema.Union([ProjectId, ThreadId]), + aggregateId: Schema.Union([ProjectId, ThreadId, WorkLaneId]), occurredAt: IsoDateTime, commandId: Schema.NullOr(CommandId), causationEventId: Schema.NullOr(EventId), diff --git a/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts b/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts new file mode 100644 index 000000000000..d9b568063bb0 --- /dev/null +++ b/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts @@ -0,0 +1,139 @@ +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as SqlSchema from "effect/unstable/sql/SqlSchema"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Struct from "effect/Struct"; + +import { AcceptanceCriterion } from "@t3tools/contracts"; +import { toPersistenceSqlError } from "../Errors.ts"; +import { + GetProjectionLaneAcceptanceCriterionInput, + ListProjectionLaneAcceptanceCriteriaByLaneInput, + ProjectionLaneAcceptanceCriterion, + ProjectionLaneAcceptanceCriterionRepository, + type ProjectionLaneAcceptanceCriterionRepositoryShape, +} from "../Services/ProjectionLaneAcceptanceCriteria.ts"; + +const ProjectionLaneAcceptanceCriterionDbRow = ProjectionLaneAcceptanceCriterion.mapFields( + Struct.assign({ + criterion: Schema.fromJsonString(AcceptanceCriterion), + }), +); +type ProjectionLaneAcceptanceCriterionDbRow = typeof ProjectionLaneAcceptanceCriterionDbRow.Type; + +const makeProjectionLaneAcceptanceCriterionRepository = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + const upsertProjectionLaneAcceptanceCriterionRow = SqlSchema.void({ + Request: ProjectionLaneAcceptanceCriterion, + execute: (row) => + sql` + INSERT INTO projection_lane_acceptance_criteria ( + id, + lane_id, + criterion_json, + last_sequence + ) + VALUES ( + ${row.id}, + ${row.laneId}, + ${JSON.stringify(row.criterion)}, + ${row.lastSequence} + ) + ON CONFLICT (id) + DO UPDATE SET + lane_id = excluded.lane_id, + criterion_json = excluded.criterion_json, + last_sequence = excluded.last_sequence + `, + }); + + const getProjectionLaneAcceptanceCriterionRow = SqlSchema.findOneOption({ + Request: GetProjectionLaneAcceptanceCriterionInput, + Result: ProjectionLaneAcceptanceCriterionDbRow, + execute: ({ id }) => + sql` + SELECT + id, + lane_id AS "laneId", + criterion_json AS "criterion", + last_sequence AS "lastSequence" + FROM projection_lane_acceptance_criteria + WHERE id = ${id} + `, + }); + + const listProjectionLaneAcceptanceCriterionRowsByLane = SqlSchema.findAll({ + Request: ListProjectionLaneAcceptanceCriteriaByLaneInput, + Result: ProjectionLaneAcceptanceCriterionDbRow, + execute: ({ laneId }) => + sql` + SELECT + id, + lane_id AS "laneId", + criterion_json AS "criterion", + last_sequence AS "lastSequence" + FROM projection_lane_acceptance_criteria + WHERE lane_id = ${laneId} + ORDER BY id ASC + `, + }); + + const listProjectionLaneAcceptanceCriterionRows = SqlSchema.findAll({ + Request: Schema.Void, + Result: ProjectionLaneAcceptanceCriterionDbRow, + execute: () => + sql` + SELECT + id, + lane_id AS "laneId", + criterion_json AS "criterion", + last_sequence AS "lastSequence" + FROM projection_lane_acceptance_criteria + ORDER BY lane_id ASC, id ASC + `, + }); + + const upsert: ProjectionLaneAcceptanceCriterionRepositoryShape["upsert"] = (row) => + upsertProjectionLaneAcceptanceCriterionRow(row).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionLaneAcceptanceCriterionRepository.upsert:query"), + ), + ); + + const getById: ProjectionLaneAcceptanceCriterionRepositoryShape["getById"] = (input) => + getProjectionLaneAcceptanceCriterionRow(input).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionLaneAcceptanceCriterionRepository.getById:query"), + ), + ); + + const listByLaneId: ProjectionLaneAcceptanceCriterionRepositoryShape["listByLaneId"] = ( + input, + ) => + listProjectionLaneAcceptanceCriterionRowsByLane(input).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionLaneAcceptanceCriterionRepository.listByLaneId:query"), + ), + ); + + const listAll: ProjectionLaneAcceptanceCriterionRepositoryShape["listAll"] = () => + listProjectionLaneAcceptanceCriterionRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionLaneAcceptanceCriterionRepository.listAll:query"), + ), + ); + + return { + upsert, + getById, + listByLaneId, + listAll, + } satisfies ProjectionLaneAcceptanceCriterionRepositoryShape; +}); + +export const ProjectionLaneAcceptanceCriterionRepositoryLive = Layer.effect( + ProjectionLaneAcceptanceCriterionRepository, + makeProjectionLaneAcceptanceCriterionRepository, +); diff --git a/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts b/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts new file mode 100644 index 000000000000..497f7c6f2811 --- /dev/null +++ b/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts @@ -0,0 +1,149 @@ +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as SqlSchema from "effect/unstable/sql/SqlSchema"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Struct from "effect/Struct"; + +import { SourceTruthRevision } from "@t3tools/contracts"; +import { toPersistenceSqlError } from "../Errors.ts"; +import { + GetProjectionSourceTruthRevisionInput, + ListProjectionSourceTruthRevisionsByLaneInput, + ProjectionSourceTruthRevision, + ProjectionSourceTruthRevisionRepository, + type ProjectionSourceTruthRevisionRepositoryShape, +} from "../Services/ProjectionSourceTruthRevisions.ts"; + +const ProjectionSourceTruthRevisionDbRow = ProjectionSourceTruthRevision.mapFields( + Struct.assign({ + revision: Schema.fromJsonString(SourceTruthRevision), + }), +); +type ProjectionSourceTruthRevisionDbRow = typeof ProjectionSourceTruthRevisionDbRow.Type; + +const makeProjectionSourceTruthRevisionRepository = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + const upsertProjectionSourceTruthRevisionRow = SqlSchema.void({ + Request: ProjectionSourceTruthRevision, + execute: (row) => + sql` + INSERT INTO projection_source_truth_revisions ( + id, + lane_id, + revision_json, + produced_at, + superseded_at, + last_sequence + ) + VALUES ( + ${row.id}, + ${row.laneId}, + ${JSON.stringify(row.revision)}, + ${row.producedAt}, + ${row.supersededAt}, + ${row.lastSequence} + ) + ON CONFLICT (id) + DO UPDATE SET + lane_id = excluded.lane_id, + revision_json = excluded.revision_json, + produced_at = excluded.produced_at, + superseded_at = excluded.superseded_at, + last_sequence = excluded.last_sequence + `, + }); + + const getProjectionSourceTruthRevisionRow = SqlSchema.findOneOption({ + Request: GetProjectionSourceTruthRevisionInput, + Result: ProjectionSourceTruthRevisionDbRow, + execute: ({ id }) => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt", + last_sequence AS "lastSequence" + FROM projection_source_truth_revisions + WHERE id = ${id} + `, + }); + + const listProjectionSourceTruthRevisionRowsByLane = SqlSchema.findAll({ + Request: ListProjectionSourceTruthRevisionsByLaneInput, + Result: ProjectionSourceTruthRevisionDbRow, + execute: ({ laneId }) => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt", + last_sequence AS "lastSequence" + FROM projection_source_truth_revisions + WHERE lane_id = ${laneId} + ORDER BY produced_at ASC, id ASC + `, + }); + + const listProjectionSourceTruthRevisionRows = SqlSchema.findAll({ + Request: Schema.Void, + Result: ProjectionSourceTruthRevisionDbRow, + execute: () => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt", + last_sequence AS "lastSequence" + FROM projection_source_truth_revisions + ORDER BY produced_at ASC, id ASC + `, + }); + + const upsert: ProjectionSourceTruthRevisionRepositoryShape["upsert"] = (row) => + upsertProjectionSourceTruthRevisionRow(row).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionSourceTruthRevisionRepository.upsert:query"), + ), + ); + + const getById: ProjectionSourceTruthRevisionRepositoryShape["getById"] = (input) => + getProjectionSourceTruthRevisionRow(input).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionSourceTruthRevisionRepository.getById:query"), + ), + ); + + const listByLaneId: ProjectionSourceTruthRevisionRepositoryShape["listByLaneId"] = (input) => + listProjectionSourceTruthRevisionRowsByLane(input).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionSourceTruthRevisionRepository.listByLaneId:query"), + ), + ); + + const listAll: ProjectionSourceTruthRevisionRepositoryShape["listAll"] = () => + listProjectionSourceTruthRevisionRows(undefined).pipe( + Effect.mapError( + toPersistenceSqlError("ProjectionSourceTruthRevisionRepository.listAll:query"), + ), + ); + + return { + upsert, + getById, + listByLaneId, + listAll, + } satisfies ProjectionSourceTruthRevisionRepositoryShape; +}); + +export const ProjectionSourceTruthRevisionRepositoryLive = Layer.effect( + ProjectionSourceTruthRevisionRepository, + makeProjectionSourceTruthRevisionRepository, +); diff --git a/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts b/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts new file mode 100644 index 000000000000..94f62dd20391 --- /dev/null +++ b/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts @@ -0,0 +1,176 @@ +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as SqlSchema from "effect/unstable/sql/SqlSchema"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import * as Struct from "effect/Struct"; + +import { WorkLane } from "@t3tools/contracts"; +import { toPersistenceSqlError } from "../Errors.ts"; +import { + GetProjectionWorkLaneInput, + ProjectionWorkLane, + ProjectionWorkLaneRepository, + type ProjectionWorkLaneRepositoryShape, +} from "../Services/ProjectionWorkLanes.ts"; + +const ProjectionWorkLaneDbRow = ProjectionWorkLane.mapFields( + Struct.assign({ + lane: Schema.fromJsonString(WorkLane), + }), +); +type ProjectionWorkLaneDbRow = typeof ProjectionWorkLaneDbRow.Type; + +const makeProjectionWorkLaneRepository = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + const upsertProjectionWorkLaneRow = SqlSchema.void({ + Request: ProjectionWorkLane, + execute: (row) => + sql` + INSERT INTO projection_work_lanes ( + id, + project_id, + title, + state, + priority, + classification, + environment_id, + branch, + worktree_path, + source_truth_revision_id, + primary_thread_id, + imported_thread_id, + objective_summary, + lane_json, + created_at, + updated_at, + completed_at, + last_sequence + ) + VALUES ( + ${row.id}, + ${row.projectId}, + ${row.title}, + ${row.state}, + ${row.priority}, + ${row.classification}, + ${row.environmentId}, + ${row.branch}, + ${row.worktreePath}, + ${row.sourceTruthRevisionId}, + ${row.primaryThreadId}, + ${row.importedThreadId}, + ${row.objectiveSummary}, + ${JSON.stringify(row.lane)}, + ${row.createdAt}, + ${row.updatedAt}, + ${row.completedAt}, + ${row.lastSequence} + ) + ON CONFLICT (id) + DO UPDATE SET + project_id = excluded.project_id, + title = excluded.title, + state = excluded.state, + priority = excluded.priority, + classification = excluded.classification, + environment_id = excluded.environment_id, + branch = excluded.branch, + worktree_path = excluded.worktree_path, + source_truth_revision_id = excluded.source_truth_revision_id, + primary_thread_id = excluded.primary_thread_id, + imported_thread_id = excluded.imported_thread_id, + objective_summary = excluded.objective_summary, + lane_json = excluded.lane_json, + created_at = excluded.created_at, + updated_at = excluded.updated_at, + completed_at = excluded.completed_at, + last_sequence = excluded.last_sequence + `, + }); + + const getProjectionWorkLaneRow = SqlSchema.findOneOption({ + Request: GetProjectionWorkLaneInput, + Result: ProjectionWorkLaneDbRow, + execute: ({ id }) => + sql` + SELECT + id, + project_id AS "projectId", + title, + state, + priority, + classification, + environment_id AS "environmentId", + branch, + worktree_path AS "worktreePath", + source_truth_revision_id AS "sourceTruthRevisionId", + primary_thread_id AS "primaryThreadId", + imported_thread_id AS "importedThreadId", + objective_summary AS "objectiveSummary", + lane_json AS "lane", + created_at AS "createdAt", + updated_at AS "updatedAt", + completed_at AS "completedAt", + last_sequence AS "lastSequence" + FROM projection_work_lanes + WHERE id = ${id} + `, + }); + + const listProjectionWorkLaneRows = SqlSchema.findAll({ + Request: Schema.Void, + Result: ProjectionWorkLaneDbRow, + execute: () => + sql` + SELECT + id, + project_id AS "projectId", + title, + state, + priority, + classification, + environment_id AS "environmentId", + branch, + worktree_path AS "worktreePath", + source_truth_revision_id AS "sourceTruthRevisionId", + primary_thread_id AS "primaryThreadId", + imported_thread_id AS "importedThreadId", + objective_summary AS "objectiveSummary", + lane_json AS "lane", + created_at AS "createdAt", + updated_at AS "updatedAt", + completed_at AS "completedAt", + last_sequence AS "lastSequence" + FROM projection_work_lanes + ORDER BY created_at ASC, id ASC + `, + }); + + const upsert: ProjectionWorkLaneRepositoryShape["upsert"] = (row) => + upsertProjectionWorkLaneRow(row).pipe( + Effect.mapError(toPersistenceSqlError("ProjectionWorkLaneRepository.upsert:query")), + ); + + const getById: ProjectionWorkLaneRepositoryShape["getById"] = (input) => + getProjectionWorkLaneRow(input).pipe( + Effect.mapError(toPersistenceSqlError("ProjectionWorkLaneRepository.getById:query")), + ); + + const listAll: ProjectionWorkLaneRepositoryShape["listAll"] = () => + listProjectionWorkLaneRows(undefined).pipe( + Effect.mapError(toPersistenceSqlError("ProjectionWorkLaneRepository.listAll:query")), + ); + + return { + upsert, + getById, + listAll, + } satisfies ProjectionWorkLaneRepositoryShape; +}); + +export const ProjectionWorkLaneRepositoryLive = Layer.effect( + ProjectionWorkLaneRepository, + makeProjectionWorkLaneRepository, +); diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index d25895671a91..61d92391227f 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -47,6 +47,7 @@ import Migration0031 from "./Migrations/031_AuthAuthorizationScopes.ts"; import Migration0032 from "./Migrations/032_AuthPairingProofKeyThumbprint.ts"; import Migration0033 from "./Migrations/033_ProjectionThreadsSettled.ts"; import Migration0034 from "./Migrations/034_ProjectionThreadsSnoozed.ts"; +import Migration0035 from "./Migrations/035_WorkLanesAndSourceTruth.ts"; /** * Migration loader with all migrations defined inline. @@ -93,6 +94,7 @@ export const migrationEntries = [ [32, "AuthPairingProofKeyThumbprint", Migration0032], [33, "ProjectionThreadsSettled", Migration0033], [34, "ProjectionThreadsSnoozed", Migration0034], + [35, "WorkLanesAndSourceTruth", Migration0035], ] as const; export const makeMigrationLoader = (throughId?: number) => diff --git a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts new file mode 100644 index 000000000000..782b94db37c6 --- /dev/null +++ b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts @@ -0,0 +1,319 @@ +// @effect-diagnostics preferSchemaOverJson:off +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const emptyLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const importLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +const insertProject = (sql: SqlClient.SqlClient, projectId: string) => sql` + INSERT INTO projection_projects ( + project_id, + title, + workspace_root, + default_model_selection_json, + scripts_json, + created_at, + updated_at, + deleted_at + ) + VALUES ( + ${projectId}, + 'Project', + '/tmp/project', + NULL, + '[]', + '2026-07-30T00:00:00.000Z', + '2026-07-30T00:00:00.000Z', + NULL + ) +`; + +const insertThread = ( + sql: SqlClient.SqlClient, + input: { + readonly threadId: string; + readonly projectId: string; + readonly title: string; + readonly archivedAt?: string | null; + }, +) => sql` + INSERT INTO projection_threads ( + thread_id, + project_id, + title, + model_selection_json, + runtime_mode, + interaction_mode, + branch, + worktree_path, + latest_turn_id, + created_at, + updated_at, + archived_at, + settled_override, + settled_at, + snoozed_until, + snoozed_at, + latest_user_message_at, + pending_approval_count, + pending_user_input_count, + has_actionable_proposed_plan, + deleted_at + ) + VALUES ( + ${input.threadId}, + ${input.projectId}, + ${input.title}, + '{"provider":"codex","model":"gpt-5.4"}', + 'full-access', + 'default', + 'feature/import', + '/tmp/worktree', + NULL, + '2026-07-30T00:00:00.000Z', + '2026-07-30T01:00:00.000Z', + ${input.archivedAt ?? null}, + NULL, + NULL, + NULL, + NULL, + NULL, + 0, + 0, + 0, + NULL + ) +`; + +const insertSession = ( + sql: SqlClient.SqlClient, + input: { + readonly threadId: string; + readonly status: string; + }, +) => sql` + INSERT INTO projection_thread_sessions ( + thread_id, + status, + provider_name, + provider_session_id, + provider_thread_id, + active_turn_id, + last_error, + updated_at, + runtime_mode, + provider_instance_id + ) + VALUES ( + ${input.threadId}, + ${input.status}, + 'codex', + 'provider-session-1', + 'provider-thread-1', + NULL, + NULL, + '2026-07-30T01:00:00.000Z', + 'full-access', + NULL + ) +`; + +const insertUserMessage = ( + sql: SqlClient.SqlClient, + input: { + readonly messageId: string; + readonly threadId: string; + readonly text: string; + readonly createdAt: string; + }, +) => sql` + INSERT INTO projection_thread_messages ( + message_id, + thread_id, + turn_id, + role, + text, + attachments_json, + is_streaming, + created_at, + updated_at + ) + VALUES ( + ${input.messageId}, + ${input.threadId}, + NULL, + 'user', + ${input.text}, + NULL, + 0, + ${input.createdAt}, + ${input.createdAt} + ) +`; + +emptyLayer("035_WorkLanesAndSourceTruth empty", (it) => { + it.effect("creates empty projection tables when there are no threads", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 34 }); + yield* runMigrations({ toMigrationInclusive: 35 }); + + const tables = yield* sql<{ readonly name: string }>` + SELECT name + FROM sqlite_master + WHERE type = 'table' + AND name IN ( + 'projection_work_lanes', + 'projection_source_truth_revisions', + 'projection_lane_acceptance_criteria' + ) + ORDER BY name + `; + assert.deepStrictEqual( + tables.map((row) => row.name), + [ + "projection_lane_acceptance_criteria", + "projection_source_truth_revisions", + "projection_work_lanes", + ], + ); + + const lanes = yield* sql`SELECT id FROM projection_work_lanes`; + assert.strictEqual(lanes.length, 0); + + const events = yield* sql` + SELECT event_id + FROM orchestration_events + WHERE event_type = 'lane.imported' + `; + assert.strictEqual(events.length, 0); + }), + ); +}); + +importLayer("035_WorkLanesAndSourceTruth import", (it) => { + it.effect( + "imports idle→queued, active→recovery-required, archived→queued; never completed; idempotent", + () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* runMigrations({ toMigrationInclusive: 34 }); + + yield* insertProject(sql, "project-1"); + yield* insertThread(sql, { + threadId: "thread-idle", + projectId: "project-1", + title: "Idle thread", + }); + yield* insertSession(sql, { threadId: "thread-idle", status: "idle" }); + yield* insertUserMessage(sql, { + messageId: "msg-1", + threadId: "thread-idle", + text: "Please fix the bug", + createdAt: "2026-07-30T00:30:00.000Z", + }); + yield* insertUserMessage(sql, { + messageId: "msg-2", + threadId: "thread-idle", + text: "Later message", + createdAt: "2026-07-30T00:45:00.000Z", + }); + + yield* insertThread(sql, { + threadId: "thread-running", + projectId: "project-1", + title: "Running thread", + }); + yield* insertSession(sql, { threadId: "thread-running", status: "running" }); + + yield* insertThread(sql, { + threadId: "thread-archived", + projectId: "project-1", + title: "Archived thread", + archivedAt: "2026-07-29T00:00:00.000Z", + }); + + yield* runMigrations({ toMigrationInclusive: 35 }); + + const lanes = yield* sql<{ + readonly id: string; + readonly state: string; + readonly objective_summary: string; + readonly completed_at: string | null; + readonly lane_json: string; + }>` + SELECT id, state, objective_summary, completed_at, lane_json + FROM projection_work_lanes + ORDER BY id ASC + `; + assert.strictEqual(lanes.length, 3); + + const idle = lanes.find((row) => row.id === "lane:import:thread-idle"); + assert.ok(idle); + assert.strictEqual(idle.state, "queued"); + assert.strictEqual(idle.objective_summary, "Please fix the bug"); + assert.strictEqual(idle.completed_at, null); + const idleLane = JSON.parse(idle.lane_json) as { + readonly taskContract: { readonly objectiveDerivation: string }; + readonly completedAt: string | null; + }; + assert.strictEqual(idleLane.taskContract.objectiveDerivation, "UNKNOWN"); + assert.strictEqual(idleLane.completedAt, null); + + const running = lanes.find((row) => row.id === "lane:import:thread-running"); + assert.ok(running); + assert.strictEqual(running.state, "recovery-required"); + assert.strictEqual(running.completed_at, null); + const runningLane = JSON.parse(running.lane_json) as { + readonly legacyExecutorRef: { readonly sessionStatus: string } | null; + readonly completedAt: string | null; + }; + assert.strictEqual(runningLane.legacyExecutorRef?.sessionStatus, "running"); + assert.strictEqual(runningLane.completedAt, null); + + const archived = lanes.find((row) => row.id === "lane:import:thread-archived"); + assert.ok(archived); + assert.strictEqual(archived.state, "queued"); + assert.strictEqual(archived.completed_at, null); + assert.strictEqual(archived.objective_summary, "Archived thread"); + + const events = yield* sql<{ + readonly event_id: string; + readonly command_id: string | null; + }>` + SELECT event_id, command_id + FROM orchestration_events + WHERE event_type = 'lane.imported' + ORDER BY event_id ASC + `; + assert.strictEqual(events.length, 3); + assert.ok(events.every((event) => event.command_id === null)); + + const migration = yield* Effect.promise( + () => import("./035_WorkLanesAndSourceTruth.ts"), + ); + yield* migration.default; + + const lanesAfter = yield* sql`SELECT id FROM projection_work_lanes`; + const eventsAfter = yield* sql` + SELECT event_id + FROM orchestration_events + WHERE event_type = 'lane.imported' + `; + assert.strictEqual(lanesAfter.length, 3); + assert.strictEqual(eventsAfter.length, 3); + + const completedCount = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count + FROM projection_work_lanes + WHERE state = 'completed' OR completed_at IS NOT NULL + `; + assert.strictEqual(completedCount[0]?.count, 0); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts new file mode 100644 index 000000000000..cf50887bb15a --- /dev/null +++ b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts @@ -0,0 +1,377 @@ +// @effect-diagnostics nodeBuiltinImport:off +// @effect-diagnostics preferSchemaOverJson:off +/** + * Migration 035 — work-lane / source-truth projection tables + legacy thread import. + * + * Reads the persisted environment-id file when ServerConfig is available; otherwise + * stamps `unknown-environment`. JSON payloads are written as TEXT for the event + * store / projection tables (same shape as other projectors). + */ +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as fs from "node:fs"; +import * as path from "node:path"; + +import { ServerConfig } from "../../config.ts"; + +const ACTIVE_SESSION_STATUSES = new Set(["starting", "running", "ready", "interrupted", "error"]); + +const UNKNOWN_ENVIRONMENT_ID = "unknown-environment"; + +type ImportThreadRow = { + readonly threadId: string; + readonly projectId: string; + readonly title: string; + readonly branch: string | null; + readonly worktreePath: string | null; + readonly createdAt: string; + readonly updatedAt: string; + readonly workspaceRoot: string; + readonly sessionStatus: string | null; + readonly providerName: string | null; + readonly providerSessionId: string | null; + readonly earliestUserMessage: string | null; +}; + +const readEnvironmentIdFromPath = (environmentIdPath: string): string | null => { + try { + const raw = fs.readFileSync(environmentIdPath, "utf8").trim(); + return raw.length > 0 ? raw : null; + } catch { + return null; + } +}; + +const resolveEnvironmentId = Effect.gen(function* () { + const configOption = yield* Effect.serviceOption(ServerConfig); + if (Option.isSome(configOption)) { + const environmentIdPath = configOption.value.environmentIdPath; + const fileSystemOption = yield* Effect.serviceOption(FileSystem.FileSystem); + if (Option.isSome(fileSystemOption)) { + const exists = yield* fileSystemOption.value + .exists(environmentIdPath) + .pipe(Effect.orElseSucceed(() => false)); + if (exists) { + const raw = yield* fileSystemOption.value.readFileString(environmentIdPath).pipe( + Effect.map((value) => value.trim()), + Effect.orElseSucceed(() => ""), + ); + if (raw.length > 0) { + return raw; + } + } + } else { + const fromDisk = readEnvironmentIdFromPath(environmentIdPath); + if (fromDisk !== null) { + return fromDisk; + } + } + } + + const home = process.env.T3CODE_HOME; + if (typeof home === "string" && home.length > 0) { + for (const relative of ["userdata/environment-id", "dev/environment-id", "environment-id"]) { + const fromDisk = readEnvironmentIdFromPath(path.join(home, relative)); + if (fromDisk !== null) { + return fromDisk; + } + } + } + + return UNKNOWN_ENVIRONMENT_ID; +}); + +function importStateForSession(sessionStatus: string | null): "queued" | "recovery-required" { + if (sessionStatus !== null && ACTIVE_SESSION_STATUSES.has(sessionStatus)) { + return "recovery-required"; + } + return "queued"; +} + +function buildImportedLanePayload(input: { + readonly environmentId: string; + readonly row: ImportThreadRow; +}) { + const laneId = `lane:import:${input.row.threadId}`; + const title = input.row.title.trim().length > 0 ? input.row.title.trim() : "Imported thread"; + const objectiveText = + input.row.earliestUserMessage !== null && input.row.earliestUserMessage.trim().length > 0 + ? input.row.earliestUserMessage.trim() + : title; + const state = importStateForSession(input.row.sessionStatus); + const legacyExecutorRef = + input.row.sessionStatus === null + ? null + : { + threadId: input.row.threadId, + runtimeSessionId: input.row.providerSessionId, + providerName: input.row.providerName, + sessionStatus: input.row.sessionStatus, + }; + + const lane = { + id: laneId, + projectId: input.row.projectId, + title, + taskContract: { + objective: objectiveText, + constraints: [], + nonGoals: [], + deliverableRequirement: "none" as const, + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: [], + prohibitedActions: [], + completionReportRequired: true as const, + objectiveDerivation: "UNKNOWN" as const, + }, + state, + priority: "normal" as const, + classification: "substantial" as const, + environmentId: input.environmentId, + repositoryIdentity: null, + baseRef: null, + branch: input.row.branch, + worktreePath: input.row.worktreePath, + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + activePlanRevisionId: null, + acceptanceCriterionIds: [], + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId: input.row.threadId, + importedThreadId: input.row.threadId, + threadIds: [input.row.threadId], + legacyExecutorRef, + resumeState: null, + createdAt: input.row.createdAt, + updatedAt: input.row.updatedAt, + completedAt: null, + }; + + return { + lane, + acceptanceCriteria: [] as const, + importedFromThreadId: input.row.threadId, + importReason: "legacy-thread-migration", + }; +} + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const environmentId = yield* resolveEnvironmentId; + + yield* sql` + CREATE TABLE IF NOT EXISTS projection_work_lanes ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + title TEXT NOT NULL, + state TEXT NOT NULL, + priority TEXT NOT NULL, + classification TEXT NOT NULL, + environment_id TEXT NOT NULL, + branch TEXT, + worktree_path TEXT, + source_truth_revision_id TEXT, + primary_thread_id TEXT, + imported_thread_id TEXT, + objective_summary TEXT NOT NULL, + lane_json TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + last_sequence INTEGER NOT NULL + ) + `; + + yield* sql` + CREATE INDEX IF NOT EXISTS idx_projection_work_lanes_project_id + ON projection_work_lanes(project_id) + `; + + yield* sql` + CREATE INDEX IF NOT EXISTS idx_projection_work_lanes_updated_at + ON projection_work_lanes(updated_at) + `; + + yield* sql` + CREATE TABLE IF NOT EXISTS projection_source_truth_revisions ( + id TEXT PRIMARY KEY, + lane_id TEXT NOT NULL, + revision_json TEXT NOT NULL, + produced_at TEXT NOT NULL, + superseded_at TEXT, + last_sequence INTEGER NOT NULL + ) + `; + + yield* sql` + CREATE INDEX IF NOT EXISTS idx_projection_source_truth_revisions_lane_id + ON projection_source_truth_revisions(lane_id) + `; + + yield* sql` + CREATE TABLE IF NOT EXISTS projection_lane_acceptance_criteria ( + id TEXT PRIMARY KEY, + lane_id TEXT NOT NULL, + criterion_json TEXT NOT NULL, + last_sequence INTEGER NOT NULL + ) + `; + + yield* sql` + CREATE INDEX IF NOT EXISTS idx_projection_lane_acceptance_criteria_lane_id + ON projection_lane_acceptance_criteria(lane_id) + `; + + const threads = yield* sql` + SELECT + t.thread_id AS "threadId", + t.project_id AS "projectId", + t.title AS "title", + t.branch AS "branch", + t.worktree_path AS "worktreePath", + t.created_at AS "createdAt", + t.updated_at AS "updatedAt", + p.workspace_root AS "workspaceRoot", + s.status AS "sessionStatus", + s.provider_name AS "providerName", + s.provider_session_id AS "providerSessionId", + ( + SELECT m.text + FROM projection_thread_messages m + WHERE m.thread_id = t.thread_id + AND m.role = 'user' + ORDER BY m.created_at ASC, m.message_id ASC + LIMIT 1 + ) AS "earliestUserMessage" + FROM projection_threads t + INNER JOIN projection_projects p + ON p.project_id = t.project_id + LEFT JOIN projection_thread_sessions s + ON s.thread_id = t.thread_id + WHERE t.deleted_at IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM projection_work_lanes wl + WHERE wl.id = 'lane:import:' || t.thread_id + ) + AND NOT EXISTS ( + SELECT 1 + FROM orchestration_events e + WHERE e.event_id = 'lane-import-event:' || t.thread_id + ) + AND NOT EXISTS ( + SELECT 1 + FROM orchestration_events e + WHERE e.aggregate_kind = 'lane' + AND e.stream_id = 'lane:import:' || t.thread_id + AND e.event_type IN ('lane.imported', 'lane.created') + ) + ORDER BY t.created_at ASC, t.thread_id ASC + `; + + for (const row of threads) { + const payload = buildImportedLanePayload({ environmentId, row }); + const laneId = payload.lane.id; + const eventId = `lane-import-event:${row.threadId}`; + const payloadJson = JSON.stringify(payload); + const laneJson = JSON.stringify(payload.lane); + + const inserted = yield* sql<{ readonly sequence: number }>` + INSERT INTO orchestration_events ( + event_id, + aggregate_kind, + stream_id, + stream_version, + event_type, + occurred_at, + command_id, + causation_event_id, + correlation_id, + actor_kind, + payload_json, + metadata_json + ) + VALUES ( + ${eventId}, + 'lane', + ${laneId}, + COALESCE( + ( + SELECT stream_version + 1 + FROM orchestration_events + WHERE aggregate_kind = 'lane' + AND stream_id = ${laneId} + ORDER BY stream_version DESC + LIMIT 1 + ), + 0 + ), + 'lane.imported', + ${row.updatedAt}, + NULL, + NULL, + NULL, + 'server', + ${payloadJson}, + '{}' + ) + RETURNING sequence + `; + + const sequence = inserted[0]?.sequence; + if (sequence === undefined) { + continue; + } + + yield* sql` + INSERT INTO projection_work_lanes ( + id, + project_id, + title, + state, + priority, + classification, + environment_id, + branch, + worktree_path, + source_truth_revision_id, + primary_thread_id, + imported_thread_id, + objective_summary, + lane_json, + created_at, + updated_at, + completed_at, + last_sequence + ) + VALUES ( + ${payload.lane.id}, + ${payload.lane.projectId}, + ${payload.lane.title}, + ${payload.lane.state}, + ${payload.lane.priority}, + ${payload.lane.classification}, + ${payload.lane.environmentId}, + ${payload.lane.branch}, + ${payload.lane.worktreePath}, + ${payload.lane.sourceTruthRevisionId}, + ${payload.lane.primaryThreadId}, + ${payload.lane.importedThreadId}, + ${payload.lane.taskContract.objective}, + ${laneJson}, + ${payload.lane.createdAt}, + ${payload.lane.updatedAt}, + ${payload.lane.completedAt}, + ${sequence} + ) + ON CONFLICT (id) DO NOTHING + `; + } +}); diff --git a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts index 1498984827e5..a34e52804671 100644 --- a/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts +++ b/apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts @@ -14,6 +14,7 @@ import { OrchestrationCommandReceiptStatus, ProjectId, ThreadId, + WorkLaneId, } from "@t3tools/contracts"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; @@ -25,7 +26,7 @@ import type { OrchestrationCommandReceiptRepositoryError } from "../Errors.ts"; export const OrchestrationCommandReceipt = Schema.Struct({ commandId: CommandId, aggregateKind: OrchestrationAggregateKind, - aggregateId: Schema.Union([ProjectId, ThreadId]), + aggregateId: Schema.Union([ProjectId, ThreadId, WorkLaneId]), acceptedAt: IsoDateTime, resultSequence: NonNegativeInt, status: OrchestrationCommandReceiptStatus, diff --git a/apps/server/src/persistence/Services/ProjectionLaneAcceptanceCriteria.ts b/apps/server/src/persistence/Services/ProjectionLaneAcceptanceCriteria.ts new file mode 100644 index 000000000000..705be157bbc0 --- /dev/null +++ b/apps/server/src/persistence/Services/ProjectionLaneAcceptanceCriteria.ts @@ -0,0 +1,66 @@ +/** + * ProjectionLaneAcceptanceCriterionRepository - Projection repository for lane acceptance criteria. + * + * @module ProjectionLaneAcceptanceCriteria + */ +import { + AcceptanceCriterion, + AcceptanceCriterionId, + NonNegativeInt, + WorkLaneId, +} from "@t3tools/contracts"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; + +import type { ProjectionRepositoryError } from "../Errors.ts"; + +export const ProjectionLaneAcceptanceCriterion = Schema.Struct({ + id: AcceptanceCriterionId, + laneId: WorkLaneId, + criterion: AcceptanceCriterion, + lastSequence: NonNegativeInt, +}); +export type ProjectionLaneAcceptanceCriterion = typeof ProjectionLaneAcceptanceCriterion.Type; + +export const GetProjectionLaneAcceptanceCriterionInput = Schema.Struct({ + id: AcceptanceCriterionId, +}); +export type GetProjectionLaneAcceptanceCriterionInput = + typeof GetProjectionLaneAcceptanceCriterionInput.Type; + +export const ListProjectionLaneAcceptanceCriteriaByLaneInput = Schema.Struct({ + laneId: WorkLaneId, +}); +export type ListProjectionLaneAcceptanceCriteriaByLaneInput = + typeof ListProjectionLaneAcceptanceCriteriaByLaneInput.Type; + +export interface ProjectionLaneAcceptanceCriterionRepositoryShape { + readonly upsert: ( + row: ProjectionLaneAcceptanceCriterion, + ) => Effect.Effect; + readonly getById: ( + input: GetProjectionLaneAcceptanceCriterionInput, + ) => Effect.Effect< + Option.Option, + ProjectionRepositoryError + >; + readonly listByLaneId: ( + input: ListProjectionLaneAcceptanceCriteriaByLaneInput, + ) => Effect.Effect< + ReadonlyArray, + ProjectionRepositoryError + >; + readonly listAll: () => Effect.Effect< + ReadonlyArray, + ProjectionRepositoryError + >; +} + +export class ProjectionLaneAcceptanceCriterionRepository extends Context.Service< + ProjectionLaneAcceptanceCriterionRepository, + ProjectionLaneAcceptanceCriterionRepositoryShape +>()( + "t3/persistence/Services/ProjectionLaneAcceptanceCriteria/ProjectionLaneAcceptanceCriterionRepository", +) {} diff --git a/apps/server/src/persistence/Services/ProjectionSourceTruthRevisions.ts b/apps/server/src/persistence/Services/ProjectionSourceTruthRevisions.ts new file mode 100644 index 000000000000..c50a6e2f9562 --- /dev/null +++ b/apps/server/src/persistence/Services/ProjectionSourceTruthRevisions.ts @@ -0,0 +1,63 @@ +/** + * ProjectionSourceTruthRevisionRepository - Projection repository for source-truth revisions. + * + * @module ProjectionSourceTruthRevisions + */ +import { + IsoDateTime, + NonNegativeInt, + SourceTruthRevision, + SourceTruthRevisionId, + WorkLaneId, +} from "@t3tools/contracts"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; + +import type { ProjectionRepositoryError } from "../Errors.ts"; + +export const ProjectionSourceTruthRevision = Schema.Struct({ + id: SourceTruthRevisionId, + laneId: WorkLaneId, + revision: SourceTruthRevision, + producedAt: IsoDateTime, + supersededAt: Schema.NullOr(IsoDateTime), + lastSequence: NonNegativeInt, +}); +export type ProjectionSourceTruthRevision = typeof ProjectionSourceTruthRevision.Type; + +export const GetProjectionSourceTruthRevisionInput = Schema.Struct({ + id: SourceTruthRevisionId, +}); +export type GetProjectionSourceTruthRevisionInput = + typeof GetProjectionSourceTruthRevisionInput.Type; + +export const ListProjectionSourceTruthRevisionsByLaneInput = Schema.Struct({ + laneId: WorkLaneId, +}); +export type ListProjectionSourceTruthRevisionsByLaneInput = + typeof ListProjectionSourceTruthRevisionsByLaneInput.Type; + +export interface ProjectionSourceTruthRevisionRepositoryShape { + readonly upsert: ( + row: ProjectionSourceTruthRevision, + ) => Effect.Effect; + readonly getById: ( + input: GetProjectionSourceTruthRevisionInput, + ) => Effect.Effect, ProjectionRepositoryError>; + readonly listByLaneId: ( + input: ListProjectionSourceTruthRevisionsByLaneInput, + ) => Effect.Effect, ProjectionRepositoryError>; + readonly listAll: () => Effect.Effect< + ReadonlyArray, + ProjectionRepositoryError + >; +} + +export class ProjectionSourceTruthRevisionRepository extends Context.Service< + ProjectionSourceTruthRevisionRepository, + ProjectionSourceTruthRevisionRepositoryShape +>()( + "t3/persistence/Services/ProjectionSourceTruthRevisions/ProjectionSourceTruthRevisionRepository", +) {} diff --git a/apps/server/src/persistence/Services/ProjectionWorkLanes.ts b/apps/server/src/persistence/Services/ProjectionWorkLanes.ts new file mode 100644 index 000000000000..e27c7d2f701d --- /dev/null +++ b/apps/server/src/persistence/Services/ProjectionWorkLanes.ts @@ -0,0 +1,65 @@ +/** + * ProjectionWorkLaneRepository - Projection repository for work lanes. + * + * @module ProjectionWorkLanes + */ +import { + EnvironmentId, + IsoDateTime, + NonNegativeInt, + ProjectId, + SourceTruthRevisionId, + ThreadId, + TrimmedNonEmptyString, + WorkLane, + WorkLaneClassification, + WorkLaneId, + WorkLaneState, + WorkPriority, +} from "@t3tools/contracts"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import * as Context from "effect/Context"; +import type * as Effect from "effect/Effect"; + +import type { ProjectionRepositoryError } from "../Errors.ts"; + +export const ProjectionWorkLane = Schema.Struct({ + id: WorkLaneId, + projectId: ProjectId, + title: TrimmedNonEmptyString, + state: WorkLaneState, + priority: WorkPriority, + classification: WorkLaneClassification, + environmentId: EnvironmentId, + branch: Schema.NullOr(TrimmedNonEmptyString), + worktreePath: Schema.NullOr(TrimmedNonEmptyString), + sourceTruthRevisionId: Schema.NullOr(SourceTruthRevisionId), + primaryThreadId: Schema.NullOr(ThreadId), + importedThreadId: Schema.NullOr(ThreadId), + objectiveSummary: TrimmedNonEmptyString, + lane: WorkLane, + createdAt: IsoDateTime, + updatedAt: IsoDateTime, + completedAt: Schema.NullOr(IsoDateTime), + lastSequence: NonNegativeInt, +}); +export type ProjectionWorkLane = typeof ProjectionWorkLane.Type; + +export const GetProjectionWorkLaneInput = Schema.Struct({ + id: WorkLaneId, +}); +export type GetProjectionWorkLaneInput = typeof GetProjectionWorkLaneInput.Type; + +export interface ProjectionWorkLaneRepositoryShape { + readonly upsert: (row: ProjectionWorkLane) => Effect.Effect; + readonly getById: ( + input: GetProjectionWorkLaneInput, + ) => Effect.Effect, ProjectionRepositoryError>; + readonly listAll: () => Effect.Effect, ProjectionRepositoryError>; +} + +export class ProjectionWorkLaneRepository extends Context.Service< + ProjectionWorkLaneRepository, + ProjectionWorkLaneRepositoryShape +>()("t3/persistence/Services/ProjectionWorkLanes/ProjectionWorkLaneRepository") {} diff --git a/apps/server/src/project/ProjectSetupScriptRunner.test.ts b/apps/server/src/project/ProjectSetupScriptRunner.test.ts index 15612908079a..3953a525bfa1 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.test.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.test.ts @@ -44,6 +44,8 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + getLaneShellById: () => Effect.die("unused"), + getLaneDetail: () => Effect.die("unused"), }); const makeTerminalManagerLayer = ( diff --git a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts index 3843c8acbcd9..aad86b9e8e31 100644 --- a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts +++ b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts @@ -212,6 +212,8 @@ describe("ProviderSessionReaper", () => { ), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + getLaneShellById: () => Effect.die("unused"), + getLaneDetail: () => Effect.die("unused"), }), ), Layer.provideMerge(NodeServices.layer), diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts index 74a4de594a15..adc61741c23c 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts @@ -483,6 +483,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { snapshotSequence: 1, projects: [project], threads: [thread], + lanes: [], updatedAt: now, } satisfies OrchestrationShellSnapshot), getThreadShellById: () => @@ -674,6 +675,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { snapshotSequence: 1, projects: [project], threads: [thread], + lanes: [], updatedAt: now, } satisfies OrchestrationShellSnapshot), getThreadShellById: () => Effect.succeed(Option.some(thread)), diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 0b233d7d9f7a..bb63a8f16728 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -183,6 +183,7 @@ const makeDefaultOrchestrationReadModel = () => { deletedAt: null, }, ], + lanes: [], }; }; @@ -720,6 +721,7 @@ const buildAppUnderTest = (options?: { snapshotSequence: 0, projects: [], threads: [], + lanes: [], updatedAt: "1970-01-01T00:00:00.000Z", }), getArchivedShellSnapshot: () => @@ -727,6 +729,7 @@ const buildAppUnderTest = (options?: { snapshotSequence: 0, projects: [], threads: [], + lanes: [], updatedAt: "1970-01-01T00:00:00.000Z", }), getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), @@ -734,6 +737,8 @@ const buildAppUnderTest = (options?: { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }), getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()), getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()), @@ -5717,6 +5722,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { deletedAt: null, }, ], + lanes: [], }; yield* buildAppUnderTest({ @@ -5895,6 +5901,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { snapshotSequence: 1, projects: [], threads: [makeDefaultOrchestrationThreadShell()], + lanes: [], updatedAt: "2026-01-01T00:00:00.000Z", }; }), @@ -6010,6 +6017,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { snapshotSequence: 100_000, projects: [], threads: [makeDefaultOrchestrationThreadShell({ id: snapshotThreadId })], + lanes: [], updatedAt: now, }), }, @@ -6057,6 +6065,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { snapshotSequence: 5, projects: [], threads: [], + lanes: [], updatedAt: "2026-01-01T00:00:00.000Z", }), }, diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index b8102bda9add..12ce1b7b9e82 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -97,6 +97,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + getLaneShellById: () => Effect.succeed(Option.none()), + getLaneDetail: () => Effect.succeed(Option.none()), }), Effect.provideService(AnalyticsService.AnalyticsService, { record: () => Effect.void, @@ -160,6 +162,8 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + getLaneShellById: () => Effect.die("unused"), + getLaneDetail: () => Effect.die("unused"), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, @@ -204,6 +208,8 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + getLaneShellById: () => Effect.die("unused"), + getLaneDetail: () => Effect.die("unused"), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, @@ -254,6 +260,8 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + getLaneShellById: () => Effect.die("unused"), + getLaneDetail: () => Effect.die("unused"), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 6c021c9af803..8483ef1047a0 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -26,6 +26,7 @@ import { type OrchestrationShellStreamEvent, type OrchestrationShellStreamItem, type OrchestrationThreadStreamItem, + type OrchestrationLaneStreamItem, OrchestrationGetFullThreadDiffError, OrchestrationGetSnapshotError, OrchestrationGetTurnDiffError, @@ -47,6 +48,7 @@ import { RpcClientId, EnvironmentAuthorizationError, ThreadId, + WorkLaneId, type TerminalAttachStreamEvent, type TerminalError, type TerminalEvent, @@ -117,6 +119,7 @@ import * as SessionStore from "./auth/SessionStore.ts"; import { failEnvironmentAuthInvalid, failEnvironmentInternal } from "./auth/http.ts"; import * as RelayClient from "@t3tools/shared/relayClient"; const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError); +const isOrchestrationGetSnapshotError = Schema.is(OrchestrationGetSnapshotError); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const EDITOR_DISCOVERY_TIMEOUT = Duration.seconds(5); @@ -552,6 +555,9 @@ const makeWsRpcLayer = ( case "thread.unarchived": return threadUpsertOrRemove(event.payload.threadId, event.sequence); default: + if (event.aggregateKind === "lane") { + return laneUpsertOrRemove(WorkLaneId.make(event.aggregateId), event.sequence); + } if (event.aggregateKind !== "thread") { return Effect.succeed(Option.none()); } @@ -565,7 +571,7 @@ const makeWsRpcLayer = ( // If both attempts fail, log and drop the stream item; treating an error as // a missing row would incorrectly remove a still-active aggregate. const retryShellProjectionRead = ( - aggregateKind: "project" | "thread", + aggregateKind: "project" | "thread" | "lane", aggregateId: string, read: Effect.Effect, ): Effect.Effect, never, never> => @@ -650,6 +656,35 @@ const makeWsRpcLayer = ( ), ); + const laneUpsertOrRemove = ( + laneId: WorkLaneId, + sequence: number, + ): Effect.Effect, never, never> => + retryShellProjectionRead( + "lane", + laneId, + projectionSnapshotQuery.getLaneShellById(laneId), + ).pipe( + Effect.map( + Option.flatMap((lane) => + Option.match(lane, { + onNone: () => + Option.some({ + kind: "lane-removed" as const, + sequence, + laneId, + }), + onSome: (nextLane) => + Option.some({ + kind: "lane-upserted" as const, + sequence, + lane: nextLane, + }), + }), + ), + ), + ); + // Turn a batch of domain events into shell stream items, coalescing by // aggregate first. `toShellStreamEvent` re-reads the *current* projected // shell for an aggregate, so within a batch only the latest event per @@ -1335,6 +1370,120 @@ const makeWsRpcLayer = ( }), { "rpc.aggregate": "orchestration" }, ), + [ORCHESTRATION_WS_METHODS.getLaneDetail]: (input) => + observeRpcEffect( + ORCHESTRATION_WS_METHODS.getLaneDetail, + projectionSnapshotQuery.getLaneDetail(input.laneId).pipe( + Effect.flatMap((snapshot) => + Option.match(snapshot, { + onNone: () => + Effect.fail( + new OrchestrationGetSnapshotError({ + message: `Lane ${input.laneId} was not found`, + cause: input.laneId, + }), + ), + onSome: (value) => Effect.succeed(value), + }), + ), + Effect.mapError((cause) => + isOrchestrationGetSnapshotError(cause) + ? cause + : new OrchestrationGetSnapshotError({ + message: `Failed to load lane ${input.laneId}`, + cause, + }), + ), + ), + { "rpc.aggregate": "orchestration" }, + ), + [ORCHESTRATION_WS_METHODS.subscribeLane]: (input) => + observeRpcStreamEffect( + ORCHESTRATION_WS_METHODS.subscribeLane, + Effect.gen(function* () { + const isThisLaneEvent = (event: OrchestrationEvent) => + event.aggregateKind === "lane" && event.aggregateId === input.laneId; + + const liveStream = orchestrationEngine.streamDomainEvents.pipe( + Stream.filter(isThisLaneEvent), + Stream.map((event) => ({ + kind: "event" as const, + event, + })), + ); + + const liveBuffer = yield* Queue.unbounded(); + yield* Effect.forkScoped( + liveStream.pipe(Stream.runForEach((item) => Queue.offer(liveBuffer, item))), + ); + const bufferedLiveStream = Stream.fromQueue(liveBuffer); + + if (input.afterSequence !== undefined) { + const afterSequence = input.afterSequence; + const catchUpStream = orchestrationEngine + .readEvents(afterSequence, Number.MAX_SAFE_INTEGER) + .pipe( + Stream.filter(isThisLaneEvent), + Stream.map((event) => ({ + kind: "event" as const, + event, + })), + Stream.mapError( + (cause) => + new OrchestrationGetSnapshotError({ + message: `Failed to replay lane ${input.laneId} events`, + cause, + }), + ), + ); + const afterCatchUp = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; + return Stream.concat(catchUpStream, afterCatchUp); + } + + const snapshot = yield* projectionSnapshotQuery.getLaneDetail(input.laneId).pipe( + Effect.mapError( + (cause) => + new OrchestrationGetSnapshotError({ + message: `Failed to load lane ${input.laneId}`, + cause, + }), + ), + ); + + if (Option.isNone(snapshot)) { + return yield* new OrchestrationGetSnapshotError({ + message: `Lane ${input.laneId} was not found`, + cause: input.laneId, + }); + } + + const afterSnapshot = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; + return Stream.concat( + Stream.make({ + kind: "snapshot" as const, + snapshot: snapshot.value, + }), + afterSnapshot, + ); + }), + { "rpc.aggregate": "orchestration" }, + ), [WS_METHODS.serverProbe]: (_input) => observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), { "rpc.aggregate": "server", diff --git a/docs/macbrains/CAMPAIGN_MANIFEST.json b/docs/macbrains/CAMPAIGN_MANIFEST.json index e1d5c1f26754..1666c716fe92 100644 --- a/docs/macbrains/CAMPAIGN_MANIFEST.json +++ b/docs/macbrains/CAMPAIGN_MANIFEST.json @@ -45,202 +45,357 @@ { "id": "F0", "title": "Work lane and source-truth contracts", - "status": "NOT_STARTED", + "status": "IMPLEMENTED", "dependencies": [], "ownershipGroup": "contracts-orchestration-foundation", "advisorRequired": true, - "primaryAcceptancePrefixes": ["A", "B"], - "blocks": ["F1", "F3", "F4", "F5", "N0"] + "primaryAcceptancePrefixes": [ + "A", + "B" + ], + "blocks": [ + "F1", + "F3", + "F4", + "F5", + "N0" + ] }, { "id": "F1", "title": "Typed receipt and evidence substrate", "status": "NOT_STARTED", - "dependencies": ["F0"], + "dependencies": [ + "F0" + ], "ownershipGroup": "contracts-evidence-server", "advisorRequired": true, - "primaryAcceptancePrefixes": ["G"], - "blocks": ["F2", "D0", "R0"] + "primaryAcceptancePrefixes": [ + "G" + ], + "blocks": [ + "F2", + "D0", + "R0" + ] }, { "id": "F2", "title": "Checks, acceptance criteria, and completion gate", "status": "NOT_STARTED", - "dependencies": ["F0", "F1"], + "dependencies": [ + "F0", + "F1" + ], "ownershipGroup": "orchestration-completion", "advisorRequired": true, - "primaryAcceptancePrefixes": ["H"], - "blocks": ["D0", "D1", "E0"] + "primaryAcceptancePrefixes": [ + "H" + ], + "blocks": [ + "D0", + "D1", + "E0" + ] }, { "id": "F3", "title": "Agent assignments and topology", "status": "NOT_STARTED", - "dependencies": ["F0"], + "dependencies": [ + "F0" + ], "ownershipGroup": "assignments-topology", "advisorRequired": true, - "primaryAcceptancePrefixes": ["D"], - "blocks": ["P0", "F6"] + "primaryAcceptancePrefixes": [ + "D" + ], + "blocks": [ + "P0", + "F6" + ] }, { "id": "F4", "title": "Worktree and ownership manager", "status": "NOT_STARTED", - "dependencies": ["F0"], + "dependencies": [ + "F0" + ], "ownershipGroup": "git-worktrees", "advisorRequired": true, - "primaryAcceptancePrefixes": ["C"], - "blocks": ["G0", "F6"] + "primaryAcceptancePrefixes": [ + "C" + ], + "blocks": [ + "G0", + "F6" + ] }, { "id": "F5", "title": "Command Center and projections", "status": "NOT_STARTED", - "dependencies": ["F0", "F1", "F2", "F3"], + "dependencies": [ + "F0", + "F1", + "F2", + "F3" + ], "ownershipGroup": "client-runtime-command-center", "advisorRequired": false, - "primaryAcceptancePrefixes": ["K"], - "blocks": ["F6"] + "primaryAcceptancePrefixes": [ + "K" + ], + "blocks": [ + "F6" + ] }, { "id": "F6", "title": "Lane detail and cross-surface controls", "status": "NOT_STARTED", - "dependencies": ["F2", "F3", "F4", "F5"], + "dependencies": [ + "F2", + "F3", + "F4", + "F5" + ], "ownershipGroup": "web-desktop-mobile-lane-ui", "advisorRequired": false, - "primaryAcceptancePrefixes": ["J", "K", "L"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "J", + "K", + "L" + ], + "blocks": [ + "E0" + ] }, { "id": "P0", "title": "Provider event normalization", "status": "NOT_STARTED", - "dependencies": ["F3"], + "dependencies": [ + "F3" + ], "ownershipGroup": "provider-adapters", "advisorRequired": true, - "primaryAcceptancePrefixes": ["E"], - "blocks": ["P1", "P2"] + "primaryAcceptancePrefixes": [ + "E" + ], + "blocks": [ + "P1", + "P2" + ] }, { "id": "P1", "title": "Background activity leases and session reaper correctness", "status": "NOT_STARTED", - "dependencies": ["P0"], + "dependencies": [ + "P0" + ], "ownershipGroup": "provider-lifecycle", "advisorRequired": true, - "primaryAcceptancePrefixes": ["F"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "F" + ], + "blocks": [ + "E0" + ] }, { "id": "P2", "title": "Context health, compaction, and recovery handoff", "status": "NOT_STARTED", - "dependencies": ["F1", "F3", "P0"], + "dependencies": [ + "F1", + "F3", + "P0" + ], "ownershipGroup": "provider-context-recovery", "advisorRequired": true, - "primaryAcceptancePrefixes": ["E", "F"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "E", + "F" + ], + "blocks": [ + "E0" + ] }, { "id": "N0", "title": "First-class environment and node policy", "status": "NOT_STARTED", - "dependencies": ["F0"], + "dependencies": [ + "F0" + ], "ownershipGroup": "environment-node-contracts", "advisorRequired": true, - "primaryAcceptancePrefixes": ["I", "J"], - "blocks": ["N1"] + "primaryAcceptancePrefixes": [ + "I", + "J" + ], + "blocks": [ + "N1" + ] }, { "id": "N1", "title": "Node job runtime", "status": "NOT_STARTED", - "dependencies": ["F1", "N0"], + "dependencies": [ + "F1", + "N0" + ], "ownershipGroup": "node-runtime", "advisorRequired": true, - "primaryAcceptancePrefixes": ["I"], - "blocks": ["N2"] + "primaryAcceptancePrefixes": [ + "I" + ], + "blocks": [ + "N2" + ] }, { "id": "N2", "title": "Artifact return and Mac integration", "status": "NOT_STARTED", - "dependencies": ["F1", "F2", "N1"], + "dependencies": [ + "F1", + "F2", + "N1" + ], "ownershipGroup": "artifact-transfer-integration", "advisorRequired": true, - "primaryAcceptancePrefixes": ["I"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "I" + ], + "blocks": [ + "E0" + ] }, { "id": "R0", "title": "Atomic persistence and last-known-good recovery", "status": "NOT_STARTED", - "dependencies": ["F1"], + "dependencies": [ + "F1" + ], "ownershipGroup": "persistence-reliability", "advisorRequired": true, - "primaryAcceptancePrefixes": ["P"], - "blocks": ["R1"] + "primaryAcceptancePrefixes": [ + "P" + ], + "blocks": [ + "R1" + ] }, { "id": "R1", "title": "Event/projection and heavy-thread resilience", "status": "NOT_STARTED", - "dependencies": ["F1", "R0"], + "dependencies": [ + "F1", + "R0" + ], "ownershipGroup": "projection-resilience", "advisorRequired": true, - "primaryAcceptancePrefixes": ["P", "Q"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "P", + "Q" + ], + "blocks": [ + "E0" + ] }, { "id": "D0", "title": "Deliverable registry and launch actions", "status": "NOT_STARTED", - "dependencies": ["F1", "F2"], + "dependencies": [ + "F1", + "F2" + ], "ownershipGroup": "deliverables", "advisorRequired": false, - "primaryAcceptancePrefixes": ["M"], - "blocks": ["D1", "D2"] + "primaryAcceptancePrefixes": [ + "M" + ], + "blocks": [ + "D1", + "D2" + ] }, { "id": "D1", "title": "User-visible UI acceptance", "status": "NOT_STARTED", - "dependencies": ["D0", "F6"], + "dependencies": [ + "D0", + "F6" + ], "ownershipGroup": "ui-acceptance", "advisorRequired": false, - "primaryAcceptancePrefixes": ["M", "U"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "M", + "U" + ], + "blocks": [ + "E0" + ] }, { "id": "D2", "title": "Actionable notifications", "status": "NOT_STARTED", - "dependencies": ["F1", "D0"], + "dependencies": [ + "F1", + "D0" + ], "ownershipGroup": "notifications", "advisorRequired": true, - "primaryAcceptancePrefixes": ["N"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "N" + ], + "blocks": [ + "E0" + ] }, { "id": "G0", "title": "GitHub and source-control evidence integration", "status": "NOT_STARTED", - "dependencies": ["F1", "F4"], + "dependencies": [ + "F1", + "F4" + ], "ownershipGroup": "source-control-providers", "advisorRequired": false, - "primaryAcceptancePrefixes": ["O"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "O" + ], + "blocks": [ + "E0" + ] }, { "id": "S0", "title": "Project instruction and skill registry", "status": "NOT_STARTED", - "dependencies": ["F0", "F1"], + "dependencies": [ + "F0", + "F1" + ], "ownershipGroup": "instructions-skills", "advisorRequired": true, - "primaryAcceptancePrefixes": ["T"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "T" + ], + "blocks": [ + "E0" + ] }, { "id": "U0", @@ -249,28 +404,49 @@ "dependencies": [], "ownershipGroup": "fork-identity-release", "advisorRequired": true, - "primaryAcceptancePrefixes": ["S"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "S" + ], + "blocks": [ + "E0" + ] }, { "id": "U1", "title": "Settings and policy editor", "status": "NOT_STARTED", - "dependencies": ["F0", "F3", "N0"], + "dependencies": [ + "F0", + "F3", + "N0" + ], "ownershipGroup": "settings-policy-ui", "advisorRequired": false, - "primaryAcceptancePrefixes": ["R", "S", "T"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "R", + "S", + "T" + ], + "blocks": [ + "E0" + ] }, { "id": "V0", "title": "Performance and telemetry hardening", "status": "NOT_STARTED", - "dependencies": ["F5", "R1"], + "dependencies": [ + "F5", + "R1" + ], "ownershipGroup": "performance-observability", "advisorRequired": true, - "primaryAcceptancePrefixes": ["Q"], - "blocks": ["E0"] + "primaryAcceptancePrefixes": [ + "Q" + ], + "blocks": [ + "E0" + ] }, { "id": "E0", @@ -293,7 +469,9 @@ ], "ownershipGroup": "integration-verification", "advisorRequired": true, - "primaryAcceptancePrefixes": ["U"], + "primaryAcceptancePrefixes": [ + "U" + ], "blocks": [] } ], @@ -306,4 +484,4 @@ "desktopWebIphoneIpadApplicabilityCovered": true, "macAndLinuxRestartRecoveryProven": true } -} \ No newline at end of file +} diff --git a/docs/macbrains/IMPLEMENTATION_LEDGER.md b/docs/macbrains/IMPLEMENTATION_LEDGER.md index a88d78617c11..528af1f97278 100644 --- a/docs/macbrains/IMPLEMENTATION_LEDGER.md +++ b/docs/macbrains/IMPLEMENTATION_LEDGER.md @@ -41,7 +41,7 @@ All foundations -> E0 end-to-end acceptance ## F0 — Work lane and source-truth contracts -**Status:** NOT_STARTED +**Status:** IMPLEMENTED **Objective:** Add the durable `WorkLane`, `TaskContract`, `AcceptanceCriterion`, `SourceTruthRevision`, lifecycle state, commands, events, decider invariants, projector support, persistence schema, and compatibility migration. diff --git a/docs/macbrains/receipts/F0_ADVISOR.md b/docs/macbrains/receipts/F0_ADVISOR.md new file mode 100644 index 000000000000..cab0898ea81c --- /dev/null +++ b/docs/macbrains/receipts/F0_ADVISOR.md @@ -0,0 +1,23 @@ +# F0 Advisor Receipt + +- Advisor: independent read-only architecture review (cloud subagent) +- Worktree: /workspace/.worktrees/f0-worklane-source-truth +- Branch: cursor/f0-worklane-source-truth-7986 @ 1e03d719 +- Verdict: APPROVE_WITH_CONSTRAINTS +- Recorded: 2026-07-30 + +## Constraints applied + +1. First-class aggregateKind `lane` on existing event store/engine/receipts; dual projector surfaces (in-memory projector.ts + SQL ProjectionPipeline) and ProjectionSnapshotQuery/REQUIRED_SNAPSHOT_PROJECTORS. +2. Contracts split: workLane.ts / sourceTruth.ts + IDs in baseSchemas; thin orchestration unions; no second barrel. +3. F0 TaskContract + minimal AcceptanceCriterion; reserved assignment/deliverable/plan/blocker IDs; legacyExecutorRef only. +4. WorkLaneClassification: substantial | bounded-readonly | tiny-reversible; gates use classification. +5. SourceTruthRevision is lane-owned append-only lineage (not separate aggregateKind); ArtifactReference is opaque F0 ref only. +6. Explicit transition matrix including block/unblock/resumeState, recovery.request, completion.request rejected, completion.invalidate only from completed; ownership among non-terminal includes failed/blocked/recovery-required. +7. Shell: WorkLaneShell + lanes default []; lane-upserted/lane-removed stream events + shellReducer; HTTP GET lane detail + WS subscribe; workLanes capability flag. +8. Migration 035: empty projection tables; append lane.imported events (event-first); stable WorkLaneId from ThreadId string; environmentId from persisted environment-id file or deferred bootstrap stamp; never completed. +9. Preserve importedThreadId / primaryThreadId linkage; client-runtime decode only; no UI; no PR. + +## Risks acknowledged + +Shell/reconnect skew, REQUIRED_SNAPSHOT_PROJECTORS lag, import classification edge cases, environmentId at migrate time, ArtifactReference scope creep, exclusive worktree false negatives, shared choke-point files. diff --git a/docs/macbrains/receipts/F0_DOWNGRADE.md b/docs/macbrains/receipts/F0_DOWNGRADE.md new file mode 100644 index 000000000000..10eb3dac62a7 --- /dev/null +++ b/docs/macbrains/receipts/F0_DOWNGRADE.md @@ -0,0 +1,20 @@ +# F0 Downgrade / Rollback + +Automatic downgrade from F0 (work lanes + source-truth events) is **not supported**. + +## Why + +New orchestration event types (`lane.*`, `source-truth.*`) and projection tables are append-only. Older server binaries that do not know these event types will fail schema decode when reading the event stream or rebuilding projections. + +## Rollback procedure + +1. Stop the newer server. +2. Restore `state.sqlite` (and `-wal`/`-shm` siblings) from a pre-upgrade backup taken while no writer held the DB open. +3. Restore any paired `secrets` / `settings.json` only if the backup set included them. +4. Start the older binary against the restored database. + +Do not attempt to delete only the new events/tables in place and continue on an old binary — event sequences and projector cursors will diverge. + +## Forward-only note + +If you must keep the upgraded database, stay on an F0-capable server build. Clients without `workLanes` capability should ignore lane shell fields via decode defaults. diff --git a/docs/macbrains/receipts/F0_VERIFIER.md b/docs/macbrains/receipts/F0_VERIFIER.md new file mode 100644 index 000000000000..0543c5d246e3 --- /dev/null +++ b/docs/macbrains/receipts/F0_VERIFIER.md @@ -0,0 +1,144 @@ +# F0 Verifier Receipt + +- Verifier: independent F0 read-only verification (cloud subagent) +- Worktree: `/workspace/.worktrees/f0-worklane-source-truth` +- Branch: `cursor/f0-worklane-source-truth-7986` +- HEAD (committed): `1e03d7198ae186c48dc065e3c9dcdeaa90003086` +- Base: `origin/macbrains/agent-workflow-overhaul` @ same SHA (empty committed diff) +- F0 implementation: **uncommitted working tree** (modified + untracked); not yet committed +- Recorded: 2026-07-30 +- PR: **none opened** (task contract / `pullRequestRequiresTaskAuthorization`; verifier must not open one) + +## VERDICT: PASS_WITH_GAPS + +Foundation contracts, decider gates, in-memory projection replay, migration 035, shell decode/reducer, and focused tests are coherent and green. Several F0-scoped acceptance rows remain only partially proven (schema/scaffolding or command-level only) versus the matrix’s stronger required proof (real Git, restart, UI, SQL rebuild). + +## Source truth refresh + +| Check | Result | +|---|---| +| `git status` | Clean vs remote tip; large unstaged/untracked F0 diff | +| HEAD | `1e03d719` — `docs: finalize durable implementation handoff` | +| `diff --stat origin/macbrains/agent-workflow-overhaul...HEAD` | empty (all F0 work is working-tree only) | +| Working tree vs HEAD | ~41 modified + 17 untracked (contracts, decider, projector, 035, client-runtime, docs) | + +### Key files inspected + +- `packages/contracts/src/workLane.ts`, `sourceTruth.ts` +- `apps/server/src/orchestration/decider.ts`, `workLaneTransitions.ts`, `commandInvariants.ts` +- `apps/server/src/orchestration/projector.ts` +- `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (workLanes projector) +- `apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts` (lane shell/detail) +- `apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts` (+ test) +- `packages/client-runtime/src/state/shellReducer.ts`, `workLanes.ts` +- Tests: `workLane.test.ts`, `decider.workLane.test.ts`, `projector.workLane.test.ts`, `035_*.test.ts`, `shellReducer.test.ts` + +## Confirmations (item 5–6) + +| Claim | Status | Evidence | +|---|---|---| +| No false historical completion in migration | **CONFIRMED** | Import always `completedAt: null`; idle→queued, active→recovery-required, archived→queued; test asserts zero completed rows; idempotent re-run | +| `completion.request` rejected | **CONFIRMED** | Decider always fails with `completion gate reserved until F2`; multi-state test | +| Substantial needs worktree | **CONFIRMED** | `execution.start` refuses substantial + null worktree; bounded-readonly allowed with source-truth | +| Exclusive ownership | **CONFIRMED** | `requireWorktreeExclusive` on `execution.start`; second lane same path rejected | +| Shell compact | **CONFIRMED** | `WorkLaneShell` / `toWorkLaneShell` omit `taskContract`; projector + contract tests; shell summary only | +| No PR | **CONFIRMED** | Verifier did not open a PR; campaign requires task authorization | + +## FINDINGS + +### Must-fix + +**None found.** No clear F0-scope code bugs requiring a fix before this receipt. Focused suite is green. Gaps below are incomplete acceptance proof or deferred enforcement, not broken implementations of the confirmed gates. + +### Accepted gaps (not must-fix in this pass) + +1. **A01–A06 largely scaffolding:** `SourceTruthRevision` carries the fields; execution only requires a revision **id** (+ worktree for substantial). No completeness gate for root/branch/HEAD/dirty/remotes/ops/instructions/manifests/tests/ownership; no real-worktree preflight receipt; `activeGitOperation` / `unknownsThatChangeAction` / conflict events do not block transitions; no generated/vendored ownership enforcement; remotes only via optional `RepositoryIdentity`, not a dedicated remotes list. +2. **Read model does not retain revision bodies** — only `lane.sourceTruthRevisionId`. Stronger A04/A06 gates need revision storage on the read model or equivalent (larger than a one-line fix). +3. **B01** structural lane aggregate + migration independence proven; **restart / provider-replacement** test missing. +4. **B07** block/unblock tested; **cancel / supersede / recovery.request** implemented in decider but **not covered** by `decider.workLane.test.ts`; UI controls out of F0. +5. **C01** F0 portion is “refuse substantial execution without worktree path”; **does not create/attach** isolated worktrees (F4 / real Git). +6. **P05** in-memory projector double-replay determinism proven; **SQL ProjectionPipeline wipe/rebuild-from-events** for work lanes not tested. Migration also dual-writes projection rows (event + projection insert) — fine for bootstrap, not a full rebuild proof. +7. **Exclusive ownership checked only at `execution.start`**, not at create/meta-update — two non-terminal lanes can share a path until execute (acceptable for F0; fuller ownership is F4). +8. **Campaign/ledger status `IMPLEMENTED`** while acceptance matrix rows remain `NOT_STARTED` — correct until evidence is linked; do not treat as `PROVEN`. +9. **All F0 code is uncommitted** — verifier evaluated the working tree; a later commit must re-run focused tests before package handoff claims final SHA. + +## ACCEPTANCE ROWS PROVEN + +Proven here means **F0 foundation proof via current focused tests/source**, not full matrix required-proof where that proof is UI/e2e/real-Git. + +| Row | Proven slice | +|---|---| +| **B02** | Documented transition matrix + disallowed jumps; decider uses matrix; happy-path + negative transition tests | +| **B03** | Substantial without worktree refused; missing source-truth revision refused before execution | +| **B04** | `lane.completion.request` always rejected (F2 gate); cannot complete via that command | +| **B05** | `completion.invalidate` from constructed `completed` → `recovery-required` | +| **B06** | Migration fixtures: never completed; idle/active/archived mapping; idempotent | +| **B07** (partial) | block/unblock + resumeState restore | +| **C01** (F0 portion) | Substantial execution requires worktree path | +| **C03** (F0 portion) | Second worktree-owning lane cannot `execution.start` on same path | +| **A03** (partial) | Preflight record + supersession updates `sourceTruthRevisionId` / `previousRevisionId` | +| **P05** (partial) | In-memory projector replay of lane events is deterministic | + +Also covered (supporting, not matrix IDs): contract roundtrip/invalid decode; shell default `lanes: []`; shellReducer lane-upserted/removed; compact shell shape. + +## ACCEPTANCE ROWS STILL MISSING + +| Row | Missing for F0 / matrix | +|---|---| +| **A01** | Real preflight receipt from exact worktree; completeness of recorded authorities before edits | +| **A02** | Conflict vs narrative authority that **blocks** unsafe transition | +| **A03** | UI revision history; actual refresh producer (request event only) | +| **A04** | Temp-repo integration; decider block on active merge/rebase/cherry-pick/bisect/revert | +| **A05** | Generated/vendored boundary respect beyond schema roles | +| **A06** | Unknowns→typed blockers; irrelevant unknowns do not halt (decider behavior) | +| **B01** | Restart + provider replacement persistence test | +| **B07** | Cancel/supersede/recovery command tests; UI controls | +| **C01** | Real Git create/attach isolated worktree by default | +| **P05** | SQL projection rebuild-from-canonical-events for work lanes | + +UI / e2e / provider-adapter / completion-gate rows outside F0 remain out of scope (do not mark proven from this scaffolding). + +## COMMAND RECEIPTS + +```text +$ git status + On branch cursor/f0-worklane-source-truth-7986 + up to date with origin/macbrains/agent-workflow-overhaul + modified: 41 files (unstaged); untracked: F0 implementation + receipts + +$ git rev-parse HEAD + 1e03d7198ae186c48dc065e3c9dcdeaa90003086 + +$ git diff --stat origin/macbrains/agent-workflow-overhaul...HEAD + (empty) + +$ git diff --stat HEAD + 41 files changed, 2291 insertions(+), 87 deletions(-) + (+ untracked: workLane/sourceTruth, 035 migration, decider/projector tests, workLanes.ts, …) + +$ export PATH="$HOME/.nvm/versions/node/v24.18.1/bin:$HOME/.local/bin:$PATH" +$ cd /workspace/.worktrees/f0-worklane-source-truth +$ npx vitest run \ + packages/contracts/src/workLane.test.ts \ + apps/server/src/orchestration/decider.workLane.test.ts \ + apps/server/src/orchestration/projector.workLane.test.ts \ + apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts \ + packages/client-runtime/src/state/shellReducer.test.ts \ + --config vite.config.ts + Test Files 5 passed (5) + Tests 33 passed (33) + Duration ~1.21s + EXIT 0 +``` + +## Fixes applied + +**None.** No must-fix F0 bugs identified; no code changes by verifier. Receipt only. + +## Next safe action + +1. Commit F0 working tree with conventional message after any owner polish. +2. Optionally add focused tests for cancel/supersede/recovery and/or SQL rebuild (closes B07/P05 gaps without UI). +3. Leave A01–A06 enforcement that needs revision bodies / real Git for follow-on packages (F4 / preflight reactor), or expand read model deliberately before claiming those rows PROVEN. +4. Do **not** open a PR unless explicitly authorized. +5. Do **not** set acceptance matrix rows to PROVEN until evidence links point at the final commit SHA. diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index 4fa05f850e59..151091613e2a 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -123,6 +123,10 @@ "types": "./src/state/threads.ts", "default": "./src/state/threads.ts" }, + "./state/work-lanes": { + "types": "./src/state/workLanes.ts", + "default": "./src/state/workLanes.ts" + }, "./state/thread-sort": { "types": "./src/state/threadSort.ts", "default": "./src/state/threadSort.ts" diff --git a/packages/client-runtime/src/connection/registry.test.ts b/packages/client-runtime/src/connection/registry.test.ts index 9354db9c998a..181201065612 100644 --- a/packages/client-runtime/src/connection/registry.test.ts +++ b/packages/client-runtime/src/connection/registry.test.ts @@ -116,6 +116,7 @@ const CACHED_SNAPSHOT: OrchestrationShellSnapshot = { snapshotSequence: 1, projects: [], threads: [], + lanes: [], updatedAt: "2026-06-06T00:00:00.000Z", }; diff --git a/packages/client-runtime/src/state/entities.test.ts b/packages/client-runtime/src/state/entities.test.ts index e08fd9e552f2..28a793893202 100644 --- a/packages/client-runtime/src/state/entities.test.ts +++ b/packages/client-runtime/src/state/entities.test.ts @@ -108,6 +108,7 @@ const THREAD_SHELL = { const SNAPSHOT: OrchestrationShellSnapshot = { snapshotSequence: 1, updatedAt: "2026-06-01T00:00:00.000Z", + lanes: [], projects: [ { id: PROJECT_ID, diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index 62d2a2c28b91..cba28604d372 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -44,6 +44,7 @@ const LIVE_SHELL_SNAPSHOT: OrchestrationShellSnapshot = { snapshotSequence: 1, projects: [], threads: [], + lanes: [], updatedAt: "2026-06-06T00:00:00.000Z", }; @@ -156,12 +157,14 @@ describe("environment shell synchronization", () => { snapshotSequence: 5, projects: [], threads: [{ id: "stale-thread" } as never], + lanes: [], updatedAt: "2026-06-06T00:00:00.000Z", }; const httpSnapshot: OrchestrationShellSnapshot = { ...cachedSnapshot, snapshotSequence: 9, threads: [], + lanes: [], updatedAt: "2026-06-07T00:00:00.000Z", }; const events = yield* Queue.unbounded(); diff --git a/packages/client-runtime/src/state/shell.test.ts b/packages/client-runtime/src/state/shell.test.ts index f1326e0a5cbe..23c69091ca8a 100644 --- a/packages/client-runtime/src/state/shell.test.ts +++ b/packages/client-runtime/src/state/shell.test.ts @@ -38,6 +38,7 @@ function shellState(input: { updatedAt: input.updatedAt, projects: [], threads: [], + lanes: [], }), status: input.status, error: input.error === undefined ? Option.none() : Option.some(input.error), diff --git a/packages/client-runtime/src/state/shellReducer.test.ts b/packages/client-runtime/src/state/shellReducer.test.ts index fdccc4c47dd8..a1d48d919226 100644 --- a/packages/client-runtime/src/state/shellReducer.test.ts +++ b/packages/client-runtime/src/state/shellReducer.test.ts @@ -9,6 +9,7 @@ const baseSnapshot: OrchestrationShellSnapshot = { snapshotSequence: 0, projects: [], threads: [], + lanes: [], updatedAt: "2026-04-01T00:00:00.000Z", }; @@ -182,4 +183,44 @@ describe("applyShellStreamEvent", () => { const next = applyShellStreamEvent(baseSnapshot, unknownEvent); expect(next).toBe(baseSnapshot); }); + + describe("lane-upserted / lane-removed", () => { + const stubLane = { + id: "lane-1" as const, + projectId: ProjectId.make("project-1"), + title: "Lane", + state: "queued" as const, + priority: "normal" as const, + classification: "substantial" as const, + environmentId: "env-1" as const, + branch: null, + worktreePath: null, + sourceTruthRevisionId: null, + sourceTruthSummary: null, + primaryThreadId: null, + importedThreadId: null, + objectiveSummary: "Do the thing", + createdAt: "2026-04-01T00:00:00.000Z", + updatedAt: "2026-04-01T00:00:00.000Z", + completedAt: null, + }; + + it("adds and removes lanes while advancing sequence", () => { + const upserted = applyShellStreamEvent(baseSnapshot, { + kind: "lane-upserted", + sequence: 1, + lane: stubLane as never, + }); + expect(upserted.lanes).toHaveLength(1); + expect(upserted.snapshotSequence).toBe(1); + + const removed = applyShellStreamEvent(upserted, { + kind: "lane-removed", + sequence: 2, + laneId: stubLane.id as never, + }); + expect(removed.lanes).toHaveLength(0); + expect(removed.snapshotSequence).toBe(2); + }); + }); }); diff --git a/packages/client-runtime/src/state/shellReducer.ts b/packages/client-runtime/src/state/shellReducer.ts index 3d3b22a1289f..1946b3116cdd 100644 --- a/packages/client-runtime/src/state/shellReducer.ts +++ b/packages/client-runtime/src/state/shellReducer.ts @@ -7,7 +7,7 @@ import type { OrchestrationShellSnapshot, OrchestrationShellStreamEvent } from " * web and mobile can use to keep their local shell snapshot in sync. * * Returns the original snapshot reference unchanged if the event is not - * recognized (forward-compatible). + * recognized (forward-compatible) or is stale. */ export function applyShellStreamEvent( snapshot: OrchestrationShellSnapshot, @@ -15,29 +15,45 @@ export function applyShellStreamEvent( ): OrchestrationShellSnapshot { if (event.sequence <= snapshot.snapshotSequence) return snapshot; + const lanes = snapshot.lanes ?? []; + switch (event.kind) { case "project-upserted": { const projects = snapshot.projects.some((p) => p.id === event.project.id) ? Arr.map(snapshot.projects, (p) => (p.id === event.project.id ? event.project : p)) : Arr.append(snapshot.projects, event.project); - return { ...snapshot, projects, snapshotSequence: event.sequence }; + return { ...snapshot, projects, lanes, snapshotSequence: event.sequence }; } case "project-removed": return { ...snapshot, projects: Arr.filter(snapshot.projects, (p) => p.id !== event.projectId), + lanes, snapshotSequence: event.sequence, }; case "thread-upserted": { const threads = snapshot.threads.some((t) => t.id === event.thread.id) ? Arr.map(snapshot.threads, (t) => (t.id === event.thread.id ? event.thread : t)) : Arr.append(snapshot.threads, event.thread); - return { ...snapshot, threads, snapshotSequence: event.sequence }; + return { ...snapshot, threads, lanes, snapshotSequence: event.sequence }; } case "thread-removed": return { ...snapshot, threads: Arr.filter(snapshot.threads, (t) => t.id !== event.threadId), + lanes, + snapshotSequence: event.sequence, + }; + case "lane-upserted": { + const nextLanes = lanes.some((lane) => lane.id === event.lane.id) + ? Arr.map(lanes, (lane) => (lane.id === event.lane.id ? event.lane : lane)) + : Arr.append(lanes, event.lane); + return { ...snapshot, lanes: nextLanes, snapshotSequence: event.sequence }; + } + case "lane-removed": + return { + ...snapshot, + lanes: Arr.filter(lanes, (lane) => lane.id !== event.laneId), snapshotSequence: event.sequence, }; default: diff --git a/packages/client-runtime/src/state/workLanes.ts b/packages/client-runtime/src/state/workLanes.ts new file mode 100644 index 000000000000..62e62560c269 --- /dev/null +++ b/packages/client-runtime/src/state/workLanes.ts @@ -0,0 +1,66 @@ +import type { + EnvironmentId, + OrchestrationLaneStreamItem, + WorkLaneDetailSnapshot, + WorkLaneId, + WorkLaneShell, +} from "@t3tools/contracts"; + +/** + * In-memory lane shells keyed by environment, mirroring thread shell storage. + * Full lane detail is loaded on demand via HTTP/WS — not kept in the shell map. + */ +export type WorkLaneShellMap = ReadonlyMap; + +export function emptyWorkLaneShellMap(): WorkLaneShellMap { + return new Map(); +} + +export function upsertWorkLaneShell(map: WorkLaneShellMap, lane: WorkLaneShell): WorkLaneShellMap { + const next = new Map(map); + next.set(lane.id, lane); + return next; +} + +export function removeWorkLaneShell(map: WorkLaneShellMap, laneId: WorkLaneId): WorkLaneShellMap { + if (!map.has(laneId)) return map; + const next = new Map(map); + next.delete(laneId); + return next; +} + +export function workLaneShellsFromSnapshot( + lanes: ReadonlyArray | undefined, +): WorkLaneShellMap { + const map = new Map(); + for (const lane of lanes ?? []) { + map.set(lane.id, lane); + } + return map; +} + +/** + * Apply a lane detail stream item. Snapshot replaces local detail; events are + * expected to be handled by reloading detail or by shell upserts for list views. + */ +export function applyLaneStreamItem( + current: WorkLaneDetailSnapshot | null, + item: OrchestrationLaneStreamItem, +): WorkLaneDetailSnapshot | null { + switch (item.kind) { + case "snapshot": + return item.snapshot; + case "synchronized": + return current; + case "event": + // Detail bodies are reloaded after sequence catch-up; keep current until then. + return current; + default: + return current; + } +} + +export type ScopedWorkLaneRef = { + readonly environmentId: EnvironmentId; + readonly laneId: WorkLaneId; +}; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index c3bd819023b7..1d5269393102 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -18,6 +18,14 @@ "./relay": { "types": "./src/relay.ts", "import": "./src/relay.ts" + }, + "./workLane": { + "types": "./src/workLane.ts", + "import": "./src/workLane.ts" + }, + "./sourceTruth": { + "types": "./src/sourceTruth.ts", + "import": "./src/sourceTruth.ts" } }, "scripts": { diff --git a/packages/contracts/src/baseSchemas.ts b/packages/contracts/src/baseSchemas.ts index a8fa565cef43..9b8e90bcb26c 100644 --- a/packages/contracts/src/baseSchemas.ts +++ b/packages/contracts/src/baseSchemas.ts @@ -60,3 +60,26 @@ export const ApprovalRequestId = makeEntityId("ApprovalRequestId"); export type ApprovalRequestId = typeof ApprovalRequestId.Type; export const CheckpointRef = makeEntityId("CheckpointRef"); export type CheckpointRef = typeof CheckpointRef.Type; + +/** Durable MacBrains work-lane identity (distinct from ThreadId). */ +export const WorkLaneId = makeEntityId("WorkLaneId"); +export type WorkLaneId = typeof WorkLaneId.Type; +export const SourceTruthRevisionId = makeEntityId("SourceTruthRevisionId"); +export type SourceTruthRevisionId = typeof SourceTruthRevisionId.Type; +export const AcceptanceCriterionId = makeEntityId("AcceptanceCriterionId"); +export type AcceptanceCriterionId = typeof AcceptanceCriterionId.Type; +export const PlanRevisionId = makeEntityId("PlanRevisionId"); +export type PlanRevisionId = typeof PlanRevisionId.Type; +export const BlockerId = makeEntityId("BlockerId"); +export type BlockerId = typeof BlockerId.Type; +export const ArtifactRefId = makeEntityId("ArtifactRefId"); +export type ArtifactRefId = typeof ArtifactRefId.Type; +/** Reserved for F3 agent topology — do not populate as a full aggregate in F0. */ +export const AgentAssignmentId = makeEntityId("AgentAssignmentId"); +export type AgentAssignmentId = typeof AgentAssignmentId.Type; +/** Reserved for deliverable aggregate (later packages). */ +export const DeliverableId = makeEntityId("DeliverableId"); +export type DeliverableId = typeof DeliverableId.Type; +/** Reserved for typed receipt store (F1). */ +export const ReceiptId = makeEntityId("ReceiptId"); +export type ReceiptId = typeof ReceiptId.Type; diff --git a/packages/contracts/src/environment.ts b/packages/contracts/src/environment.ts index 7f4b6c165410..defac4e4e7bb 100644 --- a/packages/contracts/src/environment.ts +++ b/packages/contracts/src/environment.ts @@ -47,6 +47,9 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({ /** Server understands thread.snooze / thread.unsnooze commands. Same version-skew contract as threadSettlement. */ threadSnooze: Schema.optionalKey(Schema.Boolean), + /** Server understands work-lane / source-truth commands and shell.lanes. + Absent on pre-F0 servers — clients must not send lane commands under skew. */ + workLanes: Schema.optionalKey(Schema.Boolean), /** The update path clients should offer for this server. Absent on servers that must be relaunched manually (dev checkouts, Windows foreground runs, pre-update servers). */ diff --git a/packages/contracts/src/environmentHttp.ts b/packages/contracts/src/environmentHttp.ts index 2d40dad60cc4..80e1fc876bad 100644 --- a/packages/contracts/src/environmentHttp.ts +++ b/packages/contracts/src/environmentHttp.ts @@ -24,7 +24,7 @@ import { AuthWebSocketTicketResult, ServerAuthSessionMethod, } from "./auth.ts"; -import { AuthSessionId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { AuthSessionId, ThreadId, TrimmedNonEmptyString, WorkLaneId } from "./baseSchemas.ts"; import { ExecutionEnvironmentDescriptor } from "./environment.ts"; import { ClientOrchestrationCommand, @@ -33,6 +33,7 @@ import { OrchestrationShellSnapshot, OrchestrationThreadDetailSnapshot, } from "./orchestration.ts"; +import { WorkLaneDetailSnapshot } from "./workLane.ts"; import { RelayCloudEnvironmentHealthRequest, RelayCloudMintCredentialRequest, @@ -83,6 +84,7 @@ export const EnvironmentInternalErrorReason = Schema.Literals([ "client_session_revoke_failed", "orchestration_snapshot_failed", "orchestration_thread_snapshot_failed", + "orchestration_lane_snapshot_failed", "orchestration_dispatch_failed", "internal_error", ]); @@ -158,7 +160,10 @@ export class EnvironmentInternalError extends Schema.TaggedErrorClass()( @@ -457,6 +462,10 @@ const EnvironmentOrchestrationThreadSnapshotParams = Schema.Struct({ threadId: ThreadId, }); +const EnvironmentOrchestrationLaneSnapshotParams = Schema.Struct({ + laneId: WorkLaneId, +}); + export class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestration") .add( HttpApiEndpoint.get("snapshot", "/api/orchestration/snapshot", { @@ -480,6 +489,14 @@ export class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestr error: EnvironmentOrchestrationThreadSnapshotErrors, }).middleware(EnvironmentAuthenticatedAuth), ) + .add( + HttpApiEndpoint.get("laneSnapshot", "/api/orchestration/lanes/:laneId", { + headers: OptionalBearerHeaders, + params: EnvironmentOrchestrationLaneSnapshotParams, + success: WorkLaneDetailSnapshot, + error: EnvironmentOrchestrationThreadSnapshotErrors, + }).middleware(EnvironmentAuthenticatedAuth), + ) .add( HttpApiEndpoint.post("dispatch", "/api/orchestration/dispatch", { headers: OptionalBearerHeaders, diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index f0ee1889177f..0ce725bb3c76 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -19,6 +19,8 @@ export * from "./git.ts"; export * from "./vcs.ts"; export * from "./sourceControl.ts"; export * from "./orchestration.ts"; +export * from "./workLane.ts"; +export * from "./sourceTruth.ts"; export * from "./t3ProjectFile.ts"; export * from "./editor.ts"; export * from "./project.ts"; diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index b947bd63e4ca..17a92b8749f7 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -19,8 +19,47 @@ import { ThreadId, TrimmedNonEmptyString, TurnId, + WorkLaneId, } from "./baseSchemas.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; +import { + LaneBlockCommand, + LaneCancelCommand, + LaneCompletionInvalidateCommand, + LaneCompletionRequestCommand, + LaneCreateCommand, + LaneCreatedPayload, + LaneDeliverableRegisterCommand, + LaneDeliverableRegisteredPayload, + LaneExecutionStartCommand, + LaneFailCommand, + LaneImportedPayload, + LaneMetaUpdateCommand, + LaneMetaUpdatedPayload, + LaneOrientationRecordCommand, + LanePlanActivateCommand, + LanePlanActivatedPayload, + LanePlanProposeCommand, + LanePlanProposedPayload, + LanePreflightRequestCommand, + LaneRecoveryRequestCommand, + LaneReviewRequestCommand, + LaneStateChangedPayload, + LaneSupersedeCommand, + LaneTaskContractUpdateCommand, + LaneTaskContractUpdatedPayload, + LaneTestingStartCommand, + LaneUnblockCommand, + SourceTruthConflictRecordCommand, + SourceTruthConflictRecordedPayload, + SourceTruthPreflightRecordCommand, + SourceTruthPreflightRecordedPayload, + SourceTruthRefreshRequestCommand, + SourceTruthRefreshRequestedPayload, + WorkLane, + WorkLaneDetailSnapshot, + WorkLaneShell, +} from "./workLane.ts"; export const ORCHESTRATION_WS_METHODS = { dispatchCommand: "orchestration.dispatchCommand", @@ -29,6 +68,8 @@ export const ORCHESTRATION_WS_METHODS = { getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot", subscribeShell: "orchestration.subscribeShell", subscribeThread: "orchestration.subscribeThread", + subscribeLane: "orchestration.subscribeLane", + getLaneDetail: "orchestration.getLaneDetail", } as const; export const ProviderApprovalPolicy = Schema.Literals([ @@ -381,6 +422,7 @@ export const OrchestrationReadModel = Schema.Struct({ snapshotSequence: NonNegativeInt, projects: Schema.Array(OrchestrationProject), threads: Schema.Array(OrchestrationThread), + lanes: Schema.Array(WorkLane).pipe(Schema.withDecodingDefault(Effect.succeed([]))), updatedAt: IsoDateTime, }); export type OrchestrationReadModel = typeof OrchestrationReadModel.Type; @@ -430,6 +472,7 @@ export const OrchestrationShellSnapshot = Schema.Struct({ snapshotSequence: NonNegativeInt, projects: Schema.Array(OrchestrationProjectShell), threads: Schema.Array(OrchestrationThreadShell), + lanes: Schema.Array(WorkLaneShell).pipe(Schema.withDecodingDefault(Effect.succeed([]))), updatedAt: IsoDateTime, }); export type OrchestrationShellSnapshot = typeof OrchestrationShellSnapshot.Type; @@ -455,6 +498,16 @@ export const OrchestrationShellStreamEvent = Schema.Union([ sequence: NonNegativeInt, threadId: ThreadId, }), + Schema.Struct({ + kind: Schema.Literal("lane-upserted"), + sequence: NonNegativeInt, + lane: WorkLaneShell, + }), + Schema.Struct({ + kind: Schema.Literal("lane-removed"), + sequence: NonNegativeInt, + laneId: WorkLaneId, + }), ]); export type OrchestrationShellStreamEvent = typeof OrchestrationShellStreamEvent.Type; @@ -506,6 +559,18 @@ export const OrchestrationSubscribeThreadInput = Schema.Struct({ }); export type OrchestrationSubscribeThreadInput = typeof OrchestrationSubscribeThreadInput.Type; +export const OrchestrationSubscribeLaneInput = Schema.Struct({ + laneId: WorkLaneId, + afterSequence: Schema.optionalKey(NonNegativeInt), + requestCompletionMarker: Schema.optionalKey(Schema.Boolean), +}); +export type OrchestrationSubscribeLaneInput = typeof OrchestrationSubscribeLaneInput.Type; + +export const OrchestrationGetLaneDetailInput = Schema.Struct({ + laneId: WorkLaneId, +}); +export type OrchestrationGetLaneDetailInput = typeof OrchestrationGetLaneDetailInput.Type; + export const OrchestrationThreadDetailSnapshot = Schema.Struct({ snapshotSequence: NonNegativeInt, thread: OrchestrationThread, @@ -766,6 +831,28 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ ThreadUserInputRespondCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, + LaneCreateCommand, + LaneTaskContractUpdateCommand, + LanePreflightRequestCommand, + LaneOrientationRecordCommand, + LanePlanProposeCommand, + LanePlanActivateCommand, + LaneExecutionStartCommand, + LaneTestingStartCommand, + LaneReviewRequestCommand, + LaneDeliverableRegisterCommand, + LaneCompletionRequestCommand, + LaneBlockCommand, + LaneUnblockCommand, + LaneCancelCommand, + LaneSupersedeCommand, + LaneRecoveryRequestCommand, + LaneCompletionInvalidateCommand, + LaneFailCommand, + LaneMetaUpdateCommand, + SourceTruthPreflightRecordCommand, + SourceTruthConflictRecordCommand, + SourceTruthRefreshRequestCommand, ]); export type DispatchableClientOrchestrationCommand = typeof DispatchableClientOrchestrationCommand.Type; @@ -791,6 +878,28 @@ export const ClientOrchestrationCommand = Schema.Union([ ThreadUserInputRespondCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, + LaneCreateCommand, + LaneTaskContractUpdateCommand, + LanePreflightRequestCommand, + LaneOrientationRecordCommand, + LanePlanProposeCommand, + LanePlanActivateCommand, + LaneExecutionStartCommand, + LaneTestingStartCommand, + LaneReviewRequestCommand, + LaneDeliverableRegisterCommand, + LaneCompletionRequestCommand, + LaneBlockCommand, + LaneUnblockCommand, + LaneCancelCommand, + LaneSupersedeCommand, + LaneRecoveryRequestCommand, + LaneCompletionInvalidateCommand, + LaneFailCommand, + LaneMetaUpdateCommand, + SourceTruthPreflightRecordCommand, + SourceTruthConflictRecordCommand, + SourceTruthRefreshRequestCommand, ]); export type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type; @@ -903,10 +1012,21 @@ export const OrchestrationEventType = Schema.Literals([ "thread.proposed-plan-upserted", "thread.turn-diff-completed", "thread.activity-appended", + "lane.created", + "lane.imported", + "lane.state-changed", + "lane.task-contract-updated", + "lane.meta-updated", + "lane.plan-proposed", + "lane.plan-activated", + "lane.deliverable-registered", + "source-truth.preflight-recorded", + "source-truth.conflict-recorded", + "source-truth.refresh-requested", ]); export type OrchestrationEventType = typeof OrchestrationEventType.Type; -export const OrchestrationAggregateKind = Schema.Literals(["project", "thread"]); +export const OrchestrationAggregateKind = Schema.Literals(["project", "thread", "lane"]); export type OrchestrationAggregateKind = typeof OrchestrationAggregateKind.Type; export const OrchestrationActorKind = Schema.Literals(["client", "server", "provider"]); @@ -1119,7 +1239,7 @@ const EventBaseFields = { sequence: NonNegativeInt, eventId: EventId, aggregateKind: OrchestrationAggregateKind, - aggregateId: Schema.Union([ProjectId, ThreadId]), + aggregateId: Schema.Union([ProjectId, ThreadId, WorkLaneId]), occurredAt: IsoDateTime, commandId: Schema.NullOr(CommandId), causationEventId: Schema.NullOr(EventId), @@ -1258,6 +1378,61 @@ export const OrchestrationEvent = Schema.Union([ type: Schema.Literal("thread.activity-appended"), payload: ThreadActivityAppendedPayload, }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.created"), + payload: LaneCreatedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.imported"), + payload: LaneImportedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.state-changed"), + payload: LaneStateChangedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.task-contract-updated"), + payload: LaneTaskContractUpdatedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.meta-updated"), + payload: LaneMetaUpdatedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.plan-proposed"), + payload: LanePlanProposedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.plan-activated"), + payload: LanePlanActivatedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("lane.deliverable-registered"), + payload: LaneDeliverableRegisteredPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("source-truth.preflight-recorded"), + payload: SourceTruthPreflightRecordedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("source-truth.conflict-recorded"), + payload: SourceTruthConflictRecordedPayload, + }), + Schema.Struct({ + ...EventBaseFields, + type: Schema.Literal("source-truth.refresh-requested"), + payload: SourceTruthRefreshRequestedPayload, + }), ]); export type OrchestrationEvent = typeof OrchestrationEvent.Type; @@ -1276,6 +1451,21 @@ export const OrchestrationThreadStreamItem = Schema.Union([ ]); export type OrchestrationThreadStreamItem = typeof OrchestrationThreadStreamItem.Type; +export const OrchestrationLaneStreamItem = Schema.Union([ + Schema.Struct({ + kind: Schema.Literal("synchronized"), + }), + Schema.Struct({ + kind: Schema.Literal("snapshot"), + snapshot: WorkLaneDetailSnapshot, + }), + Schema.Struct({ + kind: Schema.Literal("event"), + event: OrchestrationEvent, + }), +]); +export type OrchestrationLaneStreamItem = typeof OrchestrationLaneStreamItem.Type; + export const OrchestrationCommandReceiptStatus = Schema.Literals(["accepted", "rejected"]); export type OrchestrationCommandReceiptStatus = typeof OrchestrationCommandReceiptStatus.Type; @@ -1387,6 +1577,14 @@ export const OrchestrationRpcSchemas = { input: OrchestrationSubscribeShellInput, output: OrchestrationShellStreamItem, }, + subscribeLane: { + input: OrchestrationSubscribeLaneInput, + output: OrchestrationLaneStreamItem, + }, + getLaneDetail: { + input: OrchestrationGetLaneDetailInput, + output: WorkLaneDetailSnapshot, + }, } as const; export class OrchestrationGetSnapshotError extends Schema.TaggedErrorClass()( diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 0701e15a6689..f3037bcb1314 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -704,6 +704,19 @@ export const WsOrchestrationSubscribeThreadRpc = Rpc.make( }, ); +export const WsOrchestrationSubscribeLaneRpc = Rpc.make(ORCHESTRATION_WS_METHODS.subscribeLane, { + payload: OrchestrationRpcSchemas.subscribeLane.input, + success: OrchestrationRpcSchemas.subscribeLane.output, + error: Schema.Union([OrchestrationGetSnapshotError, EnvironmentAuthorizationError]), + stream: true, +}); + +export const WsOrchestrationGetLaneDetailRpc = Rpc.make(ORCHESTRATION_WS_METHODS.getLaneDetail, { + payload: OrchestrationRpcSchemas.getLaneDetail.input, + success: OrchestrationRpcSchemas.getLaneDetail.output, + error: Schema.Union([OrchestrationGetSnapshotError, EnvironmentAuthorizationError]), +}); + export const WsSubscribeTerminalEventsRpc = Rpc.make(WS_METHODS.subscribeTerminalEvents, { payload: Schema.Struct({}), success: TerminalEvent, @@ -830,4 +843,6 @@ export const WsRpcGroup = RpcGroup.make( WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc, + WsOrchestrationSubscribeLaneRpc, + WsOrchestrationGetLaneDetailRpc, ); diff --git a/packages/contracts/src/sourceTruth.ts b/packages/contracts/src/sourceTruth.ts new file mode 100644 index 000000000000..575d8cecd814 --- /dev/null +++ b/packages/contracts/src/sourceTruth.ts @@ -0,0 +1,133 @@ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { + ArtifactRefId, + AgentAssignmentId, + EnvironmentId, + IsoDateTime, + SourceTruthRevisionId, + ThreadId, + TrimmedNonEmptyString, + WorkLaneId, +} from "./baseSchemas.ts"; +import { RepositoryIdentity } from "./environment.ts"; + +/** + * Canonical claim labels for machine-generated conclusions. + * No synonyms such as confirmed/likely/validated may replace these in persisted evidence. + */ +export const ClaimLabel = Schema.Literals(["PROVEN", "INFERRED", "SUSPECTED", "UNKNOWN"]); +export type ClaimLabel = typeof ClaimLabel.Type; + +/** + * Opaque F0 artifact pointer. Does not implement the F1 receipt/blob store — + * only a forward-compatible reference for large command output. + */ +export const ArtifactReference = Schema.Struct({ + id: ArtifactRefId, + kind: Schema.Literals(["path", "blob", "log"]), + ref: TrimmedNonEmptyString, + contentType: Schema.optional(TrimmedNonEmptyString), + byteLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), +}); +export type ArtifactReference = typeof ArtifactReference.Type; + +export const GitOperationState = Schema.Literals([ + "none", + "merge", + "rebase", + "cherry-pick", + "revert", + "bisect", +]); +export type GitOperationState = typeof GitOperationState.Type; + +export const WorktreeOwnershipOverlapResult = Schema.Literals([ + "exclusive", + "overlap", + "unknown", + "not-applicable", +]); +export type WorktreeOwnershipOverlapResult = typeof WorktreeOwnershipOverlapResult.Type; + +export const SourceTruthDirtyStatus = Schema.Struct({ + fingerprint: Schema.NullOr(TrimmedNonEmptyString), + summary: Schema.NullOr(TrimmedNonEmptyString), + isDirty: Schema.Boolean, +}); +export type SourceTruthDirtyStatus = typeof SourceTruthDirtyStatus.Type; + +export const SourceTruthFileReference = Schema.Struct({ + path: TrimmedNonEmptyString, + role: Schema.optional( + Schema.Literals(["instruction", "manifest", "relevant", "test", "generated", "vendored"]), + ), +}); +export type SourceTruthFileReference = typeof SourceTruthFileReference.Type; + +export const SourceTruthRevision = Schema.Struct({ + id: SourceTruthRevisionId, + laneId: WorkLaneId, + repositoryIdentity: Schema.NullOr(RepositoryIdentity), + repositoryRoot: Schema.NullOr(TrimmedNonEmptyString), + branch: Schema.NullOr(TrimmedNonEmptyString), + detached: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + headSha: Schema.NullOr(TrimmedNonEmptyString), + baseSha: Schema.NullOr(TrimmedNonEmptyString), + worktreePath: Schema.NullOr(TrimmedNonEmptyString), + dirty: SourceTruthDirtyStatus, + instructionFiles: Schema.Array(SourceTruthFileReference).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + manifests: Schema.Array(SourceTruthFileReference).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + buildTestCandidates: Schema.Array(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + relevantFiles: Schema.Array(SourceTruthFileReference).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + relevantTests: Schema.Array(SourceTruthFileReference).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + activeGitOperation: GitOperationState.pipe(Schema.withDecodingDefault(Effect.succeed("none"))), + ownershipOverlap: WorktreeOwnershipOverlapResult.pipe( + Schema.withDecodingDefault(Effect.succeed("unknown")), + ), + canonicalExternalSourceRefs: Schema.Array(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + unknownsThatChangeAction: Schema.Array(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + safeNextAction: Schema.NullOr(TrimmedNonEmptyString), + producedAt: IsoDateTime, + producerAssignmentId: Schema.NullOr(AgentAssignmentId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + producerThreadId: Schema.NullOr(ThreadId).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + environmentId: Schema.optional(EnvironmentId), + rawOutputArtifactRef: Schema.NullOr(ArtifactReference).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + supersededAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + supersedesRevisionId: Schema.NullOr(SourceTruthRevisionId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), +}); +export type SourceTruthRevision = typeof SourceTruthRevision.Type; + +/** Compact shell fields — never embed full revision payloads in shell records. */ +export const SourceTruthRevisionShellSummary = Schema.Struct({ + revisionId: SourceTruthRevisionId, + branch: Schema.NullOr(TrimmedNonEmptyString), + headSha: Schema.NullOr(TrimmedNonEmptyString), + worktreePath: Schema.NullOr(TrimmedNonEmptyString), + isDirty: Schema.Boolean, + activeGitOperation: GitOperationState, + ownershipOverlap: WorktreeOwnershipOverlapResult, + producedAt: IsoDateTime, +}); +export type SourceTruthRevisionShellSummary = typeof SourceTruthRevisionShellSummary.Type; diff --git a/packages/contracts/src/workLane.test.ts b/packages/contracts/src/workLane.test.ts new file mode 100644 index 000000000000..262ad6c5b5ca --- /dev/null +++ b/packages/contracts/src/workLane.test.ts @@ -0,0 +1,203 @@ +import { assert, describe, it } from "@effect/vitest"; +import * as Schema from "effect/Schema"; + +import { + AcceptanceCriterionId, + EnvironmentId, + ProjectId, + SourceTruthRevisionId, + ThreadId, + WorkLaneId, +} from "./baseSchemas.ts"; +import { + isAllowedWorkLaneTransition, + TaskContract, + WORK_LANE_NORMAL_TRANSITIONS, + WorkLane, + WorkLaneShell, + WorkLaneState, + importedWorkLaneIdForThread, +} from "./workLane.ts"; +import { SourceTruthRevision } from "./sourceTruth.ts"; +import { OrchestrationShellSnapshot } from "./orchestration.ts"; + +const decodeWorkLane = Schema.decodeUnknownSync(WorkLane); +const decodeShell = Schema.decodeUnknownSync(WorkLaneShell); +const decodeTaskContract = Schema.decodeUnknownSync(TaskContract); +const decodeRevision = Schema.decodeUnknownSync(SourceTruthRevision); +const decodeShellSnapshot = Schema.decodeUnknownSync(OrchestrationShellSnapshot); + +describe("workLane contracts", () => { + it("roundtrips a minimal WorkLane", () => { + const lane = decodeWorkLane({ + id: "lane-1", + projectId: "project-1", + title: "Ship F0", + taskContract: { + objective: "Add work lanes", + constraints: [{ kind: "scope", summary: "F0 only" }], + nonGoals: ["full UI"], + deliverableRequirement: "required", + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: ["edit", "test"], + prohibitedActions: ["force-push"], + completionReportRequired: true, + objectiveDerivation: "PROVEN", + }, + state: "queued", + priority: "normal", + classification: "substantial", + environmentId: "env-1", + repositoryIdentity: null, + baseRef: null, + branch: null, + worktreePath: null, + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + activePlanRevisionId: null, + acceptanceCriterionIds: [], + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId: null, + importedThreadId: null, + threadIds: [], + legacyExecutorRef: null, + resumeState: null, + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + completedAt: null, + }); + + assert.strictEqual(lane.id, WorkLaneId.make("lane-1")); + assert.strictEqual(lane.environmentId, EnvironmentId.make("env-1")); + assert.strictEqual(lane.projectId, ProjectId.make("project-1")); + }); + + it("rejects invalid lifecycle states", () => { + assert.throws(() => Schema.decodeUnknownSync(WorkLaneState)("done")); + }); + + it("rejects untyped permission blobs on task contracts", () => { + assert.throws(() => + decodeTaskContract({ + objective: "x", + constraints: "allow-all", + nonGoals: [], + deliverableRequirement: "none", + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: ["not-a-real-action"], + prohibitedActions: [], + completionReportRequired: true, + }), + ); + }); + + it("defaults shell snapshot lanes to empty for legacy payloads", () => { + const snapshot = decodeShellSnapshot({ + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-30T00:00:00.000Z", + }); + assert.deepStrictEqual(snapshot.lanes, []); + }); + + it("derives stable imported lane ids from threads", () => { + const threadId = ThreadId.make("thread-abc"); + assert.strictEqual( + importedWorkLaneIdForThread(threadId), + WorkLaneId.make("lane:import:thread-abc"), + ); + }); +}); + +describe("workLane transition matrix", () => { + it("allows every documented normal transition", () => { + for (const [from, tos] of Object.entries(WORK_LANE_NORMAL_TRANSITIONS) as Array< + [WorkLaneState, ReadonlyArray] + >) { + for (const to of tos) { + assert.isTrue(isAllowedWorkLaneTransition(from, to)); + } + } + }); + + it("disallows jumping from queued to executing", () => { + assert.isFalse(isAllowedWorkLaneTransition("queued", "executing")); + }); + + it("disallows leaving cancelled or superseded", () => { + assert.isFalse(isAllowedWorkLaneTransition("cancelled", "queued")); + assert.isFalse(isAllowedWorkLaneTransition("superseded", "preflight")); + }); +}); + +describe("sourceTruth contracts", () => { + it("roundtrips a revision and keeps large output as a reference", () => { + const revision = decodeRevision({ + id: "str-1", + laneId: "lane-1", + repositoryIdentity: null, + repositoryRoot: "/repo", + branch: "cursor/f0", + detached: false, + headSha: "abc", + baseSha: "def", + worktreePath: "/repo/.worktrees/f0", + dirty: { fingerprint: "fp", summary: "clean", isDirty: false }, + instructionFiles: [{ path: "AGENTS.md", role: "instruction" }], + manifests: [], + buildTestCandidates: ["vp test"], + relevantFiles: [], + relevantTests: [], + activeGitOperation: "none", + ownershipOverlap: "exclusive", + canonicalExternalSourceRefs: [], + unknownsThatChangeAction: [], + safeNextAction: "implement contracts", + producedAt: "2026-07-30T00:00:00.000Z", + producerAssignmentId: null, + producerThreadId: null, + rawOutputArtifactRef: { + id: "art-1", + kind: "log", + ref: "artifacts/preflight.log", + }, + supersededAt: null, + supersedesRevisionId: null, + }); + + assert.strictEqual(revision.id, SourceTruthRevisionId.make("str-1")); + assert.strictEqual(revision.rawOutputArtifactRef?.ref, "artifacts/preflight.log"); + }); + + it("keeps WorkLaneShell compact without embedding task contracts", () => { + const shell = decodeShell({ + id: "lane-1", + projectId: "project-1", + title: "Ship F0", + state: "queued", + priority: "normal", + classification: "substantial", + environmentId: "env-1", + branch: null, + worktreePath: null, + sourceTruthRevisionId: null, + sourceTruthSummary: null, + primaryThreadId: null, + importedThreadId: null, + objectiveSummary: "Add work lanes", + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + completedAt: null, + }); + assert.isFalse("taskContract" in shell); + assert.strictEqual(shell.objectiveSummary, "Add work lanes"); + assert.isTrue(Boolean(AcceptanceCriterionId.make("ac-1"))); + }); +}); diff --git a/packages/contracts/src/workLane.ts b/packages/contracts/src/workLane.ts new file mode 100644 index 000000000000..1114583c3967 --- /dev/null +++ b/packages/contracts/src/workLane.ts @@ -0,0 +1,668 @@ +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { + AcceptanceCriterionId, + AgentAssignmentId, + BlockerId, + CommandId, + DeliverableId, + EnvironmentId, + IsoDateTime, + NonNegativeInt, + PlanRevisionId, + ProjectId, + ReceiptId, + RuntimeSessionId, + SourceTruthRevisionId, + ThreadId, + TrimmedNonEmptyString, + WorkLaneId, +} from "./baseSchemas.ts"; +import { RepositoryIdentity } from "./environment.ts"; +import { ClaimLabel, SourceTruthRevision, SourceTruthRevisionShellSummary } from "./sourceTruth.ts"; + +export const WorkLaneState = Schema.Literals([ + "queued", + "preflight", + "oriented", + "planned", + "executing", + "testing", + "reviewing", + "deliverable-ready", + "completed", + "blocked", + "failed", + "cancelled", + "superseded", + "recovery-required", +]); +export type WorkLaneState = typeof WorkLaneState.Type; + +export const WorkPriority = Schema.Literals(["low", "normal", "high", "urgent"]); +export type WorkPriority = typeof WorkPriority.Type; + +export const WorkLaneClassification = Schema.Literals([ + "substantial", + "bounded-readonly", + "tiny-reversible", +]); +export type WorkLaneClassification = typeof WorkLaneClassification.Type; + +export const WORK_LANE_TERMINAL_STATES = [ + "completed", + "cancelled", + "superseded", +] as const satisfies ReadonlyArray; + +/** States that still own a worktree for exclusive-ownership checks. */ +export const WORK_LANE_WORKTREE_OWNING_STATES = [ + "queued", + "preflight", + "oriented", + "planned", + "executing", + "testing", + "reviewing", + "deliverable-ready", + "blocked", + "failed", + "recovery-required", +] as const satisfies ReadonlyArray; + +export const WORK_LANE_NORMAL_TRANSITIONS: Readonly< + Record> +> = { + queued: ["preflight"], + preflight: ["oriented", "blocked", "cancelled"], + oriented: ["planned", "blocked", "cancelled"], + planned: ["executing", "blocked", "cancelled"], + executing: ["testing", "blocked", "failed", "recovery-required", "cancelled"], + testing: ["executing", "reviewing", "blocked", "failed", "recovery-required"], + reviewing: [ + "executing", + "testing", + "deliverable-ready", + "blocked", + "failed", + "recovery-required", + ], + "deliverable-ready": ["completed", "executing", "testing", "reviewing", "blocked"], + completed: ["recovery-required"], + blocked: [], // unblock restores resumeState via command path + failed: ["recovery-required", "preflight"], + cancelled: [], + superseded: [], + "recovery-required": ["preflight"], +}; + +export function isWorkLaneTerminalState(state: WorkLaneState): boolean { + return (WORK_LANE_TERMINAL_STATES as ReadonlyArray).includes(state); +} + +export function isWorkLaneWorktreeOwningState(state: WorkLaneState): boolean { + return (WORK_LANE_WORKTREE_OWNING_STATES as ReadonlyArray).includes(state); +} + +export function isAllowedWorkLaneTransition(from: WorkLaneState, to: WorkLaneState): boolean { + return WORK_LANE_NORMAL_TRANSITIONS[from].includes(to); +} + +export const ConstraintKind = Schema.Literals([ + "scope", + "safety", + "compatibility", + "performance", + "security", + "policy", + "other", +]); +export type ConstraintKind = typeof ConstraintKind.Type; + +export const TaskConstraint = Schema.Struct({ + kind: ConstraintKind, + summary: TrimmedNonEmptyString, +}); +export type TaskConstraint = typeof TaskConstraint.Type; + +export const AuthorizedAction = Schema.Literals([ + "read", + "edit", + "test", + "commit", + "push", + "open-pr", + "install-deps", + "run-migration", + "other", +]); +export type AuthorizedAction = typeof AuthorizedAction.Type; + +export const ProhibitedAction = Schema.Literals([ + "force-push", + "delete-branch", + "touch-live-userdata", + "kill-by-pattern", + "open-unapproved-pr", + "mutate-historical-events", + "other", +]); +export type ProhibitedAction = typeof ProhibitedAction.Type; + +export const TaskContract = Schema.Struct({ + objective: TrimmedNonEmptyString, + constraints: Schema.Array(TaskConstraint).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + nonGoals: Schema.Array(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + deliverableRequirement: Schema.Literals(["required", "none"]), + requiresPullRequest: Schema.Boolean, + requiresUserVisibleSurface: Schema.Boolean, + authorizedActions: Schema.Array(AuthorizedAction).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + prohibitedActions: Schema.Array(ProhibitedAction).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + completionReportRequired: Schema.Literal(true).pipe( + Schema.withDecodingDefault(Effect.succeed(true as const)), + ), + objectiveDerivation: ClaimLabel.pipe( + Schema.withDecodingDefault(Effect.succeed("PROVEN" as const)), + ), +}); +export type TaskContract = typeof TaskContract.Type; + +export const AcceptanceCriterionCategory = Schema.Literals([ + "foundation", + "correctness", + "reproducibility", + "test", + "delivery", + "performance", + "security", +]); +export type AcceptanceCriterionCategory = typeof AcceptanceCriterionCategory.Type; + +export const CriterionStatus = Schema.Literals([ + "pending", + "in-progress", + "satisfied", + "failed", + "waived", +]); +export type CriterionStatus = typeof CriterionStatus.Type; + +export const AcceptanceCriterion = Schema.Struct({ + id: AcceptanceCriterionId, + laneId: WorkLaneId, + description: TrimmedNonEmptyString, + category: AcceptanceCriterionCategory, + required: Schema.Boolean, + status: CriterionStatus.pipe(Schema.withDecodingDefault(Effect.succeed("pending" as const))), + supportingReceiptIds: Schema.Array(ReceiptId).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), +}); +export type AcceptanceCriterion = typeof AcceptanceCriterion.Type; + +/** + * Forward-compatible executor association for migrated threads. + * Not a full AgentAssignment aggregate (F3). + */ +export const LegacyExecutorRef = Schema.Struct({ + threadId: ThreadId, + runtimeSessionId: Schema.optional(Schema.NullOr(RuntimeSessionId)), + providerName: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + sessionStatus: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), +}); +export type LegacyExecutorRef = typeof LegacyExecutorRef.Type; + +export const GitRef = Schema.Struct({ + name: TrimmedNonEmptyString, + sha: Schema.NullOr(TrimmedNonEmptyString).pipe(Schema.withDecodingDefault(Effect.succeed(null))), +}); +export type GitRef = typeof GitRef.Type; + +export const WorkLane = Schema.Struct({ + id: WorkLaneId, + projectId: ProjectId, + title: TrimmedNonEmptyString, + taskContract: TaskContract, + state: WorkLaneState, + priority: WorkPriority.pipe(Schema.withDecodingDefault(Effect.succeed("normal" as const))), + classification: WorkLaneClassification.pipe( + Schema.withDecodingDefault(Effect.succeed("substantial" as const)), + ), + environmentId: EnvironmentId, + repositoryIdentity: Schema.NullOr(RepositoryIdentity).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + baseRef: Schema.NullOr(GitRef).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + branch: Schema.NullOr(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + worktreePath: Schema.NullOr(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + ownerAssignmentId: Schema.NullOr(AgentAssignmentId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + advisorAssignmentIds: Schema.Array(AgentAssignmentId).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + verifierAssignmentIds: Schema.Array(AgentAssignmentId).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + sourceTruthRevisionId: Schema.NullOr(SourceTruthRevisionId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + activePlanRevisionId: Schema.NullOr(PlanRevisionId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + acceptanceCriterionIds: Schema.Array(AcceptanceCriterionId).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + requiredReceiptKinds: Schema.Array(TrimmedNonEmptyString).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + deliverableIds: Schema.Array(DeliverableId).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + blockerIds: Schema.Array(BlockerId).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + primaryThreadId: Schema.NullOr(ThreadId).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + importedThreadId: Schema.NullOr(ThreadId).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + threadIds: Schema.Array(ThreadId).pipe(Schema.withDecodingDefault(Effect.succeed([]))), + legacyExecutorRef: Schema.NullOr(LegacyExecutorRef).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + resumeState: Schema.NullOr(WorkLaneState).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + createdAt: IsoDateTime, + updatedAt: IsoDateTime, + completedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), +}); +export type WorkLane = typeof WorkLane.Type; + +/** Compact command-center / shell record — no full task contract or source-truth body. */ +export const WorkLaneShell = Schema.Struct({ + id: WorkLaneId, + projectId: ProjectId, + title: TrimmedNonEmptyString, + state: WorkLaneState, + priority: WorkPriority, + classification: WorkLaneClassification, + environmentId: EnvironmentId, + branch: Schema.NullOr(TrimmedNonEmptyString), + worktreePath: Schema.NullOr(TrimmedNonEmptyString), + sourceTruthRevisionId: Schema.NullOr(SourceTruthRevisionId), + sourceTruthSummary: Schema.NullOr(SourceTruthRevisionShellSummary).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + primaryThreadId: Schema.NullOr(ThreadId), + importedThreadId: Schema.NullOr(ThreadId), + objectiveSummary: TrimmedNonEmptyString, + createdAt: IsoDateTime, + updatedAt: IsoDateTime, + completedAt: Schema.NullOr(IsoDateTime), +}); +export type WorkLaneShell = typeof WorkLaneShell.Type; + +export const WorkLaneDetail = Schema.Struct({ + lane: WorkLane, + acceptanceCriteria: Schema.Array(AcceptanceCriterion).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + sourceTruthRevisions: Schema.Array(SourceTruthRevision).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), +}); +export type WorkLaneDetail = typeof WorkLaneDetail.Type; + +export const WorkLaneDetailSnapshot = Schema.Struct({ + snapshotSequence: NonNegativeInt, + detail: WorkLaneDetail, +}); +export type WorkLaneDetailSnapshot = typeof WorkLaneDetailSnapshot.Type; + +export function toWorkLaneShell( + lane: WorkLane, + sourceTruthSummary: SourceTruthRevisionShellSummary | null = null, +): WorkLaneShell { + return { + id: lane.id, + projectId: lane.projectId, + title: lane.title, + state: lane.state, + priority: lane.priority, + classification: lane.classification, + environmentId: lane.environmentId, + branch: lane.branch, + worktreePath: lane.worktreePath, + sourceTruthRevisionId: lane.sourceTruthRevisionId, + sourceTruthSummary, + primaryThreadId: lane.primaryThreadId, + importedThreadId: lane.importedThreadId, + objectiveSummary: lane.taskContract.objective, + createdAt: lane.createdAt, + updatedAt: lane.updatedAt, + completedAt: lane.completedAt, + }; +} + +/** Stable imported WorkLaneId derived from a legacy ThreadId (distinct brand). */ +export function importedWorkLaneIdForThread(threadId: ThreadId): WorkLaneId { + return WorkLaneId.make(`lane:import:${threadId}`); +} + +// --- Commands --- + +export const LaneCreateCommand = Schema.Struct({ + type: Schema.Literal("lane.create"), + commandId: CommandId, + laneId: WorkLaneId, + projectId: ProjectId, + title: TrimmedNonEmptyString, + taskContract: TaskContract, + priority: WorkPriority.pipe(Schema.withDecodingDefault(Effect.succeed("normal" as const))), + classification: WorkLaneClassification.pipe( + Schema.withDecodingDefault(Effect.succeed("substantial" as const)), + ), + environmentId: EnvironmentId, + repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), + baseRef: Schema.optional(Schema.NullOr(GitRef)), + branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + primaryThreadId: Schema.optional(Schema.NullOr(ThreadId)), + acceptanceCriteria: Schema.optional(Schema.Array(AcceptanceCriterion)), + createdAt: IsoDateTime, +}); + +export const LaneTaskContractUpdateCommand = Schema.Struct({ + type: Schema.Literal("lane.task-contract.update"), + commandId: CommandId, + laneId: WorkLaneId, + taskContract: TaskContract, + updatedAt: IsoDateTime, +}); + +export const LanePreflightRequestCommand = Schema.Struct({ + type: Schema.Literal("lane.preflight.request"), + commandId: CommandId, + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const LaneOrientationRecordCommand = Schema.Struct({ + type: Schema.Literal("lane.orientation.record"), + commandId: CommandId, + laneId: WorkLaneId, + recordedAt: IsoDateTime, +}); + +export const LanePlanProposeCommand = Schema.Struct({ + type: Schema.Literal("lane.plan.propose"), + commandId: CommandId, + laneId: WorkLaneId, + planRevisionId: PlanRevisionId, + proposedAt: IsoDateTime, +}); + +export const LanePlanActivateCommand = Schema.Struct({ + type: Schema.Literal("lane.plan.activate"), + commandId: CommandId, + laneId: WorkLaneId, + planRevisionId: PlanRevisionId, + activatedAt: IsoDateTime, +}); + +export const LaneExecutionStartCommand = Schema.Struct({ + type: Schema.Literal("lane.execution.start"), + commandId: CommandId, + laneId: WorkLaneId, + startedAt: IsoDateTime, +}); + +export const LaneTestingStartCommand = Schema.Struct({ + type: Schema.Literal("lane.testing.start"), + commandId: CommandId, + laneId: WorkLaneId, + startedAt: IsoDateTime, +}); + +export const LaneReviewRequestCommand = Schema.Struct({ + type: Schema.Literal("lane.review.request"), + commandId: CommandId, + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const LaneDeliverableRegisterCommand = Schema.Struct({ + type: Schema.Literal("lane.deliverable.register"), + commandId: CommandId, + laneId: WorkLaneId, + deliverableId: DeliverableId, + registeredAt: IsoDateTime, +}); + +export const LaneCompletionRequestCommand = Schema.Struct({ + type: Schema.Literal("lane.completion.request"), + commandId: CommandId, + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const LaneBlockCommand = Schema.Struct({ + type: Schema.Literal("lane.block"), + commandId: CommandId, + laneId: WorkLaneId, + blockerId: Schema.optional(BlockerId), + reason: Schema.optional(TrimmedNonEmptyString), + blockedAt: IsoDateTime, +}); + +export const LaneUnblockCommand = Schema.Struct({ + type: Schema.Literal("lane.unblock"), + commandId: CommandId, + laneId: WorkLaneId, + unblockedAt: IsoDateTime, +}); + +export const LaneCancelCommand = Schema.Struct({ + type: Schema.Literal("lane.cancel"), + commandId: CommandId, + laneId: WorkLaneId, + cancelledAt: IsoDateTime, +}); + +export const LaneSupersedeCommand = Schema.Struct({ + type: Schema.Literal("lane.supersede"), + commandId: CommandId, + laneId: WorkLaneId, + supersedingLaneId: Schema.optional(WorkLaneId), + supersededAt: IsoDateTime, +}); + +export const LaneRecoveryRequestCommand = Schema.Struct({ + type: Schema.Literal("lane.recovery.request"), + commandId: CommandId, + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const LaneCompletionInvalidateCommand = Schema.Struct({ + type: Schema.Literal("lane.completion.invalidate"), + commandId: CommandId, + laneId: WorkLaneId, + reason: Schema.optional(TrimmedNonEmptyString), + invalidatedAt: IsoDateTime, +}); + +export const LaneFailCommand = Schema.Struct({ + type: Schema.Literal("lane.fail"), + commandId: CommandId, + laneId: WorkLaneId, + reason: Schema.optional(TrimmedNonEmptyString), + failedAt: IsoDateTime, +}); + +export const LaneMetaUpdateCommand = Schema.Struct({ + type: Schema.Literal("lane.meta.update"), + commandId: CommandId, + laneId: WorkLaneId, + title: Schema.optional(TrimmedNonEmptyString), + priority: Schema.optional(WorkPriority), + classification: Schema.optional(WorkLaneClassification), + branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + baseRef: Schema.optional(Schema.NullOr(GitRef)), + repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), + updatedAt: IsoDateTime, +}); + +export const SourceTruthPreflightRecordCommand = Schema.Struct({ + type: Schema.Literal("source-truth.preflight.record"), + commandId: CommandId, + laneId: WorkLaneId, + revision: SourceTruthRevision, + recordedAt: IsoDateTime, +}); + +export const SourceTruthConflictRecordCommand = Schema.Struct({ + type: Schema.Literal("source-truth.conflict.record"), + commandId: CommandId, + laneId: WorkLaneId, + summary: TrimmedNonEmptyString, + recordedAt: IsoDateTime, +}); + +export const SourceTruthRefreshRequestCommand = Schema.Struct({ + type: Schema.Literal("source-truth.refresh.request"), + commandId: CommandId, + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const WorkLaneClientCommand = Schema.Union([ + LaneCreateCommand, + LaneTaskContractUpdateCommand, + LanePreflightRequestCommand, + LaneOrientationRecordCommand, + LanePlanProposeCommand, + LanePlanActivateCommand, + LaneExecutionStartCommand, + LaneTestingStartCommand, + LaneReviewRequestCommand, + LaneDeliverableRegisterCommand, + LaneCompletionRequestCommand, + LaneBlockCommand, + LaneUnblockCommand, + LaneCancelCommand, + LaneSupersedeCommand, + LaneRecoveryRequestCommand, + LaneCompletionInvalidateCommand, + LaneFailCommand, + LaneMetaUpdateCommand, + SourceTruthPreflightRecordCommand, + SourceTruthConflictRecordCommand, + SourceTruthRefreshRequestCommand, +]); +export type WorkLaneClientCommand = typeof WorkLaneClientCommand.Type; + +// --- Event payloads --- + +export const LaneCreatedPayload = Schema.Struct({ + lane: WorkLane, + acceptanceCriteria: Schema.Array(AcceptanceCriterion).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), +}); + +export const LaneImportedPayload = Schema.Struct({ + lane: WorkLane, + acceptanceCriteria: Schema.Array(AcceptanceCriterion).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + ), + importedFromThreadId: ThreadId, + importReason: TrimmedNonEmptyString, +}); + +export const LaneStateChangedPayload = Schema.Struct({ + laneId: WorkLaneId, + fromState: WorkLaneState, + toState: WorkLaneState, + resumeState: Schema.NullOr(WorkLaneState).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + reason: Schema.optional(TrimmedNonEmptyString), + updatedAt: IsoDateTime, +}); + +export const LaneTaskContractUpdatedPayload = Schema.Struct({ + laneId: WorkLaneId, + taskContract: TaskContract, + updatedAt: IsoDateTime, +}); + +export const LaneMetaUpdatedPayload = Schema.Struct({ + laneId: WorkLaneId, + title: Schema.optional(TrimmedNonEmptyString), + priority: Schema.optional(WorkPriority), + classification: Schema.optional(WorkLaneClassification), + branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), + baseRef: Schema.optional(Schema.NullOr(GitRef)), + repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), + updatedAt: IsoDateTime, +}); + +export const LanePlanProposedPayload = Schema.Struct({ + laneId: WorkLaneId, + planRevisionId: PlanRevisionId, + proposedAt: IsoDateTime, +}); + +export const LanePlanActivatedPayload = Schema.Struct({ + laneId: WorkLaneId, + planRevisionId: PlanRevisionId, + activatedAt: IsoDateTime, + updatedAt: IsoDateTime, +}); + +export const LaneDeliverableRegisteredPayload = Schema.Struct({ + laneId: WorkLaneId, + deliverableId: DeliverableId, + registeredAt: IsoDateTime, + updatedAt: IsoDateTime, +}); + +export const SourceTruthPreflightRecordedPayload = Schema.Struct({ + laneId: WorkLaneId, + revision: SourceTruthRevision, + previousRevisionId: Schema.NullOr(SourceTruthRevisionId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), + recordedAt: IsoDateTime, +}); + +export const SourceTruthConflictRecordedPayload = Schema.Struct({ + laneId: WorkLaneId, + summary: TrimmedNonEmptyString, + recordedAt: IsoDateTime, +}); + +export const SourceTruthRefreshRequestedPayload = Schema.Struct({ + laneId: WorkLaneId, + requestedAt: IsoDateTime, +}); + +export const WorkLaneEventType = Schema.Literals([ + "lane.created", + "lane.imported", + "lane.state-changed", + "lane.task-contract-updated", + "lane.meta-updated", + "lane.plan-proposed", + "lane.plan-activated", + "lane.deliverable-registered", + "source-truth.preflight-recorded", + "source-truth.conflict-recorded", + "source-truth.refresh-requested", +]); +export type WorkLaneEventType = typeof WorkLaneEventType.Type; From 4e9c2ca81561cee787efa49373c66c12c8545b20 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 19:45:45 +0000 Subject: [PATCH 31/32] fix(orchestration): harden work-lane foundation before pull Narrow worktree ownership, fix migration environment id and projection cursor seeding, slim shell SQL, allow execution re-entry, and close P1 gate gaps so F0 is safe to pull locally. Co-authored-by: Nayte --- apps/server/src/laneResume.ts | 12 + .../Layers/ProjectionPipeline.ts | 15 +- .../Layers/ProjectionSnapshotQuery.ts | 179 +++++++--- apps/server/src/orchestration/decider.ts | 136 +++++++- .../orchestration/decider.workLane.test.ts | 320 +++++++++++++++++- apps/server/src/orchestration/projector.ts | 44 ++- .../orchestration/projector.workLane.test.ts | 5 + .../ProjectionLaneAcceptanceCriteria.ts | 1 + .../Layers/ProjectionSourceTruthRevisions.ts | 1 + .../persistence/Layers/ProjectionWorkLanes.ts | 1 + .../035_WorkLanesAndSourceTruth.test.ts | 139 +++++++- .../Migrations/035_WorkLanesAndSourceTruth.ts | 112 ++++-- apps/server/src/ws.laneResume.test.ts | 17 + apps/server/src/ws.ts | 62 +++- docs/macbrains/receipts/F0_HARDEN.md | 45 +++ .../src/state/shellReducer.test.ts | 67 ++++ .../client-runtime/src/state/workLanes.ts | 17 +- packages/contracts/src/orchestration.ts | 47 +-- packages/contracts/src/workLane.test.ts | 18 + packages/contracts/src/workLane.ts | 39 ++- 20 files changed, 1090 insertions(+), 187 deletions(-) create mode 100644 apps/server/src/laneResume.ts create mode 100644 apps/server/src/ws.laneResume.test.ts create mode 100644 docs/macbrains/receipts/F0_HARDEN.md diff --git a/apps/server/src/laneResume.ts b/apps/server/src/laneResume.ts new file mode 100644 index 000000000000..a7db15dfb6d8 --- /dev/null +++ b/apps/server/src/laneResume.ts @@ -0,0 +1,12 @@ +/** Default bound matching SHELL_RESUME_MAX_GAP / event-store page size. */ +export const LANE_RESUME_MAX_GAP = 1_000; + +/** Prefer a detail snapshot when the resume cursor is missing or too far behind. */ +export function shouldResumeLaneWithSnapshot( + latestSequence: number, + afterSequence: number, + maxGap: number = LANE_RESUME_MAX_GAP, +): boolean { + const replayGap = latestSequence - afterSequence; + return replayGap < 0 || replayGap > maxGap; +} diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 38db9da4ac71..87645b944048 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -1606,11 +1606,21 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti const lane = existing.value.lane; let next = lane; switch (event.type) { - case "lane.state-changed": + case "lane.state-changed": { + const blockerIds = + event.payload.blockerId === undefined + ? lane.blockerIds + : lane.blockerIds.includes(event.payload.blockerId) + ? lane.blockerIds + : [...lane.blockerIds, event.payload.blockerId]; next = { ...lane, state: event.payload.toState, resumeState: event.payload.resumeState, + blockerIds, + ...(event.payload.supersedingLaneId !== undefined + ? { supersedingLaneId: event.payload.supersedingLaneId } + : {}), updatedAt: event.payload.updatedAt, completedAt: event.payload.toState === "completed" @@ -1620,6 +1630,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti : lane.completedAt, }; break; + } case "lane.task-contract-updated": next = { ...lane, @@ -1713,6 +1724,8 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti { ...existing.value.lane, sourceTruthRevisionId: event.payload.revision.id, + sourceTruthActiveGitOperation: event.payload.revision.activeGitOperation, + sourceTruthOwnershipOverlap: event.payload.revision.ownershipOverlap, updatedAt: event.payload.recordedAt, }, event.sequence, diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 40eaff46c404..b1fdcdd2f159 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -22,15 +22,20 @@ import { type OrchestrationSession, type OrchestrationThreadActivity, type OrchestrationThreadShell, + EnvironmentId, ModelSelection, ProjectId, SourceTruthRevision, + SourceTruthRevisionId, ThreadId, + TrimmedNonEmptyString, WorkLane, + WorkLaneClassification, WorkLaneDetailSnapshot, WorkLaneId, WorkLaneShell, - toWorkLaneShell, + WorkLaneState, + WorkPriority, type SourceTruthRevisionShellSummary, } from "@t3tools/contracts"; import * as Arr from "effect/Array"; @@ -152,6 +157,25 @@ const ProjectionWorkLaneDbRowSchema = Schema.Struct({ lane: Schema.fromJsonString(WorkLane), updatedAt: IsoDateTime, }); +const ProjectionWorkLaneShellDbRowSchema = Schema.Struct({ + id: WorkLaneId, + projectId: ProjectId, + title: TrimmedNonEmptyString, + state: WorkLaneState, + priority: WorkPriority, + classification: WorkLaneClassification, + environmentId: EnvironmentId, + branch: Schema.NullOr(TrimmedNonEmptyString), + worktreePath: Schema.NullOr(TrimmedNonEmptyString), + sourceTruthRevisionId: Schema.NullOr(SourceTruthRevisionId), + primaryThreadId: Schema.NullOr(ThreadId), + importedThreadId: Schema.NullOr(ThreadId), + objectiveSummary: TrimmedNonEmptyString, + createdAt: IsoDateTime, + updatedAt: IsoDateTime, + completedAt: Schema.NullOr(IsoDateTime), + sourceTruthRevision: Schema.NullOr(Schema.fromJsonString(SourceTruthRevision)), +}); const ProjectionSourceTruthRevisionDbRowSchema = Schema.Struct({ id: Schema.String, laneId: WorkLaneId, @@ -167,6 +191,9 @@ const ProjectionLaneAcceptanceCriterionDbRowSchema = Schema.Struct({ const LaneIdLookupInput = Schema.Struct({ laneId: WorkLaneId, }); +const SourceTruthRevisionIdLookupInput = Schema.Struct({ + revisionId: SourceTruthRevisionId, +}); function toSourceTruthShellSummary( revision: SourceTruthRevision, @@ -183,6 +210,31 @@ function toSourceTruthShellSummary( }; } +function shellRowToWorkLaneShell( + row: Schema.Schema.Type, +): WorkLaneShell { + return { + id: row.id, + projectId: row.projectId, + title: row.title, + state: row.state, + priority: row.priority, + classification: row.classification, + environmentId: row.environmentId, + branch: row.branch, + worktreePath: row.worktreePath, + sourceTruthRevisionId: row.sourceTruthRevisionId, + sourceTruthSummary: + row.sourceTruthRevision === null ? null : toSourceTruthShellSummary(row.sourceTruthRevision), + primaryThreadId: row.primaryThreadId, + importedThreadId: row.importedThreadId, + objectiveSummary: row.objectiveSummary, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + completedAt: row.completedAt, + }; +} + const REQUIRED_SNAPSHOT_PROJECTORS = [ ORCHESTRATION_PROJECTOR_NAMES.projects, ORCHESTRATION_PROJECTOR_NAMES.threads, @@ -1005,19 +1057,34 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { `, }); - const listSourceTruthRevisionRows = SqlSchema.findAll({ + const listWorkLaneShellRows = SqlSchema.findAll({ Request: Schema.Void, - Result: ProjectionSourceTruthRevisionDbRowSchema, + Result: ProjectionWorkLaneShellDbRowSchema, execute: () => sql` SELECT - id, - lane_id AS "laneId", - revision_json AS "revision", - produced_at AS "producedAt", - superseded_at AS "supersededAt" - FROM projection_source_truth_revisions - ORDER BY produced_at ASC, id ASC + wl.id, + wl.project_id AS "projectId", + wl.title, + wl.state, + wl.priority, + wl.classification, + wl.environment_id AS "environmentId", + wl.branch, + wl.worktree_path AS "worktreePath", + wl.source_truth_revision_id AS "sourceTruthRevisionId", + wl.primary_thread_id AS "primaryThreadId", + wl.imported_thread_id AS "importedThreadId", + wl.objective_summary AS "objectiveSummary", + wl.created_at AS "createdAt", + wl.updated_at AS "updatedAt", + wl.completed_at AS "completedAt", + str.revision_json AS "sourceTruthRevision" + FROM projection_work_lanes wl + LEFT JOIN projection_source_truth_revisions str + ON str.id = wl.source_truth_revision_id + AND str.superseded_at IS NULL + ORDER BY wl.created_at ASC, wl.id ASC `, }); @@ -1035,6 +1102,50 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { `, }); + const getWorkLaneShellRow = SqlSchema.findOneOption({ + Request: LaneIdLookupInput, + Result: ProjectionWorkLaneShellDbRowSchema, + execute: ({ laneId }) => + sql` + SELECT + wl.id, + wl.project_id AS "projectId", + wl.title, + wl.state, + wl.priority, + wl.classification, + wl.environment_id AS "environmentId", + wl.branch, + wl.worktree_path AS "worktreePath", + wl.source_truth_revision_id AS "sourceTruthRevisionId", + wl.primary_thread_id AS "primaryThreadId", + wl.imported_thread_id AS "importedThreadId", + wl.objective_summary AS "objectiveSummary", + wl.created_at AS "createdAt", + wl.updated_at AS "updatedAt", + wl.completed_at AS "completedAt", + NULL AS "sourceTruthRevision" + FROM projection_work_lanes wl + WHERE wl.id = ${laneId} + `, + }); + + const getSourceTruthRevisionRowById = SqlSchema.findOneOption({ + Request: SourceTruthRevisionIdLookupInput, + Result: ProjectionSourceTruthRevisionDbRowSchema, + execute: ({ revisionId }) => + sql` + SELECT + id, + lane_id AS "laneId", + revision_json AS "revision", + produced_at AS "producedAt", + superseded_at AS "supersededAt" + FROM projection_source_truth_revisions + WHERE id = ${revisionId} + `, + }); + const listSourceTruthRevisionRowsByLane = SqlSchema.findAll({ Request: LaneIdLookupInput, Result: ProjectionSourceTruthRevisionDbRowSchema, @@ -1624,7 +1735,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ), ), - listWorkLaneRows(undefined).pipe( + listWorkLaneShellRows(undefined).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "ProjectionSnapshotQuery.getShellSnapshot:listWorkLanes:query", @@ -1632,14 +1743,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ), ), - listSourceTruthRevisionRows(undefined).pipe( - Effect.mapError( - toPersistenceSqlOrDecodeError( - "ProjectionSnapshotQuery.getShellSnapshot:listSourceTruthRevisions:query", - "ProjectionSnapshotQuery.getShellSnapshot:listSourceTruthRevisions:decodeRows", - ), - ), - ), listProjectionStateRows(undefined).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( @@ -1658,7 +1761,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { sessionRows, latestTurnRows, workLaneRows, - sourceTruthRows, stateRows, ]) => Effect.gen(function* () { @@ -1695,9 +1797,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { const sessionByThread = new Map( sessionRows.map((row) => [row.threadId, mapSessionRow(row)] as const), ); - const sourceTruthById = new Map( - sourceTruthRows.map((row) => [row.id, row.revision] as const), - ); const snapshot = { snapshotSequence: computeSnapshotSequence(stateRows), @@ -1735,15 +1834,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { } satisfies OrchestrationThreadShell) : Result.failVoid, ), - lanes: workLaneRows.map((row) => { - const revisionId = row.lane.sourceTruthRevisionId; - const revision = - revisionId === null ? null : (sourceTruthById.get(revisionId) ?? null); - return toWorkLaneShell( - row.lane, - revision === null ? null : toSourceTruthShellSummary(revision), - ); - }), + lanes: workLaneRows.map(shellRowToWorkLaneShell), updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z", }; @@ -2300,23 +2391,27 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ); const getLaneShellById: ProjectionSnapshotQueryShape["getLaneShellById"] = (laneId) => - getWorkLaneRow({ laneId }).pipe( + getWorkLaneShellRow({ laneId }).pipe( Effect.flatMap((row) => Effect.gen(function* () { if (Option.isNone(row)) { return Option.none(); } - const revisionId = row.value.lane.sourceTruthRevisionId; - if (revisionId === null) { - return Option.some(toWorkLaneShell(row.value.lane, null)); + const shellRow = row.value; + if (shellRow.sourceTruthRevisionId === null) { + return Option.some(shellRowToWorkLaneShell(shellRow)); } - const revisions = yield* listSourceTruthRevisionRowsByLane({ laneId }); - const revision = revisions.find((entry) => entry.id === revisionId)?.revision ?? null; + const revisionRow = yield* getSourceTruthRevisionRowById({ + revisionId: shellRow.sourceTruthRevisionId, + }); return Option.some( - toWorkLaneShell( - row.value.lane, - revision === null ? null : toSourceTruthShellSummary(revision), - ), + shellRowToWorkLaneShell({ + ...shellRow, + sourceTruthRevision: Option.match(revisionRow, { + onNone: () => null, + onSome: (value) => value.revision, + }), + }), ); }), ), diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index a5174b5ab157..8f20b826f970 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -1,9 +1,14 @@ import { EventId, + isWorkLaneTerminalState, + isWorkLaneWorktreeOwningState, + WORK_LANE_EXECUTION_START_STATES, + type BlockerId, type OrchestrationCommand, type OrchestrationEvent, type OrchestrationReadModel, type WorkLane, + type WorkLaneId, type WorkLaneState, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; @@ -202,6 +207,8 @@ function buildLaneCreated(input: { advisorAssignmentIds: [], verifierAssignmentIds: [], sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "unknown", activePlanRevisionId: null, acceptanceCriterionIds: (input.command.acceptanceCriteria ?? []).map( (criterion) => criterion.id, @@ -214,12 +221,28 @@ function buildLaneCreated(input: { threadIds: primaryThreadId === null ? [] : [primaryThreadId], legacyExecutorRef: null, resumeState: null, + supersedingLaneId: null, createdAt: input.command.createdAt, updatedAt: input.command.createdAt, completedAt: null, }; } +function requireLaneMutable(input: { + readonly command: OrchestrationCommand; + readonly lane: WorkLane; +}): Effect.Effect { + if (!isWorkLaneTerminalState(input.lane.state)) { + return Effect.void; + } + return Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: input.command.type, + detail: `Lane '${input.lane.id}' is terminal ('${input.lane.state}') and cannot accept '${input.command.type}'.`, + }), + ); +} + type PlannedOrchestrationEvent = Omit; const emitLaneStateChanged = Effect.fn("emitLaneStateChanged")(function* ({ @@ -230,6 +253,8 @@ const emitLaneStateChanged = Effect.fn("emitLaneStateChanged")(function* ({ occurredAt, resumeState = null, reason, + blockerId, + supersedingLaneId, }: { readonly command: OrchestrationCommand; readonly laneId: WorkLane["id"]; @@ -238,6 +263,8 @@ const emitLaneStateChanged = Effect.fn("emitLaneStateChanged")(function* ({ readonly occurredAt: string; readonly resumeState?: WorkLaneState | null; readonly reason?: string; + readonly blockerId?: BlockerId; + readonly supersedingLaneId?: WorkLaneId; }): Effect.fn.Return< PlannedOrchestrationEvent, OrchestrationCommandInvariantError | PlatformError.PlatformError, @@ -257,6 +284,8 @@ const emitLaneStateChanged = Effect.fn("emitLaneStateChanged")(function* ({ toState, resumeState, ...(reason !== undefined ? { reason } : {}), + ...(blockerId !== undefined ? { blockerId } : {}), + ...(supersedingLaneId !== undefined ? { supersedingLaneId } : {}), updatedAt: occurredAt, }, }; @@ -1267,7 +1296,29 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, laneId: command.laneId, }); + const acceptanceCriteria = command.acceptanceCriteria ?? []; + for (const criterion of acceptanceCriteria) { + if (criterion.laneId !== command.laneId) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Acceptance criterion '${criterion.id}' laneId '${criterion.laneId}' does not match lane '${command.laneId}'.`, + }), + ); + } + } const lane = buildLaneCreated({ command }); + if ( + lane.worktreePath !== null && + isWorkLaneWorktreeOwningState(lane.state) + ) { + yield* requireWorktreeExclusive({ + readModel, + command, + worktreePath: lane.worktreePath, + exceptLaneId: command.laneId, + }); + } return { ...(yield* withEventBase({ aggregateKind: "lane", @@ -1278,7 +1329,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" type: "lane.created" as const, payload: { lane, - acceptanceCriteria: command.acceptanceCriteria ?? [], + acceptanceCriteria, }, }; } @@ -1403,11 +1454,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, laneId: command.laneId, }); - if (lane.state !== "planned") { + if ( + !(WORK_LANE_EXECUTION_START_STATES as ReadonlyArray).includes(lane.state) + ) { return yield* Effect.fail( new OrchestrationCommandInvariantError({ commandType: command.type, - detail: `Lane '${command.laneId}' must be in 'planned' state to start execution (current: '${lane.state}').`, + detail: `Lane '${command.laneId}' must be in planned|testing|reviewing|deliverable-ready to start execution (current: '${lane.state}').`, }), ); } @@ -1419,6 +1472,22 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }), ); } + if (lane.sourceTruthOwnershipOverlap === "overlap") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' cannot execute while source-truth ownership overlap is 'overlap'.`, + }), + ); + } + if (lane.sourceTruthActiveGitOperation !== "none") { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Lane '${command.laneId}' cannot execute while git operation '${lane.sourceTruthActiveGitOperation}' is active.`, + }), + ); + } if (lane.classification === "substantial" && lane.worktreePath === null) { return yield* Effect.fail( new OrchestrationCommandInvariantError({ @@ -1554,6 +1623,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" occurredAt: command.blockedAt, resumeState: lane.state, ...(command.reason !== undefined ? { reason: command.reason } : {}), + ...(command.blockerId !== undefined ? { blockerId: command.blockerId } : {}), }); } @@ -1571,7 +1641,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }), ); } - const toState: WorkLaneState = lane.resumeState ?? "recovery-required"; + const resumeState = lane.resumeState; + const toState: WorkLaneState = + resumeState !== null && + !isWorkLaneTerminalState(resumeState) && + resumeState !== "blocked" + ? resumeState + : "recovery-required"; return yield* emitLaneStateChanged({ command, laneId: command.laneId, @@ -1624,6 +1700,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" toState: "superseded", occurredAt: command.supersededAt, resumeState: null, + ...(command.supersedingLaneId !== undefined + ? { supersedingLaneId: command.supersedingLaneId } + : {}), }); } @@ -1675,6 +1754,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" from: lane.state, to: "recovery-required", }); + yield* requireWorktreeExclusive({ + readModel, + command, + worktreePath: lane.worktreePath, + exceptLaneId: command.laneId, + }); return yield* emitLaneStateChanged({ command, laneId: command.laneId, @@ -1721,11 +1806,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } case "lane.task-contract.update": { - yield* requireLane({ + const lane = yield* requireLane({ readModel, command, laneId: command.laneId, }); + yield* requireLaneMutable({ command, lane }); return { ...(yield* withEventBase({ aggregateKind: "lane", @@ -1743,11 +1829,23 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } case "lane.meta.update": { - yield* requireLane({ + const lane = yield* requireLane({ readModel, command, laneId: command.laneId, }); + yield* requireLaneMutable({ command, lane }); + if ( + command.worktreePath !== undefined && + isWorkLaneWorktreeOwningState(lane.state) + ) { + yield* requireWorktreeExclusive({ + readModel, + command, + worktreePath: command.worktreePath, + exceptLaneId: command.laneId, + }); + } return { ...(yield* withEventBase({ aggregateKind: "lane", @@ -1780,6 +1878,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" command, laneId: command.laneId, }); + yield* requireLaneMutable({ command, lane }); if (command.revision.laneId !== command.laneId) { return yield* Effect.fail( new OrchestrationCommandInvariantError({ @@ -1788,6 +1887,25 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }), ); } + if (command.revision.id === lane.sourceTruthRevisionId) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Source-truth revision '${command.revision.id}' is already current for lane '${command.laneId}'.`, + }), + ); + } + const revisionAlreadyOwned = readModel.lanes.some( + (entry) => entry.sourceTruthRevisionId === command.revision.id, + ); + if (revisionAlreadyOwned) { + return yield* Effect.fail( + new OrchestrationCommandInvariantError({ + commandType: command.type, + detail: `Source-truth revision '${command.revision.id}' already exists.`, + }), + ); + } return { ...(yield* withEventBase({ aggregateKind: "lane", @@ -1809,11 +1927,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } case "source-truth.conflict.record": { - yield* requireLane({ + const conflictLane = yield* requireLane({ readModel, command, laneId: command.laneId, }); + yield* requireLaneMutable({ command, lane: conflictLane }); return { ...(yield* withEventBase({ aggregateKind: "lane", @@ -1831,11 +1950,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" } case "source-truth.refresh.request": { - yield* requireLane({ + const refreshLane = yield* requireLane({ readModel, command, laneId: command.laneId, }); + yield* requireLaneMutable({ command, lane: refreshLane }); return { ...(yield* withEventBase({ aggregateKind: "lane", diff --git a/apps/server/src/orchestration/decider.workLane.test.ts b/apps/server/src/orchestration/decider.workLane.test.ts index addd97b68212..b37cb1bd8bd2 100644 --- a/apps/server/src/orchestration/decider.workLane.test.ts +++ b/apps/server/src/orchestration/decider.workLane.test.ts @@ -1,4 +1,5 @@ import { + AcceptanceCriterionId, CommandId, DeliverableId, EnvironmentId, @@ -62,6 +63,8 @@ function makeWorkLane(overrides: Partial = {}): WorkLane { advisorAssignmentIds: [], verifierAssignmentIds: [], sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "unknown", activePlanRevisionId: null, acceptanceCriterionIds: [], requiredReceiptKinds: [], @@ -72,6 +75,7 @@ function makeWorkLane(overrides: Partial = {}): WorkLane { threadIds: [], legacyExecutorRef: null, resumeState: null, + supersedingLaneId: null, createdAt: NOW, updatedAt: NOW, completedAt: null, @@ -306,7 +310,7 @@ it.layer(NodeServices.layer)("work lane decider", (it) => { }), ]), }).pipe(Effect.flip); - expectInvariant(queuedError, "must be in 'planned' state"); + expectInvariant(queuedError, "must be in planned|testing|reviewing|deliverable-ready"); const orientedToTesting = yield* decideOrchestrationCommand({ command: { @@ -632,13 +636,17 @@ it.layer(NodeServices.layer)("work lane decider", (it) => { commandId: CommandId.make("cmd-supersede"), laneId: LANE_ID, supersededAt: NOW, + supersedingLaneId: WorkLaneId.make("lane-replacement"), }, readModel: makeReadModel([makeWorkLane({ state: "planned" })]), }), ); - expect(payloadOf<{ toState: string }>(superseded[0], "lane.state-changed").toState).toBe( - "superseded", - ); + const supersededPayload = payloadOf<{ + toState: string; + supersedingLaneId?: string; + }>(superseded[0], "lane.state-changed"); + expect(supersededPayload.toState).toBe("superseded"); + expect(supersededPayload.supersedingLaneId).toBe("lane-replacement"); const recovered = asEvents( yield* decideOrchestrationCommand({ @@ -656,4 +664,308 @@ it.layer(NodeServices.layer)("work lane decider", (it) => { ); }), ); + + it.effect("allows two queued lanes to share a worktree until one owns it", () => + Effect.gen(function* () { + const otherLaneId = WorkLaneId.make("lane-2"); + const revisionId = SourceTruthRevisionId.make("str-1"); + const otherRevisionId = SourceTruthRevisionId.make("str-2"); + const shared = makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "queued", + worktreePath: WORKTREE, + }), + makeWorkLane({ + id: otherLaneId, + state: "queued", + worktreePath: WORKTREE, + }), + ]); + + const firstStart = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-first-exec"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "planned", + worktreePath: WORKTREE, + sourceTruthRevisionId: revisionId, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "exclusive", + }), + makeWorkLane({ + id: otherLaneId, + state: "queued", + worktreePath: WORKTREE, + }), + ]), + }), + ); + expect(payloadOf<{ toState: string }>(firstStart[0], "lane.state-changed").toState).toBe( + "executing", + ); + + const secondBlocked = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-second-exec"), + laneId: otherLaneId, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "executing", + worktreePath: WORKTREE, + sourceTruthRevisionId: revisionId, + }), + makeWorkLane({ + id: otherLaneId, + state: "planned", + worktreePath: WORKTREE, + sourceTruthRevisionId: otherRevisionId, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "exclusive", + }), + ]), + }).pipe(Effect.flip); + expectInvariant(secondBlocked, "already owned by lane"); + + // Soft-state meta path updates remain allowed while neither owns exclusively. + const metaOk = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.meta.update", + commandId: CommandId.make("cmd-meta-soft"), + laneId: otherLaneId, + worktreePath: WORKTREE, + updatedAt: LATER, + }, + readModel: shared, + }), + ); + expect(metaOk[0]?.type).toBe("lane.meta-updated"); + }), + ); + + it.effect("enforces exclusivity on meta.update while owning and on invalidate re-entry", () => + Effect.gen(function* () { + const otherLaneId = WorkLaneId.make("lane-2"); + const metaConflict = yield* decideOrchestrationCommand({ + command: { + type: "lane.meta.update", + commandId: CommandId.make("cmd-meta-owning"), + laneId: otherLaneId, + worktreePath: WORKTREE, + updatedAt: LATER, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "executing", + worktreePath: WORKTREE, + }), + makeWorkLane({ + id: otherLaneId, + state: "testing", + worktreePath: "/tmp/other", + }), + ]), + }).pipe(Effect.flip); + expectInvariant(metaConflict, "already owned by lane"); + + const invalidateConflict = yield* decideOrchestrationCommand({ + command: { + type: "lane.completion.invalidate", + commandId: CommandId.make("cmd-invalidate-exclusive"), + laneId: otherLaneId, + reason: "reopen", + invalidatedAt: LATER, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "executing", + worktreePath: WORKTREE, + }), + makeWorkLane({ + id: otherLaneId, + state: "completed", + worktreePath: WORKTREE, + completedAt: NOW, + }), + ]), + }).pipe(Effect.flip); + expectInvariant(invalidateConflict, "already owned by lane"); + }), + ); + + it.effect("allows execution.start re-entry from testing/reviewing/deliverable-ready", () => + Effect.gen(function* () { + for (const state of ["testing", "reviewing", "deliverable-ready"] as const) { + const decided = asEvents( + yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make(`cmd-reenter-${state}`), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state, + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "exclusive", + }), + ]), + }), + ); + const payload = payloadOf<{ fromState: string; toState: string }>( + decided[0], + "lane.state-changed", + ); + expect(payload.fromState).toBe(state); + expect(payload.toState).toBe("executing"); + } + }), + ); + + it.effect("rejects execution.start when preflight gate fields are unsafe", () => + Effect.gen(function* () { + const overlap = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-overlap"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "planned", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + sourceTruthOwnershipOverlap: "overlap", + }), + ]), + }).pipe(Effect.flip); + expectInvariant(overlap, "ownership overlap is 'overlap'"); + + const gitOp = yield* decideOrchestrationCommand({ + command: { + type: "lane.execution.start", + commandId: CommandId.make("cmd-rebase"), + laneId: LANE_ID, + startedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "planned", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-1"), + sourceTruthActiveGitOperation: "rebase", + sourceTruthOwnershipOverlap: "exclusive", + }), + ]), + }).pipe(Effect.flip); + expectInvariant(gitOp, "git operation 'rebase' is active"); + }), + ); + + it.effect("rejects duplicate source-truth revision ids and terminal mutations", () => + Effect.gen(function* () { + const revisionId = SourceTruthRevisionId.make("str-dup"); + const duplicateCurrent = yield* decideOrchestrationCommand({ + command: { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-dup-current"), + laneId: LANE_ID, + revision: makeSourceTruthRevision({ id: revisionId }), + recordedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + state: "planned", + sourceTruthRevisionId: revisionId, + }), + ]), + }).pipe(Effect.flip); + expectInvariant(duplicateCurrent, "already current"); + + const otherLaneId = WorkLaneId.make("lane-2"); + const duplicateExists = yield* decideOrchestrationCommand({ + command: { + type: "source-truth.preflight.record", + commandId: CommandId.make("cmd-dup-exists"), + laneId: LANE_ID, + revision: makeSourceTruthRevision({ id: revisionId }), + recordedAt: NOW, + }, + readModel: makeReadModel([ + makeWorkLane({ + id: LANE_ID, + state: "planned", + sourceTruthRevisionId: SourceTruthRevisionId.make("str-other"), + }), + makeWorkLane({ + id: otherLaneId, + state: "queued", + sourceTruthRevisionId: revisionId, + }), + ]), + }).pipe(Effect.flip); + expectInvariant(duplicateExists, "already exists"); + + for (const state of ["completed", "cancelled", "superseded"] as const) { + const terminalMeta = yield* decideOrchestrationCommand({ + command: { + type: "lane.meta.update", + commandId: CommandId.make(`cmd-terminal-meta-${state}`), + laneId: LANE_ID, + title: "nope", + updatedAt: LATER, + }, + readModel: makeReadModel([makeWorkLane({ state })]), + }).pipe(Effect.flip); + expectInvariant(terminalMeta, "is terminal"); + } + }), + ); + + it.effect("rejects acceptance criteria whose laneId does not match create", () => + Effect.gen(function* () { + const error = yield* decideOrchestrationCommand({ + command: { + type: "lane.create", + commandId: CommandId.make("cmd-bad-criterion"), + laneId: LANE_ID, + projectId: PROJECT_ID, + title: "Lane", + taskContract: makeTaskContract(), + priority: "normal", + classification: "substantial", + environmentId: ENV_ID, + createdAt: NOW, + acceptanceCriteria: [ + { + id: AcceptanceCriterionId.make("crit-1"), + laneId: WorkLaneId.make("lane-other"), + description: "wrong lane", + category: "correctness", + required: true, + status: "pending", + supportingReceiptIds: [], + }, + ], + }, + readModel: makeReadModel(), + }).pipe(Effect.flip); + expectInvariant(error, "does not match lane"); + }), + ); }); diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index 11361a8e0c7c..3946957107ef 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -805,21 +805,33 @@ export function projectEvent( case "lane.state-changed": return decodeForEvent(LaneStateChangedPayload, event.payload, event.type, "payload").pipe( - Effect.map((payload) => ({ - ...nextBase, - lanes: updateLane(nextBase.lanes, payload.laneId, { - state: payload.toState, - resumeState: payload.resumeState, - updatedAt: payload.updatedAt, - completedAt: - payload.toState === "completed" - ? payload.updatedAt - : payload.fromState === "completed" - ? null - : (nextBase.lanes.find((lane) => lane.id === payload.laneId)?.completedAt ?? - null), - }), - })), + Effect.map((payload) => { + const existing = nextBase.lanes.find((lane) => lane.id === payload.laneId); + const blockerIds = + payload.blockerId === undefined || existing === undefined + ? existing?.blockerIds + : existing.blockerIds.includes(payload.blockerId) + ? existing.blockerIds + : [...existing.blockerIds, payload.blockerId]; + return { + ...nextBase, + lanes: updateLane(nextBase.lanes, payload.laneId, { + state: payload.toState, + resumeState: payload.resumeState, + updatedAt: payload.updatedAt, + ...(blockerIds !== undefined ? { blockerIds } : {}), + ...(payload.supersedingLaneId !== undefined + ? { supersedingLaneId: payload.supersedingLaneId } + : {}), + completedAt: + payload.toState === "completed" + ? payload.updatedAt + : payload.fromState === "completed" + ? null + : (existing?.completedAt ?? null), + }), + }; + }), ); case "lane.task-contract-updated": @@ -916,6 +928,8 @@ export function projectEvent( ...nextBase, lanes: updateLane(nextBase.lanes, payload.laneId, { sourceTruthRevisionId: payload.revision.id, + sourceTruthActiveGitOperation: payload.revision.activeGitOperation, + sourceTruthOwnershipOverlap: payload.revision.ownershipOverlap, updatedAt: payload.recordedAt, }), })), diff --git a/apps/server/src/orchestration/projector.workLane.test.ts b/apps/server/src/orchestration/projector.workLane.test.ts index aad35f6ce05e..b280f3108686 100644 --- a/apps/server/src/orchestration/projector.workLane.test.ts +++ b/apps/server/src/orchestration/projector.workLane.test.ts @@ -55,6 +55,8 @@ function makeWorkLane(overrides: Partial = {}): WorkLane { advisorAssignmentIds: [], verifierAssignmentIds: [], sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "unknown", activePlanRevisionId: null, acceptanceCriterionIds: [], requiredReceiptKinds: [], @@ -65,6 +67,7 @@ function makeWorkLane(overrides: Partial = {}): WorkLane { threadIds: [], legacyExecutorRef: null, resumeState: null, + supersedingLaneId: null, createdAt: NOW, updatedAt: NOW, completedAt: null, @@ -179,6 +182,8 @@ it.effect("replays lane.created + state-changed + preflight-recorded determinist expect(firstPass.lanes).toHaveLength(1); expect(firstPass.lanes[0]?.state).toBe("preflight"); expect(firstPass.lanes[0]?.sourceTruthRevisionId).toBe(revision.id); + expect(firstPass.lanes[0]?.sourceTruthActiveGitOperation).toBe(revision.activeGitOperation); + expect(firstPass.lanes[0]?.sourceTruthOwnershipOverlap).toBe(revision.ownershipOverlap); expect(firstPass.lanes[0]?.updatedAt).toBe(LATER); }), ); diff --git a/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts b/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts index d9b568063bb0..f6d2d3aec737 100644 --- a/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts +++ b/apps/server/src/persistence/Layers/ProjectionLaneAcceptanceCriteria.ts @@ -46,6 +46,7 @@ const makeProjectionLaneAcceptanceCriterionRepository = Effect.gen(function* () lane_id = excluded.lane_id, criterion_json = excluded.criterion_json, last_sequence = excluded.last_sequence + WHERE excluded.last_sequence >= projection_lane_acceptance_criteria.last_sequence `, }); diff --git a/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts b/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts index 497f7c6f2811..061a397c5f46 100644 --- a/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts +++ b/apps/server/src/persistence/Layers/ProjectionSourceTruthRevisions.ts @@ -52,6 +52,7 @@ const makeProjectionSourceTruthRevisionRepository = Effect.gen(function* () { produced_at = excluded.produced_at, superseded_at = excluded.superseded_at, last_sequence = excluded.last_sequence + WHERE excluded.last_sequence >= projection_source_truth_revisions.last_sequence `, }); diff --git a/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts b/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts index 94f62dd20391..e5fce0a2d361 100644 --- a/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts +++ b/apps/server/src/persistence/Layers/ProjectionWorkLanes.ts @@ -87,6 +87,7 @@ const makeProjectionWorkLaneRepository = Effect.gen(function* () { updated_at = excluded.updated_at, completed_at = excluded.completed_at, last_sequence = excluded.last_sequence + WHERE excluded.last_sequence >= projection_work_lanes.last_sequence `, }); diff --git a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts index 782b94db37c6..cbe7f8039a36 100644 --- a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts +++ b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts @@ -1,14 +1,75 @@ // @effect-diagnostics preferSchemaOverJson:off +// @effect-diagnostics nodeBuiltinImport:off import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as SqlClient from "effect/unstable/sql/SqlClient"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { ServerConfig } from "../../config.ts"; import { runMigrations } from "../Migrations.ts"; import * as NodeSqliteClient from "../NodeSqliteClient.ts"; -const emptyLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); -const importLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); +const TEST_ENVIRONMENT_ID = "env-035-worklanes"; +const environmentIdDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-035-env-")); +const environmentIdPath = path.join(environmentIdDir, "environment-id"); +fs.writeFileSync(environmentIdPath, `${TEST_ENVIRONMENT_ID}\n`); + +const serverConfigLayer = Layer.succeed( + ServerConfig, + ServerConfig.of({ + logLevel: "Error", + traceMinLevel: "Info", + traceTimingEnabled: true, + traceBatchWindowMs: 200, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + cwd: process.cwd(), + baseDir: environmentIdDir, + stateDir: environmentIdDir, + dbPath: path.join(environmentIdDir, "state.sqlite"), + keybindingsConfigPath: path.join(environmentIdDir, "keybindings.json"), + settingsPath: path.join(environmentIdDir, "settings.json"), + providerStatusCacheDir: path.join(environmentIdDir, "caches"), + worktreesDir: path.join(environmentIdDir, "worktrees"), + attachmentsDir: path.join(environmentIdDir, "attachments"), + logsDir: path.join(environmentIdDir, "logs"), + serverLogPath: path.join(environmentIdDir, "logs", "server.log"), + serverTracePath: path.join(environmentIdDir, "logs", "server.trace.ndjson"), + providerLogsDir: path.join(environmentIdDir, "logs", "provider"), + providerEventLogPath: path.join(environmentIdDir, "logs", "provider", "events.log"), + terminalLogsDir: path.join(environmentIdDir, "logs", "terminals"), + anonymousIdPath: path.join(environmentIdDir, "anonymous-id"), + environmentIdPath, + serverRuntimeStatePath: path.join(environmentIdDir, "server-runtime.json"), + secretsDir: path.join(environmentIdDir, "secrets"), + mode: "web", + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + tailscaleServeEnabled: false, + tailscaleServePort: 443, + port: 0, + host: undefined, + desktopBootstrapToken: undefined, + desktopTelemetryFd: undefined, + desktopTelemetryControlFd: undefined, + resourceMonitorPath: undefined, + staticDir: undefined, + devUrl: undefined, + devAllowedOrigins: [], + noBrowser: false, + startupPresentation: "browser", + }), +); + +const emptyLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory(), serverConfigLayer)); +const importLayer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory(), serverConfigLayer)); const insertProject = (sql: SqlClient.SqlClient, projectId: string) => sql` INSERT INTO projection_projects ( @@ -156,6 +217,19 @@ const insertUserMessage = ( ) `; +const noServerConfigLayer = it.layer(NodeSqliteClient.layerMemory()); + +noServerConfigLayer("035_WorkLanesAndSourceTruth without ServerConfig", (it) => { + it.effect("fails migration when ServerConfig environment id is unavailable", () => + Effect.gen(function* () { + yield* runMigrations({ toMigrationInclusive: 34 }); + const migration = yield* Effect.promise(() => import("./035_WorkLanesAndSourceTruth.ts")); + const error = yield* migration.default.pipe(Effect.flip); + assert.match(String(error), /requires ServerConfig|environment id/i); + }), + ); +}); + emptyLayer("035_WorkLanesAndSourceTruth empty", (it) => { it.effect("creates empty projection tables when there are no threads", () => Effect.gen(function* () { @@ -192,13 +266,24 @@ emptyLayer("035_WorkLanesAndSourceTruth empty", (it) => { WHERE event_type = 'lane.imported' `; assert.strictEqual(events.length, 0); + + const projectionState = yield* sql<{ + readonly projector: string; + readonly last_applied_sequence: number; + }>` + SELECT projector, last_applied_sequence + FROM projection_state + WHERE projector = 'projection.work-lanes' + `; + assert.strictEqual(projectionState.length, 1); + assert.strictEqual(projectionState[0]?.last_applied_sequence, 0); }), ); }); importLayer("035_WorkLanesAndSourceTruth import", (it) => { it.effect( - "imports idle→queued, active→recovery-required, archived→queued; never completed; idempotent", + "imports idle→queued, active→recovery-required; skips archived; stamps env + projection_state; never completed; idempotent", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; @@ -243,31 +328,42 @@ importLayer("035_WorkLanesAndSourceTruth import", (it) => { const lanes = yield* sql<{ readonly id: string; readonly state: string; + readonly environment_id: string; readonly objective_summary: string; readonly completed_at: string | null; readonly lane_json: string; }>` - SELECT id, state, objective_summary, completed_at, lane_json + SELECT id, state, environment_id, objective_summary, completed_at, lane_json FROM projection_work_lanes ORDER BY id ASC `; - assert.strictEqual(lanes.length, 3); + assert.strictEqual(lanes.length, 2); const idle = lanes.find((row) => row.id === "lane:import:thread-idle"); assert.ok(idle); assert.strictEqual(idle.state, "queued"); + assert.strictEqual(idle.environment_id, TEST_ENVIRONMENT_ID); assert.strictEqual(idle.objective_summary, "Please fix the bug"); assert.strictEqual(idle.completed_at, null); const idleLane = JSON.parse(idle.lane_json) as { + readonly environmentId: string; readonly taskContract: { readonly objectiveDerivation: string }; + readonly sourceTruthActiveGitOperation: string; + readonly sourceTruthOwnershipOverlap: string; + readonly supersedingLaneId: string | null; readonly completedAt: string | null; }; + assert.strictEqual(idleLane.environmentId, TEST_ENVIRONMENT_ID); assert.strictEqual(idleLane.taskContract.objectiveDerivation, "UNKNOWN"); + assert.strictEqual(idleLane.sourceTruthActiveGitOperation, "none"); + assert.strictEqual(idleLane.sourceTruthOwnershipOverlap, "unknown"); + assert.strictEqual(idleLane.supersedingLaneId, null); assert.strictEqual(idleLane.completedAt, null); const running = lanes.find((row) => row.id === "lane:import:thread-running"); assert.ok(running); assert.strictEqual(running.state, "recovery-required"); + assert.strictEqual(running.environment_id, TEST_ENVIRONMENT_ID); assert.strictEqual(running.completed_at, null); const runningLane = JSON.parse(running.lane_json) as { readonly legacyExecutorRef: { readonly sessionStatus: string } | null; @@ -276,11 +372,10 @@ importLayer("035_WorkLanesAndSourceTruth import", (it) => { assert.strictEqual(runningLane.legacyExecutorRef?.sessionStatus, "running"); assert.strictEqual(runningLane.completedAt, null); - const archived = lanes.find((row) => row.id === "lane:import:thread-archived"); - assert.ok(archived); - assert.strictEqual(archived.state, "queued"); - assert.strictEqual(archived.completed_at, null); - assert.strictEqual(archived.objective_summary, "Archived thread"); + assert.strictEqual( + lanes.find((row) => row.id === "lane:import:thread-archived"), + undefined, + ); const events = yield* sql<{ readonly event_id: string; @@ -291,9 +386,27 @@ importLayer("035_WorkLanesAndSourceTruth import", (it) => { WHERE event_type = 'lane.imported' ORDER BY event_id ASC `; - assert.strictEqual(events.length, 3); + assert.strictEqual(events.length, 2); assert.ok(events.every((event) => event.command_id === null)); + const maxSequence = yield* sql<{ readonly maxSequence: number | null }>` + SELECT MAX(sequence) AS "maxSequence" + FROM orchestration_events + `; + const projectionState = yield* sql<{ + readonly projector: string; + readonly last_applied_sequence: number; + }>` + SELECT projector, last_applied_sequence + FROM projection_state + WHERE projector = 'projection.work-lanes' + `; + assert.strictEqual(projectionState.length, 1); + assert.strictEqual( + projectionState[0]?.last_applied_sequence, + maxSequence[0]?.maxSequence ?? 0, + ); + const migration = yield* Effect.promise( () => import("./035_WorkLanesAndSourceTruth.ts"), ); @@ -305,8 +418,8 @@ importLayer("035_WorkLanesAndSourceTruth import", (it) => { FROM orchestration_events WHERE event_type = 'lane.imported' `; - assert.strictEqual(lanesAfter.length, 3); - assert.strictEqual(eventsAfter.length, 3); + assert.strictEqual(lanesAfter.length, 2); + assert.strictEqual(eventsAfter.length, 2); const completedCount = yield* sql<{ readonly count: number }>` SELECT COUNT(*) AS count diff --git a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts index cf50887bb15a..319a740205bb 100644 --- a/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts +++ b/apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.ts @@ -1,25 +1,24 @@ // @effect-diagnostics nodeBuiltinImport:off // @effect-diagnostics preferSchemaOverJson:off +// @effect-diagnostics globalErrorInEffectFailure:off +// @effect-diagnostics globalDateInEffect:off /** * Migration 035 — work-lane / source-truth projection tables + legacy thread import. * - * Reads the persisted environment-id file when ServerConfig is available; otherwise - * stamps `unknown-environment`. JSON payloads are written as TEXT for the event - * store / projection tables (same shape as other projectors). + * Requires ServerConfig with a readable environmentIdPath so imported lanes stamp a + * real environment id. Archived threads are skipped. After import, stamps + * `projection.work-lanes` projection_state to MAX(orchestration_events.sequence). */ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as fs from "node:fs"; -import * as path from "node:path"; import { ServerConfig } from "../../config.ts"; const ACTIVE_SESSION_STATUSES = new Set(["starting", "running", "ready", "interrupted", "error"]); -const UNKNOWN_ENVIRONMENT_ID = "unknown-environment"; - type ImportThreadRow = { readonly threadId: string; readonly projectId: string; @@ -46,41 +45,55 @@ const readEnvironmentIdFromPath = (environmentIdPath: string): string | null => const resolveEnvironmentId = Effect.gen(function* () { const configOption = yield* Effect.serviceOption(ServerConfig); - if (Option.isSome(configOption)) { - const environmentIdPath = configOption.value.environmentIdPath; - const fileSystemOption = yield* Effect.serviceOption(FileSystem.FileSystem); - if (Option.isSome(fileSystemOption)) { - const exists = yield* fileSystemOption.value - .exists(environmentIdPath) - .pipe(Effect.orElseSucceed(() => false)); - if (exists) { - const raw = yield* fileSystemOption.value.readFileString(environmentIdPath).pipe( - Effect.map((value) => value.trim()), - Effect.orElseSucceed(() => ""), - ); - if (raw.length > 0) { - return raw; - } - } - } else { - const fromDisk = readEnvironmentIdFromPath(environmentIdPath); - if (fromDisk !== null) { - return fromDisk; - } - } + if (Option.isNone(configOption)) { + return yield* Effect.fail( + new Error( + "Migration 035_WorkLanesAndSourceTruth requires ServerConfig with a readable environmentIdPath", + ), + ); } - const home = process.env.T3CODE_HOME; - if (typeof home === "string" && home.length > 0) { - for (const relative of ["userdata/environment-id", "dev/environment-id", "environment-id"]) { - const fromDisk = readEnvironmentIdFromPath(path.join(home, relative)); - if (fromDisk !== null) { - return fromDisk; - } + const environmentIdPath = configOption.value.environmentIdPath; + const fileSystemOption = yield* Effect.serviceOption(FileSystem.FileSystem); + if (Option.isSome(fileSystemOption)) { + const exists = yield* fileSystemOption.value + .exists(environmentIdPath) + .pipe(Effect.orElseSucceed(() => false)); + if (!exists) { + return yield* Effect.fail( + new Error( + `Migration 035_WorkLanesAndSourceTruth: environment id file missing at '${environmentIdPath}'`, + ), + ); } + const raw = yield* fileSystemOption.value.readFileString(environmentIdPath).pipe( + Effect.map((value) => value.trim()), + Effect.mapError( + (cause) => + new Error( + `Migration 035_WorkLanesAndSourceTruth: failed to read environment id at '${environmentIdPath}': ${String(cause)}`, + ), + ), + ); + if (raw.length === 0) { + return yield* Effect.fail( + new Error( + `Migration 035_WorkLanesAndSourceTruth: environment id file at '${environmentIdPath}' is empty`, + ), + ); + } + return raw; } - return UNKNOWN_ENVIRONMENT_ID; + const fromDisk = readEnvironmentIdFromPath(environmentIdPath); + if (fromDisk === null) { + return yield* Effect.fail( + new Error( + `Migration 035_WorkLanesAndSourceTruth: environment id file missing or unreadable at '${environmentIdPath}'`, + ), + ); + } + return fromDisk; }); function importStateForSession(sessionStatus: string | null): "queued" | "recovery-required" { @@ -139,7 +152,10 @@ function buildImportedLanePayload(input: { advisorAssignmentIds: [], verifierAssignmentIds: [], sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none" as const, + sourceTruthOwnershipOverlap: "unknown" as const, activePlanRevisionId: null, + supersedingLaneId: null, acceptanceCriterionIds: [], requiredReceiptKinds: [], deliverableIds: [], @@ -256,6 +272,7 @@ export default Effect.gen(function* () { LEFT JOIN projection_thread_sessions s ON s.thread_id = t.thread_id WHERE t.deleted_at IS NULL + AND t.archived_at IS NULL AND NOT EXISTS ( SELECT 1 FROM projection_work_lanes wl @@ -374,4 +391,27 @@ export default Effect.gen(function* () { ON CONFLICT (id) DO NOTHING `; } + + const maxSequenceRows = yield* sql<{ readonly maxSequence: number | null }>` + SELECT MAX(sequence) AS "maxSequence" + FROM orchestration_events + `; + const lastAppliedSequence = maxSequenceRows[0]?.maxSequence ?? 0; + const stampedAt = new Date().toISOString(); + + yield* sql` + INSERT INTO projection_state ( + projector, + last_applied_sequence, + updated_at + ) + VALUES ( + 'projection.work-lanes', + ${lastAppliedSequence}, + ${stampedAt} + ) + ON CONFLICT (projector) DO UPDATE SET + last_applied_sequence = excluded.last_applied_sequence, + updated_at = excluded.updated_at + `; }); diff --git a/apps/server/src/ws.laneResume.test.ts b/apps/server/src/ws.laneResume.test.ts new file mode 100644 index 000000000000..6ebd0c818482 --- /dev/null +++ b/apps/server/src/ws.laneResume.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { shouldResumeLaneWithSnapshot } from "./laneResume.ts"; + +describe("shouldResumeLaneWithSnapshot", () => { + it("uses a snapshot when the cursor is behind by more than the max gap", () => { + expect(shouldResumeLaneWithSnapshot(2_000, 500, 1_000)).toBe(true); + }); + + it("replays events when the gap is within the bound", () => { + expect(shouldResumeLaneWithSnapshot(1_500, 500, 1_000)).toBe(false); + }); + + it("uses a snapshot when the cursor is ahead of head", () => { + expect(shouldResumeLaneWithSnapshot(10, 20, 1_000)).toBe(true); + }); +}); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 8483ef1047a0..2453f2a6a9a5 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -117,6 +117,7 @@ import * as VcsProcess from "./vcs/VcsProcess.ts"; import * as PairingGrantStore from "./auth/PairingGrantStore.ts"; import * as SessionStore from "./auth/SessionStore.ts"; import { failEnvironmentAuthInvalid, failEnvironmentInternal } from "./auth/http.ts"; +import { shouldResumeLaneWithSnapshot } from "./laneResume.ts"; import * as RelayClient from "@t3tools/shared/relayClient"; const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError); const isOrchestrationGetSnapshotError = Schema.is(OrchestrationGetSnapshotError); @@ -554,10 +555,17 @@ const makeWsRpcLayer = ( ); case "thread.unarchived": return threadUpsertOrRemove(event.payload.threadId, event.sequence); + case "lane.created": + case "lane.imported": + case "lane.state-changed": + case "lane.task-contract-updated": + case "lane.meta-updated": + case "lane.plan-proposed": + case "lane.plan-activated": + case "lane.deliverable-registered": + case "source-truth.preflight-recorded": + return laneUpsertOrRemove(WorkLaneId.make(event.aggregateId), event.sequence); default: - if (event.aggregateKind === "lane") { - return laneUpsertOrRemove(WorkLaneId.make(event.aggregateId), event.sequence); - } if (event.aggregateKind !== "thread") { return Effect.succeed(Option.none()); } @@ -1420,8 +1428,45 @@ const makeWsRpcLayer = ( if (input.afterSequence !== undefined) { const afterSequence = input.afterSequence; + const headSequence = yield* orchestrationEngine.latestSequence; + const replayGap = headSequence - afterSequence; + const afterCatchUp = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; + + if (shouldResumeLaneWithSnapshot(headSequence, afterSequence)) { + const snapshot = yield* projectionSnapshotQuery.getLaneDetail(input.laneId).pipe( + Effect.mapError( + (cause) => + new OrchestrationGetSnapshotError({ + message: `Failed to load lane ${input.laneId}`, + cause, + }), + ), + ); + if (Option.isNone(snapshot)) { + return yield* new OrchestrationGetSnapshotError({ + message: `Lane ${input.laneId} was not found`, + cause: input.laneId, + }); + } + return Stream.concat( + Stream.make({ + kind: "snapshot" as const, + snapshot: snapshot.value, + }), + afterCatchUp, + ); + } + const catchUpStream = orchestrationEngine - .readEvents(afterSequence, Number.MAX_SAFE_INTEGER) + .readEvents(afterSequence, replayGap) .pipe( Stream.filter(isThisLaneEvent), Stream.map((event) => ({ @@ -1436,15 +1481,6 @@ const makeWsRpcLayer = ( }), ), ); - const afterCatchUp = - input.requestCompletionMarker === true - ? Stream.concat( - Stream.fromEffect( - Queue.offer(liveBuffer, { kind: "synchronized" as const }), - ).pipe(Stream.drain), - bufferedLiveStream, - ) - : bufferedLiveStream; return Stream.concat(catchUpStream, afterCatchUp); } diff --git a/docs/macbrains/receipts/F0_HARDEN.md b/docs/macbrains/receipts/F0_HARDEN.md new file mode 100644 index 000000000000..d92e22414535 --- /dev/null +++ b/docs/macbrains/receipts/F0_HARDEN.md @@ -0,0 +1,45 @@ +# F0 Harden Receipt + +- Branch: `cursor/f0-worklane-source-truth-7986` +- SHA: `c3e5be231ae14d3ea7978a71e228eeb7613b5cc0` +- Commit: `fix(orchestration): harden work-lane foundation before pull` +- Safe to pull: **yes** after green push of this harden commit + +## Check receipts + +```text +vp run --filter @t3tools/contracts typecheck # pass +vp run --filter t3 typecheck # pass +vp run --filter @t3tools/client-runtime typecheck # pass +npx vitest run \ + packages/contracts/src/workLane.test.ts \ + apps/server/src/orchestration/decider.workLane.test.ts \ + apps/server/src/orchestration/projector.workLane.test.ts \ + apps/server/src/persistence/Migrations/035_WorkLanesAndSourceTruth.test.ts \ + packages/client-runtime/src/state/shellReducer.test.ts \ + apps/server/src/ws.laneResume.test.ts \ + --config vite.config.ts +# 6 files / 46 tests passed +``` + +## Fixed (P0/P1) + +- Narrowed worktree ownership to active/recovery states (imports may share paths while queued). +- Exclusivity enforced on meta.update path changes (owning states) and completion.invalidate re-entry. +- Migration requires ServerConfig environment id (no `unknown-environment`); skips archived; seeds `projection.work-lanes` cursor. +- Shell SQL uses denormalized columns + current revision join (`superseded_at IS NULL`; not full `lane_json` / all revisions). +- `execution.start` allowed from planned|testing|reviewing|deliverable-ready. +- Duplicate source-truth revision ids rejected; terminal lanes reject mutations. +- `objectiveDerivation` defaults to `UNKNOWN`. +- `blockerId` / `supersedingLaneId` persisted on state-changed; criteria `laneId` validated. +- Compact preflight gate fields on lane (`sourceTruthActiveGitOperation`, `sourceTruthOwnershipOverlap`). +- `subscribeLane` resume gap → snapshot; shell lane upsert allowlist. +- Projection upserts are sequence-monotonic. + +## Deferred P2 / known limits (not blockers for pull) + +- Unbounded source-truth revision body retention until F1 artifact/receipt store. +- Full SQL wipe + rebuild-from-events for work lanes remains R1. +- Path comparison does not realpath / collapse `..` segments. +- Exclusive ownership is not claimed in queued…planned soft states (intentional). +- Historical superseded revision ids not in command RM (duplicate check covers current lane pointers). diff --git a/packages/client-runtime/src/state/shellReducer.test.ts b/packages/client-runtime/src/state/shellReducer.test.ts index a1d48d919226..52f77c58d2d1 100644 --- a/packages/client-runtime/src/state/shellReducer.test.ts +++ b/packages/client-runtime/src/state/shellReducer.test.ts @@ -4,6 +4,7 @@ import { ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; import type { OrchestrationShellSnapshot, OrchestrationShellStreamEvent } from "@t3tools/contracts"; import { applyShellStreamEvent } from "./shellReducer.ts"; +import { applyLaneStreamItem } from "./workLanes.ts"; const baseSnapshot: OrchestrationShellSnapshot = { snapshotSequence: 0, @@ -224,3 +225,69 @@ describe("applyShellStreamEvent", () => { }); }); }); + +describe("applyLaneStreamItem", () => { + it("advances snapshotSequence on stream events", () => { + const current = { + snapshotSequence: 4, + detail: { + lane: { + id: "lane-1", + projectId: ProjectId.make("project-1"), + title: "Lane", + taskContract: { + objective: "x", + constraints: [], + nonGoals: [], + deliverableRequirement: "none" as const, + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: [], + prohibitedActions: [], + completionReportRequired: true, + objectiveDerivation: "UNKNOWN" as const, + }, + state: "queued" as const, + priority: "normal" as const, + classification: "substantial" as const, + environmentId: "env-1", + repositoryIdentity: null, + baseRef: null, + branch: null, + worktreePath: null, + ownerAssignmentId: null, + advisorAssignmentIds: [], + verifierAssignmentIds: [], + sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none" as const, + sourceTruthOwnershipOverlap: "unknown" as const, + activePlanRevisionId: null, + supersedingLaneId: null, + acceptanceCriterionIds: [], + requiredReceiptKinds: [], + deliverableIds: [], + blockerIds: [], + primaryThreadId: null, + importedThreadId: null, + threadIds: [], + legacyExecutorRef: null, + resumeState: null, + createdAt: "2026-04-01T00:00:00.000Z", + updatedAt: "2026-04-01T00:00:00.000Z", + completedAt: null, + }, + acceptanceCriteria: [], + sourceTruthRevisions: [], + }, + } as const; + + const next = applyLaneStreamItem(current as never, { + kind: "event", + event: { + sequence: 9, + type: "lane.meta-updated", + } as never, + }); + expect(next?.snapshotSequence).toBe(9); + }); +}); diff --git a/packages/client-runtime/src/state/workLanes.ts b/packages/client-runtime/src/state/workLanes.ts index 62e62560c269..06a4595d2345 100644 --- a/packages/client-runtime/src/state/workLanes.ts +++ b/packages/client-runtime/src/state/workLanes.ts @@ -40,8 +40,9 @@ export function workLaneShellsFromSnapshot( } /** - * Apply a lane detail stream item. Snapshot replaces local detail; events are - * expected to be handled by reloading detail or by shell upserts for list views. + * Apply a lane detail stream item. Snapshot replaces local detail; events advance + * snapshotSequence so reconnect cursors do not ignore progress while bodies are + * reloaded separately. */ export function applyLaneStreamItem( current: WorkLaneDetailSnapshot | null, @@ -53,8 +54,16 @@ export function applyLaneStreamItem( case "synchronized": return current; case "event": - // Detail bodies are reloaded after sequence catch-up; keep current until then. - return current; + if (current === null) { + return null; + } + if (item.event.sequence <= current.snapshotSequence) { + return current; + } + return { + ...current, + snapshotSequence: item.event.sequence, + }; default: return current; } diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 17a92b8749f7..ccdd6cd29cf8 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -57,6 +57,7 @@ import { SourceTruthRefreshRequestCommand, SourceTruthRefreshRequestedPayload, WorkLane, + WorkLaneClientCommand, WorkLaneDetailSnapshot, WorkLaneShell, } from "./workLane.ts"; @@ -831,28 +832,7 @@ const DispatchableClientOrchestrationCommand = Schema.Union([ ThreadUserInputRespondCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, - LaneCreateCommand, - LaneTaskContractUpdateCommand, - LanePreflightRequestCommand, - LaneOrientationRecordCommand, - LanePlanProposeCommand, - LanePlanActivateCommand, - LaneExecutionStartCommand, - LaneTestingStartCommand, - LaneReviewRequestCommand, - LaneDeliverableRegisterCommand, - LaneCompletionRequestCommand, - LaneBlockCommand, - LaneUnblockCommand, - LaneCancelCommand, - LaneSupersedeCommand, - LaneRecoveryRequestCommand, - LaneCompletionInvalidateCommand, - LaneFailCommand, - LaneMetaUpdateCommand, - SourceTruthPreflightRecordCommand, - SourceTruthConflictRecordCommand, - SourceTruthRefreshRequestCommand, + WorkLaneClientCommand, ]); export type DispatchableClientOrchestrationCommand = typeof DispatchableClientOrchestrationCommand.Type; @@ -878,28 +858,7 @@ export const ClientOrchestrationCommand = Schema.Union([ ThreadUserInputRespondCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, - LaneCreateCommand, - LaneTaskContractUpdateCommand, - LanePreflightRequestCommand, - LaneOrientationRecordCommand, - LanePlanProposeCommand, - LanePlanActivateCommand, - LaneExecutionStartCommand, - LaneTestingStartCommand, - LaneReviewRequestCommand, - LaneDeliverableRegisterCommand, - LaneCompletionRequestCommand, - LaneBlockCommand, - LaneUnblockCommand, - LaneCancelCommand, - LaneSupersedeCommand, - LaneRecoveryRequestCommand, - LaneCompletionInvalidateCommand, - LaneFailCommand, - LaneMetaUpdateCommand, - SourceTruthPreflightRecordCommand, - SourceTruthConflictRecordCommand, - SourceTruthRefreshRequestCommand, + WorkLaneClientCommand, ]); export type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type; diff --git a/packages/contracts/src/workLane.test.ts b/packages/contracts/src/workLane.test.ts index 262ad6c5b5ca..50c1b9f1379c 100644 --- a/packages/contracts/src/workLane.test.ts +++ b/packages/contracts/src/workLane.test.ts @@ -57,6 +57,8 @@ describe("workLane contracts", () => { advisorAssignmentIds: [], verifierAssignmentIds: [], sourceTruthRevisionId: null, + sourceTruthActiveGitOperation: "none", + sourceTruthOwnershipOverlap: "unknown", activePlanRevisionId: null, acceptanceCriterionIds: [], requiredReceiptKinds: [], @@ -67,6 +69,7 @@ describe("workLane contracts", () => { threadIds: [], legacyExecutorRef: null, resumeState: null, + supersedingLaneId: null, createdAt: "2026-07-30T00:00:00.000Z", updatedAt: "2026-07-30T00:00:00.000Z", completedAt: null, @@ -81,6 +84,21 @@ describe("workLane contracts", () => { assert.throws(() => Schema.decodeUnknownSync(WorkLaneState)("done")); }); + it("defaults objectiveDerivation to UNKNOWN when omitted", () => { + const contract = decodeTaskContract({ + objective: "x", + constraints: [], + nonGoals: [], + deliverableRequirement: "none", + requiresPullRequest: false, + requiresUserVisibleSurface: false, + authorizedActions: [], + prohibitedActions: [], + completionReportRequired: true, + }); + assert.strictEqual(contract.objectiveDerivation, "UNKNOWN"); + }); + it("rejects untyped permission blobs on task contracts", () => { assert.throws(() => decodeTaskContract({ diff --git a/packages/contracts/src/workLane.ts b/packages/contracts/src/workLane.ts index 1114583c3967..43dd10be9e3b 100644 --- a/packages/contracts/src/workLane.ts +++ b/packages/contracts/src/workLane.ts @@ -20,7 +20,13 @@ import { WorkLaneId, } from "./baseSchemas.ts"; import { RepositoryIdentity } from "./environment.ts"; -import { ClaimLabel, SourceTruthRevision, SourceTruthRevisionShellSummary } from "./sourceTruth.ts"; +import { + ClaimLabel, + GitOperationState, + SourceTruthRevision, + SourceTruthRevisionShellSummary, + WorktreeOwnershipOverlapResult, +} from "./sourceTruth.ts"; export const WorkLaneState = Schema.Literals([ "queued", @@ -56,12 +62,12 @@ export const WORK_LANE_TERMINAL_STATES = [ "superseded", ] as const satisfies ReadonlyArray; -/** States that still own a worktree for exclusive-ownership checks. */ +/** + * States that claim exclusive worktree ownership. + * Soft lifecycle states (queued…planned) may share a path; exclusivity applies + * once a lane is actively executing or in recovery-bound owning states. + */ export const WORK_LANE_WORKTREE_OWNING_STATES = [ - "queued", - "preflight", - "oriented", - "planned", "executing", "testing", "reviewing", @@ -71,6 +77,13 @@ export const WORK_LANE_WORKTREE_OWNING_STATES = [ "recovery-required", ] as const satisfies ReadonlyArray; +export const WORK_LANE_EXECUTION_START_STATES = [ + "planned", + "testing", + "reviewing", + "deliverable-ready", +] as const satisfies ReadonlyArray; + export const WORK_LANE_NORMAL_TRANSITIONS: Readonly< Record> > = { @@ -169,7 +182,7 @@ export const TaskContract = Schema.Struct({ Schema.withDecodingDefault(Effect.succeed(true as const)), ), objectiveDerivation: ClaimLabel.pipe( - Schema.withDecodingDefault(Effect.succeed("PROVEN" as const)), + Schema.withDecodingDefault(Effect.succeed("UNKNOWN" as const)), ), }); export type TaskContract = typeof TaskContract.Type; @@ -258,9 +271,19 @@ export const WorkLane = Schema.Struct({ sourceTruthRevisionId: Schema.NullOr(SourceTruthRevisionId).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), + /** Compact preflight gate fields — avoids loading full revision bodies into the command RM. */ + sourceTruthActiveGitOperation: GitOperationState.pipe( + Schema.withDecodingDefault(Effect.succeed("none" as const)), + ), + sourceTruthOwnershipOverlap: WorktreeOwnershipOverlapResult.pipe( + Schema.withDecodingDefault(Effect.succeed("unknown" as const)), + ), activePlanRevisionId: Schema.NullOr(PlanRevisionId).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), + supersedingLaneId: Schema.NullOr(WorkLaneId).pipe( + Schema.withDecodingDefault(Effect.succeed(null)), + ), acceptanceCriterionIds: Schema.Array(AcceptanceCriterionId).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), @@ -591,6 +614,8 @@ export const LaneStateChangedPayload = Schema.Struct({ toState: WorkLaneState, resumeState: Schema.NullOr(WorkLaneState).pipe(Schema.withDecodingDefault(Effect.succeed(null))), reason: Schema.optional(TrimmedNonEmptyString), + blockerId: Schema.optional(BlockerId), + supersedingLaneId: Schema.optional(WorkLaneId), updatedAt: IsoDateTime, }); From cadd29630772916547f46f380e77d620388940c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 19:46:22 +0000 Subject: [PATCH 32/32] docs(macbrains): point F0 harden receipt at pushed SHA Co-authored-by: Nayte --- docs/macbrains/receipts/F0_HARDEN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/macbrains/receipts/F0_HARDEN.md b/docs/macbrains/receipts/F0_HARDEN.md index d92e22414535..2b6829744d0b 100644 --- a/docs/macbrains/receipts/F0_HARDEN.md +++ b/docs/macbrains/receipts/F0_HARDEN.md @@ -1,7 +1,7 @@ # F0 Harden Receipt - Branch: `cursor/f0-worklane-source-truth-7986` -- SHA: `c3e5be231ae14d3ea7978a71e228eeb7613b5cc0` +- SHA: `4e9c2ca81561cee787efa49373c66c12c8545b20` - Commit: `fix(orchestration): harden work-lane foundation before pull` - Safe to pull: **yes** after green push of this harden commit