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 .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spacedock",
"version": "0.28.0-pre1",
"version": "0.28.0-pre2",
"description": "Turn directories of markdown files into structured workflows operated by AI agents",
"author": {
"name": "CL Kao"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spacedock",
"version": "0.28.0-pre1",
"version": "0.28.0-pre2",
"description": "Turn directories of markdown files into structured workflows operated by AI agents",
"author": {
"name": "CL Kao"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/runtime-live-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
run: go test ./...

- name: Run deterministic live-harness controls offline
run: go test ./internal/ensigncycle -count=1 -run 'TestCleanupKeepMovingRootRetainsOnlyFailures|TestCodexLiveRunnerExecArgvEnablesMultiAgentV2|TestCodexLiveRunnerUsesSpacedockFrontDoorBeforeHostArgs|TestAssertRecordedGateHoldLogAcceptsPrepareFirstLifecycle|TestShallowBootFixtureContainsOnlyHeldGate|TestPiLiveSmokePromptRequiresExactStageReportHeading|TestPiLiveEnvDropsForeignRuntimeMarkers|TestPiLiveEnvScrubsAmbientPiSubagentMarkers|TestPiIntercomPackageRootDefaultsBesideSubagents'
run: go test ./internal/ensigncycle -count=1 -run 'TestCleanupKeepMovingRootRetainsOnlyFailures|TestCodexLiveRunnerExecArgvEnablesMultiAgentV2|TestCodexLiveRunnerUsesSpacedockFrontDoorBeforeHostArgs|TestAssertRecordedGateHoldLogAcceptsPrepareFirstLifecycle|TestShallowBootFixtureContainsOnlyHeldGate|TestPiLiveEnvDropsForeignRuntimeMarkers|TestPiLiveEnvScrubsAmbientPiSubagentMarkers|TestPiIntercomPackageRootDefaultsBesideSubagents'

# Pull requests normalize to one Sonnet 5 leg. Manual dispatches choose the
# routine Sonnet cadence or the separately approved Opus pre-release cadence.
Expand Down Expand Up @@ -811,7 +811,7 @@ jobs:
if: ${{ !cancelled() }}
run: |
set -o pipefail
gotestsum --jsonfile pi-front-door-smoke-detail.jsonl --format pkgname -- -tags live -count=1 -timeout 15m -run 'TestLivePiFrontDoorSmoke|TestLivePiNonSelfDescribingDispatch' ./internal/ensigncycle
gotestsum --jsonfile pi-front-door-smoke-detail.jsonl --format pkgname -- -tags live -count=1 -timeout 15m -run TestLivePiFrontDoorSmoke ./internal/ensigncycle

- name: Upload live artifacts
if: always()
Expand Down
17 changes: 7 additions & 10 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,10 @@ homebrew_casks:
# release, pinning the per-arch url + sha256 from the release assets +
# checksums.txt — the seam the homebrew-tap entity consumes. `brews:` is
# hard-deprecated at goreleaser v2.16; casks auto-detect the bare `spacedock`
# binary from the archive root, so no install/test stanza is given. Two casks
# ship per release: the STABLE `spacedock` cask (from the stable archive,
# installs the `main` plugin) and the EDGE `spacedock@next` cask (from the edge
# archive, installs the `next` plugin). Each pins ONLY its own channel's archive
# via `ids: [<archive-id>]` so the two casks never cross-pin.
# binary from the archive root, so no install/test stanza is given. The config
# defines two casks, but each tag publishes only its matching channel. A final
# tag publishes `spacedock`. A prerelease tag publishes `spacedock@next`. Each
# cask pins only its channel archive via `ids: [<archive-id>]`.
- name: spacedock
# `auto` skips the cask bump when goreleaser marks the release a pre-release
# (release.prerelease: auto flags a hyphenated tag like v0.23.0-pre), so a
Expand Down Expand Up @@ -174,11 +173,9 @@ homebrew_casks:
# devBranch=next binary that installs the `next` plugin. Its on-demand re-pull
# cadence rides the unchanged next-publish.yml; goreleaser only emits the cask.
- name: spacedock@next
# `false` publishes the edge cask on every tag, including pre-releases, so the
# edge tap tracks `-pre` cuts. This diverges from the stable cask's `auto`
# (which skips on a pre-release) — the edge channel is meant to surface
# pre-release builds, while stable never gets a pre-release.
skip_upload: false
# Skip final tags. Publish only prerelease tags, where `.Prerelease` is not
# empty. This is the inverse of the stable cask's `auto` routing.
skip_upload: '{{ eq .Prerelease "" }}'
ids:
- spacedock-edge
# The archive ships the binary as `spacedock`, so the edge command is also
Expand Down
3 changes: 2 additions & 1 deletion docs/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ The FO contract's Operating Principles and Working Principles govern generic pro

- **No prose-grep over instruction files.** A string, substring, or regex match over an instruction file the model reads (the FO/ensign contract, this README, a skill) never proves a behavioral claim — a valid paraphrase fails it and an inverted clause passes it. To settle a case, ask whether the expected value comes from outside the file under test; if it does not, the check is a tautology and is banned. A check that binds two independent values that can diverge, such as the plugin manifest's version sharing a major.minor with the binary's version, is legitimate and is not prose-grep. Captain ruling (2026-07-20, verbatim): prose-greps are one-off validation evidence, never committed tests. A grep whose output is pasted into the validation report is legitimate external evidence for that run; the same grep committed as a test is banned. A grep soundly establishes presence or absence when that existence fact is itself the claim; it never establishes what a program or an agent actually does — express that claim in a form that can be exercised.
- **Evidence must be able to fail.** Each AC's cited evidence names the concrete change that would flip it — the falsifying edit. A criterion whose author cannot name that change does not count. The gate reads the falsifying change, not a pass count.
- **One proof owner per failure mode.** Reuse or modify an existing behavioral test before adding one. Add another committed check only when a distinct falsifying edit would escape the primary owner; otherwise combine or delete it. Use one-off manual validation for release provenance or external wiring that a committed test cannot reproduce truthfully.
- **Detached adversarial audit (high-stakes surfaces).** Before merging a change to one of four high-stakes surfaces (the front-door launcher, the `status` mutation and guard paths, the shipped contract and scaffolding, and the CI and release machinery), run a read-only audit on a throwaway checkout. It tries to refute the validation by constructing an adversarial edit the deliverable's own tests should catch, then confirms they do. A test that stays green under a claim-breaking edit is a hole. Material findings route back through validation to implementation, and "refuted nothing material" is a valid recorded outcome. The audit also fires on AC provenance: when an AC's expected value is derived from the same package's production functions or constants, run the adversarial-edit check on it. Scope it to that provenance form; the broader equality/byte-identity form over-fires on ordinary unit tests. The two triggers are independent: the four-surface trigger runs the full audit on a throwaway checkout, while the provenance trigger fires wherever such an AC appears — including on a change routine enough to skip the full audit — and covers only that AC's adversarial-edit check, not the whole change.
- **Required CI lanes are a function of the diff, not the FO's read of "relatedness."** Merging on the deterministic lanes (build/install/offline) alone is allowed ONLY when the diff provably touches nothing a live lane loads or drives. When the diff touches a file a live lane exercises — the shipped FO/ensign contract or a host adapter (`skills/**/references/**`), the dispatch/launch path, or the lane's own live tests — that lane is REQUIRED green before merge, and a flake there is grounds to re-run to green (serial, isolated), NEVER to skip, leave its deployment unapproved, or wave off as "the known flake." The path→lane mapping is the gate: a change to the Claude adapter requires `claude-live`; to a host adapter, that host's lane; to the host-neutral dispatch core, every host lane.
- **Instruction-file read quarantine.** Tests do not read prompt or instruction files except in two cases. First, in `internal/contractlint`, and there only for structural checks: reference closure, frontmatter validity, structural absence, and dedup. Second, to extract a shipped runnable block and execute it against independent fixture conditions, where the oracle is the fixture's on-disk state or the block's observed output and never the file's wording — `skills/integration/survey_probe_test.go` is the reference shape. Prose-grep and prose-to-code consistency checks never substitute for running the behavior.
Expand Down Expand Up @@ -148,7 +149,7 @@ A task moves to ideation when a pilot starts fleshing out the idea: clarify the
- At least one AC must MEASURE the end-value the entity exists for, against an independent baseline that can move the wrong way (a number/delta/count/timing, a behavior, or resulting on-disk state). An AC that only asserts its mechanism shipped — "the prose updates to X", "the verb owns Y", "the section is rewritten to Z" — is end-state phrasing of a *means*; it counts only paired with the value-measuring AC it serves (cf. `trim-dispatch-adapter-prose` AC-1: cumulative line delta vs origin/main is NEGATIVE).
- Every task must produce a real, checkable change (code, a fixture, on-disk state, or instruction text whose effect a separate check can confirm). If the task's only output is a decision with nothing shipped, it does not belong in this queue; record the decision in the roadmap instead. Cleanup and overhaul qualify: the change is the new code plus passing tests.
- When the design rests on an unverified mechanism (a parser round-trip, a runtime handoff, an on-disk format, a tool actually supporting a flag), spike the riskiest path first (see Proof policy above) and record the result in the task body. The throwaway exercise seeds the implementation's first test. If nothing is unverified, record "no spike needed: {the proven mechanisms it relies on}".
- Test plans should state what verifies the implementation, estimated cost/complexity, and whether fixture, CLI, or live workflow tests are needed.
- Test plans should name the existing primary proof owner (or explain why none exists), the distinct falsifying edit for each additional check, estimated cost/complexity, and whether deterministic, live, or one-off manual validation is needed.
- Plans should describe intended behavior at the level a future worker or validator needs to reason about it. Prefer observable behavior over implementation internals unless the task is specifically about that internal representation.
- For every new mechanism in the proposed approach or test plan, name the value AC it serves, the simplest alternative considered, and why that alternative is insufficient. An enabling mechanism is not justified by proving its own internals.
- Prove behavior by exercising it and observing the outcome (output bytes, exit code, resulting on-disk state, or a test feeding many inputs and asserting uniform handling): Go unit tests for parser and command behavior, golden fixtures for status output, behavior fixtures that drive the binary for command-level claims, and live workflow smoke tests only when runtime behavior is the claim. See the Proof policy above for what counts as proof and what does not.
Expand Down
5 changes: 3 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ marketplace source.
prerelease's `_stable` tarball — it exists only because the cask pipe cannot
tolerate a skipped build;
- publishes the GitHub Release with those assets;
- bumps BOTH `spacedock-dev/homebrew-tap` casks (`spacedock` stable +
`spacedock@next` edge) via `HOMEBREW_TAP_TOKEN`;
- updates one matching cask in `spacedock-dev/homebrew-tap` via
`HOMEBREW_TAP_TOKEN`: final tags update `spacedock`, and prerelease tags update
`spacedock@next`;
- stamps the plugin manifests' `version` on `main` on a `.0` release only (a
patch tag does not touch `main`), then advances the stable channel ref (see
below).
Expand Down
16 changes: 0 additions & 16 deletions docs/runtime-live-ci-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,22 +313,6 @@ limited to the named runtime boundary.
- **Fixture:** `pi/split-root-smoke` — a current-checkout Pi environment and
split-root workflow with one child-dispatchable member.

### `pi-non-self-describing-dispatch`

- **Entry point:** `TestLivePiNonSelfDescribingDispatch`
- **Lane:** `pi-live`
- **Required outcome:** A Pi worker dispatched with a checklist equal to a real
entity's acceptance criteria (no skill-path, stage-report heading, or
DONE/Summary hints) still writes a complete `## Stage Report: implementation`
with a clean state-checkout commit. The worker's only stage-report format
source is the `### Stage Report format` block the dispatch build artifact
embeds for host=pi. This is the tautology-closing lane: reverting the body
embed makes it RED while the self-describing `pi-front-door-subagent-dispatch`
lane stays green.
- **Fixture:** `pi/non-self-describing-smoke` — a split-root Pi workflow whose
implementation stage-def names only the real work (no stage-report mention),
so the embedded dispatch body block is the worker's only format source.

## Non-gating live experiments

These tests are intentionally not release evidence and are not selected by a
Expand Down
51 changes: 11 additions & 40 deletions internal/cli/gate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,54 +128,25 @@ func TestGatePrepareCLIPrintsExactRoomBindingAndCurrentV1HelpSurface(t *testing.
}

func TestGatePrepareCLIPassesStateRelativeArtifactWithoutCwdJoin(t *testing.T) {
workflow, state, _ := gatePrepareCLIFixture(t)
selected := filepath.Join(state, "selected", "gate-review.md")
if err := os.MkdirAll(filepath.Dir(selected), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(selected, []byte("# Selected review\n"), 0o644); err != nil {
t.Fatal(err)
}
git(t, state, "add", "selected")
git(t, state, "commit", "-q", "-m", "committed selected artifact")

var out, errOut bytes.Buffer
code := run(context.Background(), []string{
"gate", "prepare", "task",
"--question", "Advance?",
"--artifact", filepath.ToSlash(filepath.Join("selected", "gate-review.md")),
"--summary", "state-relative artifact",
"--workflow-dir", workflow,
}, nil, workflow, nil, &out, &errOut, &status.NativeRunner{}, nil)
if code != 0 {
t.Fatalf("prepare exit=%d stdout=%q stderr=%q", code, out.String(), errOut.String())
}
if !strings.Contains(out.String(), "state=open") {
t.Fatalf("prepare stdout=%q want state=open", out.String())
}
}

func TestGatePrepareCLIResolvesLaunchRelativeSelectedSources(t *testing.T) {
for _, flag := range []string{"--artifact", "--reference"} {
t.Run(flag, func(t *testing.T) {
for _, form := range []string{"state-relative", "launch-relative"} {
t.Run(form, func(t *testing.T) {
workflow, state, artifact := gatePrepareCLIFixture(t)
selected := filepath.Join(state, "selected", "review.md")
selected := filepath.Join(state, "selected", "gate-review.md")
if err := os.MkdirAll(filepath.Dir(selected), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, selected, "# Selected review\n")
git(t, state, "add", "selected")
git(t, state, "commit", "-q", "-m", "selected source")
git(t, state, "commit", "-q", "-m", "committed selected artifact")

launchDir := filepath.Dir(filepath.Dir(workflow))
relative := filepath.ToSlash(filepath.Join("docs", "dev", ".state", "selected", "review.md"))
args := []string{"gate", "prepare", "task", "--question", "Advance?", "--artifact", artifact,
"--summary", "launch-relative source", "--workflow-dir", workflow}
if flag == "--artifact" {
args[6] = relative
} else {
args = append(args, "--reference", relative)
launchDir := workflow
artifact = filepath.ToSlash(filepath.Join("selected", "gate-review.md"))
if form == "launch-relative" {
launchDir = filepath.Dir(filepath.Dir(workflow))
artifact = filepath.ToSlash(filepath.Join("docs", "dev", ".state", "selected", "gate-review.md"))
}
args := []string{"gate", "prepare", "task", "--question", "Advance?", "--artifact", artifact,
"--summary", form + " artifact", "--workflow-dir", workflow}
var out, errOut bytes.Buffer
code := run(context.Background(), args, nil, launchDir, nil, &out, &errOut, &status.NativeRunner{}, nil)
if code != 0 || errOut.Len() != 0 || !strings.Contains(out.String(), "state=open") {
Expand Down
Loading