You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Spec for M1: Shell and Catalog. Cut from the milestone issue and its Starting context, pinned at 29d038f; no triage label per the milestone loop. /to-tickets cuts tracer bullets from this under the Secant slicing rulebook.
Problem Statement
After M0 the repository is an empty-but-green Secant: the installed secant command answers --help and --version and nothing else. A user cannot open Secant in a project directory, cannot see which Workflow Bundles are installed, and cannot turn an authoring folder into a .wfb file, install it, or learn whether its Routing composes. An agent starting M2 has no shell to put a Run into, no Catalog to pin a Bundle Snapshot against, no Projection Port for either client to speak, and no proof that OpenTUI-based presentation builds, runs, and tears down cleanly on Windows, macOS, and Linux under Node 24.
Two settled decisions are still only words. ADR 0018 says the OpenCode presentation subset is a pinned vendored copy behind a Renderer Port with a process.stdin-before-renderer.destroy() teardown invariant, but no copy, no UPSTREAM record, no third-party notices file, and no test enforcing the ordering exist. ADR 0027 says a real-terminal lifecycle suite runs as its own blocking CI job on three operating systems and the legacy-conhost check rests on a human, but neither gate exists.
Solution
Land the shell and the non-executing Catalog, in both clients, with zero Runs.
From the user's point of view, after M1:
Running secant in a directory for the first time asks, once, for approval of that exact absolute path as the Workspace. Declining exits cleanly. Approving opens Workspace Home, and every later launch from that path opens Home directly.
Workspace Home names the Workspace and offers the one entry M1 has a real command behind: Workflow Bundles. Opening it lists every Installed Bundle, and selecting one inspects it: identity, digest, origin, platforms, engine range, launch inputs, Routing summary, Workspace prerequisites, produced artifacts, the generated Execution summary, and the Composition check findings. State is never signalled by colour alone.
Quitting by key, Ctrl+C, or hangup leaves the terminal exactly as it was found, on all three operating systems, including the legacy Windows console host.
Headlessly, secant bundle build <folder> validates and normalizes an authoring folder, reports the digest, and installs it atomically; --output also writes the .wfb; --no-install requires --output and skips only the Catalog write. secant bundle install <file.wfb> imports an already-built Bundle through the identical path. secant bundle list and secant bundle inspect show the same facts the TUI shows, from the same Projection. secant workspace approve and secant workspace give the headless client the same Workspace approval and Home facts.
Every ingestion failure is a precise Problem: rejected archive shape, unknown manifest field, unclaimed or overlapping asset tree, unresolvable Prompt slot, unbound artifact, missing Repeat-group verdict, understated engine range, exceeded budget, identity collision, or the plain "already installed".
The Proof Bundle, the Test Repair Workflow, exists as a real authoring folder under bundles/, builds, installs, and passes the Composition check in CI on all three operating systems, without anything in target source knowing its name.
CI is the acceptance path: the canonical gate grows the package smoke to exercise the headless Bundle path from the installed package, and a new blocking real-terminal job proves the shell's lifecycle under a pseudo-terminal on all three operating systems. The legacy-conhost human check is run and recorded on the milestone.
User Stories
As a developer launching Secant in a project directory for the first time, I want to be shown the exact absolute path and asked to approve it as the Workspace, so that Secant never operates on a directory I did not consciously accept.
As a developer who declines Workspace approval, I want Secant to exit immediately with the terminal restored and nothing persisted, so that a wrong directory costs me nothing.
As a developer relaunching from an approved directory, I want Workspace Home to open without asking again, so that approval is a one-time act per exact path.
As a developer on Workspace Home, I want to see the Workspace path and only entries that do something today, so that I am never offered a dead control.
As a developer on Workspace Home, I want to open Workflow Bundles and see one row per Installed Bundle sorted by name then version descending, so that I can find the Bundle I care about at a glance.
As a developer with an empty Catalog, I want the Workflow Bundles screen to say so and name the headless commands that install one, so that the empty state tells me what to do next.
As a developer inspecting a Bundle in the TUI, I want its identity, digest, origin, stability, platforms, engine range, description, and author metadata, so that I know exactly what is installed.
As a developer inspecting a Bundle, I want its launch inputs, Routing summary with Step kinds and Repeat groups, Workspace prerequisites, and produced-artifact declarations, so that I understand what the Workflow will ask of me before any Run exists.
As a developer inspecting a Bundle, I want the generated Execution summary and the Composition check findings, so that I see what authority the Bundle would exercise and whether it composes, before M2 ever asks me to trust it.
As a developer whose Bundle needs a newer Secant, I want the inspection to say "needs Secant ≥ x.y" plainly, so that a version mismatch is never a surprise at launch.
As a colourblind developer, I want every status in the shell carried by text or a glyph as well as colour, so that I can read the TUI with any theme.
As a keyboard-only developer, I want every visible control and key binding on Home, the Bundles list, the inspection view, and the approval dialog to dispatch a current Action Offer or navigation, so that nothing on screen is decorative.
As a developer resizing my terminal, I want Home, the list, and the inspection view to relayout without corruption or overflow, so that small terminals remain usable.
As a developer quitting Secant, I want the terminal restored on the quit key, Ctrl+C, and hangup, with no escape junk, no stuck raw mode, and no lingering alternate screen, so that my shell is usable the instant Secant exits.
As a developer on the legacy Windows console host, I want Secant's exit not to wedge the console, so that legacy conhost stays a supported terminal.
As a Bundle author, I want secant bundle build <folder> to validate my authoring folder, normalize it into exact .wfb bytes, print the digest, and install it atomically, so that one command takes me from folder to Catalog.
As a Bundle author, I want --output <file> to write those same bytes to disk, so that I can hand the Bundle to someone else.
As a release engineer, I want --no-install to run the full validator, normalizer, and Composition check and refuse to run without --output, so that CI can build Shipped Bundles without touching the runner's Catalog.
As a Bundle recipient, I want secant bundle install <file.wfb> to import through the identical validator, managed store, and Catalog path, so that a built and an imported Bundle are indistinguishable once installed.
As a Bundle author, I want build and install to never modify my folder or file, execute Bundle content, load code, or fetch anything remote, so that packaging is a pure, non-executing act.
As a Bundle author, I want an authoring manifest without requires.engine to have the minimum compatible engine range derived and written by the builder, so that I never guess a version constraint.
As a Bundle author, I want an omitted platforms to default to my build host while an authored subset is preserved and validated, so that the manifest is honest about what was proven.
As a Bundle author, I want unknown top-level or nested manifest fields rejected with the field path named, so that typos never silently become no-ops.
As a Bundle author, I want every non-manifest archive entry to belong to exactly one declared asset tree and overlapping trees rejected, so that nothing rides along unnoticed.
As a Bundle author, I want each asset kind checked against its rule (one prompt file, a skill folder with a root SKILL.md, one schema JSON, one script text file, one opaque resource), so that a kind mismatch is caught at build.
As a Bundle author, I want every Prompt slot {{artifact:name}} to name an artifact the Step requires, with unknown or malformed slots failing composition, so that prompts and bindings never drift apart.
As a Bundle author, I want the Composition check to prove every required artifact is bound earlier by a producer or launch input with a matching type, every Repeat group's verdict is bound before entry, every schema use is valid, and every supported platform resolves exactly one command invocation, so that a Bundle that installs is a Bundle that can compose.
As a Bundle author, I want each Repeat group's reviewCheckpoint validated as a positive-integer interval under Secant's safety ceiling with a plain-text message, so that M2's Review checkpoint has valid data to run on.
As a Bundle author, I want command invocations validated as a static PATH-resolved executable name, structured argument tokens, a safe working directory, and structured environment additions, so that no shell interpolation or absolute executable ever enters a Bundle.
As a Catalog owner, I want installing an identity already present to be discarded, reporting "already installed" for an equal digest and an identity collision for a different one, so that first install always wins.
As a Catalog owner, I want budgets on input bytes, expanded bytes, and entry count estimated before extraction and enforced during it, so that a hostile archive cannot exhaust my machine.
As a Catalog owner, I want a failed install to leave no partial state in the managed store or Catalog, so that the Catalog is always consistent.
As a Catalog owner, I want the managed bytes verified against the digest after storing, so that a corrupted copy is caught at install.
As a Catalog owner, I want two concurrent Secant processes to be unable to corrupt the Catalog, so that a headless build during a TUI session is safe.
As a headless user, I want secant bundle list and secant bundle inspect to print the same facts the TUI shows, with --json for machine consumption, so that scripts and CI read exactly what a human reads.
As a headless user, I want secant workspace approve [path] and secant workspace to approve and show the Workspace, so that the headless client has the same approval and Home facts as the TUI.
As a headless user, I want every failure to exit non-zero and print the Problem's code, explanation, and remediation, so that CI logs are self-explaining.
As a user on an unsupported Node, I want the CLI to fail fast at startup naming the required range, so that a confusing crash deeper in never happens.
As an M2 agent, I want the Projection Port to exist with the workspace, bundle-catalog, and operation families and the approve-workspace Operation, so that Run families are added to a working contract rather than invented alongside it.
As an M2 agent, I want an execution-free Workflow Module holding the four Step-kind contracts, the five artifact types, Routing with Repeat groups, Prompt slot grammar, the Workspace prerequisite set, and the Composition check, so that Run execution imports contracts rather than redefining them.
As an M2 agent, I want Catalog Entries with identity, digest, origin, and a private installation generation, so that a Bundle Snapshot has something exact to pin.
As a maintainer of the vendored subset, I want an UPSTREAM record naming the OpenCode commit, every copied path, every local modification, and the date, created in the same commit as the first copy and updated by every later one, so that provenance is never reconstructed after the fact.
As a release engineer, I want THIRD-PARTY-NOTICES.md at the repository root, created in that same extraction commit, listed in the package's shipped files, carrying the MIT notices for OpenCode and OpenTUI and one attribution line per shipped theme, so that M4's release gate has something to verify.
As a reviewer, I want the structural gate to fail on any @opencode-ai/* import, any bun: import or Bun. call, and any node-pty import in target source, so that the vendor boundary does not depend on who reviews the pull request.
As a reviewer, I want every feature Secant lacks deleted at copy time rather than hidden or left as a no-op, so that the copied subset stays small enough to own forever.
As a reviewer, I want every rebuilt component's implementing ticket to record what was taken from its OpenCode counterpart and what was deliberately changed, so that rebuilds are informed rather than blank-page designs.
As a maintainer, I want the OpenTUI pin at 0.4.5 owned by Secant with every bump its own ticket, so that upstream lockfile movement never drags the shell along.
As a Windows user, I want the teardown site to release process.stdin before renderer.destroy() with a comment pointing at the upstream report, and a test that fails if the order is reversed, so that a future tidy-up cannot reintroduce console death.
As a release engineer, I want a small real-terminal lifecycle suite as its own blocking CI job on Windows, macOS, and Linux, outside the canonical check, so that a broken terminal after exit blocks merge.
As a release engineer, I want the human legacy-conhost real-terminal check run against the M1 build and its report recorded on the milestone in the ADR 0027 report shape, so that both Windows support-matrix rows rest on evidence.
As a reviewer, I want every M1 slice to name one CI-run scenario on the three-OS matrix, so that "verified locally" is never acceptance.
As a reviewer, I want nothing in target source to inspect a Bundle id, name, or asset path to change behaviour, so that the Proof Bundle proves genericity rather than being special-cased.
As a maintainer, I want the Test Repair Workflow authored as the first folder under bundles/, declaring all three platforms and passing the Composition check in CI on each, so that the Proof Bundle role is occupied from the first Catalog slice.
As a security-minded maintainer, I want nothing published to npm and no Run, trust acknowledgement, or step execution in M1, so that the milestone stays exactly as large as its name.
Implementation Decisions
Authority and vocabulary
ADRs at HEAD supersede ticket wording. ADR 0018 and ADR 0027 already carry the Approve Crucible's architecture and migration handoff #22 handoff corrections; ADR 0029 fixes how built-ins ship and is honoured here only where M1's build path touches it.
Modules created by M1, each by the first slice that needs its behaviour: cli (dispatch to the selected client, fail-fast engine check), composition (outer root and child roots for TUI and headless), application (Projection Port contract, Bundle-management contract, Workspace approval and Bundle ingestion use cases), workflow (execution-free contracts and the Composition check), bundle (archive validation, build, normalization, digest, Execution summary), catalog (the catalog database, managed store, installation records, Workspace approvals), tui (Secant presentation plus the vendored subset), renderer (Renderer Port and teardown ordering), headless (commands). Run execution, Run Store, artifacts, and Harness Modules are not created.
Import directions are exactly the checked policy table. Bundle and Catalog depend only on Workflow. Clients import only the two Application contracts. SQLite lives in Catalog; OpenTUI lives in tui and renderer.
Renderer Port and lifecycle (authority: ADR 0018, #6, #33)
The Renderer Port is lifecycle only: size, onKey, onResize, destroy, destroyed. It has two Adapters: the production one over OpenTUI's createCliRenderer at the owned pin 0.4.5, and a fake with no terminal and no native library. Drawing goes through OpenTUI's Solid integration directly and never through the Port.
The composition root owns the lifecycle in one cohesive place: create renderer, mount, await shutdown, tear down exactly once, then print any epilogue or error to the restored terminal. Exit paths covered: quit binding, Ctrl+C, SIGHUP, SIGTERM, startup rejection, render failure, unhandled error. process.exit is never called on the normal path; the exit code is set.
Teardown invariant: release process.stdin (listeners off, setRawMode(false), pause(), unref(), destroy()) beforerenderer.destroy(). Async work after destroy() is permitted. The site carries a comment linking anomalyco/opentui#1405. A test over the fake Adapter records the ordering and fails when it is reversed.
Startup fails fast when process.version is below the engines.node floor, before any renderer or database is touched.
The copy is taken from the local OpenCode checkout at commit 1ead9e3d7f, whose TUI is a workspace package at OpenTUI 0.4.5. UPSTREAM records that commit; a later copy from a newer commit records its own.
Extraction is ongoing and need-driven. M1 vendors only what M1 screens use; each later screen copies what it needs and updates UPSTREAM.
Vendored in M1: the dialog primitive (backdrop, stack, focus save and restore, escape and Ctrl+C dismissal, modal keymap mode), the confirm and alert dialog shapes, the border tables, the theme module (theme type, resolver, ANSI-to-RGBA, system-theme generation from terminal colours, syntax styles) with the pruned theme assets, the theme provider, the simple-context provider factory, the runtime paths and terminal-environment providers, the keymap adapter (mode stack, timed leader, alias expansion, sequence formatters) and the keybind schema mechanics (parse, defaults, binding lookup) with the OpenCode command rows removed, the exit and epilogue contexts, and the generic layout, scroll, locale, record, signal, path, error, and persistence utilities. The grouped fuzzy select dialog, toast, spinner, which-key overlay, external-editor round trip, and diff viewer are vendored by the first later slice that needs them, not now.
Rebuilt in M1 against the Projection Port, after studying the OpenCode counterpart and recording what was taken and changed: the application root and provider composition, the route model (approval, Home, Bundles list, Bundle inspection), the Home screen, the Bundles list and inspection screens, and the command table. Every command has a Projection Port Operation or a navigation behind it, so a screen dispatching an unimplemented command fails to compile.
Deleted at copy time: model, variant, agent, and provider pickers; provider login; MCP and LSP status; upgrade and status panels; console-org switcher; workspace dialogs; the upsell background art; plugin manager, plugin host, and slot registry; session dialogs; sidebar and tips; notification sounds and audio assets; the Zed editor integration; the Windows FFI console helper; the tree-sitter parser table; and every keybind row for those features.
Runtime neutrality: the copied persistence utility's Bun.file and Bun.write calls become node:fs/promises at copy time. Bun.stringWidth sits only in prompt code, which M1 does not copy; the substitute is chosen by the slice that copies the prompt. Every @opencode-ai/* import in a vendored file is a type or a ≤20-line utility (flag, global paths, glob, file lock, which, installation version) and is replaced by a local value or node: built-in.
Themes: the 33 upstream themes are taken and attributed, minus the five ADR 0018 drops (Material, Monokai, Cursor, Vercel, GitHub). This spec also drops the three OpenCode-branded themes (opencode, orng, lucent-orng) under the same naming reasoning, leaving 25. THIRD-PARTY-NOTICES.md carries one line per shipped theme plus the MIT notices for OpenCode and OpenTUI. A default theme is chosen; there is no theme picker or persisted preference in M1.
Build toolchain: Solid JSX under Node is compiled with Solid's universal transform (babel-preset-solid with moduleName set to the OpenTUI Solid package and generate: "universal") as a tsup/esbuild plugin step; jsxImportSource points at the OpenTUI Solid package. The Prove an OpenCode-derived Crucible shell across Windows, macOS, and Linux #6 proof covered only the core renderer, so the first shell slice proves this build on all three operating systems from the installed package. Theme JSON is imported through a generated barrel rather than import attributes if the bundler cannot preserve them.
The structural gate already rejects @opencode-ai/*, bun:, and node-pty imports in target source; it gains a check for Bun. global calls and for THIRD-PARTY-NOTICES.md and UPSTREAM existing once any vendored file exists.
Projection Port (authority: ADR 0024, #19 and its 2026-09-05 amendment)
One in-memory Interface with openProjection, submit, and readResource. Opening returns snapshot, catch-up, updates, and idempotent close. Every failure crossing the Port is a normalized Problem: stable code, safe explanation, code-specific details, possible-effects classification, optional field violations. Throws are reserved for caller-contract violations.
M1 implements three closed families. The union grows in M2 and M5; families not yet needed are not declared.
workspace is added as the seventh family. Selector: none, the process has one launch Workspace. Snapshot: the resolved canonical absolute path, approval state (approved or unapproved), approval time, and the Action Offer approve-workspace when unapproved. Catch-up is rebase-only. This extends the Define Crucible's TUI-facing command and projection interface #19 vocabulary and ADR 0024's "six families"; the implementing slice records the amendment on Define Crucible's TUI-facing command and projection interface #19 and in ADR 0024.
approve-workspace is a durable Operation: caller-generated id, idempotent on equal input, settles applied or not-applied. Application resolves the path with realpath, case preserved, and compares exactly. There is no revocation action and no Workspace picker.
bundle-catalog: list summaries carry exact identity, digest, name, description, origin, stability (stable or prerelease), platforms, engine range with a "needs Secant ≥" note when the running engine is outside it, and trust state (M1 shows External Bundles as not yet trusted; no trust action exists). Exact focus adds author metadata, launch-input declarations, Routing summary (Step ids and kinds in order, Repeat groups with their Review checkpoint), Workspace prerequisites, produced-artifact declarations, the generated Execution summary, and findings from the Composition check. This family offers no Action Offers. Archive objects and storage paths never cross. Rows sort by name, then version descending.
operation: the receipt of one submitted intent, pending then applied or not-applied with a Problem.
No Resource References are needed in M1; readResource exists in the contract with no M1 vocabulary.
Build, install, and future uninstall stay outside the Projection Port in Application's separate Bundle-management contract, consumed by the headless client only. build(folder, { output?, noInstall }) and install(file) return the identity, digest, written output path when any, and findings, or a Problem. Inspection and listing are not on this Interface; they are the bundle-catalog Projection.
Consequently the TUI has no build or install workflow in M1. Both clients observe every installed Bundle, its findings, and its Execution summary through the same Projection, which is how "in both clients" is met for build, install, inspect, and Composition check.
Execution-free. Declares the four Step kinds (Agent step, Interactive agent step, Human Gate with approve-reject and free-text shapes, Command step) through one uniform contract of seven facts: requires, produces, session, preconditions, capability needs, outcome classification, reconciliation. No Git step family. Nothing in this Module executes anything; M2 adds the private executable table beside it.
Declares the five artifact types (text, file, file-set, verdict, choice), artifact home (store or workspace with optional relative path), Routing as an ordered list of Steps and non-nesting Repeat groups with a required reviewCheckpoint (positive-integer interval under an engine-owned ceiling, plain-text message), the Prompt slot grammar {{artifact:name}} with no expressions, and the closed Workspace prerequisite set { git-worktree-root }.
Owns the Composition check over a validated manifest: every required artifact is bound earlier with a matching type or is a launch input; every Prompt slot names a required artifact; every Repeat group's verdict is bound before entry; every schema-bearing declaration names a valid schema asset; every supported platform resolves exactly one command invocation; every referenced asset exists with the right kind. It returns findings, each with a stable code, severity, the Step or field it points at, and a human explanation. A Bundle with any error-severity finding does not install.
Bundle Module (authority: #9 resolution and its 2026-09-03 and 2026-09-07 amendments, ADR 0021)
Reads an authoring folder or a .wfb file into a validated manifest and asset tree without executing anything. Manifest is strict JSON with the seven top-level fields and no unknown fields anywhere. Bundle id is lowercase reverse-domain; version is strict SemVer.
Archive constraints are Secant-owned regardless of any library: UTF-8 relative paths, regular files only, no absolute or traversing paths, links, special files, duplicate or case-colliding paths, encryption, multipart archives, or compression other than store and deflate. Budgets for input bytes, expanded bytes, and entry count are Interface parameters with defaults wired by composition; there is no user-facing setting in M1 and a Bundle can never raise them.
Deflate uses node:zlib. The ZIP container reader and writer are either a small cohesive Secant file or one exact-pinned library earned on the ticket's Dependencies: line after checking how OpenCode decides the same question; the validator above is Secant's either way.
The builder normalizes entry order, timestamps, permissions, and manifest encoding, derives requires.engine from a fixed feature-to-minimum-version table (every v1 feature maps to the Secant version that introduced format version 1), inserts the build host into platforms when omitted, computes SHA-256 over the exact bytes, and refuses --no-install without --output. An installer accepts semantically compatible ZIP metadata and independently rejects an understated engine range.
Asset kinds and their rules, the {"asset":…} versus {"artifact":…} reference forms, structured command invocations with optional per-platform parameter overrides, and the rule that every non-manifest entry belongs to exactly one non-overlapping declared asset tree are validated here.
Generates the Execution summary from the manifest for the selected platform: identity, digest, origin, platforms, Step-kind counts, selected commands, working directories, environment variable names, scripts, and the fixed warning that commands and Harness actions run with the current user's authority. It is projected in M1 and acknowledged in M2.
Owns the catalog database and the managed store under the Secant home, ~/.secant on every platform, overridable with SECANT_HOME. Every test uses a temporary home.
Records: Catalog Entries (id, version, digest, origin, install time, private installation generation) and Workspace approvals (canonical path, approval time). Physical ownership is explicit: Application asks Catalog; it never reaches into rows.
Install is one atomic sequence: stage bytes, validate, store under the digest, verify stored bytes against the digest, commit the Entry. Any failure leaves no store bytes and no Entry. Identity is first-install-wins with the two distinct results. Concurrent processes are serialized by the database; a second writer waits or fails with a precise Problem, never corrupts.
Bundle origin in M1 is local-build with the resolved folder or local-file with the original path. The built-in origin and the startup ensure-installed use case of ADR 0029 arrive with the first Shipped Bundle in M6.
Headless client
secant with no subcommand launches the TUI. secant bundle build <folder> [--output <file>] [--no-install], secant bundle install <file>, secant bundle list [--json], secant bundle inspect <id>[@<version>] [--json] (version omitted selects the highest stable installed version; a prerelease must be named), secant workspace approve [path] (defaults to the current directory), and secant workspace [--json] (the headless Home: path, approval state, Installed Bundle count). --help and --version keep working.
Human output is plain text with status carried by words. --json prints the Projection snapshot or Operation result verbatim. Any Problem prints code, explanation, and remediation and exits non-zero. Bundle commands do not require an approved Workspace; the Catalog is global.
TUI client
Launch resolves the Workspace, opens the workspace Projection, and shows the approval dialog when unapproved: the exact path, Approve, Decline. Approve submits approve-workspace; Decline exits. Home shows the Workspace path and the Workflow Bundles entry plus the quit binding. Workflow Bundles opens the bundle-catalog list; Enter opens exact focus in an inspection view; Escape returns; the quit binding works everywhere. The empty state names the headless commands.
Every M1 TUI slice copies the applicable Prototype Crucible's launch and Run information architecture #23 deferred items into its acceptance criteria: keymap and focus (exact shortcuts, Tab order, focus restoration, list/detail/dialog transitions, terminal and OS conflicts), terminal layout (small-terminal breakpoints, resize, overflow, visible focus indicators), interaction tuning (dialog dismissal, receipt duration), visual and accessibility checks (density, themes, keyboard-only traversal, contrast, colour never the only signal), and renderer and platform evidence (in-memory renderer tests, real-PTY tests, real keyboard, focus, resize, and teardown checks on all three operating systems). Timeline mechanics and large content are M2.
Gates (authority: ADR 0027)
The canonical check keeps the three-OS matrix. The package smoke grows: from the installed package it builds the Proof Bundle with --no-install --output, installs the output into a temporary SECANT_HOME, lists and inspects it with --json, asserts identity, digest, all three platforms, and zero error findings, re-installs and asserts "already installed", then installs a byte-different archive claiming the same identity and asserts an identity collision.
A new blocking workflow job runs the real-terminal suite on all three operating systems outside npm run check, with node-pty as a devDependency for that suite only: launch the built CLI under a pseudo-terminal against an approved temporary Workspace, await readiness over a side-channel file (never by scraping the stream on Windows), exercise quit, Ctrl+C, and, where the platform supports it, SIGHUP, and assert exit code, exactly-once teardown, and restored terminal modes measured over a real API or side channel. Byte-level assertions on Windows are limited to what ConPTY demonstrably forwards. No CI retries; a flaky case is fixed or moved to the opt-in suite and the ticket says which.
The legacy-conhost human real-terminal check is run against the M1 build by a human and recorded on the milestone issue in the ADR 0027 report shape (check name, OS and version, terminal, Node version, package version and digest, outcome, timestamp). The check script ships with M1; the per-release checklist and release workflow that consume it are M4.
Proof Bundle
The Test Repair Workflow is authored as the first folder under bundles/: one file launch input for the failing test, a Repeat group over an Agent step that fixes without committing and a Command step that runs the test producing the verdict, with a Review checkpoint, followed by an approve-reject Human Gate and a Command step that commits. It declares all three platforms with per-platform command parameter overrides where the executable differs, and passes the Composition check in CI on each. It is exercised through the real build and install path, never fixtured, and nothing in target source knows its id.
Testing Decisions
What a good test is here. It observes behaviour through the same Interface a client uses, on real deterministic local resources: a temporary SECANT_HOME, real .wfb bytes built from bundles/, a real SQLite database. It never asserts row shapes, file layouts, escape sequences, or Solid internals. Internal refactoring never requires test rewrites.
One behavioural seam: the Projection Port and the Bundle-management contract. Contract tests build and install real Bundles, then open workspace, bundle-catalog, and operation Projections and submit approve-workspace, asserting snapshots, Action Offers, update ordering, idempotency, and every Problem case: each rejected archive shape, unknown field, asset-tree violation, unresolved slot, unbound artifact, missing Repeat verdict, understated engine range, exceeded budget, already installed, identity collision, atomicity after an injected failure, and concurrent-writer safety. Bundle and Workflow Modules are exercised through these tests plus focused Composition check cases over hand-written manifests; Catalog is exercised through them with a real temporary database.
Two lifecycle seams, as ADR 0018 obliges. The Renderer Port fake drives the whole shell lifecycle with no terminal and records call order; the teardown test fails when renderer.destroy() precedes the stdin release, and every exit path tears down exactly once. The real-PTY suite is deliberately small and catches exactly one class: a terminal left broken after exit.
Presentation tests use OpenTUI's in-memory test renderer (createTestRenderer: frames, key input, resize) over fake Projection snapshots for the approval dialog, Home, the Bundles list, and the inspection view: content present, status readable without colour, keybindings firing, layout at small widths. Snapshot tests are optional and not required.
The CLI process boundary is the CI acceptance seam for headless work, reusing the package smoke's install-then-run pattern rather than adding a second harness.
Prior art: the architecture suites and the package smoke from M0 (reused and extended); the Prove an OpenCode-derived Crucible shell across Windows, macOS, and Linux #6 prototype's fake renderer, side-channel readiness file, ConPTY-scoped assertions, and pseudo-terminal harness (read as evidence at its issue, written fresh under the target Interfaces); OpenCode's provider tests that mount real Solid providers and feed typed events (pattern only).
Structural gates gain the Bun. call check and the UPSTREAM and notices presence check, tested with synthetic graphs like the existing checker tests.
Out of Scope
Runs of any kind: Run Store, Run execution, executable Step kinds, Preflight beyond the Composition check, git-worktree-root probing, launch, Previous Runs, Run Workbench, timeline mechanics, large content. M2.
Trust acknowledgement, Trust grants, the trust prompt, and any use of the Execution summary beyond projecting it. M2.
Harness discovery, qualification, Adapters, the Harness catalog, recordings, and replayers. M3 to M5.
Bundle uninstall, forced removal, and the installation-generation guard's Run-side use. M2 or later.
Shipped Bundles, bundles/builtin.lock.json, the startup ensure-installed use case, and the built-in origin. M6 with the first built-in.
The release workflow, release environment, release checklist template, scripts/release-checks/ machinery, docs/support-matrix.md, and license inventory generation. M4. M1 only ships the human check script and records one report on the milestone.
Start a Run, Previous Runs, and Harnesses entries on Home, catalog search mechanics, theme picker, persisted preferences, prompt and transcript components, which-key, toast, diff viewer. Later screens copy these when they need them.
Any OpenTUI version bump, any Bun runtime path, any patch to OpenTUI internals.
Amendments this spec commits the implementing slices to record: the seventh workspace Projection family and the approve-workspace Operation on Define Crucible's TUI-facing command and projection interface #19 and ADR 0024; the three extra dropped OpenCode-branded themes on ADR 0018; the Workspace approval record's home in the Catalog database on ADR 0025 if the reviewer judges it a change of ownership rather than a detail.
Known risks the first slices settle by evidence: Solid JSX compiling under the tsup build on Node 24 on all three operating systems; JSON import attributes for themes under the bundler; node-pty building on the Node 24 CI runners; OpenTUI's Solid package resolving its Node entry points at 0.4.5. Each is a slice-level fact to prove in CI, not a decision to reopen.
Rulebook reminders for /to-tickets: both clients over the same Projection; one CI scenario per slice; no scaffolding; Owner:, Ratchet:, Deletes:, Gate:, Dependencies: packet lines with every runtime dependency re-earned (expected candidates: the three OpenTUI packages at 0.4.5 exact, solid-js, possibly one ZIP library; dev-only: node-pty, babel-preset-solid and its Babel core); Bundles exercised not fixtured; no branching on workflow identity; TUI slices copy the Prototype Crucible's launch and Run information architecture #23 deferred items; publish without ready-for-agent.
Spec for M1: Shell and Catalog. Cut from the milestone issue and its Starting context, pinned at
29d038f; no triage label per the milestone loop./to-ticketscuts tracer bullets from this under the Secant slicing rulebook.Problem Statement
After M0 the repository is an empty-but-green Secant: the installed
secantcommand answers--helpand--versionand nothing else. A user cannot open Secant in a project directory, cannot see which Workflow Bundles are installed, and cannot turn an authoring folder into a.wfbfile, install it, or learn whether its Routing composes. An agent starting M2 has no shell to put a Run into, no Catalog to pin a Bundle Snapshot against, no Projection Port for either client to speak, and no proof that OpenTUI-based presentation builds, runs, and tears down cleanly on Windows, macOS, and Linux under Node 24.Two settled decisions are still only words. ADR 0018 says the OpenCode presentation subset is a pinned vendored copy behind a Renderer Port with a
process.stdin-before-renderer.destroy()teardown invariant, but no copy, noUPSTREAMrecord, no third-party notices file, and no test enforcing the ordering exist. ADR 0027 says a real-terminal lifecycle suite runs as its own blocking CI job on three operating systems and the legacy-conhost check rests on a human, but neither gate exists.Solution
Land the shell and the non-executing Catalog, in both clients, with zero Runs.
From the user's point of view, after M1:
secantin a directory for the first time asks, once, for approval of that exact absolute path as the Workspace. Declining exits cleanly. Approving opens Workspace Home, and every later launch from that path opens Home directly.secant bundle build <folder>validates and normalizes an authoring folder, reports the digest, and installs it atomically;--outputalso writes the.wfb;--no-installrequires--outputand skips only the Catalog write.secant bundle install <file.wfb>imports an already-built Bundle through the identical path.secant bundle listandsecant bundle inspectshow the same facts the TUI shows, from the same Projection.secant workspace approveandsecant workspacegive the headless client the same Workspace approval and Home facts.bundles/, builds, installs, and passes the Composition check in CI on all three operating systems, without anything in target source knowing its name.User Stories
secant bundle build <folder>to validate my authoring folder, normalize it into exact.wfbbytes, print the digest, and install it atomically, so that one command takes me from folder to Catalog.--output <file>to write those same bytes to disk, so that I can hand the Bundle to someone else.--no-installto run the full validator, normalizer, and Composition check and refuse to run without--output, so that CI can build Shipped Bundles without touching the runner's Catalog.secant bundle install <file.wfb>to import through the identical validator, managed store, and Catalog path, so that a built and an imported Bundle are indistinguishable once installed.requires.engineto have the minimum compatible engine range derived and written by the builder, so that I never guess a version constraint.platformsto default to my build host while an authored subset is preserved and validated, so that the manifest is honest about what was proven.SKILL.md, one schema JSON, one script text file, one opaque resource), so that a kind mismatch is caught at build.{{artifact:name}}to name an artifact the Step requires, with unknown or malformed slots failing composition, so that prompts and bindings never drift apart.reviewCheckpointvalidated as a positive-integer interval under Secant's safety ceiling with a plain-text message, so that M2's Review checkpoint has valid data to run on.secant bundle listandsecant bundle inspectto print the same facts the TUI shows, with--jsonfor machine consumption, so that scripts and CI read exactly what a human reads.secant workspace approve [path]andsecant workspaceto approve and show the Workspace, so that the headless client has the same approval and Home facts as the TUI.workspace,bundle-catalog, andoperationfamilies and theapprove-workspaceOperation, so that Run families are added to a working contract rather than invented alongside it.UPSTREAMrecord naming the OpenCode commit, every copied path, every local modification, and the date, created in the same commit as the first copy and updated by every later one, so that provenance is never reconstructed after the fact.THIRD-PARTY-NOTICES.mdat the repository root, created in that same extraction commit, listed in the package's shipped files, carrying the MIT notices for OpenCode and OpenTUI and one attribution line per shipped theme, so that M4's release gate has something to verify.@opencode-ai/*import, anybun:import orBun.call, and anynode-ptyimport in target source, so that the vendor boundary does not depend on who reviews the pull request.process.stdinbeforerenderer.destroy()with a comment pointing at the upstream report, and a test that fails if the order is reversed, so that a future tidy-up cannot reintroduce console death.bundles/, declaring all three platforms and passing the Composition check in CI on each, so that the Proof Bundle role is occupied from the first Catalog slice.Implementation Decisions
Authority and vocabulary
HEADsupersede ticket wording. ADR 0018 and ADR 0027 already carry the Approve Crucible's architecture and migration handoff #22 handoff corrections; ADR 0029 fixes how built-ins ship and is honoured here only where M1's build path touches it.CONTEXT.md: Renderer Port, Projection Port, Projection, Operation, Action Offer, Problem, Resource Reference, Workflow Bundle, Bundle Asset, Installed Bundle, Catalog Entry, Bundle origin, Composition check, Workspace, Proof Bundle. Crucible in older texts reads as Secant.Module map (authority: ADR 0025, topology)
cli(dispatch to the selected client, fail-fast engine check),composition(outer root and child roots for TUI and headless),application(Projection Port contract, Bundle-management contract, Workspace approval and Bundle ingestion use cases),workflow(execution-free contracts and the Composition check),bundle(archive validation, build, normalization, digest, Execution summary),catalog(the catalog database, managed store, installation records, Workspace approvals),tui(Secant presentation plus the vendored subset),renderer(Renderer Port and teardown ordering),headless(commands). Run execution, Run Store, artifacts, and Harness Modules are not created.tuiandrenderer.Renderer Port and lifecycle (authority: ADR 0018, #6, #33)
size,onKey,onResize,destroy,destroyed. It has two Adapters: the production one over OpenTUI'screateCliRendererat the owned pin0.4.5, and a fake with no terminal and no native library. Drawing goes through OpenTUI's Solid integration directly and never through the Port.process.exitis never called on the normal path; the exit code is set.process.stdin(listeners off,setRawMode(false),pause(),unref(),destroy()) beforerenderer.destroy(). Async work afterdestroy()is permitted. The site carries a comment linkinganomalyco/opentui#1405. A test over the fake Adapter records the ordering and fails when it is reversed.process.versionis below theengines.nodefloor, before any renderer or database is touched.Presentation subset: vendored, rebuilt, deleted (authority: ADR 0018 tiebreak, #17)
1ead9e3d7f, whose TUI is a workspace package at OpenTUI0.4.5.UPSTREAMrecords that commit; a later copy from a newer commit records its own.UPSTREAM.Bun.fileandBun.writecalls becomenode:fs/promisesat copy time.Bun.stringWidthsits only in prompt code, which M1 does not copy; the substitute is chosen by the slice that copies the prompt. Every@opencode-ai/*import in a vendored file is a type or a ≤20-line utility (flag, global paths, glob, file lock,which, installation version) and is replaced by a local value ornode:built-in.THIRD-PARTY-NOTICES.mdcarries one line per shipped theme plus the MIT notices for OpenCode and OpenTUI. A default theme is chosen; there is no theme picker or persisted preference in M1.babel-preset-solidwithmoduleNameset to the OpenTUI Solid package andgenerate: "universal") as a tsup/esbuild plugin step;jsxImportSourcepoints at the OpenTUI Solid package. The Prove an OpenCode-derived Crucible shell across Windows, macOS, and Linux #6 proof covered only the core renderer, so the first shell slice proves this build on all three operating systems from the installed package. Theme JSON is imported through a generated barrel rather than import attributes if the bundler cannot preserve them.@opencode-ai/*,bun:, andnode-ptyimports in target source; it gains a check forBun.global calls and forTHIRD-PARTY-NOTICES.mdandUPSTREAMexisting once any vendored file exists.Projection Port (authority: ADR 0024, #19 and its 2026-09-05 amendment)
openProjection,submit, andreadResource. Opening returns snapshot, catch-up, updates, and idempotent close. Every failure crossing the Port is a normalized Problem: stable code, safe explanation, code-specific details, possible-effects classification, optional field violations. Throws are reserved for caller-contract violations.workspaceis added as the seventh family. Selector: none, the process has one launch Workspace. Snapshot: the resolved canonical absolute path, approval state (approvedorunapproved), approval time, and the Action Offerapprove-workspacewhen unapproved. Catch-up is rebase-only. This extends the Define Crucible's TUI-facing command and projection interface #19 vocabulary and ADR 0024's "six families"; the implementing slice records the amendment on Define Crucible's TUI-facing command and projection interface #19 and in ADR 0024.approve-workspaceis a durable Operation: caller-generated id, idempotent on equal input, settlesappliedornot-applied. Application resolves the path withrealpath, case preserved, and compares exactly. There is no revocation action and no Workspace picker.bundle-catalog: list summaries carry exact identity, digest, name, description, origin, stability (stable or prerelease), platforms, engine range with a "needs Secant ≥" note when the running engine is outside it, and trust state (M1 shows External Bundles as not yet trusted; no trust action exists). Exact focus adds author metadata, launch-input declarations, Routing summary (Step ids and kinds in order, Repeat groups with their Review checkpoint), Workspace prerequisites, produced-artifact declarations, the generated Execution summary, and findings from the Composition check. This family offers no Action Offers. Archive objects and storage paths never cross. Rows sort by name, then version descending.operation: the receipt of one submitted intent,pendingthenappliedornot-appliedwith a Problem.readResourceexists in the contract with no M1 vocabulary.Bundle-management Interface (authority: #19, topology)
build(folder, { output?, noInstall })andinstall(file)return the identity, digest, written output path when any, and findings, or a Problem. Inspection and listing are not on this Interface; they are thebundle-catalogProjection.Workflow Module (authority: #13, #9, #14, ADR 0021)
approve-rejectandfree-textshapes, Command step) through one uniform contract of seven facts: requires, produces, session, preconditions, capability needs, outcome classification, reconciliation. No Git step family. Nothing in this Module executes anything; M2 adds the private executable table beside it.text,file,file-set,verdict,choice), artifact home (storeorworkspacewith optional relative path), Routing as an ordered list of Steps and non-nesting Repeat groups with a requiredreviewCheckpoint(positive-integerintervalunder an engine-owned ceiling, plain-textmessage), the Prompt slot grammar{{artifact:name}}with no expressions, and the closed Workspace prerequisite set{ git-worktree-root }.Bundle Module (authority: #9 resolution and its 2026-09-03 and 2026-09-07 amendments, ADR 0021)
.wfbfile into a validated manifest and asset tree without executing anything. Manifest is strict JSON with the seven top-level fields and no unknown fields anywhere. Bundle id is lowercase reverse-domain; version is strict SemVer.storeanddeflate. Budgets for input bytes, expanded bytes, and entry count are Interface parameters with defaults wired by composition; there is no user-facing setting in M1 and a Bundle can never raise them.node:zlib. The ZIP container reader and writer are either a small cohesive Secant file or one exact-pinned library earned on the ticket'sDependencies:line after checking how OpenCode decides the same question; the validator above is Secant's either way.requires.enginefrom a fixed feature-to-minimum-version table (every v1 feature maps to the Secant version that introduced format version 1), inserts the build host intoplatformswhen omitted, computes SHA-256 over the exact bytes, and refuses--no-installwithout--output. An installer accepts semantically compatible ZIP metadata and independently rejects an understated engine range.{"asset":…}versus{"artifact":…}reference forms, structured command invocations with optional per-platform parameter overrides, and the rule that every non-manifest entry belongs to exactly one non-overlapping declared asset tree are validated here.Catalog Module (authority: ADR 0025, ADR 0023 home directory, #21 storage)
~/.secanton every platform, overridable withSECANT_HOME. Every test uses a temporary home.node:sqliteper the Choose Crucible's process, runtime, packaging, and distribution architecture #21 runtime decision, entirely behind the Catalog Interface: no SQLite type crosses, transactions areBEGIN IMMEDIATEwith guarded rollback,busy_timeoutis set at open, and nonode:sqlitefeature newer than the Node floor is used without an explicit engines bump. The flip tobetter-sqlite3remains governed by the Choose Crucible's process, runtime, packaging, and distribution architecture #21 conditions and is not reopened here.local-buildwith the resolved folder orlocal-filewith the original path. Thebuilt-inorigin and the startup ensure-installed use case of ADR 0029 arrive with the first Shipped Bundle in M6.Headless client
secantwith no subcommand launches the TUI.secant bundle build <folder> [--output <file>] [--no-install],secant bundle install <file>,secant bundle list [--json],secant bundle inspect <id>[@<version>] [--json](version omitted selects the highest stable installed version; a prerelease must be named),secant workspace approve [path](defaults to the current directory), andsecant workspace [--json](the headless Home: path, approval state, Installed Bundle count).--helpand--versionkeep working.--jsonprints the Projection snapshot or Operation result verbatim. Any Problem prints code, explanation, and remediation and exits non-zero. Bundle commands do not require an approved Workspace; the Catalog is global.TUI client
workspaceProjection, and shows the approval dialog when unapproved: the exact path, Approve, Decline. Approve submitsapprove-workspace; Decline exits. Home shows the Workspace path and the Workflow Bundles entry plus the quit binding. Workflow Bundles opens thebundle-cataloglist; Enter opens exact focus in an inspection view; Escape returns; the quit binding works everywhere. The empty state names the headless commands.Gates (authority: ADR 0027)
--no-install --output, installs the output into a temporarySECANT_HOME, lists and inspects it with--json, asserts identity, digest, all three platforms, and zero error findings, re-installs and asserts "already installed", then installs a byte-different archive claiming the same identity and asserts an identity collision.npm run check, withnode-ptyas a devDependency for that suite only: launch the built CLI under a pseudo-terminal against an approved temporary Workspace, await readiness over a side-channel file (never by scraping the stream on Windows), exercise quit, Ctrl+C, and, where the platform supports it, SIGHUP, and assert exit code, exactly-once teardown, and restored terminal modes measured over a real API or side channel. Byte-level assertions on Windows are limited to what ConPTY demonstrably forwards. No CI retries; a flaky case is fixed or moved to the opt-in suite and the ticket says which.Proof Bundle
bundles/: onefilelaunch input for the failing test, a Repeat group over an Agent step that fixes without committing and a Command step that runs the test producing theverdict, with a Review checkpoint, followed by an approve-reject Human Gate and a Command step that commits. It declares all three platforms with per-platform command parameter overrides where the executable differs, and passes the Composition check in CI on each. It is exercised through the real build and install path, never fixtured, and nothing in target source knows its id.Testing Decisions
SECANT_HOME, real.wfbbytes built frombundles/, a real SQLite database. It never asserts row shapes, file layouts, escape sequences, or Solid internals. Internal refactoring never requires test rewrites.workspace,bundle-catalog, andoperationProjections and submitapprove-workspace, asserting snapshots, Action Offers, update ordering, idempotency, and every Problem case: each rejected archive shape, unknown field, asset-tree violation, unresolved slot, unbound artifact, missing Repeat verdict, understated engine range, exceeded budget, already installed, identity collision, atomicity after an injected failure, and concurrent-writer safety. Bundle and Workflow Modules are exercised through these tests plus focused Composition check cases over hand-written manifests; Catalog is exercised through them with a real temporary database.renderer.destroy()precedes the stdin release, and every exit path tears down exactly once. The real-PTY suite is deliberately small and catches exactly one class: a terminal left broken after exit.createTestRenderer: frames, key input, resize) over fake Projection snapshots for the approval dialog, Home, the Bundles list, and the inspection view: content present, status readable without colour, keybindings firing, layout at small widths. Snapshot tests are optional and not required.Bun.call check and theUPSTREAMand notices presence check, tested with synthetic graphs like the existing checker tests.Out of Scope
git-worktree-rootprobing, launch, Previous Runs, Run Workbench, timeline mechanics, large content. M2.bundles/builtin.lock.json, the startup ensure-installed use case, and thebuilt-inorigin. M6 with the first built-in.releaseenvironment, release checklist template,scripts/release-checks/machinery,docs/support-matrix.md, and license inventory generation. M4. M1 only ships the human check script and records one report on the milestone.Further Notes
HEAD, then the Sequence vertical slices, refactoring gates, and legacy deletion #20 resolution with its two 2026-09-07 amendments and the Approve Crucible's architecture and migration handoff #22 handoff approval, then ticket resolutions. Where the M1 milestone body says build and install land "in the TUI", the Define Crucible's TUI-facing command and projection interface #19 resolution and the topology policy place them on the headless-only Bundle-management Interface; this spec follows the policy and meets "both clients" through the sharedbundle-catalogProjection.workspaceProjection family and theapprove-workspaceOperation on Define Crucible's TUI-facing command and projection interface #19 and ADR 0024; the three extra dropped OpenCode-branded themes on ADR 0018; the Workspace approval record's home in the Catalog database on ADR 0025 if the reviewer judges it a change of ownership rather than a detail.node-ptybuilding on the Node 24 CI runners; OpenTUI's Solid package resolving its Node entry points at0.4.5. Each is a slice-level fact to prove in CI, not a decision to reopen./to-tickets: both clients over the same Projection; one CI scenario per slice; no scaffolding;Owner:,Ratchet:,Deletes:,Gate:,Dependencies:packet lines with every runtime dependency re-earned (expected candidates: the three OpenTUI packages at0.4.5exact,solid-js, possibly one ZIP library; dev-only:node-pty,babel-preset-solidand its Babel core); Bundles exercised not fixtured; no branching on workflow identity; TUI slices copy the Prototype Crucible's launch and Run information architecture #23 deferred items; publish withoutready-for-agent.--no-install. Read OpenCode's counterpart before rebuilding any component and record what was taken and changed.