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
2 changes: 1 addition & 1 deletion docs/site/reference/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The first officer runs these against workflow state as it moves entities; you op

| Command | What it does |
|---------|--------------|
| `spacedock status` | Read or mutate the state: the entity table (omits the SOURCE column by default; `--fields source` or `--all-fields` restores it), `--next`, `--where`, `--set`, `--validate`, `--boot`, `--read <ref-or-path>` (a file's structured frontmatter — including the nested `stages:` taxonomy, and projectable with `--fields` — plus a heading offset/lines map, for section-scoped reads; with `--checklist` / `--ac-scan` it extracts a stage report's checklist items with line ranges and per-AC evidence citations for the first officer's gate prep; `--stage` defaults to the entity's current `status` when omitted (so a bare `--read <entity> --checklist` reads the current stage's report), and `--stage X` reads a non-current stage) |
| `spacedock status` | Read or mutate the state: the entity table (omits the SOURCE column by default; `--fields source` or `--all-fields` restores it), `--next`, `--where`, `--set`, `--validate`, `--boot` (with `--identify`, the first officer's Startup identify — discovers the managed workflow(s), folds in the stage taxonomy, and reports the boot sections; PR_STATE is a local `pr:` view, live PR state is checked at engage; local reads only, no mutation), `--read <ref-or-path>` (a file's structured frontmatter — including the nested `stages:` taxonomy, and projectable with `--fields` — plus a heading offset/lines map, for section-scoped reads; with `--checklist` / `--ac-scan` it extracts a stage report's checklist items with line ranges and per-AC evidence citations for the first officer's gate prep; `--stage` defaults to the entity's current `status` when omitted (so a bare `--read <entity> --checklist` reads the current stage's report), and `--stage X` reads a non-current stage) |
| `spacedock new` | Create an entity (`new [--folder] SLUG`) from a body on stdin |
| `spacedock dispatch` | Build the worker dispatch artifacts (`dispatch build`, `dispatch show-stage-def`) |
| `spacedock state` | Manage a [split-root workflow](../advanced/split-root-state.md)'s state checkout (`state init` resumes one on a fresh clone, `state new` births one) |
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ _spacedock() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
verbs="claude codex pi install doctor status new state merge completion dispatch --version --help"
status_flags="--workflow-dir --next --next-id --boot --validate --archived --json --quiet --new --folder --set --where --archive --resolve --short-id --discover --root"
status_flags="--workflow-dir --next --next-id --boot --identify --validate --archived --json --quiet --new --folder --set --where --archive --resolve --short-id --discover --root"
if [ "$COMP_CWORD" -eq 1 ]; then
COMPREPLY=( $(compgen -W "$verbs" -- "$cur") )
return 0
Expand All @@ -641,7 +641,7 @@ const zshCompletion = `#compdef spacedock
_spacedock() {
local -a verbs status_flags
verbs=(claude codex pi install doctor status new state merge completion dispatch --version --help)
status_flags=(--workflow-dir --next --next-id --boot --validate --archived --json --quiet --new --folder --set --where --archive --resolve --short-id --discover --root)
status_flags=(--workflow-dir --next --next-id --boot --identify --validate --archived --json --quiet --new --folder --set --where --archive --resolve --short-id --discover --root)
if (( CURRENT == 2 )); then
compadd -- $verbs
return
Expand Down
62 changes: 62 additions & 0 deletions internal/contractlint/startup_collapse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// ABOUTME: AC-1 value check — the FO Startup recipe collapses to <=4 numbered prose
// ABOUTME: steps AND the boot-resident shared core is strictly smaller than its pre-change size.
package contractlint

import (
"os"
"path/filepath"
"regexp"
"testing"
)

// preChangeSharedCoreBytes is the byte size of first-officer-shared-core.md
// measured immediately before the "boot identifies, engage converges" edits, on
// the post-vcm-merge branch this implementation opened from (`wc -c` == 26755).
// AC-1's value half asserts the post-change file is STRICTLY smaller: a shorter
// recipe that grew the file fails.
const preChangeSharedCoreBytes = 26755

// startupStepRe matches a top-level numbered Startup step: a line beginning with
// `N.` at column zero. Sub-bullets (indented `-`) and the discovery sub-cases are
// not top-level steps, so they do not count against the <=4 budget.
var startupStepRe = regexp.MustCompile(`(?m)^[0-9]+\. `)

// startupBlock returns the `## Startup` section body (heading to the next `## `),
// so the step count is scoped to the recipe, not the whole file.
func startupBlock(t *testing.T, body string) string {
t.Helper()
loc := regexp.MustCompile(`(?m)^## Startup$`).FindStringIndex(body)
if loc == nil {
t.Fatal("shared core has no `## Startup` section")
}
rest := body[loc[1]:]
if end := regexp.MustCompile(`(?m)^## `).FindStringIndex(rest); end != nil {
rest = rest[:end[0]]
}
return rest
}

// TestStartupRecipeCollapsedAndLeaner (AC-1) is the two-sided value check: the
// Startup recipe carries <=4 top-level numbered steps (down from 8) AND the whole
// boot-resident shared core is strictly fewer bytes than before the change. Both
// halves must hold — the sprint's principal prose-remover must not merely reshape
// the recipe while leaving the resident core the same size or larger.
func TestStartupRecipeCollapsedAndLeaner(t *testing.T) {
root := repoRoot(t)
data, err := os.ReadFile(filepath.Join(root, sharedCorePath()))
if err != nil {
t.Fatalf("read shared core: %v", err)
}

steps := startupStepRe.FindAllString(startupBlock(t, string(data)), -1)
if len(steps) == 0 {
t.Fatal("Startup section has no top-level numbered steps — extractor bug; the count check would pass vacuously")
}
if len(steps) > 4 {
t.Errorf("Startup recipe has %d top-level numbered steps, want <=4", len(steps))
}

if got := len(data); got >= preChangeSharedCoreBytes {
t.Errorf("shared core is %d bytes, want strictly < %d (pre-change) — the collapse must shrink the resident core, not grow it", got, preChangeSharedCoreBytes)
}
}
2 changes: 1 addition & 1 deletion internal/ensigncycle/broad_search_detect_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// violation the captain observed (2026-06-14): after `spacedock status --discover`
// returned zero workflows, an FO ran a broad `find`/`grep -r`/`ls -R` filesystem
// sweep to hunt a workflow down instead of obeying the contract's terminal zero
// branch (Startup step 3: zero → report no workflow found and STOP). A broad sweep
// branch (Startup step 2: zero → report no workflow found and STOP). A broad sweep
// at boot is both a discipline violation (the zero branch is report-and-stop) and a
// cost/latency regression — the opposite of lean boot.
//
Expand Down
2 changes: 1 addition & 1 deletion internal/ensigncycle/shallow_boot_measure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// (the FIRST status query / --set / id lookup / issue filing, the FIRST write to
// main, or a dispatch failure — never at boot). present-gate is deliberately NOT
// here — the greet legitimately presents a ready gate via
// Skill(skill="spacedock:present-gate") (Startup step 8), so the oracle keys on the
// Skill(skill="spacedock:present-gate") (Startup step 3), so the oracle keys on the
// skill ARGUMENT, not on any Skill call.
var deferredFOSkillNames = []string{"fo-status-viewer", "fo-write-core", "fo-dispatch-recovery"}

Expand Down
2 changes: 1 addition & 1 deletion internal/ensigncycle/wrong_root_detect_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func detectWrongRootBoot(stream, fixtureRoot string) error {
clean, strings.TrimSpace(b.Input.Command), target)
}
case "Read":
// The FO reads {workflow_dir}/README.md at boot (Startup step 4). A
// The FO reads {workflow_dir}/README.md at boot (the Startup boot read). A
// workflow README read OUTSIDE the fixture means it booted the wrong
// workflow. Contract skills live under {plugin_dir}/skills/...references/,
// never a bare <root>/README.md, so this does not flag a contract read.
Expand Down
2 changes: 1 addition & 1 deletion internal/ensigncycle/zero_discover_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
// `commissioned-by: spacedock@` README), driven through the front door exactly like
// TestLiveEnsignCycle. The captain observed (2026-06-14) an FO, after a zero
// `status --discover`, run a broad find/grep filesystem sweep to hunt a workflow
// instead of obeying the contract's terminal zero branch (Startup step 3: zero →
// instead of obeying the contract's terminal zero branch (Startup step 2: zero →
// report no workflow found and STOP). This test proves the real boot:
//
// (a) reaches its greet/no-workflow report WITHOUT a TeamCreate (no workflow to
Expand Down
65 changes: 57 additions & 8 deletions internal/status/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ type prResult struct {

// checkPRStates returns (status, results) for entities with a non-empty pr and
// non-terminal status. Matches check_pr_states. status is "none",
// "gh not available", or "ok".
func checkPRStates(entities []*entity, stages []Stage, e env) (string, []prResult) {
// "gh not available", "ok", or (identify mode) "local". In identify mode the boot
// is a side-effect-free local read: it renders the stored `pr:` field as a local
// mirror (state "local", not-gh-checked) and makes NO `gh pr view` call — the live
// OPEN/MERGED/CLOSED state is filled in at «engage»'s convergence, not the greet.
func checkPRStates(entities []*entity, stages []Stage, e env, identify bool) (string, []prResult) {
stageByName := map[string]Stage{}
for _, s := range stages {
stageByName[s.Name] = s
Expand All @@ -102,6 +105,14 @@ func checkPRStates(entities []*entity, stages []Stage, e env) (string, []prResul
return "none", nil
}

if identify {
results := make([]prResult, 0, len(prEntities))
for _, ent := range prEntities {
results = append(results, prResult{id: ent.fields["id"], slug: ent.fields["slug"], pr: ent.fields["pr"], state: "local"})
}
return "local", results
}

ghPath := lookupExecutable("gh", e.get("PATH"))
if ghPath == "" {
return "gh not available", nil
Expand Down Expand Up @@ -161,13 +172,21 @@ type bootData struct {
// whether the safehouse binary resolves on PATH, so the operator sees the
// execution-isolation posture before dispatching work.
sandbox string
// Identify mode (the FO's opt-in local-identify boot). When set, the record
// folds the workflow discovery result and the stage taxonomy into the same
// envelope, PR_STATE is the local `pr:` mirror (checkPRStates skips gh), and the
// whole boot is a side-effect-free local read. discovery/stages are appended
// AFTER the existing key set so every prior key's order is preserved.
identify bool
discovery []string
stages []Stage
}

// gatherBoot runs every boot probe once and returns the result. NEXT_ID is
// minted here (timestamp-dependent for sd-b32); on a minting error it returns
// the error after the caller has emitted the stderr diagnostic.
func gatherBoot(probe claudeteam.TeamStateProbe, entities []*entity, stages []Stage, definitionDir, entityDir, gitRoot, idStyle string, e env, stderr io.Writer) (*bootData, error) {
d := &bootData{idStyle: idStyle, hooks: scanMods(definitionDir)}
func gatherBoot(probe claudeteam.TeamStateProbe, entities []*entity, stages []Stage, definitionDir, entityDir, gitRoot, idStyle string, e env, stderr io.Writer, identify bool) (*bootData, error) {
d := &bootData{idStyle: idStyle, hooks: scanMods(definitionDir), identify: identify}

if idStyle == "slug" {
d.nextID = "n/a (id-style: slug)"
Expand All @@ -181,8 +200,15 @@ func gatherBoot(probe claudeteam.TeamStateProbe, entities []*entity, stages []St
}

d.orphans = scanOrphans(entities, gitRoot)
d.prStatus, d.prResults = checkPRStates(entities, stages, e)
d.prStatus, d.prResults = checkPRStates(entities, stages, e, identify)
d.dispatchable = computeDispatchable(entities, stages)
// Identify mode folds the two hand-issued pre-greet reads — workflow discovery
// and the stage taxonomy — into this one record. Both are local reads (a
// filesystem walk; the already-parsed stages), so the boot stays side-effect-free.
if identify {
d.discovery = discoverWorkflows(gitRoot)
d.stages = stages
}
// TEAM_STATE comes from the host-supplied probe. HOME resolution stays generic
// here; only the ~/.claude read moves into the Claude seam. The hint for both
// the present and absent cases is resolved here so the renderers carry no
Expand Down Expand Up @@ -229,8 +255,8 @@ func gatherBoot(probe claudeteam.TeamStateProbe, entities []*entity, stages []St
}

// printBoot writes all boot sections in order. Matches print_boot.
func printBoot(probe claudeteam.TeamStateProbe, w io.Writer, entities []*entity, stages []Stage, definitionDir, entityDir, gitRoot, idStyle string, e env, stderr io.Writer) error {
d, err := gatherBoot(probe, entities, stages, definitionDir, entityDir, gitRoot, idStyle, e, stderr)
func printBoot(probe claudeteam.TeamStateProbe, w io.Writer, entities []*entity, stages []Stage, definitionDir, entityDir, gitRoot, idStyle string, e env, stderr io.Writer, identify bool) error {
d, err := gatherBoot(probe, entities, stages, definitionDir, entityDir, gitRoot, idStyle, e, stderr, identify)
if err != nil {
return err
}
Expand Down Expand Up @@ -273,12 +299,22 @@ func printBoot(probe claudeteam.TeamStateProbe, w io.Writer, entities []*entity,
}
}

// PR_STATE
// PR_STATE. Identify mode renders the local `pr:` mirror (state "local") under a
// labeled banner so the reader knows the live gh state is filled in at «engage».
switch d.prStatus {
case "none":
fmt.Fprintln(w, "PR_STATE: none")
case "gh not available":
fmt.Fprintln(w, "PR_STATE: gh not available")
case "local":
fmt.Fprintln(w, "PR_STATE (local view — not gh-checked)")
row := func(a, b, c, d string) string {
return padRight(a, 6) + " " + padRight(b, 30) + " " + padRight(c, 8) + " " + d
}
fmt.Fprintln(w, row("ID", "SLUG", "PR", "STATE"))
for _, r := range d.prResults {
fmt.Fprintln(w, row(r.id, r.slug, r.pr, r.state))
}
default:
fmt.Fprintln(w, "PR_STATE")
row := func(a, b, c, d string) string {
Expand Down Expand Up @@ -319,5 +355,18 @@ func printBoot(probe claudeteam.TeamStateProbe, w io.Writer, entities []*entity,

// SANDBOX: appended last so every prior section's order is preserved.
fmt.Fprintf(w, "SANDBOX: %s\n", d.sandbox)

// Identify mode folds discovery + the stage taxonomy in, appended after every
// existing section so their order is untouched.
if d.identify {
fmt.Fprintln(w, "DISCOVERY")
for _, wf := range d.discovery {
fmt.Fprintln(w, wf)
}
fmt.Fprintln(w, "STAGES")
for _, s := range d.stages {
fmt.Fprintln(w, s.Name)
}
}
return nil
}
Loading
Loading