Files
open-design-crew[bot] 7320f9b8ab feat(daemon): make OD Next the default design strategy (#7016)
* refactor(observability): isolate legacy run exporter

* refactor(daemon): extract internal run creation service

Route /api/runs and compatibility /api/chat through one daemon-owned create, claim,
resume, and start seam. Preserve HTTP responses, SSE timing, session bookkeeping,
and durable Run formats.

Validated with daemon tests/build/typecheck, workspace guard, and workspace typecheck.

* test(plugin-runtime): freeze OD Next strategy fixture boundaries

Add exact pre-Build route and stage cases plus deterministic deletion-boundary mutations without shipping an evaluation harness.

Preserve the current official and community fallback pipeline and composed verification/critique prompt sections as golden witnesses.

Validated with plugin-runtime tests and types, focused daemon prompt composition, daemon types, guard, and workspace typecheck.

* fix(daemon): finalize terminal telemetry delivery

Persist a versioned delivery record so normal failures stop after bounded retries
and only an unfinished crash window resumes on restart.

Reuse the persisted identity across Vela and Relay transport, isolate stable
direct Langfuse event ids by delivery purpose, and log only redacted sink data.

Validated with focused daemon and contracts tests, workspace typechecks, and
guard. The full daemon suite had one timeout; its four-test file passed alone.

* feat(plugins): add OD Next strategy scenario and contracts

Ship the V2 strategy package as inactive bundled content and validate its contracts without changing ordinary plugin manifest compatibility.

Keep bundled strategy bytes out of the catalog and generic apply path until a later hash-gated activation owner binds them.

* feat(observability): add normalized agent observation contract

Define a pure versioned contract for identity, Prompt, usage, timing, and status evidence with explicit availability and per-value provenance.

Add a structured main-Run compatibility adapter that preserves unknown facts, separates clocks, and omits provenance-ambiguous derived totals without changing legacy export behavior.

* feat(plugins): bind OD Next strategy content identity

* feat(daemon): persist OD Next strategy task chains

Add the daemon-owned SQLite truth for immutable strategy task identity, ordered physical Run mappings, canonical Plan Contracts, revision-CAS transitions, and terminal reconciliation.

Pin task-owned Snapshots and exclude their durable references from pruning so mixed GC sweeps remain safe.

Validated with 103 focused daemon tests, contracts tests/typecheck, daemon and workspace typechecks, and guard.

* feat(daemon): gate OD Next runtime capabilities by fixtures

Add exact CLI/adapter/fixture tuple resolution, stable capability snapshots, and offline
replay graph invariants for the four initial runtime paths.

Keep production registry empty until sanitized real fixtures exist; synthetic evidence and
version drift remain unknown.

Validated with contracts tests, focused daemon and e2e boundary suites, guard, and
workspace typecheck.

* feat(daemon): add hash-gated OD Next prompt recipe

Compose the bundled strategy from validated content, one Task Skill, and the exact stage policy.
Fail closed on package, profile, pipeline, or atom drift while preserving ordinary quality paths.

Validated with focused tests, package typechecks, workspace typecheck, and guard.

* feat(observability): aggregate OD Next task observations

Build stable task, Run, Child, model, and tool hierarchies from durable StrategyTaskExecution mappings and normalized facts while keeping missing coverage explicit.

Map task aggregates to legacy ingestion behind the consent gate. Preserve owner/exclude hierarchy while accounting usage once, enforce kind-specific Prompt boundaries, and accept absolute time only from one Unix-epoch evidence record.

Validated with 228 focused daemon tests, 353 contracts tests, daemon/contracts and workspace typechecks, guard, and the task harness.

* feat(daemon): collect bounded Codex child evidence

Read declared Codex rollout sessions without scanning arbitrary homes.
Normalize verified child facts while redacting prompts and preserving Run semantics.

* feat(daemon): add OD Next planning coordinator

Add a daemon-owned planning reducer, deterministic Resolver and Preflight facts, and a stream-safe machine protocol boundary backed by the durable task store.

Lock one clarification and one serialization-only repair across restart, preserve the Plan hash, and keep ordinary Run text unchanged.

* feat(daemon): collect native OpenCode child evidence

* feat(observability): add Langfuse OTLP task exporter

Map normalized task observations to complete OTLP/JSON v4 spans with stable parent context, allowlisted metadata, usage accounting, and bounded terminal delivery.

Fail closed on drift across root and operation input, model, timing, type, hierarchy, status, and complete usage provenance while dual remains a local-only OTLP shadow.

Keep legacy rollback, consent gates, and X5 live self-host verification boundaries intact.

* feat(daemon): normalize Claude native child evidence

Collect matched Claude Task sidechain lifecycle facts without changing main stream turn, stdin, or error bookkeeping, and adapt them to provider-neutral observations with unavailable Prompt and usage evidence.

Bind session, Task id, and parent identity immutably; poison rebinding, session drift, and conflicting terminals so ambiguous evidence cannot reach L2. Keep production capability unknown until X1 supplies an exact-version sanitized real fixture.

* feat(daemon): consume versioned Vela child evidence

Negotiate schema v1 only on AMR and enforce run-scoped start-to-terminal graph, status/source coherence, and exact usage source/completeness bindings. Emit allowlisted Normalized child facts without Vela private I/O.

Pin the unpublished producer golden for offline compatibility while keeping production capability unknown until the X1/X3 evidence gates are satisfied.

Validated with 147 focused daemon tests, 353 contract tests, daemon/workspace typechecks, guard, and the task harness.

* feat(daemon): automate OD Next simple production

Connect parsed OD Next terminal output to the durable coordinator, claim clarification, repair, and production Runs atomically, and start continuations only after the source Run ends.

Converge task failure and cancellation before physical end, require daemon-owned execution facts and canonical delivery, and keep ordinary Runs compatible.

Validated with focused daemon route and coordinator tests, contracts tests, daemon and workspace typechecks, guard, and the task harness.

* feat(daemon): enforce verified complex production

* feat(web): follow OD Next task chains across Web and CLI

Persist daemon-issued task handles and visible predecessor-prefix boundaries, then follow projected active Runs through one terminal outcome in Web and od run.

Normalize reloads to daemon activeRunId before subscribing, transfer controller ownership atomically across successors, discard stale cursors, and preserve visible prefixes without replaying predecessor machine payloads.

Validated with focused Web and daemon tests, contracts tests, workspace typecheck, guard, diff checks, and the task harness.

* feat(daemon): roll out OD Next task observability

* feat(daemon): add controlled OD Next rollout

* fix(daemon): align OpenCode and Vela evidence tuples

Pass GPT-5.6 Sol's verified high variant to OpenCode, pin native evidence to 1.18.18, retain locally recorded success and failure seeds as explicitly unattested, and align the Vela consumer golden to the local versioned producer.

Validated with 5 focused daemon files (175 tests), daemon and workspace typechecks, guard, the real local Vela ACP Sol smoke, and a minimal test-relay telemetry write.

* fix(daemon): honor OpenCode model variant capabilities

Parse the live verbose OpenCode catalog per model and forward only variants advertised by the exact detected binary. Keep fallback and one-off OPENCODE_BIN connection tests on the base model when variant support is unknown.

Record machine-checkable OpenCode 1.18.18 root, tool, child, recovery, cancel, timeout, and resume evidence without promoting the unattested seed into the production capability registry.

Validated with focused runtime and connection tests, host process-group and watchdog tests, contracts tests, daemon/workspace typechecks, guard, and an isolated SIGKILL delivery-recovery exercise.

* feat(daemon): add OD Next analytics operations report

Reduce normalized task aggregates into source-aware operations metrics without
copying the Langfuse dashboard or exposing user trace links.

Preserve main/Child accounting, stage-scoped coverage, unavailable facts, and
external-signal-only quality inputs.

Validated with focused and contract tests, the full daemon suite, guard,
workspace typecheck, and exact-trace Langfuse readback.

* feat(observability): complete child prompt and runtime provenance

Export bounded redacted child-injected Prompt content and exact CLI, companion, and
adapter versions through task-level legacy and OTLP mappings.

Register the OpenCode 1.18.18 best-effort replay tuple while keeping unknown versions
and incomplete Codex evidence fail closed.

Validated with 353 contracts tests, 196 focused daemon tests, daemon/workspace
typechecks, and guard.

* fix(daemon): recognize Codex child failure terminals

Parse Codex 0.147.0 Child task_complete.error as the runtime-owned failed terminal, honor session_meta id over its legacy root session_id, and map interrupt_agent aborts to canceled.

Add a redacted seven-path best-effort replay and exact Codex capability tuple. Unknown versions remain fail-closed and ordinary Codex runs are unchanged.

Validated with 92 focused daemon tests, 353 contracts tests, daemon and workspace typechecks, guard, and the 25-unit task harness.

* fix(daemon): report verified child runtime versions

Use versions observed from resolved CLI probes or Vela's ACP handshake when annotating Child observations. Correct Native OpenCode's allowlist key, add Claude and Vela/OpenCode companion provenance, reject Vela probe/handshake disagreement, and ignore the legacy 0.0.0 placeholder.

Validated with 252 focused daemon tests, daemon and workspace typechecks, guard, the task harness, and a real local Vela --version/-v/ACP initialize build smoke.

* test(observability): record Vela child evidence matrix

Add a sanitized provider-backed Terra replay for main, tool, child success, child failure with parent recovery, cancel, timeout, and cross-process resume. Keep the unpublished Vela tuple outside the production registry while pinning its exact candidate and OpenCode companion evidence.

Validated with focused Vela consumer/capability tests, daemon typecheck, workspace typecheck, guard, and the task harness.

* feat(daemon): enable OD Next production profiles

Make prototype, presentation, marketing, and HyperFrames the exact global OD Next production set while preserving explicit off/observe, user plugin authority, and fail-closed runtime capability gates. Feed runtime-owned profile, route, output, input, and capability facts into the planning contract and keep all other artifact kinds on ordinary Open Design.

Use bounded executable-scoped CLI version probes so headless and restarted daemons can resolve exact tuples without a prior agent-list request or cross-binary cache contamination.

Validated with four provider-backed Terra CLI chains, focused daemon/contracts/plugin-runtime suites, daemon and workspace typechecks, guard, and the 25-unit task harness.

* feat(daemon): classify OD Next exact agent inputs

Route prompt contributors through one daemon-owned choke point and version their final-text, semantic-fact, transport, and exclusion ownership for the forthcoming canonical bundle. Send OD Next continuation stages directly through native sessions without rewrapping or re-seeding stable instructions.

Validated with focused daemon and contracts tests, daemon/workspace typechecks, and guard.

* fix(daemon): close OD Next input ownership gaps

Bind semantic request facts to their real daemon-system or request producers, cover the DSH profile exact-text transport, and reject lookalike prompt-bundle root names.

Validated with focused exact-input, production-server and DSH tests, daemon typecheck, task harness, and guard.

* feat(daemon): emit canonical OD Next prompt bundles

Add strict pure TypeScript Bundle and request-Turn serializers, then route OD Next request and continuation text through their versioned XML contracts.

Split Web prior transcript from the current turn without text subtraction, keep mutable Skill and attachment inputs explicitly pending, and preserve ordinary prompt behavior.

Validated with focused contracts, daemon and web tests, the real server fixture, package and workspace typechecks, and guard.

* fix(daemon): align OD Next bundle input ownership

Register the real canonical Bundle contributors and keep current-turn, prior-transcript, pending-fact, and research-query ownership explicit at the production seam.

Reject continuation task_run_index zero and cover Web framing, ordinary compatibility, fail-closed contributor drift, and real server prompt capture.

* feat(daemon): freeze OD Next user skills

Capture explicit Skill bodies and audited side files before task claim, persist verified task-owned bytes, and replay only the frozen package for OD Next runs.

Keep legacy single-Skill CLI requests and ordinary live staging compatible while adding Web/CLI multi-Skill coverage and fail-closed snapshot checks. Validated with focused server, contract, CLI, Web, typecheck, and guard suites.

* fix(daemon): harden OD Next skill snapshots

Require an explicit frozen package for every mapped OD Next task and fail corrupt task state before any live Skill staging.

Use bounded no-follow descriptor reads with race checks, preserve legacy CLI compatibility, and make materialized roots collision-resistant. Validated with focused, real-server, workspace typecheck, and guard suites.

* feat(daemon): freeze OD Next task input snapshots

Freeze OD Next attachments and canonical configuration before the atomic run/task claim, then route runtime access through durable task-scoped snapshots while preserving ordinary attachment behavior.

Validate immutable identities, transport references, continuation propagation, tamper failure, and the four production task types with focused contract and daemon coverage.

* fix(daemon): isolate OD Next runtime input projections

Keep canonical task snapshots daemon-only, rebuild a read-only per-Run projection, and deliver frozen document resources through ACP or OD_TASK_INPUT_DIR.

Harden source and snapshot reads with no-follow same-fd identity checks and bounded caps. Validated with focused runtime tests, contracts, daemon/workspace typechecks, guard, and the task harness.

* fix(daemon): harden OD Next snapshot transport boundaries

Reject intermediate symlinks and realpath escapes while loading canonical snapshot files, including identical-byte replacement targets.

Preserve ordinary ACP duplicate image ordering while suppressing only OD Next resource/image cross-duplicates. Validated with focused tests, daemon typecheck, guard, and the task harness.

* fix(daemon): preserve structured OD Next snapshot failures

Defer frozen Skill package errors from the Task 04 preclaim lookup to the existing post-claim failure boundary so mapped Runs remain durable JSON failures without orphan input snapshots.

Validated with the combined real-server repair, missing-package, and tampered-package regressions.

* fix(daemon): keep OD Next continuation cwd stable

Separate authoritative project cwd resolution from best-effort file inventory and tolerate entries deleted between directory enumeration and stat.

This keeps native-session identity stable while frozen task inputs replace live attachments during continuation.

* feat(daemon): persist exact OD Next prompt inputs

Persist the canonical first-turn Bundle and every deterministic continuation Turn with UTF-8 and SHA-256 identities, then send only those validated texts through runtime transports.

Fail closed on mapped task drift, tamper, legacy NULL identities, missing mappings, and cold continuation while leaving ordinary Runs on the existing live composer.

Validated with the 15-case automatic server acceptance twice, 223 focused daemon tests, daemon typecheck, guard, and the task harness.

* feat(daemon): observe exact OD Next prompt inputs

* feat(daemon): bind and observe Claude native subagents

Register the replay-verified Claude 2.1.233 tuple, bind complex Build Packages to daemon-issued native Agent handles, and derive completion only from durable child lifecycle evidence.

Export bounded parent-tool, Child Agent, and Child-tool spans through the existing legacy and OTLP task hierarchy without exposing raw tool inputs, outputs, provider ids, paths, or credentials.

Validated with focused contracts and daemon suites, the 17-case real-server chain, workspace typecheck, guard, and the 26-unit task harness.

* fix(daemon): repair OD Next rollout authority and controls

* fix(daemon): harden OD Next public canaries

* refactor(daemon): extract OD Next prompt preparation

* test(daemon): align plugin fixture with project schema

* test(e2e): assert daemon-owned prototype routing

* fix(daemon): restore OD Next task observability

Default eligible OD Next tasks to privacy-aware Langfuse hierarchy delivery through relay or direct sinks, with a versioned safe quality projection.

Persist lightweight representation ownership before suppressing mapped Runs, retain best-effort startup retry semantics, and preserve single-Run compatibility when Task delivery is not eligible.

Adopt the active payload relay for historical packaged launchers and cover the real sidecar environment handoff.

Validated with the full daemon suite, focused contracts/packaged/e2e/tools-pack suites, workspace guard, and workspace typecheck.

* fix(daemon): make OD Next capability fallback invisible

Admit capability-certified runtime adapters across CLI version drift while keeping recorded fixture versions as auditable evidence and reserving a narrow incompatibility denylist.

Fall back before Run claim on preparation failures, keep continuation bound to the frozen capability snapshot, and report truthful child coverage across Langfuse legacy and OTLP without replaying an active task.

Validated with focused contracts and daemon suites, the automatic server suite, pnpm guard, pnpm typecheck, daemon typecheck, LSP diagnostics, and git diff --check.

* fix(strategy): isolate OD Next automatic task routing

* fix(daemon): persist blocked strategy-task attribution

Every blocked strategy task now stores its gate reason codes and the
agent-visible turn text (blocked_reason_codes_json / blocked_visible_text)
so operators can diagnose a blocked task from the store alone. Covers the
coordinator blockTask gate, the automatic-continuation fail-closed path,
and startup run-terminal reconciliation.

* fix(daemon): accept form-only clarification turns by deterministic inference

A first Full-Plan request turn that renders exactly one question form and no
machine-protocol block has exactly one valid protocol meaning: outcome
clarification_required with an unlocked execution mode. The coordinator now
recovers that state deterministically instead of dead-ending the task as
blocked, stamps od_next_protocol_runtime_state_inferred into the accepted
state's reasonCodes for attribution, and keeps every ambiguous shape (extra
forms, recovered plan blocks, later stages, spent clarification budget,
any other protocol issue) fail-closed. This closes the observed field
failure where the agent's first reply produced a discovery form, the task
went blocked, and the user's form answer then failed with 409
STRATEGY_TASK_STATE_MISMATCH.

* feat(contracts,daemon): project blocked strategy-task attribution to clients

A task the OD Next protocol gate settles as blocked persists its
attribution (reasonCodes + agent-visible text) since 8bc7805e62, but the
client-facing StrategyTaskProjectionV2 dropped it, so the UI could not
tell a blocked task from any other failure. Add the optional
blockedContext field to the projection contract (additive, strict shape
mirroring the task store) and have projectStrategyTask carry it through
every run-status / SSE assembly point, only on a blocked outcome.

* fix(web): terminate question-form interaction when a strategy task is blocked

A blocked OD Next task is a sticky terminal verdict, but the web client
kept the turn's discovery/clarification form fully submittable; the
answer then 409'd with STRATEGY_TASK_STATE_MISMATCH and surfaced only
the generic 'Task failed' card.

- ChatMessage gains additive strategyTaskBlocked / strategyTaskBlockedText
  fields; strategyBlockedMessageFields derives them from any terminal
  blocked projection (visibleText preferred, trimmed, else null).
- streamViaDaemon/reattachDaemonRun expose onStrategyTaskSettled with the
  terminal task projection; ProjectView stamps the blocked verdict onto
  the assistant message on live settle, on reattach settle, and on the
  reattach status probe, and persists it so it survives reloads.
- AssistantMessage disables the inline question form for a blocked turn
  (including the auto-continue countdown) and renders the gate's visible
  text - or a generic localized notice - under the form.
- resolveRunFailureUi maps STRATEGY_TASK_STATE_MISMATCH to dedicated
  halted-task copy instead of chat.runError.title.generic.
- New i18n keys (title, 409 message, form notice) added to types.ts and
  all 19 locales.

Blocked-state interaction shape is a minimal safe default pending a
product decision: disable submission and explain, no auto-retry, no
degraded re-run.

* fix(daemon): normalize premature clarification execution mode

The observed 2/2 field failure on vague first prompts: the agent emits a
well-formed Runtime State block for clarification_required but predicts
"executionMode": "simple" alongside it. The mode is daemon-locked only
after clarification resolves, so the prediction carries no authority — yet
strict schema validation classified the whole block as
od_next_protocol_runtime_state_invalid_schema (non-repairable) and
dead-ended the task. The protocol stream now discards exactly that field
before validation, records the normalization for attribution, and the
prompt recipe gains an explicit rule plus a clarification-shape Runtime
State example so agents stop copying the plan_ready example's mode.

* fix(strategy): declare the runnable-entry deliverable convention to agents

The deliverable validator requires a root-level runnable entry (root
index.html for html deliverables), but neither the frozen prompt bundle nor
the production continuation ever stated that convention — the agent was
told only 'editable prototype source with a stable runnable entry', built a
framework scaffold in good faith, and the completed task then blocked with
od_next_canonical_deliverable_invalid. The production continuation and the
prototype Task Skill now state the root-entry convention and forbid
unplanned framework/dependency scaffolds, so the contract the daemon
validates is the contract the agent was given.

* fix(daemon): stop the rollout only for real machine-contract failures

rolloutStopSignalForBlockedContinuation mapped every reason code containing
'protocol' to machine_contract_leak, whose stop mode is a hard instance-wide
off that survives restart. Two vague prompts were therefore enough to
silently disable OD Next for the entire daemon: the agent emitted a
clarification runtime state carrying a premature executionMode, the task
blocked, and every later request in the daemon fell back to the legacy
plugin with no UI signal and no automatic recovery.

The machine block never reaches the user — the protocol stream suppresses
undelimited and oversized bodies — so an agent turn that omits, duplicates,
or mis-populates a block is a per-task defect, already fail-closed for that
task and now attributable from the store. Only the two block-delimiting
failures keep raising the leak signal.

* fix(strategy): state the entry-resolution constraint without inventing a product rule

The previous wording told agents that a prototype's runnable entry must be a
root index.html and that framework scaffolds are forbidden. Neither is in the
product spec, which says only that the user must be able to open and use the
deliverable from a real entry point, and neither matches the validator, which
resolves an entry through root index.html, then a single root-level html file,
then a single kind-matching file. That turned one fallback branch into a
product constraint the agent then had to obey.

Both surfaces now state the resolution rule the daemon actually applies and
why a delivery is rejected without it, and take no position on whether a
prototype may be an application project. Whether the validator should also
resolve framework-project entries is a product decision and stays open.

* fix(strategy): assemble the OD Next prompt bundle as an XML tree

The first-turn payload was an XML envelope around four flat CDATA blobs,
with one of them holding 25.3KB of joined Markdown (89% of the bundle).
Three layers each threw structure away: the contract typed the bundle as
four strings, the daemon joined 15 individually-named contributors with
'\n\n---\n\n', and the recipe re-joined 10 sections while demoting the
stage/atom map to Markdown headings. The result inverted heading levels in
7 of 46 places and put a per-task UUID 1,268 bytes into the payload, which
pushed 23KB of byte-identical strategy content past the point where a
provider prompt cache can share a prefix.

Introduce a canonical XML tree primitive and a v2 bundle schema that nests
one element per spec slot. Because system_prompt is now the shared cache
prefix, it may hold only cross-task-constant content, so the recipe
identity, runtime planning facts, runtime tool environment, client system
prompt and form override all move into context, and user_prompt moves last.

Measured on the real strategy assets: 28,325 -> 26,010 bytes, 4 -> 16 CDATA
leaves, 7 -> 0 heading inversions, and the first byte that differs between
two tasks moves from offset 1,268 to 24,709 (past the end of system_prompt).

Persisted rows keep working: final_text_schema is dispatched per row, so a
v1 bundle still parses at its own version while new tasks mint v2.

* feat(daemon): let the main Agent choose the task route

Product spec 3.1 makes the main Agent the party that decides Direct Edit vs
Full Plan, and 6.1 puts that decision inside MVP scope. The daemon instead
locked the route before the Bundle was ever sent: routes/runs.ts passed
preference 'full_plan' with all five Direct Edit eligibility facts hardcoded
false, and decideStrategyRequestRoute short-circuits on that preference, so
Direct Edit was structurally unreachable. A local edit on an existing
artifact therefore paid a full planning round — measured at 2 Request Turns
and 61s for a colour change that the Agent itself correctly scoped to one
property.

Four of the five eligibility facts require reading the request, which the
daemon cannot do before dispatch. So the route now stays unlocked through
the request turn — a state the task store and the projection contract both
already model — and the daemon adopts the route the Agent declares in its
Runtime State. Every later turn keeps the existing lock: a route may never
change once set.

The Full-Plan-only guards for clarification inference, serialization repair,
and the repair anchor now also accept the unrouted first turn, since each of
those paths is itself a Full Plan declaration and locks the route as it
transitions. blockTask settles an unroutable turn on the spec's documented
fallback route rather than refusing to record it.

No contract field, DB column, or prompt byte changes: the recipe already
tells the Agent to choose the allowed route on the request stage, and
Runtime State has always carried .

* fix(strategy): tell the Agent it owns the request-stage route choice

Unlocking the route in the daemon was necessary but not sufficient. The
recipe still read "use the supplied task facts to choose the allowed route"
and immediately followed with "For a Full Plan route, the request and
clarification stages are planning-only", so the Agent kept assuming a Full
Plan had already been chosen for it. Verified through the real UI: an
unambiguous local edit still came back as full_plan with the Agent stating
'this turn will not modify files'.

The request-stage section now says the choice is the Agent's, that Open
Design leaves the route unset until the first Runtime State declares it,
and that the active orchestration Skill's eligibility conditions decide
between the two. It states the mechanism and defers the criteria to the
Skill rather than restating them.

Verified end-to-end in Chrome against a real codex run: a local colour edit
on an existing artifact now routes direct_edit / simple and completes in
ONE Request Turn (50s), matching spec 3.4. Ambiguous requests still route
full_plan, and a request needing clarification still routes full_plan
because Direct Edit is request-only.

* fix(observability): restore failed-Run process evidence in the task hierarchy

PR #7016 replaced the per-Run Langfuse trace with a task hierarchy
(`strategy-task:<taskExecutionId>` + `strategy-stage:*` child spans) and
durably suppresses the single-Run trace. Failure classification survived
the move, but the terminal process evidence the single-Run trace carried
— stderr/stdout tails, host close diagnostics, exit code and signal —
did not reach Langfuse at all, so a failed Run lost the only facts that
explain *why* it failed.

Carry those facts through the existing safe-projection boundary:

- `SafeRunQualityV1` gains an optional `process` section
  (`exitCode`, `signal`, `stderr`, `stdout`, `diagnostics`), with
  `SafeObservationStreamTailV1` and a bounded `SafeRunDiagnosticsV1`
  record that admits only booleans and identifier-shaped values.
- `buildSafeRunQualityProjectionV1` builds it. Stream tails reuse the
  producer's existing 20-line / 4 KiB cap plus `redactSecrets`, then add
  the wider Prompt-stack local-path masking before `safeQualityText`.
  stderr follows the run error-message rule; stdout is the agent's own
  output and needs content consent, reporting its line count with an
  explicit limitation when withheld.
- `buildSafeRunQualityProjectionFromDaemon` collects the same summaries
  the single-Run bridge collected, and the aggregation projects them
  onto the `strategy-stage:*` span metadata for both the legacy and OTLP
  exporters.

The smoke test now asserts the task-hierarchy shape (trace id, child
span id/name, failure metadata keyed by runId) and re-asserts the
diagnostics coverage there, including that a local path in stderr is
masked before transport. Each case gets its own daemon and `OD_DATA_DIR`
via `vi.resetModules()`: the OD Next rollout stop latch lives in SQLite
and fires per daemon instance, so a shared data root silently pushed
later cases onto the compatibility single-Run reporter. The one case
that legitimately stays on that reporter (deepseek, outside the OD Next
runtime path) now asserts the same coverage on the single-Run trace.

* test(daemon): make the run-failure smoke deterministic under load

The failed-run smoke could report `expected 'process_exit' to be 'auth'`
under CPU pressure. Two independent sources, both closed here.

1. The fake `claude` was not a valid OD Next Claude runtime. `detectAgents`
   parses `claude -p --help` for every flag in the runtime def's
   `capabilityFlags`; once a physical Run is mapped to an OD Next strategy
   task, `buildArgs` sets `observeNativeChildBehavior` and THROWS when the
   probe never saw `--forward-subagent-text`. The fake advertised neither
   that flag nor `--agents`, so whenever the task mapping was already
   visible to `startChatRun` the Run died on that host-side TypeError
   before the agent was ever spawned — reported as `AGENT_EXECUTION_FAILED`
   / `process_exit` instead of the auth, rate-limit or timeout failure the
   case scripted. Captured in the wild on the `hang_timeout` case
   ("Claude native Child behavior observation requires advertised
   --forward-subagent-text support.", 9 events, no spawn) and reproduced
   deterministically against `buildArgs`: old help text throws, derived
   help text builds args containing the flag.

   The fake's help text is now derived from `claudeAgentDef.capabilityFlags`,
   so it cannot drift from what the daemon probes for and the Run always
   reaches the scripted failure regardless of mapping order.

   This gate pre-existed the task-hierarchy change — the same fake and the
   same daemon check are on `main`. But that commit gave every sub-case its
   own daemon and data root, which keeps the OD Next rollout active for all
   five instead of only the first, so exposure went from one Run per file
   run to five.

2. The events log was sampled at an arbitrary moment. `GET /api/runs/:id`
   publishes terminal status before the buffered `events.jsonl` write has
   necessarily reached disk, so re-deriving a classification from that file
   could see a log whose error record was still in flight.
   `readCompletedRunEvents` waits for the log's own completion marker — the
   `end` record the daemon writes last, before closing the stream — rather
   than for a fixed delay or for the assertion to pass.

Each case now also asserts the daemon's own settled verdict
(`run.failureCategory` / `failureDetail` from the terminal status), which is
what the chat UI, the persisted message and telemetry actually consume. The
one case rejected before the runtime finish path declares
`daemonPublishesVerdict: false` rather than tolerating a null.

Verified under load average ~45-50 on a 10-core box (22 spinners plus a
concurrent full daemon suite): case 1 alone 6/6, file + tests/observability
6/6, file + observability + telemetry-message-finalization +
prompt-telemetry 5/5, every case in isolation 4/4.

* refactor(strategy): reshape the OD Next Prompt Bundle to the PRD layout

The emitted v2 bundle wrapped the cache-stable head in a `system_prompt`
element the PRD never asked for, and named three blocks differently from
the spec. Adopt the PRD-literal naming with the wrapper removed:

  open_design_prompt_bundle
    open_design_core_system_prompt   (was system_prompt/core_system_prompt)
      execution_boundary / native_execution
      discovery_and_planning_surface / core_strategy
      output_contract / echo_guard   (moved in: both are output constraints)
    session_skills
    active_stages
    task_metadata                    (was task_config)
    context
    user_first_prompt                (was user_prompt)

Block order stays load-bearing: the first three blocks are byte-identical
across tasks of the same strategy version, task type, and execution
profile, so the shared provider-cache prefix now runs through the end of
`active_stages` instead of `</system_prompt>`.

Three noise cleanups land with the reshape:

- `frozen_skill_package` and `request_input_facts` are omitted when they
  carry no substantive content, generalizing the rule the PRD already
  mandates for `attachments`. An empty Skill roster and an all-zero
  workspace/comment/MCP state cost ~540 bytes and told the model nothing.
- `recipe_identity` drops `strategy_package_hash`, `task_skill_digest`,
  and `stable_prompt_identity`. They are audit-only; observability and the
  persisted strategy task record keep them.
- Prompt text naming the old tags is updated: the echo guard constant, the
  form-override strings in both call sites, and the map notes.

Schema id `open-design.od-next-prompt-bundle/v2` is unchanged — it has
never shipped — and the v1 parser stays intact for persisted rows.

* test(daemon): make ingestion waits report what actually arrived

A bare "timed out waiting for the task observation owning run <id>" does not
say whether the daemon delivered nothing, delivered the compatibility
single-Run trace instead, or delivered the span under a different Run — so a
CI-only occurrence costs a reproduction cycle to classify. Both ingestion
waits now append the received trace/span/generation ids, names and runIds to
the timeout message.

Diagnostics only; no assertion, budget or behaviour change.

* fix(daemon): gate OD Next admission on what the installed CLI advertises

Making OD Next the default design strategy turned every prototype/ppt/
marketing/hyperframes Run with `claude` into an OD Next Run, and every
admitted Claude strategy Run asks the CLI for native Child behaviour
observation. `claude.buildArgs` refuses that unless the installed build
advertises `--forward-subagent-text`, and the refusal is a thrown
TypeError inside the spawn path, so the Run dies as
AGENT_EXECUTION_FAILED before the agent ever starts.

Admission never established that fact. `runtimeCapabilityVerified` was
derived from a bundled capability fixture keyed by runtime path + agent
id + adapter version, plus an `ensureDetectedRuntimeVersions` probe that
reads only `--version`. The fixture proves what the claude-code runtime
*path* can do; it says nothing about the build on the user's disk. So the
daemon admitted a task it could not launch.

The symptom hid on developer machines. A real Claude Code install had
already populated the process-wide `agentCapabilities` map through full
detection, and that stale entry answered "advertised" for a fake CLI that
advertised nothing — which is why four pre-existing daemon suites and one
e2e suite passed locally and failed on CI hosts with no Claude Code
installed. Ambient developer state was deciding the verdict.

Establish the fact at admission instead. `ensureDetectedRuntimeCapabilities`
runs the same bounded `-p --help` read full detection already uses, keyed
by the resolved launch path so a configured CLAUDE_BIN cannot be answered
from another binary's cached probe. An installed build that does not
advertise what OD Next will demand loses admission and takes the ordinary
route, reported as `od_next_rollout_capability_advertised_capability_missing`
with the missing keys on the `[od-next-rollout]` line.

`customAgents` is required alongside `forwardSubagentText` because a simple
task may escalate to complex after planning, and discovering that gap at
the production stage would strand the task mid-flight.

Red spec: apps/daemon/tests/od-next-advertised-capability-admission.test.ts

* fix(daemon): let a re-entered turn resume its own session

latestCompletedAssistantMessageId excludes the caller's in-flight assistant
placeholder so it cannot count as conversation advancement. Its
resumableMessageId parameter exists to admit the stored session cursor back
through that filter — but the exclusion was evaluated first, so when the
placeholder IS the cursor the row was already gone and the admission never
applied.

That collision happens when a daemon-internal restart re-enters
startChatRun with the same assistant message after that message was already
persisted as the session cursor: post-tool recovery writes the cursor, and
nativeSessionContinuePending is consumed once, so a later safe-retry restart
arrives without it and falls through to the ordinary guard. The live session
was then abandoned and the whole transcript re-seeded. For an OD Next
continuation it is worse than a slow turn — the non-request stage refuses to
cold re-seed and blocks the task instead.

The exclusion now yields to the admission for exactly that row. Advancement
detection is untouched: any later succeeded message still wins on
position DESC and still fails the cursor comparison, which a new test pins
so the fix cannot later be simplified into an unconditional cursor match.

* fix(web): hide internal OD Next rollout controls

* fix(daemon): stop OD Next from discarding delivered work

Seven defects found by running the four OD Next runtimes end to end. Each one
silently threw away a turn that had actually done its job.

- OpenCode wrote deliverables outside the project. It resolves its workspace by
  walking up to the nearest enclosing git root rather than using its process
  cwd, so a managed project directory — never a repository, and in a
  development install nested under the checkout — made it adopt the whole
  repository. The project stayed empty, artifact detection saw nothing, and the
  Run reported `no_artifact`. The BYOK definition already carried this
  workaround; the native one was missed. Sent unconditionally like BYOK does:
  gating it would turn a missing flag back into silent data loss, where an
  OpenCode build without `--dir` should fail loudly at spawn.

- The `--help` capability probe read only stdout. OpenCode writes its entire
  help to stderr, so every one of its advertised flags resolved to `false` and
  the capability mechanism was inert for that runtime — `--dir` above, and
  `--dangerously-skip-permissions` on builds that support it.

- Optional argv flags were dropped on a cold daemon. `agentCapabilities` was
  only warmed by full detection, so a turn that spawned before Settings or
  `/api/agents` built argv from an empty map. Resolve the advertised flags on
  the run path first.

- One ordinary simple Run could disable OD Next daemon-wide. A child-evidence
  coverage gap latched the rollout stop regardless of execution mode, but only
  the complex production gate consumes per-child coverage; a simple task has no
  Child agents, so `unavailable` there is accurate missingness. Recovery needed
  an operator `od strategy rollout reset`.

- A Direct Edit that delivered but never declared it was refused. The agent
  writes the canonical deliverable, `validateRunDeliverable` resolves it, and
  then the turn carries no machine block at all. No repair could rescue it:
  serialization repair needs a recovered Plan Contract to anchor on. Infer the
  completion from delivery Open Design verified itself, fail-closed on every
  weaker signal.

- A production block named nothing. Every other blocking path persists its
  reason codes; this one did not, so the most common production block reached
  the client anonymous.

- The web client remapped a blocked verdict onto a Run that had succeeded and
  delivered, hiding the artifact behind a generic failure card.

Also: tell the request stage the canonical-deliverable rule that judges a
Direct Edit completion. It shipped only in the production continuation, so
Direct Edit agents were graded on a contract they were never given.

Blocked diagnostics now report which machine blocks the turn actually emitted,
because "malformed block" and "no block at all" have different remedies and the
reason code alone cannot tell them apart.

* feat(daemon,contracts): expose which turn a strategy Run belongs to

A Full Plan task runs as several physical Runs (request -> production) that
the user asked for once, and the daemon-issued continuation carries no user
prompt of its own. Clients had no way to tell that those Runs are one
conversation turn, so the continuation surfaced as an answer nobody asked
for.

The daemon stays the single writer of one message per Run. It now also
reports the logical-task position alongside each message
(strategyTaskExecutionId + strategyTaskRunIndex, resolved in bulk from the
Run mapping) and emits a strategy_task_continuation diagnostic before the
source stream closes, so a live client learns the turn is still open. The
SSE diagnostic channel is contract-typed for the first time — the daemon has
always emitted it, ChatSseEvent just never declared it.

The read-side projection deliberately bypasses the record parser: it is for
display, so it must stay cheap across a conversation and must never let one
unverifiable task record fail the whole message list. Existing conversations
resolve their history without migration.

* fix(web): render a Full Plan task as one conversation turn

Each physical Run of a strategy task arrived as its own assistant message,
so a Full Plan answered one question with two turns: the planning Run
finished mid-answer — author line, "completed", copy/branch/rate actions —
and the production Run then opened a second turn that no user prompt
preceded.

The fold happens purely in the view. Messages sharing a task execution id
are folded into the turn's first message in Run order, appending content,
events and produced files, so nothing is dropped and nothing is repeated.
The turn reports the latest Run's status: an intermediate Run finishing is
an internal step, not the turn ending. Direct Edit (a single Run) and
ordinary runs are returned untouched.

Folding in the view rather than by re-pointing a message at the successor
Run is deliberate — the daemon owns message writes, and a client that also
re-points would have the continuation's output persisted twice.

* fix(daemon): let every runtime finish an OD Next complex task

Complex production was reachable for Codex, native OpenCode and AMR but
finishable only by Claude, and the refusal arrived after a full production Run
had already been spent — then latched OD Next off for the whole daemon.

- Scope Build Package ownership to the runtimes that can answer it. Completion
  demanded `attributes.buildPackageId` on every Child, but ownership rides on
  Claude's `--agents` / `subagent_type` transport and only that adapter stamps
  it. Assertions a runtime cannot answer are now skipped for it while every
  lifecycle, graph, parent and ordering assertion still applies to all four.
  This also makes those lifecycle checks reachable at last: a failed package
  check used to `continue` past the started/terminal verdict for that Child.

- Publish child-evidence coverage for ACP. The AMR branch emitted none, so
  every AMR task aggregated as `child_lifecycle_unavailable_not_zero` — unable
  to distinguish "no Child agents" from "nobody was observing". The consumer
  already accumulated bindings, negotiation state and rejection reasons; it now
  summarizes them, and reports `unavailable` rather than `complete` when the
  capability was never negotiated. The branch had no child-close handler at all.

- Promote native OpenCode child evidence to L2. The live root stream filters
  child sessions out, so it can only yield a terminal-only candidate that fails
  the evidence graph on `child_started_missing` and refuses every complex task.
  `opencode export --sanitize` is the one surface pairing a child's parentID
  with its transcript, and it can only be read once the child has exited. Half
  a lifecycle is worse than none — it adds an id that fails the graph while
  displacing the L1 candidate carrying the bounded Prompt — so a partial or
  unavailable export leaves that Child wholly on L1.

- Recover an undeclared production completion. A production turn is only ever
  entered from a locked Full Plan and its schema admits no non-terminal
  outcome, so a turn that ran the frozen plan, delivered a canonical entry Open
  Design resolved itself, and then answered in prose has exactly one thing it
  could have declared. Refusing it discarded a finished multi-page deliverable
  already sitting in the project.

Two diagnosis surfaces that hid the above:

- A schema failure reported only Zod's first message, so an absent required
  field read as the bare word "Required" with no way to tell which field the
  agent omitted. Carry the path.

- Repair-anchor recovery only matched a block that was nothing but a fence, so
  an agent narrating either side of its JSON produced no anchor and the one
  allowed serialization repair could not engage. Recover the first balanced
  object too; it still has to satisfy the full schema and is still only ever an
  anchor, so a wrong extraction yields nothing.

* fix(daemon,contracts): judge Build Package ownership by the evidence

Complex completion asked whether the selected agent was Claude, then skipped or
demanded `attributes.buildPackageId` on that answer. Ownership is a best-effort
attribute riding on Claude's `--agents` / structured `subagent_type` transport,
so an agent allowlist gets all three of its edge cases wrong: a runtime that
starts stamping ownership stays unverified and its real evidence is discarded, a
Claude turn whose model slipped is refused outright and latches OD Next off for
the whole daemon, and the list needs maintaining for every future runtime.

Ask the evidence instead. Ownership is asserted when every Child observation
names a package and skipped when none can, so verification follows capability
without anyone maintaining a list. Every lifecycle, graph, parent and ordering
assertion applies either way.

The binding factory keeps its own `claude` check: constructing `--agents`
handles is transport-specific and genuinely belongs to that runtime, which is a
different question from whether recorded evidence can be verified.

Also tell the Agent whether the runtime has the verified structured native Child
lifecycle that complex mode requires. The core strategy makes it a precondition
for locking complex, and the capability snapshot already answers it, but only
its hash reached the prompt — leaving the Agent to judge a capability it cannot
observe.

* fix(contracts): teach the plan contract the shapes only a complex plan emits

The canonical Plan Contract example is a simple plan, and the schema rejects a
simple plan that declares Build Packages, so that example necessarily carries
`buildPackages: []`. The prose beside it spells out the element shape of
`buildRequirements` and `readinessArtifacts` but never of `buildPackages`,
leaving the one array unique to complex mode with neither a template nor a
description. A model asked for complex had to invent seven `.strict()` field
names: codex and opencode independently guessed `dependencies` plus a stray
`boundary`, and both terminated on
`od_next_protocol_plan_contract_invalid_schema`.

The same single-item example hides a second rule. It makes
`canonicalDeliverable.id` equal the sole `requiredDeliverables` id, so the
membership invariant reads as a coincidence rather than a constraint. A complex
plan declares one deliverable per page and picks one as canonical, at which
point codex emitted a canonical id that appeared nowhere in the list.

Describe both in the output contract, next to the element shapes already
documented there. With this, codex and opencode both reach
`executionMode: complex`, and opencode carries a complex plan through to
`outcome: completed` with three native `task` children and
`child_evidence_coverage_v1` reporting complete coverage of all three.

* fix(daemon): name the codex rollout root so a complex Run can see its Children

`collectCodexChildEvidence` reads native Child lifecycle out of
`<CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*.jsonl`, and deliberately refuses a
homedir fallback so it can never attribute one install's sessions to another.
That leaves the caller owing it an explicit root — and no caller ever supplied
one. `spawnEnvForAgent`'s codex branch was a pure pass-through, so the
collector's `CODEX_HOME` guard was false on every default install and the
collector has been unreachable since it landed.

The visible symptom was a complex codex Run that locked `executionMode:
complex`, spawned three native sub-agents, delivered four valid files, and was
then refused certification for child evidence the daemon had never gone
looking for. The rollouts were on disk the whole time, with each child
declaring the parent thread id.

Backfill the root before launch, mirroring the AMR `HOME` and
`OPENCODE_TEST_HOME` backfills next to it. The collector's no-fallback
invariant is preserved: the caller still names the exact root it hands the
runtime.

* fix(daemon): report unobserved Children as missing evidence, not malformed

`resolveDaemonOwnedOdNextComplexRuntimeEvidence` always brackets the Child list
with a running/completed root pair, so the empty-observation branch in the
complex gate is unreachable in production: a Run whose Children were never
observed still arrives with two perfectly valid observations and falls through
to the graph check, which reports `od_next_complex_child_evidence_invalid`.

That sends whoever debugs it hunting a corrupt payload. In the case that
prompted this, nothing was malformed and nothing was observed — the collector
had never run at all. Name the two states apart so the reason code says which
one happened.

* fix(daemon): stop one corrupt task record from voiding a whole boot's terminals

Reshaping the OD Next Prompt Bundle changed the v2 child tag set but kept the
schema id `open-design.od-next-prompt-bundle/v2`, on the reasoning that v2 had
never shipped. That holds for releases and not for a developer's own daemon:
the previous v2 composer was HEAD long enough to persist rows that still claim
today's label over a layout its parser cannot read. Those rows raise a raw
`TypeError` out of `parseOdNextPromptBundleV2`.

Startup reconciliation called `reconcileStrategyTaskRunTerminal` unguarded, so
that TypeError escaped `reconcileDurableRunTerminals` entirely. One unreadable
record therefore skipped the task-fact seeding, the whole PostHog `run_finished`
and Langfuse replay loop, and the task-observation crash-window sweep — for
every other Run on that boot, not just its own. Twelve consecutive boots of a
local daemon lost their terminal telemetry this way, and it only stopped when
the data directory was wiped.

Absorb the failure per Run, the way `strategyTaskTurnsForRunIds` already does
for the message list: a task row nobody can read is that Run's problem alone,
and startup still owes every sibling its terminal obligation.

* test(daemon): expect the pinned workspace in the OpenCode connection argv

Pinning OpenCode's workspace with an unconditional `--dir` also reaches the
connection-test probe, which spawns in a per-probe temp directory. That is the
intended reach — a probe must not adopt the repository root as its worktree
either — but the argv assertion was still written against the pre-`--dir` shape
and had been failing since.

Assert around the minted path instead of removing the check: the fixed prefix,
the flag, an absolute value, and the remaining flags in order. The 1.3
compatibility this test guards is a property of the argument order, so the
order stays asserted byte-for-byte on both sides of the temp path.

* fix(daemon): let an agent probe end even when the CLI swallows SIGTERM

`execFile`'s `timeout` is not a settle guarantee. On expiry it only *sends*
`killSignal`, and the promise settles on `'close'` — which needs the child to
actually exit. An agent CLI that traps SIGTERM and keeps a handle open leaves
the probe pending forever, and no caller's `catch` ever runs, so the
degrade-on-probe-failure path every caller already has is unreachable.

That became a hang on the Run path once `startChatRun` began awaiting
`ensureDetectedRuntimeCapabilities`, so the argv it builds matches a daemon
that has served `GET /api/agents`. The probe's cache key includes the resolved
launch path, so a fresh binary always misses and always probes. Against a
SIGTERM-trapping CLI the Run then parked in `queued` roughly 490 lines short of
`run.status = 'running'`: never spawned, so the inactivity watchdog never
armed, so it never reached a terminal status either.

Kill probes outright instead. They are read-only metadata calls with nothing
to flush, and every caller already treats a rejection as "probe failed, use the
safe baseline". `killSignal` is listed before the option spread so a caller
that needs a gentler signal can still ask for one.

* fix(daemon): stop discarding a codex Child that its parent re-invoked

A Codex sub-agent does not run once. The parent re-invokes it, and every
invocation opens another turn in the Child's own rollout, so `started` followed
by N-1 `interacted` is the ordinary shape of one delegated Build Package. The
collector treated a Child with more than one own turn as ambiguous and dropped
it, which meant a complex Run delegating three packages had all three Children
discarded and reported `knownChildCount: 0` with
`availability: partial` — while three complete Child lifecycles sat on disk,
each declaring the parent thread id.

The guard was defending something real: terminals are read from the parent's
activity list, so handing every turn the whole session's activities would let
one parent record terminate them all and stamp them with a single `endedAtMs`.
Slice the activities by the turn they occurred in instead — the parent's
records align with the child's turn starts within tens of milliseconds — so
each turn reads its own terminal, and a turn the parent never closed falls back
to the terminal its own rollout recorded.

Known gap, left for a follow-up: `knownChildCount` is keyed per (session, turn),
so it now reports one entry per invocation rather than per sub-agent. It feeds
the coverage diagnostic only — the complex gate counts observations — but it
makes codex's figure incomparable with the per-child count OpenCode reports.

* feat(strategy): port spec normative content into OD Next assets (#7141)

* feat(daemon): admit Vela to the OD Next capability registry

Vela drives the same native OpenCode runtime this registry already admits under
`native-opencode`. Its Child mechanism is that runtime's, reached over the ACP
extension instead of the CLI stream, and its fixture passes all seven evidence
paths. Holding the tuple out therefore never withheld an unproven capability —
it refused OD Next to an agent on the separate grounds that the build is not
yet immutable, which left every AMR run on the legacy plugin with
`od_next_rollout_x1_capability_fixture_unverified`.

Admit it. Re-pin `agentCliVersion` here once Vela publishes a build reporting a
stable producer version; that is the condition the old comment was really
waiting on, and it is a version pin, not an admission question.

Verified against a live daemon: AMR now resolves `od_next_rollout_eligible` /
`active`, and a simple full-plan task runs through OD Next to
`outcome: completed` with a valid deliverable. Complex still blocks for a
separate and honest reason — the installed Vela advertises no
`child_agent_lifecycle` producer, so no Child can be observed at all — which is
a Vela-side capability, not a gate.

* fix(daemon): stop one runtime's unverifiable Children from disabling OD Next

The rollout stop latch is `daemon_instance`-scoped and survives restart, so it
must be reserved for failures of the machine contract itself — those are true
whichever agent hit them. Unverifiable Children are not: they are a property of
one runtime. Vela ships no child-lifecycle producer today, so an AMR complex
Run cannot be certified at all, and every one of them took OD Next away from
Codex, Claude and OpenCode until an operator ran `od strategy rollout reset`.

The same latch also fired after a SUCCESSFUL run. `availability` demotes to
`partial` on any limitation, and a Codex collection always carries
`codex_inherited_turn_excluded` — children replay their parent's turns — so a
complex task that completed and delivered still disabled the strategy behind
itself. This was observed on a live daemon: the task reached
`outcome: completed`, and the rollout latched `complex_child_unverified`
seconds later.

Child evidence now raises no daemon-wide signal. That task is already
fail-closed with its reason codes persisted, which is the proportionate
response; route/mode drift and machine-contract leaks still latch.
`complex_child_unverified` stays in the reason-code union so daemons already
holding that latch, and the records explaining them, keep parsing.

* fix(daemon,contracts): count Child agents, not their parent's re-entries

`knownChildCount` had no stated unit, so each adapter picked one. OpenCode
counts distinct Child ids; Codex identifies an observation per (session, turn)
and the count was derived from observation ids at the call site, so a Child its
parent re-entered was counted once per invocation. A run with three sub-agents
reported four, and the two runtimes' figures were silently incomparable.

Say the unit in the contract — distinct Child agents, not invocations — and let
the Codex adapter own its own figure, the way the OpenCode adapter already
does. The per-turn observation identity is unchanged: it is what keeps each
invocation's lifecycle separate, which the complex gate still reads.

Verified on a live codex complex run: three native sub-agents now report
`knownChildCount: 3`.

* fix(daemon): stop inferring a complex completion nobody verified

The production-completion inference recovers a task the agent finished but
failed to declare, resting on Open Design having resolved the evidence itself.
For a simple plan that evidence IS the canonical deliverable, so the inference
is sound. A complex plan additionally owes verified native Child lifecycle —
the property that makes it complex — and no deliverable check substitutes for
it.

Accepting any locked execution mode therefore certified Children nobody
observed. Caught on a live AMR complex run: the installed Vela advertises no
child-lifecycle producer, the collector reported `knownChildCount: 0` with
`availability: unavailable`, the turn declared nothing, and the task landed
`outcome: completed` — walking straight past
`evaluateOdNextComplexChildEvidence`. The same run blocks correctly once the
inference is confined to simple.

* docs(daemon): record that Vela's Child producer is verified end to end

A local Vela built from the unmerged child-evidence branch negotiates
`vela.opencode.child_agent_lifecycle`, and an OD Next complex task on AMR now
reaches `outcome: completed` with `availability: complete`,
`source: vela_opencode_acp`, `knownChildCount: 3` and no limitations — three
native OpenCode sub-agents, one per Build Package, each observed through the
ACP extension. `verifiedRuntimeSupport` is the flag that makes exactly that
claim, so it is no longer false. `published` stays false: the producer is not
on Vela's main, so no user can install a build carrying it.

Also write down why this needs no version gate. Child evidence is reached by
capability negotiation, not by sniffing `agentCliVersion`: a Vela without the
producer never advertises it, so a complex task blocks on honestly missing
evidence, and one that has it works the moment it is installed. Admitting the
tuple therefore cannot make an older Vela claim Children it never reported,
and nothing has to be walked back when the producer ships.

* fix(strategy): default prototype demo content to real referents

Field test on beta.104 (claude, active, completed): the model read the
authentic-imagery clause, then legitimately routed around it by choosing
fictional demo books with CSS-drawn typographic covers — the clause is
conditional on content referencing a real entity, and nothing constrained
that upstream choice. The user-intended behavior (real books with real
fetched covers, for texture and credibility) never triggered.

Close the loophole with an affirmative default: prototype sample data
defaults to real, well-known referents with their real fetched images;
fictional stand-ins are a disclosed exception (user request or licensing),
and de-realizing content to avoid the fetch is named and forbidden. Core
baseline gets the matching one-liner so the default reads as baseline
discipline, not a profile-local whim.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(od-next): stop offering to continue work the task already delivered

A finished Full Plan turn could read as unfinished, offer to continue itself,
and have that offer end in a second task that could only fail.

A Run carries two accounts of whether its declared work is done. OD Next's own
verdict reaches `completed` only after the coordinator confirmed BOTH a
succeeded process AND a canonical deliverable resolved on disk — the entry file
exists, is readable, was touched by this Run, and matches the project kind. The
other account is the agent's last TodoWrite snapshot, unverified narration that
agents routinely leave with a stale `pending` item after writing the artifact.

The snapshot was winning. QA saw a task settle `completed` with seven artifacts
delivered, stamped `endedWithUnfinishedWork`, offering "continue remaining
tasks"; taking that offer opened a second task with nothing left to write,
which validated as `no_artifact` and settled `blocked`. One project then held a
completed and a blocked task for the same work.

Let the verified verdict outrank the narration, through one predicate shared by
the daemon's terminal classifier and both surfaces that read the snapshot — the
pinned todo card's continue affordance and the per-message footer. A
mid-generation truncation stays an independent term, so a turn cut off by
max_tokens is unfinished whatever verdict was recorded.

The verdict also has to survive a reload. The messages table has no strategy
column, so it lives only in the task store, and the client's projection probe
skips a task that is already terminal — reopening the project brought the bogus
offer straight back. The conversation read path already joins the task store to
place each message in its logical turn; it now carries the verdict along the
same join.

Watching a real Full Plan run in Chrome surfaced a second defect on the way in.
The live stream re-points ONE assistant message at each successor Run, so that
message accumulates every stage's output, while the daemon persists one message
per Run. On the post-run refresh the two views meet and
`mergeServerMessageWithLocal`'s "a longer local body is fresher" rule (#6396)
reads the accumulation as freshness and keeps it, beside the successor's own
row: the production answer rendered twice, live, until a reload cleared it.
Scope that rule to messages with no later sibling in the same task — the final
Run has nothing to have absorbed, so the guard is untouched for it and for
every ordinary turn.

Deliberately unchanged: `artifactCount <= 0 -> no_artifact` still blocks. That
guard is what stops a turn claiming completion it never delivered, and the
recovery case QA hit disappears once the offer stops appearing on finished
work. A turn that legitimately has nothing left to write needs a "no change
required" outcome in the protocol, which is a product decision, not a defect
fix. Note also that a settled task cannot be resumed — the daemon rejects any
continuation of a terminal task with 409 — so a continue affordance has no
choice but to open a new task.

Also drops `onStrategyTaskContinuation`, a client callback with no consumer
whose contract described re-pointing the existing message at the successor Run
— the very shape that prints the answer twice. The daemon still emits the
diagnostic into the Run event log, where it documents the hand-off for anyone
reconstructing a multi-Run turn.

Red first: the end-to-end spec fails on the pre-fix daemon with
`endedWithUnfinishedWork` true, again with the reload verdict `undefined`, and
the merge spec with the production answer counted twice.

* fix(web): stop leaking the strategy package above a user prompt

An OD Next turn opened with two rows of chrome the user never asked for: a
"Design" session-mode chip and a "Using OD Next Strategy V2" disclosure that
named the internal strategy package and its version. Neither is context the
user attached to the message — the daemon binds the strategy package itself,
and the mode chip only restates the default that a strategy-owned design turn
already implies.

Key the suppression on `AppliedPluginSnapshot.strategy`, which the daemon
stamps only for an internal strategy apply (verified against real persisted
snapshots: OD Next rows carry `strategy_json`, ordinary plugin rows store
NULL). A user-picked plugin, skill, design system or workspace reference still
renders exactly as before, and Ask/Plan stay labelled everywhere because those
are per-turn behaviour choices the user made.

An OD Next design turn now opens with the prompt itself.

* fix(strategy): add action-level layout mechanics to the baseline

Field test on beta.105 (first AMR active sample, deepseek-v4-flash,
prototype completed): the artifact shipped two elementary geometry
defects — the city switcher and daily-judgment regions overlapped, and a
stat numeral's percent sign wrapped to a second line inside a fixed-size
ring. The existing rules could not have prevented either: they are
outcome-level ('overflow-free layout', 'no overflow or clipping risk'),
and under ship-on-write the model never sees the rendered outcome it is
asked to guarantee.

Replace the bet on outcomes with rules about the source itself — verifiable
while writing: flow-only layout for sibling content regions (no absolute
positioning / negative margins / transforms to stack them), fixed-size
boxes only for fixed-size content, clamp()-sized display numerals with the
unit pinned to the same line, block-level display declared before sizing
track-style elements, no hidden-overflow masking, no 1–2 character
orphans. Core gains the Layout mechanics baseline bullet; the prototype
quality floor names the two observed failure shapes.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): add measure-then-size image geometry to the baseline

Follow-on field sample: a fetched, localized movie poster rendered
cropped inside a container whose fixed ratio the model invented — a
failure class created by the real-first imagery clause itself, since
fetched assets carry intrinsic dimensions the model never authored.

The fix is procedural, not aspirational: read the file's intrinsic
width/height during Build (input probing sits inside the ship-on-write
allowance), give the container the measured ratio (aspect-ratio or
natural flow), reserve cover-cropping for decorative backdrops, and
render content-bearing images — posters, covers, artwork, product
shots — at their full frame with no double-locked axes. Core gains the
Image geometry baseline bullet; the prototype quality floor names the
observed failure shape.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): anchor overlays on the anchoring axes only

Address PerishCode's review: 'explicit inset on every side' would, taken
literally, set all four offsets on an auto-sized absolutely positioned
badge and stretch it to fill its containing block — the exact malformed
overlay geometry this baseline exists to prevent. Anchor with offsets
only on the anchoring axes (top plus right for a corner badge), size
constrained independently, and name the stretch trap explicitly.

* revert(strategy): split image geometry out of this bugfix

Per review: the measure-then-size image-geometry policy addresses a
separate field failure (localized poster cropped by an invented fixed
ratio) and belongs in its own focused PR with that reproduction. This
branch returns to its stated objective: region overlap, stat-unit
wrapping, and track sizing.

* fix(strategy): add measure-then-size image geometry to the baseline

Field reproduction (beta.105, od-next active, prototype route): the
real-first imagery clause now makes runs fetch and localize real assets
— and the first such sample shipped a localized movie poster rendered
cropped, because the model invented a fixed container ratio for an
image whose intrinsic dimensions it never authored, then let cover-crop
hide the mismatch. The page showed a partial poster frame: wrong ratio,
content cut off. This failure class is created by fetched assets
carrying dimensions the model has not seen, so it cannot be prevented
by the existing outcome-level rules.

The fix is procedural and write-time-verifiable: read the localized
file's intrinsic width/height during Build (input probing sits inside
the ship-on-write allowance), give the container the measured ratio
(aspect-ratio or natural width:100%/height:auto flow), reserve
cover-cropping for decorative backdrops, and render content-bearing
images — posters, covers, artwork, product shots — at their full frame
with no double-locked axes. Core gains the Image geometry baseline
bullet; the prototype quality floor names the observed failure shape.

Split out of #7169 per review scope discipline.

Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.

* fix(strategy): allow intentional fixed and sticky chrome

Address PerishCode's review: 'normal flow only' contradicted the
prototype profile's own environment rules, which expect fixed headers,
bottom navigation, and floating controls that reserve matching padding —
a model following the core rule literally could strip valid mobile
bottom navigation. Narrow the prohibition to its actual target
(accidental stacking of sibling content regions) and state the chrome
allowance with its padding obligation in both the core bullet and the
prototype quality-floor line.

* fix(web,daemon): keep internal turn chrome and control markers out of the chat

Two acceptance findings, both the same shape: daemon-internal plumbing reaching
the reader.

The session-mode chip flashed "Design" above a freshly sent prompt and dropped
it a beat later. The optimistic user message renders before POST /api/runs
answers, so the strategy binding the previous fix keyed on simply is not there
yet. Key the chip on the mode alone instead: it labels a turn the user steered
away from the default, so Ask and Plan stay labelled and Design — the mode every
design project already runs in — carries no chip. That is decided the moment the
message renders, with no snapshot to wait for.

`<od-title>…</od-title>` rendered as message body. The stripper was armed only
for Runs that requested a title, and the web only requests one on the first turn
of a conversation, so from turn two onward nothing consumed the marker while the
directive was still sitting in the agent's own resumed session history. Arm the
stripper on every Run and swap only the announce callback, then strip again in
the chat: the OD Next machine protocol blocks join the title marker there, and
messages already persisted with a leaked marker render clean, which no
daemon-side fix can reach. Renderable artifacts (question-form, od-card,
artifact) are deliberately excluded.

* fix(web,daemon,strategy): stop a stray question-form marker from stalling a turn

An OD Next turn that needed no clarification narrated its decision straight
into the marker — `<question-form> 无需提出——…`, an unclosed tag whose body is
prose. Four surfaces independently read "an open marker is present" as "a form
is being generated", at three different strictness levels, and none of them had
a state for markup that can never render:

- the web parser raised a loading skeleton and truncated every character after
  the marker, so the turn's real output was invisible;
- `hasPendingQuestionForm` held the turn in a clarification handshake;
- `resolveDesignDeliveryOutcome` returned `awaiting_input` before looking at
  what the run had delivered;
- `listProjectsAwaitingInput` matched the marker with a raw SQL `LIKE`, latching
  the project on `Needs input` until the user typed something.

The daemon's own strict detector meanwhile scored the stray marker as zero
forms, so the coordinator accepted the turn with an empty reason-code list. The
one component that could have reported the contract break was blind to it by
construction.

Give every one of those decisions the same precondition — an open marker only
means a form is in flight while the text after it could still parse as the form
body — and make the malformed case reportable instead of silent. The two new
coordinator codes are attribution, never a gate: every code `validateAcceptedTurn`
returns blocks the turn, so a stray marker on an otherwise valid planning turn
still reaches production.

The awaiting-input queries needed care. They take the newest form-bearing
message per partition and test only that one for an answer, so the strict check
has to run before the newest-per-partition pick. Post-filtering the winning rows
would let a stray marker win its partition, fail the check, and drop a project
that is genuinely waiting on a real unanswered form. Both directions of that
ordering are pinned by specs, and the scan streams without retaining message
content because it runs on every `GET /api/projects`.

The strategy prompt now also forbids restating the marker to announce that no
form is needed. Every clarification rule was phrased as "when to emit a form" /
"skip the form"; nothing forbade writing the bare marker as a label, which is
what produced the turn.

* fix(strategy): define the session-skill conflict order (#7188)

The Instruction order named the three session-skill layers but gave no
tie-break when they conflict inside their shared applicable scope, so a
user-selected skill and a bundled profile could pull the model in
opposite directions with no principled resolution. Make the order
explicit: user-selected skills first, then the general orchestration
Skill, then the task-type profile — and state that the tie-break never
unlocks what higher rules forbid (ship-on-write actions, the V2 machine
contract, the user's explicit requirements). Spec source of truth
updated with matching wording in both languages.

Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>

* feat(od-next): restore anti-cliché visual rules across the strategy package

The od-next rewrite dropped the legacy anti-slop blacklist (core-slim /
discovery ❌ lists), leaving the package with no background-tone rules at
all — prototype.md even referenced 'the anti-cliché rules above' that did
not exist. Models revert to their warm-cream 'AI look' prior on every
task type.

- core-system-prompt.md: new cross-task 'Anti-cliché defaults' block in
  the Design baseline (warm beige/cream grounds, purple-gradient wash,
  stock UI display faces, left-border accent cards, icon-per-heading,
  gray hover, hand-drawn SVG people, invented metrics)
- prototype.md: resolve the dangling reference with a labeled rule
  cluster; add the designer/demo-controls ban
- ppt.md: new 'Avoid deck clichés' section (gradient-wash covers,
  icon-per-bullet, center-everything, corner blobs)
- marketing.md: cream/stock-gradient ground ban + never invent awards,
  badges, press quotes, or testimonials
- hyperframes.md: cream color-wash grade + particle/glow filler ban

Asset versions unchanged: package identity tracks content via sha256
digests; tests pin '2.0.0' literally.

* feat(web,daemon,contracts): carry an official example card into OD Next

Clicking an official example card under a task type pinned that example as an
executable plugin. The pin stamped explicit_user authority, which took the task
off the automatic route entirely — no strategy task, no plan, no clarification
round — while free text under the very same chip entered OD Next normally.

An example card is a more specific choice inside a task type, not a claim on
the strategy. So send its identity instead of a plugin pin: `exampleReference`,
which the daemon re-resolves through the same local catalogue lookup
`/apply-local` performs, reading the example's bytes from the resolved path and
never from the request body. Its SKILL.md and the side files its prose names
then travel where user-chosen material belongs — `session_skills/
user_selected_skills`, a bundle region that already existed in exactly this
shape and had no producer. No bundle format change, no schema bump.

The discriminator has to be the call site, not the record: the same
`InstalledPluginRecord` is simultaneously an example card, a chip's default
scenario plugin, and a Community entry, and nothing in its data tells the three
apart. `examplePick` is therefore set only by the example-rail handler, and a
genuine plugin pick from the Community page keeps its explicit authority. An
example card under a chip with no OD Next route — wireframe, mobile, image,
video — behaves exactly as before.

Drop the planning/Build-only guard from user-selected Skill capture. Measured
against real content it rejected most of the corpus it was applied to while
protecting almost nothing: it is 15 English regexes, so the same sentence
passes or fails on language alone — "After the build, run through the checklist
and fix any defects" is refused and its Chinese equivalent is admitted. It also
false-positives on prose that merely names the word, and it never inspected
side files at all, so a `references/checklist.md` always walked past it. 47 of
183 shipped examples tripped it, including all three default cards. Conflicting
instructions are resolved by the Instruction order the core system prompt
already defines; first-party strategy assets keep the guard, where the corpus
is controlled and the rule works.

Two latent bugs in that rail surfaced once it had a caller, both of the same
shape — one bad side file deleting an entire Skill. A prose link to a file that
was since flattened away (`example-hyperframes`) and a bundled screenshot past
the per-file byte cap (`example-open-design-landing`) each sank the whole
capture. Neither says anything about whether the Skill is safe to carry, so
both are skipped; a symlink, a symlinked parent, or a file that changes under
the read still fails closed. The byte cap keeps its purpose — oversized bytes
are never read — and `readBoundedNoFollow` still enforces it on what is read.

Answer the brief's template from the card's own input defaults. `od.useCase.
query` is a template that the ordinary route fills from the applied snapshot's
inputs; an example card deliberately creates no snapshot, so without this the
Agent received `{{artifactKind}}` and `{{audience}}` raw — a broken template
rather than an assignment. A field with no declared default keeps its token
instead of inventing an answer the card never gave.

Verified against the shipped corpus: 182 of 183 example folders capture and
serialize into a `user_selected_skills` region, the one exception shipping no
SKILL.md and belonging to no OD Next route. 102 cards across Prototype, Deck
and HyperFrames now enter the automatic route; the other 81 are untouched.

* fix(web,contracts): keep a nested Prototype scene on the OD Next route

Picking a second-level scene under Prototype — Mobile app or Wireframe — took
the task out of OD Next entirely, for free text and example cards alike, while
the first-level Prototype chip worked. Two independent layers excluded them and
cross-check each other fail-closed, so neither could be relaxed alone: the
composer swapped the route id to the scene's own catalog action id, which the
route table does not carry, and the metadata re-derivation separately rejected
`fidelity: 'wireframe'` and mobile platform targets.

Neither exclusion was protecting anything. The two scenes are the Prototype
route plus metadata, as their own chip definitions say — both bind
`example-web-prototype` with `kind: 'prototype'` and `automaticDefault: true`,
differing only by `platformTargets` or `fidelity`. And the Prototype task
profile already branches on exactly those two: it downgrades content and
interaction-state requirements when fidelity is wireframe or lo-fi, and it
carries mobile guidance down to touch reach and the 375/768/1024/1440
breakpoints. The route was being denied to the cases the profile was written
for.

A second-level scene refines WHAT to build, never WHETHER the parent task
type's route applies. Make that structural rather than enumerated: routing
resolves the first-level task type through `taskTypeChipIdForChipId` instead of
reading whichever catalog action the scene happens to bind, so a scene has no
route id of its own to route by and a scene added later cannot strand its
parent. Adding table rows would have fixed today's two and left every future
scene a silent-regression candidate.

On the contracts side `intent` is now the only field that can move a project
off a route — it is the only one naming a different pipeline. `fidelity` and
`platformTargets` stay in the signature to record that the decision has seen
them and chosen not to gate on them.

Deck and HyperFrames were never affected and now have witnesses that say so:
Deck's second-level rail is a card filter that never leaves the hero, and
HyperFrames has no rail at all. A catalog sweep pins the routed chip set to
exactly deck, hyperframes, mobile, prototype and wireframe, so Image, Video,
Audio, Document, Website clone, WebGL, Figma and the rest cannot join it
silently.

* refactor(web): make a nested Prototype scene a second-level type

#6914 moved Mobile app and Wireframe out of Home's first-level rail and into
Prototype's second-level scene rail — but only in the UI. Underneath, both
stayed full first-level chips with their own `apply-scenario` action, merely
hidden from the create rail, and the scene pointed back at them through
`actionChipId`. Its own comment recorded the compromise: keep their catalog
action ids so the nested scene preserves the metadata those actions stamp.

A thing that renders as a refinement of Prototype while still being a task type
in its own right is what dropped these two scenes out of OD Next: the composer
resolved the route from whichever action the scene bound, and Mobile and
Wireframe are not routes. `f360f46e0` stopped the routing site from asking.
This removes the ambiguity it was working around.

A scene now carries its own refinement — Mobile stamps the handheld platform
targets, Wireframe stamps the lo-fi fidelity — and binds its parent. The two
chips leave the catalog, because they were never task types. The refinement
type cannot restate its parent's `kind`, so a scene is structurally incapable
of becoming a type again.

Nothing the user sees changes: same eight scenes, same labels, same metadata
stamped byte for byte, same Prototype route, same example-card behaviour. The
integration assertion was tightened from `objectContaining` to an exact match
to say so, and the unit specs compare the serialized metadata rather than its
fields, since project metadata is persisted and compared as JSON.

Legacy ids still arrive from outside the catalog, so normalization moved to the
two boundaries where that actually happens — a persisted composer draft and a
queued one-shot chip intent — instead of living at the routing site. The queued
intent had no coverage and relied on `pickChip` normalizing before `findChip`;
it has a spec now. `taskTypeChipIdForChipId` is gone: with the scenes out of
the catalog, `active.chipId` is a task type by construction, and every producer
was traced to confirm none can yield a scene.

The placeholder carousel had the same latent shape — three scenario lines bound
a scene as if it were a chip — so they bind Prototype plus a scene id, and the
carousel narrows by scene slug. A one-click create from a scene's carousel card
now has a spec proving it keeps both the refinement and the route.

* fix(web): stop a restored composer pick from costing its task type

Dismissing the pill of an example card returned the Home composer to nothing —
no task type, no OD Next route, and the user's typed brief wiped with Send left
disabled — but only after the composer had been remounted. Opening a project,
visiting Settings, any EntryView swap: the composer comes back looking
pixel-identical, same type selected, same scene, same pill, same brief, and the
dismiss now destroys instead of stepping back.

The persisted chip draft records what was picked and never how. A restored
example card therefore comes back as an ordinary chip binding, while still
rendering its own pill and dismiss control — that check asks whether the record
differs from the chip's default plugin, which is true for every example card.
The dismiss handler keyed its step-back branch on the pick being explicit, so a
restored pick missed it and fell through to the reset that clears everything.

Persist how the pick was made and restore it, so a pick is the same object
after a remount as before one. Then key the step-back on the pick naming a task
type rather than on it being explicit: whatever a pick was, if it named a type
the user chose, dismissing it returns there. Only a pick that never named one
resets the composer, and the typed draft is left exactly as typed either way.
Drafts written before this carry no flags and restore exactly as they do today.

The same lost provenance had a second victim: an example card that was never
dismissed stopped sending its `exampleReference` after a remount, so the card's
own material silently vanished from the run while the route survived — on all
three OD Next routes.

The reproduction drives the real rails and the real bundled catalog through ten
first-level and second-level combinations, each on a fresh mount and across a
remount. The differentiator was never the type or the scene: every combination
loses its route once the pick's provenance has been through storage.

* feat(od-next): ship handheld device shells with the prototype task profile

Blind evals against the live slim-sp strategy showed OD Next losing every
phone-app prototype on one detail: the live route hands the agent a real
handset seed (design-templates/mobile-app), OD Next only described one in
prose and the agent drew a rounded card by hand.

- Declare task-profile `resources` in the bundled strategy contract; the
  prototype profile ships iPhone / Android / platform-neutral shells that
  enter the package identity with the profile (rule-card and shell edits
  move the hash alike) and are re-verified at consumption.
- Stage the shells into `<cwd>/.od-frames/` on every strategy run and
  add a "Handheld device shell" section to the prototype rule card with
  the brief→shell table and the shell invariants.
- Resolve the platform from project metadata and the user's own words
  (ios / android / mobile-neutral; responsive and desktop briefs veto the
  platform-less class), latch it per conversation, and quote the selected
  shell into the stable request context as `device-frame` +
  `device-frame-shell` facts.
- Observe at run finish whether the delivered entry kept the shell and
  expose it on run_finished (od_next_device_platform / _source /
  _shell_present) so the rollout can measure adoption.
- Shells collapse to a full-bleed phone page below 480px and scale on short
  viewports so the artifact still meets the 375px rule.

* docs(od-next): describe task-profile resources in the strategy package SKILL.md

* fix(od-next): stage device shells non-destructively and cover the collab exclusion

Review: `.od-frames/` is a name this PR introduces inside a user-owned
project directory, so wiping it on every run could delete pre-existing
content. Staging now keeps an ownership manifest
(`.od-frames/.od-next-device-frames.json`): only files it recorded earlier
are replaced or retired (and retired only while their bytes are still
ours); a pre-existing file under a managed name is left untouched and
reported as skipped; unrelated files are never read, written, or deleted.
Tests prove an unrelated file survives, a foreign `iphone.html` survives
repeated staging, and a user-edited staged file is not retired.

CI: `.od-frames` joins the expected member-mirror exclusion list in
`vela-cli-resource-adapter.test.ts`, which pins that list exactly.

* fix(od-next): prove device shell bytes are ours before replacing them

Review: listing a name in the ownership manifest was not enough to claim
it. After the first staging a user could edit `.od-frames/iphone.html`
and the next run's unconditional `writeFile` destroyed that edit, and a
pre-existing `.od-next-device-frames.json` under an unrelated schema was
read as an empty manifest and then overwritten.

Ownership is now provisional on both sides. A managed name is written or
retired only when the manifest claims it AND the bytes on disk still hash
to the recorded digest — the rule the retirement path already followed,
now applied to replacement too. A digest that moved makes the file the
user's: it is reported as skipped and drops out of the manifest, so later
stagings keep their hands off it. The control file gets the same
treatment: `readOwnership` classifies the name as absent / ours / foreign
instead of collapsing unreadable, malformed, and unrelated content into
an empty manifest, and a foreign control file makes the whole directory
off limits — nothing written, nothing deleted, every managed name
reported as skipped. In each case the quoted `device-frame-shell` fact
still carries the shell source, so the run itself is unaffected.

Two fixture cases, both red on the previous head: a shell edited after
staging survives repeated staging byte-for-byte, and a pre-existing
invalid manifest plus the user's own `iphone.html` survive untouched with
no shells created.

* fix(od-next): bound the device shell manifest to the names it can stage

Review: the manifest lives inside a directory the project controls, so
its key list was letting the project widen this feature's deletion set.
The parser accepted any slash-free basename with a SHA-256 digest, and
the retirement loop then deleted any accepted entry missing from the
current package while its bytes still matched. A project holding a
same-schema `.od-next-device-frames.json` that claims `leftover.html`
with that file's real digest therefore lost `leftover.html` on the next
strategy run — verified, not hypothetical: the new fixture fails on the
previous head with `ENOENT` on that file.

The bound is now this feature's own filenames rather than whatever the
manifest names. `MANAGED_SHELL_FILES` is derived from
`OD_NEXT_DEVICE_FRAME_FILES`, and a manifest is `ours` only when every
key is in that set and every value is a 64-hex digest — that is exactly
what this materializer writes, so anything else came from somewhere else
and the whole record is refused as foreign, leaving the directory alone.
Refusing beats salvaging the well-formed entries here: salvaging is what
would let a crafted entry keep nominating an unrelated file. The
retirement loop re-checks the allowlist as well, so the deletion set
stays bounded even if the parser is later loosened.

Manifests written by the previous head hold only shell names and valid
digests, so they keep reading as ours; no migration.

* fix(od-next): let an @-mentioned Skill ride the automatic route

Mentioning a Skill in the composer took the turn off OD Next entirely. The
project fell back to its task type's legacy plugin, so the chat header
announced "正在使用 Web Prototype" and the run reached the agent as ordinary
prose with no Prompt Bundle at all — no Runtime State, no Plan Contract, none
of the strategy the task type was supposed to own.

Three layers each dropped the Skill or the route, and every one of them had to
give for the turn to survive intact:

- The admission gate counted a named Skill as explicit user authority, next to
  a pinned plugin and a pinned snapshot. A Skill is not an executable surface
  and cannot run a strategy; it is material for one. It now stays out of that
  read, and a context plugin remains the only non-plugin signal that still
  claims the route.
- Home treated the task type and the Skill as mutually exclusive: picking
  either one wiped the other, and submit dropped whichever survived. That rule
  came from #2972, which asked for a defined answer when the prompt's intent
  and the picked card disagree. "The Skill wins, drop the card" is defined, but
  it routed a user who picked 幻灯片 into a prototype project, which is exactly
  the silent misrouting #2972 objected to. The task type now owns the route and
  the Skill is material carried inside it, so the conflict resolves without
  discarding either.
- The create handoff nulled `skillId` the moment a task profile was claimed,
  which would have swallowed the Skill one layer after Home stopped dropping it.

The catalogue capture entry point for exactly this already existed and had
never had a production caller — the gate above made it unreachable. It is now
wired through a single session capture that converges both channels a session
can select material through, because one task freezes ONE package: an
@-mentioned Skill and an official example card land in the same
`session_skills/user_selected_skills` slot, the mention first, which is the
authority order the strategy's own conflict rule already assumes.

Capture is fail-soft on every leg, matching the example-card rail: a Skill
deleted between picking and running, a catalogue that will not list, a
selection past the package bounds — each drops that material and runs the task
without it. The ordinary route drops an unresolvable Skill silently too, so
this is the same contract rather than a new leniency, and an unfreezable
selection can never cost the project the example card it was created from.

Skill-catalogue scoping moved to one helper shared with the system-prompt
composer. A Skill one surface can resolve is now a Skill the other can freeze;
resolving them from separately-derived partitions would drop a Workspace Skill
on whichever side guessed narrower.

Coverage: the three request shapes that carry a Skill (`ChatRequest.skillId`,
`skillIds`, and the Skill persisted on the project row by Home's create) each
have an HTTP witness asserting the Bundle text; all four routed task types are
covered for a mention arriving alongside their default card. Two server tests
that encoded the old "Skills route ordinary" contract, and the two Home specs
that encoded the mutual exclusion, were rewritten rather than deleted — each
now states the rule that replaced it.

* fix(od-next): accept a HyperFrames composition as its canonical deliverable

A HyperFrames run that wrote an editable root `index.html` was blocked with
`od_next_canonical_deliverable_invalid`, surfacing as "任务执行失败" even though
the artifact was sitting in the project and working. Every HyperFrames
production turn in the reported diagnostics bundle hit it.

`validateRunDeliverable` gated the entry's file kind on `metadata.kind`, and
`kind: 'video'` accepted only `video` files. But `kind` records which Home
surface a project was created from, not the shape of what it delivers. For
every other kind those coincide; HyperFrames breaks the identity — it is a
local HTML-to-video renderer, so the HTML composition is the authored artifact
and the MP4 is a render of it, produced on demand and not necessarily present.

OD Next already said so on the way in: `DAEMON_OWNED_OUTPUT_KINDS.hyperframes`
admits `html` and `source` as supported deliverable kinds at plan time, so the
same daemon approved a plan the completion check then rejected.

Resolve the accepted kinds from the project's metadata instead of its bare
`kind`, via one named invariant used by both entry inference and the kind gate.
Generative video (fal / Veo / Sora / Volcengine) keeps the strict video-only
contract: there the provider returns a video file, and an HTML preview page in
its place is a genuine delivery failure.

The `hyperframes-html` discriminator moves next to `ProjectMetadata` so
delivery validation and analytics `project_kind` read one constant.

Red spec: `apps/daemon/tests/run-deliverable-validation.test.ts` reproduced the
production `type_mismatch` before the fix, and covers both guards after it.

---------

Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: Cheems <cheems1229282@proton.me>
Co-authored-by: 黄桃 <huangtao@huangtaodeMacBook-Pro.local>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: hihijack111 <216108319+hihijack111@users.noreply.github.com>
2026-08-24 08:53:36 +00:00

1015 lines
33 KiB
TypeScript

import type { ChatMessage, ChatRunStatus, ChatSessionMode } from './chat.js';
import type { OrchestratorWorkspace } from './workspaces.js';
import type {
ProjectContextConnectorRef,
ProjectContextMcpServerRef,
ProjectContextPluginRef,
} from './context.js';
import type { ProjectSyncIntent, ProjectSyncIntentEvent, ProjectSyncState } from './project-sync.js';
import type { TeamResourceState } from './team-resources.js';
import type { WorkspaceCollabContext } from './collab.js';
export type ProjectKind =
| 'prototype'
| 'deck'
| 'template'
| 'other'
| 'brand'
| 'image'
| 'video'
| 'audio';
/**
* `metadata.videoModel` value that marks a project as HyperFrames.
*
* HyperFrames is a local HTML-to-video renderer, so a HyperFrames project is
* stored as `kind: 'video'` (the Home surface that offers it) even though the
* artifact the user authors, edits and previews is an HTML composition. Every
* surface that has to tell HyperFrames apart from a generative video provider
* — analytics `project_kind`, the system prompt's media contract, the Home
* starters, delivery validation — keys off this exact value, so it lives here
* next to `ProjectMetadata` rather than being re-declared per consumer.
*/
export const HYPERFRAMES_VIDEO_MODEL = 'hyperframes-html';
export type MediaAspect = '1:1' | '16:9' | '9:16' | '4:3' | '3:4';
export type ProjectPlatform =
| 'auto'
| 'responsive'
| 'web-desktop'
| 'mobile-ios'
| 'mobile-android'
| 'tablet'
| 'desktop-app';
export type AudioKind = 'music' | 'speech' | 'sfx';
export type ProjectScenarioBindingProvenance =
| 'automatic_default'
| 'explicit_user'
| 'legacy_unknown';
export type ProjectScenarioTaskProfile =
| 'prototype'
| 'ppt'
| 'marketing'
| 'hyperframes';
/**
* Daemon-owned identity for the scenario currently pinned to a project.
*
* The snapshot and plugin ids are deliberately stored together: provenance
* is authoritative only while both still match the live project pin. Old
* rows that pre-date this contract are migrated to `legacy_unknown` and never
* acquire automatic-routing authority by inference.
*/
export interface ProjectScenarioBinding {
schemaVersion: 1;
provenance: ProjectScenarioBindingProvenance;
pluginId: string;
snapshotId: string;
taskProfile?: ProjectScenarioTaskProfile;
boundAt: number;
}
/**
* Daemon-owned automatic OD Next route selected at project creation.
*
* Unlike `ProjectScenarioBinding`, this identity is independent of an
* ordinary plugin snapshot. That lets the daemon admit OD Next for a run while
* retaining the ordinary default plugin solely as an invisible fallback.
*/
export interface ProjectStrategyBinding {
schemaVersion: 1;
provenance: 'automatic_default';
taskProfile: ProjectScenarioTaskProfile;
boundAt: number;
}
/**
* Daemon-owned identity of the official example card that seeded this project.
*
* An example card is a task-type entry point, not a plugin the user chose to
* run: it keeps the automatic OD Next route and contributes its material as a
* user-selected Skill. So this binding deliberately carries no `snapshotId`
* and is never fed into the explicit-plugin tests — naming it must not move
* the task onto the ordinary route.
*
* `pluginSource` is the exact local catalogue identity the daemon re-resolved
* the record through, and `manifestSourceDigest` pins the bytes it read. A
* later read that cannot reproduce both is stale and carries no authority.
*/
export interface ProjectExampleBinding {
schemaVersion: 1;
provenance: 'example_card';
pluginId: string;
pluginSource: string;
manifestSourceDigest: string;
boundAt: number;
}
export type ProjectDisplayStatus =
| 'not_started'
| 'queued'
| 'running'
| 'awaiting_input'
| 'succeeded'
// Terminal-but-not-really-done: the latest run stamped `succeeded` yet ended
// with unfinished declared work (a non-`completed` TodoWrite task, or a
// max_tokens truncation). Kept distinct from `succeeded` so the project pill
// never reads "Completed" for a run whose work is not actually finished
// (#1247 / #1060).
| 'incomplete'
| 'failed'
| 'canceled';
export interface ProjectStatusInfo {
value: ProjectDisplayStatus;
updatedAt?: number;
runId?: string;
}
export interface PromptTemplateMetadataSource {
repo: string;
license: string;
author?: string;
url?: string;
}
// Subset of a curated PromptTemplate kept on the project so the agent can
// reference it on every turn without re-reading the gallery file. The
// `prompt` field is the (possibly user-edited) body — when the user tunes
// it in the New Project panel before clicking Create, those edits land
// here and become authoritative for the system prompt.
export interface PromptTemplateMetadata {
id: string;
surface: 'image' | 'video';
title: string;
prompt: string;
summary?: string;
category?: string;
tags?: string[];
model?: string;
aspect?: MediaAspect;
source?: PromptTemplateMetadataSource;
}
/**
* The local Workspace/member partition from which a catalogue item was picked.
*
* This is lookup provenance only. It does not bind the project to a Workspace,
* prove current membership, or authorize a remote operation.
*/
export interface LocalCatalogScope {
workspaceId: string;
workspaceMemberId: string;
}
export interface ProjectResourceCatalogScopes {
skill?: LocalCatalogScope;
designSystem?: LocalCatalogScope;
}
export type DesignSystemReviewDecision = 'looks-good' | 'needs-work';
export type DesignSystemReviewTaskStatus = 'queued' | 'sent' | 'failed';
export interface DesignSystemReviewAgentTask {
status: DesignSystemReviewTaskStatus;
prompt: string;
queuedAt: string;
sentAt?: string;
error?: string;
}
export interface DesignSystemReviewEntry {
decision: DesignSystemReviewDecision;
updatedAt: string;
feedback?: string;
files?: string[];
agentTask?: DesignSystemReviewAgentTask;
}
export interface ProjectMetadata {
kind: ProjectKind;
// `live-artifact`: the data-backed live dashboard flow (drives the
// live-artifact skill/system-prompt path). `web-clone`: website reproduction
// projects from the Home `Website clone` card — stored as prototypes so they
// keep prototype preview behavior, but the intent routes the `example-web-clone`
// scenario plugin and splits them into their own `web_clone` analytics kind.
// `document`: resume/report/PDF projects from the Home `document` card — an
// analytics-only discriminator (no product behavior keys off it) so a created
// `other`-kind project reports `project_kind: 'document'` instead of generic
// `other`. `webgl-experience` and `worker-visualizer`: the powered-preview
// GPU / off-main-thread scenario cards — analytics-only discriminators for the
// powered-artifact chips.
intent?:
| 'live-artifact'
| 'web-clone'
| 'document'
| 'webgl-experience'
| 'worker-visualizer'
| 'marketing'
| 'hyperframes';
fidelity?: 'wireframe' | 'high-fidelity';
speakerNotes?: boolean;
slideCount?: string;
animations?: boolean;
includeLandingPage?: boolean;
includeOsWidgets?: boolean;
templateId?: string;
templateLabel?: string;
// Template/gallery duplication provenance. Unlike examplePrompt, this path
// creates a project by copying existing files and does not launch an agent.
duplicatedFromPluginId?: string;
duplicatedFromPluginEntry?: string;
/** Primary target surface selected at project creation. */
platform?: ProjectPlatform;
// Tracks whether the visible name was typed by the user or generated by
// the create flow. First-prompt auto-naming may only replace generated
// names so named imports/templates stay stable across conversations.
nameSource?: 'generated' | 'prompt' | 'agent' | 'user';
/** Concrete delivery surfaces the artifact must account for. `responsive` is a web breakpoint target, not a native app expansion. */
platformTargets?: ProjectPlatform[];
inspirationDesignSystemIds?: string[];
importedFrom?: 'claude-design' | 'folder' | string;
entryFile?: string;
sourceFileName?: string;
// Folder-import (#597): when set, the project's files live under this
// absolute path instead of .od/projects/<id>/. OD reads and writes
// directly inside the user's folder. Stored as the realpath() result so
// symlinks can't redirect writes after import time.
baseDir?: string;
// Project library location that owns baseDir when the project was created
// under a configured project location root.
projectLocationId?: string;
// PR #974: marker stamped by the daemon's HMAC-gated import handler
// when a folder import passed the desktop-main-process trust gate.
// Only set on folder-imported projects (`baseDir` set) and only when
// the import request carried a valid `X-OD-Desktop-Import-Token`
// signed with the secret the desktop main process registered with the
// daemon at startup. The desktop `shell.openPath` IPC refuses to
// forward folder-imported projects whose metadata lacks this marker,
// so a renderer cannot launder an attacker-chosen baseDir into a
// file-manager reveal even if a future codepath inadvertently lets
// it set `baseDir` outside the trusted flow. Privileged: rejected
// by `POST /api/projects` and `PATCH /api/projects/:id`.
fromTrustedPicker?: true;
// Externally prepared scratch workspace provenance. OD may read/write
// metadata.baseDir, but source authority and writeback stay outside OD.
orchestratorWorkspace?: OrchestratorWorkspace;
// Hint stamped by the Home composer working-directory chip. It records
// where the user wanted the project to live without granting write access
// to that path; actual filesystem roots still use baseDir/import flows.
userWorkingDir?: string;
imageModel?: string;
imageAspect?: MediaAspect;
imageStyle?: string;
videoModel?: string;
videoLength?: number;
videoAspect?: MediaAspect;
audioKind?: AudioKind;
audioModel?: string;
audioDuration?: number;
voice?: string;
// Curated prompt template the user picked in the image/video tab of the
// New Project panel. Treated by the system-prompt composer as a stylistic
// and structural reference for the generation request.
promptTemplate?: PromptTemplateMetadata;
// Absolute paths to local code folders the agent can read via --add-dir.
linkedDirs?: string[];
// Batch/API-created projects can opt out of the initial discovery form so
// the first agent turn builds immediately from the submitted brief.
skipDiscoveryBrief?: boolean;
// Set when the user submits an unmodified curated example prompt from the
// gallery. Skips discovery AND requests full-quality direct generation,
// treating the curated title/brief as the complete creative brief. Honored
// by both the daemon and contracts (API/BYOK) system-prompt composers so the
// feature is not mode-dependent.
examplePrompt?: boolean;
examplePromptTitle?: string;
examplePromptBrief?: Record<string, string>;
// Plugins selected through @ mentions on Home. These are additive
// context references; the explicit "Use plugin" snapshot, when present,
// remains the primary executable plugin for the run.
contextPlugins?: ProjectContextPluginRef[];
contextMcpServers?: ProjectContextMcpServerRef[];
contextConnectors?: ProjectContextConnectorRef[];
/**
* Daemon-stamped provenance for locally selected project resources. This
* keeps the first run on the same local catalogue partition when Workspace
* identity is still loading; it is never Workspace ownership or authority.
*/
localCatalogScopes?: ProjectResourceCatalogScopes;
/** Daemon-owned, exact provenance for the scenario currently pinned here. */
scenarioBinding?: ProjectScenarioBinding;
/** Daemon-owned automatic OD Next route, independent of plugin selection. */
strategyBinding?: ProjectStrategyBinding;
/** Daemon-owned identity of the official example card that seeded this project. */
exampleBinding?: ProjectExampleBinding;
// Stored on design-system projects so the review overview can remember
// which generated sections were accepted or sent back for another pass.
designSystemReview?: Record<string, DesignSystemReviewEntry>;
// Stored on brand extraction projects so the Brands library, project list,
// and design-system picker can all point back to the same source kit.
brandId?: string;
brandSourceUrl?: string;
brandDesignSystemId?: string;
// Stored on design-system projects created from an existing project so the
// generated workspace can point back to the source evidence it duplicated.
sourceProjectId?: string;
sourceProjectName?: string;
// AI-optimize (deep enrichment) state for a programmatically-extracted DS.
// `programmatic` (or unset) = fast pass only; `ai_refined` = a user-triggered
// enrichment run completed successfully. Lets analytics compare the two
// cohorts' retention/usage (tracking spec C15 / §6).
enrichmentStatus?: 'programmatic' | 'ai_refined';
enrichmentCompletedAt?: number;
// Stamped by the daemon when it registers a local placeholder record for a
// hub-shared project whose content has NOT been materialized locally yet
// (fresh data root / opened-before-pulled). While set, the daemon refuses
// to publish the project's local directory to the resource hub — the
// recvqzaDvUU6B3 fresh-install wipe guard. Cleared by the pull flow once
// real hub content lands on disk. See the daemon's
// collab/shared-project-placeholder.ts for the invariant.
sharedProjectPlaceholderAt?: number;
}
export interface Project {
id: string;
name: string;
skillId: string | null;
designSystemId: string | null;
createdAt: number;
updatedAt: number;
status?: ProjectStatusInfo;
pendingPrompt?: string;
metadata?: ProjectMetadata;
// Plan §3.A1 / spec §11.5 — set when the project was created with a
// plugin pinned (e.g. via PluginLoopHome on the web entry). Lets the
// UI hide the in-composer plugin rail and render the active plugin
// as context on user messages instead of re-prompting the user to
// pick a plugin they already selected.
appliedPluginSnapshotId?: string;
customInstructions?: string;
/**
* The daemon-authoritative visibility from this project's
* `workspace_projects` row.
*
* This is a read projection, not project storage. It lets clients classify a
* local project even when that resource is synchronized through a non-project
* hub (for example, a Design System backing project). Absent means the reader
* did not carry workspace visibility and must be treated as "no opinion".
*/
workspaceVisibility?: ProjectVisibility;
/**
* The workspace this project belongs to — exactly one, per the 2026-07-21
* ruling that 草稿 and shared projects are both bound to a workspace.
*
* A read model, not storage: the daemon resolves it from the project's single
* `workspace_projects` row. Absent means the daemon has not bound the project
* to a workspace yet (a pre-workspace project awaiting adoption on the next
* personal-workspace read), NOT "belongs to no workspace" — so a client must
* treat absence as "no opinion" and never hide a project on the strength of a
* missing value.
*/
workspaceId?: string | null;
}
export interface ProjectTemplate {
id: string;
name: string;
sourceProjectId?: string;
files: Array<{ name: string; content: string }>;
description?: string;
createdAt: number;
}
export interface ProjectBrowserWorkspaceTab {
id: string;
insertAfter?: string | null;
label: string;
title?: string;
url?: string;
iconUrl?: string;
}
export interface ProjectTabsState {
tabs: string[];
active: string | null;
browserTabs?: ProjectBrowserWorkspaceTab[];
hasSavedState?: boolean;
updatedAt?: number;
}
export interface Conversation {
id: string;
projectId: string;
title: string | null;
sessionMode?: ChatSessionMode;
messageCount?: number;
createdAt: number;
updatedAt: number;
totalDurationMs?: number;
latestRun?: {
status: ChatRunStatus;
startedAt?: number;
endedAt?: number;
durationMs?: number;
};
}
/**
* Exact local catalogue identity of an official example card, as claimed by
* the client. Both fields are required: the daemon looks the record up by
* `source` — the same lookup `/api/plugins/:id/apply-local` performs — and
* rejects the request when the resolved record's id disagrees with `pluginId`.
*/
export interface CreateProjectExampleReference {
pluginId: string;
source: string;
}
export interface CreateProjectRequest {
name: string;
/** Optional project library location id. Omit or use `default` for .od/projects. */
projectLocationId?: string;
skillId?: string | null;
/** Local lookup provenance captured when the Skill was selected. */
skillCatalogScope?: LocalCatalogScope;
designSystemId?: string | null;
/** Local lookup provenance captured when the Design System was selected. */
designSystemCatalogScope?: LocalCatalogScope;
pendingPrompt?: string;
metadata?: ProjectMetadata;
pluginId?: string;
/** Exact identity of the local catalogue record selected by the caller. */
pluginSource?: string;
appliedPluginSnapshotId?: string;
pluginInputs?: Record<string, unknown>;
/** Product-owned automatic OD Next route. The daemon validates and stamps it. */
automaticStrategyTaskProfile?: ProjectScenarioTaskProfile;
/**
* The official example card the user picked under a task type.
*
* A claim about an identity, never content: the daemon re-resolves the
* record through the local catalogue and reads the example's material from
* disk. Unlike `pluginId`/`appliedPluginSnapshotId` this does not make the
* project an explicit plugin pin, so the automatic OD Next route stays in
* force and the example travels as a user-selected Skill instead of
* replacing the strategy.
*/
exampleReference?: CreateProjectExampleReference;
/** Session mode for the default conversation seeded with the project. */
conversationMode?: ChatSessionMode;
customInstructions?: string;
/** Persisted to metadata.skipDiscoveryBrief for automated project runs. */
skipDiscoveryBrief?: boolean;
}
export interface UpdateProjectRequest {
name?: string;
skillId?: string | null;
designSystemId?: string | null;
pendingPrompt?: string | null;
metadata?: ProjectMetadata | null;
customInstructions?: string | null;
}
export interface ProjectsResponse {
projects: Project[];
}
export interface ProjectResponse {
project: Project;
}
export interface RestoreProjectAutomaticScenarioRequest {
/** Compare-and-swap guard for the project pin the caller inspected. */
expectedCurrentSnapshotId: string | null;
}
export interface RestoreProjectAutomaticScenarioResponse extends ProjectResponse {
scenarioBinding?: ProjectScenarioBinding;
strategyBinding?: ProjectStrategyBinding;
changed: boolean;
}
export type ProjectDesignTokenSuggestionProp =
| 'color'
| 'backgroundColor'
| 'borderColor'
| 'fontFamily'
| 'fontSize'
| 'fontWeight'
| 'lineHeight'
| 'letterSpacing'
| 'width'
| 'height'
| 'gap'
| 'padding'
| 'margin'
| 'borderRadius'
| 'borderWidth';
export interface ProjectDesignTokenSuggestionQuery {
file?: string;
targetId?: string;
props?: ProjectDesignTokenSuggestionProp[];
values?: Partial<Record<ProjectDesignTokenSuggestionProp, string>>;
}
export interface ProjectDesignTokenSuggestion {
prop: ProjectDesignTokenSuggestionProp;
token: string;
value: string;
sourceFile: string;
line: number;
matchReason: string;
score: number;
}
export interface ProjectDesignTokenSuggestionsResponse {
projectId: string;
query: ProjectDesignTokenSuggestionQuery;
suggestions: ProjectDesignTokenSuggestion[];
}
// Response body for `GET /api/projects/:id`. Carries the same `project`
// payload as `ProjectResponse` plus a derived `resolvedDir` so the web
// client can address the on-disk working directory directly (e.g. for
// `shell.openPath` from the desktop bridge). For folder-imported projects
// `resolvedDir === metadata.baseDir`; for native projects it is
// `path.join(<daemon projects root>, project.id)`. Computed server-side via
// `resolveProjectDir(...)` so the web client never reconstructs the path.
export interface ProjectDetailResponse extends ProjectResponse {
resolvedDir: string;
}
export type ProjectVisibility = 'personal' | 'team';
/**
* Daemon-authoritative workspace and billing scope for one persisted project.
*
* `visibility` answers whether the project itself is a private draft or shared
* with the team. It is deliberately independent from `kind`: a private draft
* may still belong to a team workspace and therefore use that workspace's
* wallet. The tagged union prevents clients from treating every non-null
* workspace id as a team-billing scope.
*/
export type ProjectWorkspaceScope =
| {
kind: 'unbound';
projectId: string;
workspaceId: null;
context: null;
}
| {
kind: 'unavailable';
projectId: string;
workspaceId: string;
visibility: ProjectVisibility;
context: null;
}
| {
kind: 'personal';
projectId: string;
workspaceId: string;
visibility: ProjectVisibility;
context: WorkspaceCollabContext & { workspaceType: 'personal' };
}
| {
kind: 'team';
projectId: string;
workspaceId: string;
visibility: ProjectVisibility;
context: WorkspaceCollabContext & { workspaceType: 'team' };
};
/** GET /api/projects/:id/workspace-scope. */
export interface ProjectWorkspaceScopeResponse {
scope: ProjectWorkspaceScope;
}
// Local D-lane placeholder until the B-owned CurrentWorkspaceContext is
// imported into open-design. The route adapter keeps this replaceable.
export type WorkspaceProjectRole = 'owner' | 'admin' | 'member';
// C owns project sync orchestration. D exposes this on its read model and emits
// intent metadata when visibility changes, but it does not upload or mirror
// project content directly.
export type WorkspaceProjectSyncIntentEvent = ProjectSyncIntentEvent;
export type WorkspaceProjectSyncIntent = ProjectSyncIntent;
export type ProjectDisabledReason =
| 'workspace_locked'
| 'workspace_deleted'
| 'permission_denied'
| 'sync_pending'
| 'resource_frozen';
export interface ProjectAccessFlags {
canOpen: boolean;
canRename: boolean;
canDelete: boolean;
canDuplicate: boolean;
canMoveToTeam: boolean;
canMoveToPersonal: boolean;
canExport: boolean;
canSendTo: boolean;
canRestoreVersion: boolean;
disabledReason?: ProjectDisabledReason;
}
export interface WorkspaceProjectSummary {
id: string;
name: string;
workspaceId: string;
visibility: ProjectVisibility;
resourceState: TeamResourceState;
createdByWorkspaceMemberId: string | null;
updatedByWorkspaceMemberId?: string | null;
/**
* E resource-hub mapping seam. When present, this is Spec E's
* ResourceRecord.id for a `kind: 'project'` cloud resource. Personal projects
* are local-only and normally keep this null; team-visible projects receive
* the cloud resource through C's orchestration of E's upload/version/mirror
* mechanism.
*/
resourceHubResourceId?: string | null;
/** Set when a previously shared cloud resource should be tombstoned. */
cloudTombstonedAt?: number | null;
currentUserAccess: ProjectAccessFlags;
syncState?: ProjectSyncState;
pendingSyncIntent?: WorkspaceProjectSyncIntent;
createdAt: number;
updatedAt: number;
metadata?: ProjectMetadata;
project: Project;
}
export interface WorkspaceProjectsResponse {
projects: WorkspaceProjectSummary[];
}
export interface MoveWorkspaceProjectRequest {
visibility: ProjectVisibility;
}
export interface BatchDeleteWorkspaceProjectsRequest {
projectIds: string[];
}
export interface BatchMoveWorkspaceProjectsRequest {
projectIds: string[];
visibility: ProjectVisibility;
}
export interface CreateProjectResponse extends ProjectResponse {
conversationId?: string;
appliedPluginSnapshotId?: string;
}
export interface CreateDesignSystemProjectFromProjectRequest {
name?: string;
pendingPrompt?: string;
}
export interface CreateDesignSystemProjectFromProjectResponse extends ProjectResponse {
conversationId: string;
designSystemId: string;
copiedFiles: string[];
}
export interface DuplicateProjectRequest {
name?: string;
}
export interface DuplicateProjectResponse extends ProjectResponse {
conversationId: string;
copiedFiles: string[];
}
export interface ProjectLocation {
id: string;
name: string;
path: string;
builtIn?: boolean;
}
export interface ProjectLocationsResponse {
locations: ProjectLocation[];
removedProjectIds?: string[];
}
export interface UpdateProjectLocationsRequest {
locations: Array<{ id?: string; name?: string; path: string }>;
}
export interface ProjectManifest {
schemaVersion: 1;
id: string;
name: string;
createdAt: number;
updatedAt: number;
skillId?: string | null;
designSystemId?: string | null;
}
export interface ScanProjectLocationsResponse {
scanned: number;
imported: Project[];
existing: string[];
skipped: Array<{ path: string; reason: string }>;
}
// POST /api/import/folder — create a project rooted at an existing local
// folder. The submitted baseDir is stored as the project's metadata.baseDir
// (after realpath canonicalization) and OD reads/writes directly inside it.
// The user owns version control; OD does not snapshot or copy.
export interface ImportFolderRequest {
baseDir: string;
name?: string;
skillId?: string | null;
designSystemId?: string | null;
orchestratorWorkspace?: OrchestratorWorkspace;
}
export interface ImportFolderResponse {
project: Project;
conversationId: string;
entryFile: string | null;
}
export interface ReplaceProjectWorkingDirRequest {
baseDir: string;
orchestratorWorkspace?: OrchestratorWorkspace;
}
export interface ReplaceProjectWorkingDirResponse {
project: Project;
baseDir: string;
entryFile: string | null;
}
export interface ConversationsResponse {
conversations: Conversation[];
}
export interface ConversationResponse {
conversation: Conversation;
}
export interface CreateConversationRequest {
title?: string | null;
sessionMode?: ChatSessionMode;
/**
* Seed the new conversation with another conversation's context by copying
* its messages. The source must belong to the same project; a missing or
* foreign id is ignored and an empty conversation is created. Powers the
* "Side Chat" launcher, which forks the current chat's context into a new
* conversation.
*/
seedFromConversationId?: string | null;
/**
* When paired with `seedFromConversationId`, copy only source messages up to
* and including this message. Used by the chat "Fork" action so the new
* conversation resumes from a specific assistant turn instead of inheriting
* future follow-ups from the source conversation.
*/
forkAfterMessageId?: string | null;
/**
* One compact client-side message used only when `forkAfterMessageId` is not
* present in the source conversation's persisted history. The daemon appends
* it to the persisted prefix, which preserves an errored/unpersisted final
* assistant turn without making normal forks upload the entire transcript.
*/
forkFallbackMessage?: ChatMessage;
/**
* The persisted message immediately before `forkFallbackMessage`, or null
* when the fallback is the first message. The daemon cuts persisted history
* at this boundary before appending the fallback, so later turns cannot leak
* into a fork from an older unpersisted assistant message.
*/
forkFallbackPredecessorMessageId?: string | null;
/**
* Client-supplied snapshot of the messages to seed the fork with, in order,
* up to and including the fork point. When present, the daemon copies these
* instead of reading the source conversation from the database by id. This
* makes "Fork" resilient to a fork point that was never persisted — e.g. an
* assistant turn whose run errored or had its connection reset before the
* message reached the database. Without it, such a fork would 404 on
* `forkAfterMessageId` and silently fail. When absent, the daemon falls back
* to copying from `seedFromConversationId` (the original Side Chat path).
*
* @deprecated Retained for older clients. New clients should first fork from
* persisted history and use `forkFallbackMessage` only after a missing fork
* point response.
*/
seedMessages?: ChatMessage[];
}
export interface UpdateConversationRequest {
title?: string | null;
sessionMode?: ChatSessionMode;
}
export interface MessagesResponse {
messages: ChatMessage[];
}
export type DeployProviderId = 'vercel-self' | 'cloudflare-pages';
export type DeploymentStatus =
| 'deploying'
| 'preparing-link'
| 'ready'
| 'link-delayed'
| 'protected'
| 'failed';
export interface CloudflarePagesConfigHints {
lastZoneId?: string;
lastZoneName?: string;
lastDomainPrefix?: string;
}
export interface CloudflarePagesZoneInfo {
id: string;
name: string;
status?: string;
type?: string;
}
export interface CloudflarePagesZonesResponse {
zones: CloudflarePagesZoneInfo[];
cloudflarePages?: CloudflarePagesConfigHints;
}
export interface CloudflarePagesDeploySelection {
zoneId: string;
zoneName: string;
domainPrefix: string;
}
export type DeploymentLinkStatus =
| 'ready'
| 'link-delayed'
| 'protected'
| 'failed';
export interface DeploymentLinkInfo {
url: string;
status: DeploymentLinkStatus;
statusMessage?: string;
reachableAt?: number;
}
export type CloudflarePagesDnsStatus =
| 'skipped'
| 'created'
| 'reused'
| 'unmarked'
| 'patched'
| 'conflict'
| 'failed';
export type CloudflarePagesDomainStatus =
| 'skipped'
| 'pending'
| 'active'
| 'conflict'
| 'failed';
export type CloudflarePagesCustomDomainStatus =
| 'pending'
| 'ready'
| 'conflict'
| 'failed';
export type CloudflarePagesDnsOwnership = 'marked' | 'unmarked' | 'external';
export interface CloudflarePagesCustomDomainInfo {
hostname: string;
url: string;
zoneId: string;
zoneName: string;
domainPrefix: string;
status: CloudflarePagesCustomDomainStatus;
statusMessage?: string;
errorCode?: string;
errorMessage?: string;
dnsStatus?: CloudflarePagesDnsStatus;
dnsRecordId?: string;
dnsOwnership?: CloudflarePagesDnsOwnership;
domainStatus?: CloudflarePagesDomainStatus;
pagesDomainStatus?: string;
validationData?: unknown;
verificationData?: unknown;
}
export interface CloudflarePagesDeploymentInfo {
projectName: string;
pagesDev: DeploymentLinkInfo;
customDomain?: CloudflarePagesCustomDomainInfo;
}
export interface DeployConfigResponse {
providerId: DeployProviderId;
configured: boolean;
tokenMask: string;
teamId: string;
teamSlug: string;
accountId?: string;
projectName?: string;
cloudflarePages?: CloudflarePagesConfigHints;
target: 'preview' | 'production';
}
export interface UpdateDeployConfigRequest {
providerId?: DeployProviderId;
token?: string;
teamId?: string;
teamSlug?: string;
accountId?: string;
projectName?: string;
cloudflarePages?: CloudflarePagesConfigHints;
}
export interface DeploymentInfo {
id: string;
projectId: string;
fileName: string;
providerId: DeployProviderId;
url: string;
deploymentId?: string;
deploymentCount: number;
target: 'preview' | 'production';
status: DeploymentStatus;
statusMessage?: string;
reachableAt?: number;
cloudflarePages?: CloudflarePagesDeploymentInfo;
createdAt: number;
updatedAt: number;
}
export interface ProjectDeploymentsResponse {
deployments: DeploymentInfo[];
}
export interface DeployProjectFileRequest {
fileName: string;
providerId?: DeployProviderId;
cloudflarePages?: CloudflarePagesDeploySelection;
target?: 'preview' | 'production';
}
export interface DeployProjectFileResponse extends DeploymentInfo {}
export interface CheckDeploymentLinkResponse extends DeploymentInfo {}
// Preflight inspects the file set that would be uploaded for a deploy
// without sending anything to the provider. Lets the UI show file count,
// total size, and warnings before the user pays the network round-trip.
export type DeployPreflightWarningCode =
| 'broken-reference'
| 'invalid-reference'
| 'large-asset'
| 'large-bundle'
| 'large-html'
| 'external-script'
| 'external-stylesheet'
| 'no-doctype'
| 'no-viewport';
export interface DeployPreflightWarning {
code: DeployPreflightWarningCode;
message: string;
path?: string;
url?: string;
size?: number;
}
export interface DeployPreflightFile {
path: string;
size: number;
mime: string;
sourcePath: string;
}
export interface DeployPreflightRequest {
fileName: string;
providerId?: DeployProviderId;
}
export interface DeployPreflightResponse {
providerId: DeployProviderId;
entry: string;
files: DeployPreflightFile[];
totalFiles: number;
totalBytes: number;
warnings: DeployPreflightWarning[];
}