diff --git a/internal/contractlint/boot_resident_closure_test.go b/internal/contractlint/boot_resident_closure_test.go index bc980dd37..662ce9e46 100644 --- a/internal/contractlint/boot_resident_closure_test.go +++ b/internal/contractlint/boot_resident_closure_test.go @@ -37,6 +37,9 @@ var foReferenceCores = map[string][]string{ filepath.Join("skills", "first-officer", "references", "fo-dispatch-core.md"): { "## Dispatch", "## Reuse and Fresh Dispatch", "## Dispatch Adapter", "## Event Loop", }, + filepath.Join("skills", "first-officer", "references", "fo-status-viewer.md"): { + "## Status Viewer", "### Captain-Facing State Display", "## Issue Filing", + }, } // bodyReferenceRe matches a sibling reference read-path named in a contract body diff --git a/internal/ensigncycle/claude_live_runner_test.go b/internal/ensigncycle/claude_live_runner_test.go index 9de849c55..3d290fc68 100644 --- a/internal/ensigncycle/claude_live_runner_test.go +++ b/internal/ensigncycle/claude_live_runner_test.go @@ -312,9 +312,10 @@ func runClaudeFilingScenario(t *testing.T, runner liveDriver, scenario sharedRun // reports MERGED) with a per-run isolated team root, and grades the durable // end-state: the FO greets and presents the gate, S7b advances+archives the merged // PR before-greet, NO team config lands on disk, and NO worker is dispatched. It -// then asserts the AC-2 behavioral signal (no TeamCreate before the greet) and the -// AC-6 measured signal (greet-turn context below the ~60k ceiling, no pre-greet -// ~89k cache_creation spike) over the captured stream. +// then asserts the AC-2 behavioral signals (no TeamCreate before the greet, and no +// pre-greet Read of the deferred fo-status-viewer.md reference) and the AC-6 measured +// signal (greet-turn context below the ~60k ceiling, no pre-greet ~89k +// cache_creation spike) over the captured stream. func runClaudeShallowBootScenario(t *testing.T, runner liveDriver, scenario sharedRuntimeScenario) { t.Helper() workflowRoot := t.TempDir() @@ -338,6 +339,13 @@ func runClaudeShallowBootScenario(t *testing.T, runner liveDriver, scenario shar if err := assertNoTeamCreateBeforeGreet(result.stream); err != nil { t.Fatalf("%v\nArtifacts: %s", err, result.artifactDir) } + // AC-2: the greet reads no deferred Status-Viewer reference. The staged plugin + // ships the real fo-status-viewer.md (livePluginDir copies skills/), so the FO + // COULD read it — this asserts the greet-and-stop boot renders from status --boot + // without loading the deferred display rules. + if err := assertGreetReadsNoDeferredStatusReference(result.stream); err != nil { + t.Fatalf("%v\nArtifacts: %s", err, result.artifactDir) + } // AC-6: the greet-turn context is below the ceiling and no pre-greet 89k // cache_creation spike (measured, over the captured token stream). if err := assertShallowBootMeasured(result.stream); err != nil { diff --git a/internal/ensigncycle/shallow_boot_measure_test.go b/internal/ensigncycle/shallow_boot_measure_test.go index 143bc9b77..13216483e 100644 --- a/internal/ensigncycle/shallow_boot_measure_test.go +++ b/internal/ensigncycle/shallow_boot_measure_test.go @@ -2,10 +2,17 @@ package ensigncycle import ( "fmt" + "strings" "github.com/spacedock-dev/spacedock/internal/journeymetrics" ) +// deferredStatusReference is the lazily-loaded FO reference the Status Viewer + +// Issue Filing sections moved into. A greet-and-stop boot composes its summary from +// «state.boot» JSON + README frontmatter and must NOT read this reference — it loads +// only at the FIRST status query, --set mutation, id lookup, or issue filing. +const deferredStatusReference = "fo-status-viewer.md" + // AC-6 boot-window measurement thresholds. The ceiling is the milestone's ~60k // greet-turn context ceiling; the spike threshold is set below the ~89k team-mode // prefix re-cache (TeamCreate re-caching the whole conversation prefix to the 1h @@ -85,6 +92,37 @@ func assertNoTeamCreateBeforeGreet(stream string) error { return nil } +// assertGreetReadsNoDeferredStatusReference is the AC-2 behavioral oracle over the +// captured stream's tool-call sequence: no pre-greet turn reads the deferred +// Status-Viewer reference (a Read/Grep/Bash-cat naming fo-status-viewer.md in the +// turns up to and including the greet turn). It is a behavioral observation over the +// real run's tool ordering, NOT a contract grep — the sibling of +// assertNoTeamCreateBeforeGreet. The greet must compose its summary from +// status --boot + README frontmatter; a regression that re-inlined the Status-Viewer +// display rules into the greet path, or a boot that eagerly loaded the deferred +// reference, surfaces a pre-greet read of fo-status-viewer.md and fails this. +func assertGreetReadsNoDeferredStatusReference(stream string) error { + turns, err := journeymetrics.ParseClaudeTurns([]byte(stream)) + if err != nil { + return fmt.Errorf("parse stream for AC-2 deferred-reference check: %w", err) + } + if len(turns) == 0 { + return fmt.Errorf("stream carried no assistant turns — nothing to check") + } + greet := greetTurnIndex(turns) + if greet < 0 { + return fmt.Errorf("every assistant turn dispatched — no greet turn produced") + } + for i := 0; i <= greet; i++ { + for _, target := range turns[i].ReadTargets { + if strings.Contains(target, deferredStatusReference) { + return fmt.Errorf("pre-greet turn %d read the deferred Status-Viewer reference (%s) via %q — the greet must render from status --boot, not the deferred display rules", i, deferredStatusReference, target) + } + } + } + return nil +} + // assertShallowBootMeasured is the AC-6 measured-saving oracle over a captured // claude-stream.jsonl: it parses the stream per turn, identifies the greet turn and // the pre-greet window (turns up to and including the greet turn), and asserts diff --git a/internal/ensigncycle/shallow_boot_measure_unit_test.go b/internal/ensigncycle/shallow_boot_measure_unit_test.go index c5949b2ba..d76515375 100644 --- a/internal/ensigncycle/shallow_boot_measure_unit_test.go +++ b/internal/ensigncycle/shallow_boot_measure_unit_test.go @@ -70,6 +70,37 @@ func TestAssertNoTeamCreateBeforeGreetCatchesLaterDeltaTeamCreate(t *testing.T) } } +// TestAssertGreetReadsNoDeferredStatusReferenceOffline validates the AC-2 +// deferred-reference oracle against committed MULTI-DELTA streams: the shallow-boot +// positive (the greet renders from status --boot with zero Read of fo-status-viewer.md) +// passes; the NEW negative (a pre-greet Read of fo-status-viewer.md, on a LATER delta +// of its message) fails. Multi-delta is mandatory — the Read lands on a later delta, +// so a first-delta-only parse would FALSE-PASS (the documented hollow-AC defect). +func TestAssertGreetReadsNoDeferredStatusReferenceOffline(t *testing.T) { + if err := assertGreetReadsNoDeferredStatusReference(readMeasureFixture(t, "shallow-boot-greet.stream.jsonl")); err != nil { + t.Fatalf("shallow-boot positive fixture (renders from status --boot, no deferred-reference Read) must pass AC-2: %v", err) + } + if err := assertGreetReadsNoDeferredStatusReference(readMeasureFixture(t, "greet-reads-status-viewer.stream.jsonl")); err == nil { + t.Fatal("the negative fixture (multi-delta pre-greet Read of fo-status-viewer.md) must FAIL AC-2 — the Read lands on a later delta, so a first-delta-only parse would false-pass") + } +} + +// TestAssertGreetReadsNoDeferredStatusReferenceCatchesLaterDeltaRead is the AC-2 +// positive control over the parser extension: a stream whose Read of +// fo-status-viewer.md is on a LATER delta of its message (the real runner shape, NOT +// a synthetic single-delta one) must make assertGreetReadsNoDeferredStatusReference +// RED. A first-delta-only parse drops the later-delta Read entirely — the read target +// would be invisible and the greet-independence proof hollow. +func TestAssertGreetReadsNoDeferredStatusReferenceCatchesLaterDeltaRead(t *testing.T) { + // msg_read: thinking on delta[0], Read of fo-status-viewer.md on delta[1]; then a text greet. + stream := `{"type":"assistant","message":{"id":"msg_read","model":"claude-opus-4-8","usage":{"input_tokens":8,"cache_read_input_tokens":21000,"cache_creation_input_tokens":600},"content":[{"type":"thinking","thinking":"load the status viewer rules"}]}} +{"type":"assistant","message":{"id":"msg_read","model":"claude-opus-4-8","usage":{"input_tokens":8,"cache_read_input_tokens":21000,"cache_creation_input_tokens":600},"content":[{"type":"tool_use","id":"toolu_read","name":"Read","input":{"file_path":"skills/first-officer/references/fo-status-viewer.md"}}]}} +{"type":"assistant","message":{"id":"msg_greet","model":"claude-opus-4-8","usage":{"input_tokens":100,"cache_read_input_tokens":5000,"cache_creation_input_tokens":0},"content":[{"type":"text","text":"Workflow overview: ... Gate review: ... Decision: approve or reject?"}]}}` + if err := assertGreetReadsNoDeferredStatusReference(stream); err == nil { + t.Fatal("a pre-greet Read of fo-status-viewer.md on a LATER delta must make assertGreetReadsNoDeferredStatusReference RED — the parser must merge later-delta read targets, not read only the first delta") + } +} + // TestParserExtractsTeamCallsFromRealHangCapture is the validator-named ready // oracle: the committed real-runner stream `sonnet_teamdelete_hang.stream.jsonl` // (20/27 message ids multi-delta; its lone TeamCreate and TeamDelete each land on a diff --git a/internal/ensigncycle/testdata/greet-reads-status-viewer.stream.jsonl b/internal/ensigncycle/testdata/greet-reads-status-viewer.stream.jsonl new file mode 100644 index 000000000..a40502b87 --- /dev/null +++ b/internal/ensigncycle/testdata/greet-reads-status-viewer.stream.jsonl @@ -0,0 +1,9 @@ +{"type":"system","subtype":"init","model":"claude-opus-4-8"} +{"type":"assistant","message":{"id":"msg_boot1","model":"claude-opus-4-8","usage":{"input_tokens":4,"output_tokens":12,"cache_creation_input_tokens":1200,"cache_read_input_tokens":8000},"content":[{"type":"thinking","thinking":"check the version"}]}} +{"type":"assistant","message":{"id":"msg_boot1","model":"claude-opus-4-8","usage":{"input_tokens":4,"output_tokens":12,"cache_creation_input_tokens":1200,"cache_read_input_tokens":8000},"content":[{"type":"tool_use","id":"toolu_ver","name":"Bash","input":{"command":"spacedock --version"}}]}} +{"type":"assistant","message":{"id":"msg_boot2","model":"claude-opus-4-8","usage":{"input_tokens":6,"output_tokens":40,"cache_creation_input_tokens":900,"cache_read_input_tokens":18000},"content":[{"type":"thinking","thinking":"read the boot state"}]}} +{"type":"assistant","message":{"id":"msg_boot2","model":"claude-opus-4-8","usage":{"input_tokens":6,"output_tokens":40,"cache_creation_input_tokens":900,"cache_read_input_tokens":18000},"content":[{"type":"tool_use","id":"toolu_boot","name":"Bash","input":{"command":"spacedock status --boot --json"}}]}} +{"type":"assistant","message":{"id":"msg_read","model":"claude-opus-4-8","usage":{"input_tokens":8,"output_tokens":20,"cache_creation_input_tokens":600,"cache_read_input_tokens":21000},"content":[{"type":"thinking","thinking":"let me load the status viewer display rules before greeting"}]}} +{"type":"assistant","message":{"id":"msg_read","model":"claude-opus-4-8","usage":{"input_tokens":8,"output_tokens":20,"cache_creation_input_tokens":600,"cache_read_input_tokens":21000},"content":[{"type":"tool_use","id":"toolu_read","name":"Read","input":{"file_path":"/staged/skills/first-officer/references/fo-status-viewer.md"}}]}} +{"type":"assistant","message":{"id":"msg_greet","model":"claude-opus-4-8","usage":{"input_tokens":120,"output_tokens":300,"cache_creation_input_tokens":400,"cache_read_input_tokens":42000},"content":[{"type":"text","text":"Workflow overview: 1 task at the review gate. Gate review: ... Decision: approve or reject?"}]}} +{"type":"result","subtype":"success","usage":{"input_tokens":130,"output_tokens":352,"cache_creation_input_tokens":2500,"cache_read_input_tokens":68000},"total_cost_usd":0.04,"result":"Workflow overview: 1 task at the review gate. Gate review: ... Decision: approve or reject?"} diff --git a/internal/journeymetrics/claude.go b/internal/journeymetrics/claude.go index 1e653cc49..19c158537 100644 --- a/internal/journeymetrics/claude.go +++ b/internal/journeymetrics/claude.go @@ -140,6 +140,10 @@ type ClaudeTurn struct { // turn that dispatches a worker carries an "Agent" (or "TeamCreate") name, so a // caller can split the transcript at the first dispatch turn. ToolNames []string + // ReadTargets is the file paths / commands of this turn's read-like tool_use + // blocks (Read's file_path, Grep's path, Bash's command), so a caller can detect + // whether a turn read a specific file before some boundary turn. + ReadTargets []string } // Context returns this turn's context-window size as the boot analysis defines it: @@ -191,6 +195,7 @@ func ParseClaudeTurns(data []byte) ([]ClaudeTurn, error) { id = fmt.Sprintf("line-%d", lineNo) } var names []string + var readTargets []string for _, block := range msg.Content { if block.Type != "tool_use" { continue @@ -201,16 +206,20 @@ func ParseClaudeTurns(data []byte) ([]ClaudeTurn, error) { } seenTool[toolKey] = true names = append(names, block.Name) + if target := readToolTarget(block.Name, block.Input); target != "" { + readTargets = append(readTargets, target) + } } if pos, ok := index[id]; ok { // A later delta of a message already seen: merge its NEW tool_use names // (the per-block dedup above keeps a repeated delta from double-counting). // Usage is identical across deltas, so the first-delta usage is kept. turns[pos].ToolNames = append(turns[pos].ToolNames, names...) + turns[pos].ReadTargets = append(turns[pos].ReadTargets, readTargets...) continue } index[id] = len(turns) - turns = append(turns, ClaudeTurn{ID: id, Usage: msg.Usage, ToolNames: names}) + turns = append(turns, ClaudeTurn{ID: id, Usage: msg.Usage, ToolNames: names, ReadTargets: readTargets}) } if err := scanner.Err(); err != nil { return nil, err diff --git a/internal/journeymetrics/readadoption.go b/internal/journeymetrics/readadoption.go index 09ec496af..dd4f48e2b 100644 --- a/internal/journeymetrics/readadoption.go +++ b/internal/journeymetrics/readadoption.go @@ -113,6 +113,36 @@ func readInputIsScoped(input json.RawMessage) bool { return scope.Offset != 0 || scope.Limit != 0 } +// readToolTarget returns the file path or command a read-like tool_use names, so a +// caller can detect whether a turn touched a specific file before some boundary +// turn. Read names file_path, Grep names path, and Bash (a `cat`/`grep` of a file +// included) names command. A non-read tool, or a tool whose input lacks the field, +// returns "". +func readToolTarget(name string, input json.RawMessage) string { + switch name { + case "Read": + return jsonStringField(input, "file_path") + case "Grep": + return jsonStringField(input, "path") + case "Bash": + return bashCommand(input) + } + return "" +} + +// jsonStringField decodes one named string field from a tool_use input object, +// returning "" when the input is empty, malformed, or lacks the field. +func jsonStringField(input json.RawMessage, field string) string { + if len(input) == 0 { + return "" + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(input, &obj); err != nil { + return "" + } + return rawString(obj[field]) +} + // bashCommand extracts the command string from a Bash tool_use input. func bashCommand(input json.RawMessage) string { if len(input) == 0 { diff --git a/skills/first-officer/references/first-officer-shared-core.md b/skills/first-officer/references/first-officer-shared-core.md index 578ca205d..2b0de77ae 100644 --- a/skills/first-officer/references/first-officer-shared-core.md +++ b/skills/first-officer/references/first-officer-shared-core.md @@ -32,42 +32,11 @@ Shared first-officer semantics — the boot-resident core. The dispatch and merg - **Headless:** do NOT greet-stop — drive every dispatchable entity through the event loop to its first `gate: true` stage or to terminal/blocked, then EXIT reporting each entity's stop reason. Stop AT gates (a gate is human-owned); do not resolve them. **When the stop reason is a `gate: true` stage, the FO MUST author the FULL gate review at that stop, for EACH gate, BEFORE exiting** — invoke `Skill(skill="spacedock:present-gate")` and render its complete template (the `Gate review:` heading, the chosen-direction prose, the checklist roll-up, and the `Decision:` prompt) per `## Completion and Gates`, as the interactive path does. A terse stop-reason line is NOT sufficient: the human who picks up the headless transcript decides from the authored `Gate review:` … `Decision:` content. The FO still does NOT resolve the gate headless (no verdict, no terminalize) — it presents and stops; only "given the conn" (below) resolves. - **Headless + given the conn to auto-approve (prose):** additionally resolve gates **per `## Completion and Gates`** and drive to terminal. The grant must be a phrase you can QUOTE from the prompt ("auto-approve gates" / "drive to done" / "you have the conn", per `skills/commission/SKILL.md`); a bare "Drive the workflow" is NOT a grant — present and stop. -## Status Viewer +## Status Viewer and Issue Filing (deferred module) -The `${SPACEDOCK_BIN:-spacedock} status` launcher owns path resolution and mutation guards; skill instructions stay declarative and never reference a plugin-private script path. - -Invoke it as: -``` -${SPACEDOCK_BIN:-spacedock} status --workflow-dir {workflow_dir} [--next-id|--next|--archived|--where ...|--boot|--validate|--resolve REF] -``` - -- `--boot` — startup roll-up (mods, ID style, next-ID candidate, orphans, PR state, dispatchables). Incompatible with `--next`, `--next-id`, `--archived`, `--where`. -- `--validate` — run before trusting manually edited workflow state. -- `--resolve REF` — deterministic lookup by slug, exact stored ID, or sd-b32 address prefix; `--root` rejects unqualified cross-workflow ambiguity rather than guessing. -- `--next-id` — preview the next-id candidate for `sequential` and `sd-b32` (n/a for `slug`). For `sd-b32`, pass `--id-seed "{slug-or-title}"` and optionally `--id-actor "{actor-or-agent}"` so creation context enters the candidate. To file a new entity, do NOT pair `--next-id` with a hand-written file — use `spacedock new` (see FO Write Scope), which mints the id and atomically writes the stamped entity in one call. `--next-id` is candidate-preview only. -- `--next` / `--where "pr !="` — targeted event-loop queries. - -The `--set` flag updates entity frontmatter fields: -- `--set {slug} field=value` sets a field -- `--set {slug} field=` clears a field -- `--set {slug} started` or `completed` auto-fills a UTC ISO 8601 timestamp (skipped if already set) - -### Captain-Facing State Display - -The commissioned README directs the captain to dispatch the FO to inspect workflow state. Invoke `status` for captain-facing display on questions like: - -- "what's the workflow state?" / "show me the workflow" / "what's going on?" -- "what's dispatchable?" / "what's ready?" / "what's next?" -- "what's archived?" / "show me the done entities" -- any ad-hoc question a `status` view answers (a single entity, entities in a stage, PR-pending). - -**Canonical invocations** (all start with `${SPACEDOCK_BIN:-spacedock} status --workflow-dir {workflow_dir}`): -- Overview: no extra flags. -- Dispatchables: `--next`. -- Archive view: `--archived`. -- Single-entity: `--resolve {ref}` then `--where slug={resolved-slug}`. - -**Output rendering guidance.** Forward `status` stdout verbatim inside a fenced code block, with a one-line preface naming the request ("Workflow overview:", "Dispatchable entities:", "Archived entities:"). On empty results, render a literal note ("No dispatchable entities right now.") instead of an empty fence. Do not paraphrase rows, omit columns, invent fields, summarize counts, or editorialize. +- → **reference**: `references/fo-status-viewer.md` (host-neutral; no per-host adapter), loaded at the FIRST status query, `--set` mutation, `--next-id`/`--resolve` lookup, or GitHub-issue filing. +- **done-when:** the FO answers an ad-hoc status question, mutates frontmatter via `--set`, previews/resolves an id, or files a GitHub issue — the `status` query/mutate command surface (flag docs, the canonical captain-facing invocations, the Captain-Facing State Display rendering) and the issue-filing approval gate are resident. +- **guard:** a greet-and-stop boot reads neither — the greet composes its summary from `«state.boot»` JSON + README frontmatter (Startup step 8) and presents any ready gate via `present-gate`; no boot-resident path reaches the Status-Viewer display rules. ## ID Styles @@ -228,7 +197,3 @@ Don't ask permission for a step the contract already allows (the reversible-work - When checking whether tool X supports Y, read X's schema via ToolSearch before grepping for callers — usage presence is not existence evidence. - Prefer Grep over Read for targeted entity-body inspection; Read whole only when you need the full text. Anchor on a heading or field name (`## Stage Report`, `### Feedback Cycles`, a frontmatter field): `grep -n` gives the heading line (the section offset) and the next heading bounds its span, so the follow-up `Read(offset, limit)` is section-scoped. `status --read --json` is the fence-safe fallback when markdown-like fenced content makes grep over-count headings. - On Claude Code, a `Read` followed by a Bash mutation of the same file (including `status --set`) triggers the file-staleness safety net, echoing the file back as cache-write tokens. Grep does not participate. Trust `status --set` stdout (`field: old -> new`, `field: old -> ` for clear-to-empty, `field: -> {timestamp}` for bare-timestamp auto-fill) to narrate mutations without re-reading. - -## Issue Filing - -Do not file GitHub issues without explicit human approval. diff --git a/skills/first-officer/references/fo-status-viewer.md b/skills/first-officer/references/fo-status-viewer.md new file mode 100644 index 000000000..37eb7bf25 --- /dev/null +++ b/skills/first-officer/references/fo-status-viewer.md @@ -0,0 +1,44 @@ +# First Officer Status Viewer (host-neutral) + +The `status` query/mutate command surface — the launcher invocation, the full query/mutate flag docs, the `--set` field docs, the canonical captain-facing invocations, and the Captain-Facing State Display rendering — plus the GitHub-issue-filing approval gate. Lazily loaded (named by the boot-resident core) at the FIRST ad-hoc status question, `--set` mutation, `--next-id`/`--resolve` lookup, or issue filing; a greet-and-stop boot never reads it. Host-neutral: pure `status`-command and filing reference, identical on Claude, Codex, and Pi, with no per-host adapter. + +## Status Viewer + +The `${SPACEDOCK_BIN:-spacedock} status` launcher owns path resolution and mutation guards; skill instructions stay declarative and never reference a plugin-private script path. + +Invoke it as: +``` +${SPACEDOCK_BIN:-spacedock} status --workflow-dir {workflow_dir} [--next-id|--next|--archived|--where ...|--boot|--validate|--resolve REF] +``` + +- `--boot` — startup roll-up (mods, ID style, next-ID candidate, orphans, PR state, dispatchables). Incompatible with `--next`, `--next-id`, `--archived`, `--where`. +- `--validate` — run before trusting manually edited workflow state. +- `--resolve REF` — deterministic lookup by slug, exact stored ID, or sd-b32 address prefix; `--root` rejects unqualified cross-workflow ambiguity rather than guessing. +- `--next-id` — preview the next-id candidate for `sequential` and `sd-b32` (n/a for `slug`). For `sd-b32`, pass `--id-seed "{slug-or-title}"` and optionally `--id-actor "{actor-or-agent}"` so creation context enters the candidate. To file a new entity, do NOT pair `--next-id` with a hand-written file — use `spacedock new` (see FO Write Scope), which mints the id and atomically writes the stamped entity in one call. `--next-id` is candidate-preview only. +- `--next` / `--where "pr !="` — targeted event-loop queries. + +The `--set` flag updates entity frontmatter fields: +- `--set {slug} field=value` sets a field +- `--set {slug} field=` clears a field +- `--set {slug} started` or `completed` auto-fills a UTC ISO 8601 timestamp (skipped if already set) + +### Captain-Facing State Display + +The commissioned README directs the captain to dispatch the FO to inspect workflow state. Invoke `status` for captain-facing display on questions like: + +- "what's the workflow state?" / "show me the workflow" / "what's going on?" +- "what's dispatchable?" / "what's ready?" / "what's next?" +- "what's archived?" / "show me the done entities" +- any ad-hoc question a `status` view answers (a single entity, entities in a stage, PR-pending). + +**Canonical invocations** (all start with `${SPACEDOCK_BIN:-spacedock} status --workflow-dir {workflow_dir}`): +- Overview: no extra flags. +- Dispatchables: `--next`. +- Archive view: `--archived`. +- Single-entity: `--resolve {ref}` then `--where slug={resolved-slug}`. + +**Output rendering guidance.** Forward `status` stdout verbatim inside a fenced code block, with a one-line preface naming the request ("Workflow overview:", "Dispatchable entities:", "Archived entities:"). On empty results, render a literal note ("No dispatchable entities right now.") instead of an empty fence. Do not paraphrase rows, omit columns, invent fields, summarize counts, or editorialize. + +## Issue Filing + +Do not file GitHub issues without explicit human approval.