Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions .derived/codebase-index/by-spec/006-template-upgrade-verb.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
{
"diagnostics": {
"errors": [],
"warnings": [
{
"code": "W-001",
"message": "spec '006-template-upgrade-verb' symbol unit 'stagecraft_cli::verbs::template' did not resolve"
}
]
},
"mapping": {
"dependsOn": [
"002-crate-scaffold"
],
"implementingPaths": [],
"implementingPaths": [
{
"path": "src/verbs/mod.rs",
"source": "spec-edge"
}
],
"resolvedUnits": [
{
"locations": [],
"locations": [
{
"file": "src/verbs/mod.rs",
"span": {
"endLine": 19,
"startLine": 19
}
}
],
"ownership": true,
"sourceField": "establishes",
"unit": {
Expand All @@ -28,5 +32,5 @@
"specStatus": "approved"
},
"schemaVersion": "1.1.0",
"shardHash": "8f277e7de51ae5e57e7866e965f5e35b265d7cbcd8620b22acc8aa2b948fad2b"
"shardHash": "dfc07fb9566e067f5e7850b1ce101bfd545cb104499f819f51e76f98f5d23c74"
}
7 changes: 4 additions & 3 deletions .derived/spec-registry/by-spec/006-template-upgrade-verb.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@
}
],
"id": "006-template-upgrade-verb",
"implementation": "pending",
"implementation": "in-progress",
"sectionHeadings": [
"006: template upgrade verb",
"1. Cross-repo dependencies (read first)",
"2. Behavior",
"3. Acceptance",
"4. Out of scope"
"4. Out of scope",
"5. Status (2026-07-15)"
],
"specPath": "specs/006-template-upgrade-verb/spec.md",
"status": "approved",
"summary": "The upgrade half of the 2026-07-14 packaging decision: templates stay small because the chassis ships as versioned npm packages (enrahitu spec 018), and upgrading a stamped app is a verb, not a migration project. `stagecraft template upgrade`, run in a stamped app checkout, reads template.toml, bumps the chassis package pins, applies template-shipped codemods, runs the contract verify verb, and commits on a branch. The CLI orchestrates; all structure knowledge stays in the template and its packages. This verb is the boundary that keeps the CLI from ever becoming a build daemon.\n",
"title": "stagecraft template upgrade: chassis upgrades as a governed verb"
},
"shardHash": "24d0395b2d3ceaa98f86d8301c675e49a3658d61d8091e218ba641b79542610f",
"shardHash": "5c1f7935e130ca8d1a42dfe060bf18f01e8a19f24be733760bc9d37cfc98f646",
"specVersion": "1.1.0"
}
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ clap_complete = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
# Semantic-version comparison for the template upgrade verb (spec 006): the
# compat gate matches a target against the contract's `[requires]` range.
# Pure Rust, no TLS surface; keeps the tree rustls-only.
semver = "1"
# Error handling: anyhow for operational context, thiserror for the taxonomy.
anyhow = "1"
thiserror = "2"
Expand Down
82 changes: 81 additions & 1 deletion specs/006-template-upgrade-verb/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: "006-template-upgrade-verb"
title: "stagecraft template upgrade: chassis upgrades as a governed verb"
status: approved
created: "2026-07-14"
implementation: pending
implementation: in-progress
depends_on:
- "002-crate-scaffold"
establishes:
Expand Down Expand Up @@ -84,3 +84,83 @@ re-import path; do not attempt a tree merge.
018 owns the packages; this verb only consumes published versions).
- Automatic PR creation and merge (deliberate: a human or the
platform decides; v1 prints the suggestion).

## 5. Status (2026-07-15)

Implemented: `stagecraft template upgrade` in the new
`stagecraft_cli::verbs::template` module (the symbol this spec
establishes). It is the one *local* governed verb: unlike the spec 004
verbs it never calls the control plane. It reads `template.toml`, bumps
the chassis package pins in `package.json`, refreshes the lockfile,
runs the reserved codemod hook, runs the contract's verify verb, and
commits on a `template-upgrade/<from>-<to>` branch, emitting the §2.6
`{from, to, pins, codemodsRun, verify}` result inside the shared
`{ok,data|error}` envelope. The enrahitu spec 018 gate is cleared
(chassis packages published at v0.1.0, `implementation: complete`), so
this verb is no longer parked.

v1 decisions (surfaced here for review rather than encoded silently):

- **Target ↔ pin coupling.** The template version and chassis package
version move in lockstep (enrahitu ships root, `template.toml`, and
all `@enrahitu/*` packages at one version), so the resolved target is
applied as the exact pin for each discovered chassis package.
- **Compat gate = the contract's own ranges.** A `--to` (or resolved
latest) that does not satisfy `[requires]`'s chassis range is the
"major upgrade requires the migration path" refusal
(`incompatible_target`). When a contract names more than one chassis
range, the target must satisfy *all* of them, so no range can be
crossed silently. The version policy stays in the template.
- **Chassis discovery is scope-derived.** Seeds are `package.json`
deps whose unscoped name matches a `[requires]` key (so `node` drops
out and `toolchain` resolves to `@enrahitu/toolchain`); the bump set
is every exact-pinned dep sharing a seed's scope, catching companions
like `@enrahitu/hiqlite-native` with no hardcoded scope in the CLI.
Accepted trade-off: this assumes every exact-pinned package under a
chassis scope releases in lockstep with the chassis (true for
`@enrahitu/*` today); an independently-versioned same-scope package
would need an allowlist, revisited if enrahitu ever ships one.
- **Refusal taxonomy** (all exit 1, JSON `error.kind`): `not_stamped`
(no `template.toml`/`package.json`), `pre_chassis` (spec 006 §1: no
chassis packages), `local_chassis` (chassis present but as `file:`
links, i.e. a template-development tree, not a stamped app),
`bad_target`, `incompatible_target`, `dirty_tree`. Preflight order:
structural refusals first, then the dirty-tree gate (before target
resolution, so a dirty tree fails fast with no wasted registry read),
then target/compat. A verify failure is not a refusal: matching how
`stamp status --watch` renders a terminal `failed` job, it is a
completed run whose `{ok:true, data}` result carries `verify:"fail"`
and whose exit code (1) carries the failure, with the branch left for
inspection. The two-shape envelope contract holds (`ok:true` -> read
`data`, `ok:false` -> read `error`); the verdict lives in the data.
- **Dependency added:** `semver` (pure Rust) for correct
version/range comparison. Couples to spec 002's Cargo territory; the
standing `Spec-Drift-Waiver` covers the crate-file edits. Note: this
is Cargo-flavored semver, where a bare `[requires]` range (`"0.1.0"`)
parses as caret, not an npm exact pin; contract authors must write an
explicit operator (`^0.1`, `>=24`), as enrahitu's contract does.
- **`--to` omitted** resolves the greatest published version the range
allows via an `npm view` registry read, behind the runner seam so
tests stay offline.

Covered by tests (all offline; every git/npm/node effect is behind a
`Runner` trait): scope-derived discovery with companions, the
`pre_chassis`/`local_chassis`/`not_stamped`/`incompatible_target`/`dirty_tree`
refusals, the multi-range gate refusing when any seed range excludes
the target, format-preserving pin rewrite, the dry-run plan that
mutates nothing, latest-resolution when `--to` is omitted, the no-op
that still bumps a companion left behind the primary, the happy path
(branch -> lock -> verify -> commit), the verify-failure path that
leaves the branch uncommitted, and the result envelope staying
`{ok:true, data:{verify:"fail"}}` with a `Rendered` exit 1. Three
end-to-end binary checks drive the offline paths (not-stamped,
pre-018, dry-run) with the process working directory set to a fixture.

Outstanding: the §3 live check cannot run yet. It needs a real stamped
app carrying published-version chassis pins plus a newer published
template version to upgrade across; enrahitu's scaffold verb (spec 014)
is still v0 and "edits no dependencies", so no such app exists, and
there is only the v0.1.0 release to pin to. This spec stays
`implementation: in-progress` until a stamped app and a patch release
exist; at that point the only open item is the live upgrade transcript
(no code change), and the commit records its pointer.
25 changes: 25 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ pub enum Command {
#[command(subcommand)]
command: FleetCommand,
},
/// Chassis template upgrades, run in a stamped app checkout (spec 006).
Template {
#[command(subcommand)]
command: TemplateCommand,
},
/// Run the MCP server over stdio (spec 005).
Mcp {
/// Print the .mcp.json install snippet for this server, then exit.
Expand Down Expand Up @@ -208,6 +213,26 @@ pub enum FleetCommand {
},
}

#[derive(Debug, Subcommand)]
pub enum TemplateCommand {
/// Upgrade a stamped app's chassis pins to a newer template version (spec 006).
///
/// Run from the repo root of a stamped app. Reads template.toml, bumps the
/// chassis package pins, refreshes the lockfile, runs template-owned
/// codemods, runs the contract's verify verb, and commits on a branch.
Upgrade {
/// Target template version; omit to resolve the latest compatible one.
#[arg(long, value_name = "VERSION")]
to: Option<String>,
/// Print the plan and diffs without changing anything.
#[arg(long)]
dry_run: bool,
/// Commit on the current branch instead of a new template-upgrade branch.
#[arg(long)]
no_branch: bool,
},
}

#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
/// Print the effective, merged configuration with sources annotated.
Expand Down
26 changes: 25 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use std::path::PathBuf;
use clap::CommandFactory;
use serde::Serialize;

use crate::cli::{Cli, Command, ConfigCommand, FleetCommand, StampCommand, TenantsCommand};
use crate::cli::{
Cli, Command, ConfigCommand, FleetCommand, StampCommand, TemplateCommand, TenantsCommand,
};
use crate::config::{self, FlagConfig, ResolvedConfig, Sourced};
use crate::error::AppResult;
use crate::output::{self, OutputFormat};
Expand All @@ -28,6 +30,7 @@ pub fn dispatch(cli: Cli) -> AppResult<()> {
Command::Tenants { command } => tenants(command, &resolved, format, cli.debug),
Command::Stamp { command } => stamp(command, &resolved, format, cli.debug),
Command::Fleet { command } => fleet(command, &resolved, format, cli.debug),
Command::Template { command } => template(command, format, cli.debug),
Command::Mcp { print_config } => {
if *print_config {
crate::mcp::print_config(&resolved)
Expand Down Expand Up @@ -136,6 +139,27 @@ fn fleet(
}
}

// --- local governed verb (spec 006) ----------------------------------------

/// `template upgrade`: the one verb that never touches the control plane. It
/// operates on the stamped app checkout in the current working directory, so it
/// takes no `resolved` config (no base URL, no token); the app dir is cwd.
fn template(command: &TemplateCommand, format: OutputFormat, debug: bool) -> AppResult<()> {
use crate::verbs::template;
let dir = std::env::current_dir().map_err(|e| {
crate::error::AppError::Operational(anyhow::anyhow!(
"cannot determine the current directory: {e}"
))
})?;
match command {
TemplateCommand::Upgrade {
to,
dry_run,
no_branch,
} => template::upgrade(&dir, to.as_deref(), *dry_run, *no_branch, format, debug),
}
}

// --- working verbs ---------------------------------------------------------

/// Version payload; stable JSON shape (API from the first verb).
Expand Down
6 changes: 5 additions & 1 deletion src/verbs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
//!
//! Submodules ([`tenants`], [`stamp`], [`fleet`]) hold one verb family each and
//! call the private helpers here directly (a child module sees its parent's
//! private items).
//! private items). [`template`] (spec 006) is the exception: a *local* verb that
//! never calls the control plane (it operates on a stamped app checkout), yet
//! still renders through the same `{ok,data|error}` envelope
//! ([`success_envelope_value`], [`error_envelope`]) so both faces stay uniform.

pub mod fleet;
pub mod stamp;
pub mod template;
pub mod tenants;

use serde::Serialize;
Expand Down
Loading
Loading