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
147 changes: 24 additions & 123 deletions docs/apply-archive.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,31 @@

These two commands are the enforcement surface. `apply` is the gate an agent
must clear before writing code; `archive` is the verified ship step. Both are
deterministic and both are documented here as a user-facing contract — the
generated skills only ever tell the agent to run the command and obey its exit
code.

## Exit codes

| code | meaning |
| ---- | ----------------------------------------------------------------------------------------------- |
| `0` | success (including "nothing to do") |
| `1` | failure — validation errors, verification failure, unknown item, parse error, drift/usage error |
| `2` | blocked (`apply` only) — missing required artifacts **or** unchecked hard blockers |
| `3` | soft-blocked (`apply` only) — unconfirmed soft blockers; re-run with `--allow-soft` |

## `cospec apply <change> [--allow-soft] [--json]`

1. Resolve the change (unknown → exit 1 with a fuzzy suggestion) and its schema.
A legacy schema delegates `openspec instructions apply --json` verbatim with
no gate and an INFO note.
2. Run full validation in fast mode. Errors → exit 1 with the report.
3. Compute missing artifacts from `schema.apply.requires`. If any are missing →
print them, set JSON `gate.reason: "missing-artifacts"`, exit 2.
4. **The blocker gate** (deterministic):
- Parse `blocking-changes.md` (a parse error → exit 1).
- Build the archive index from `openspec/changes/archive/` dirs matching
`^(\d{4}-\d{2}-\d{2})-(.+)$` (duplicate slug → keep the latest date, warn).
- **Self-heal**: for each unchecked entry whose slug is in the archive index,
rewrite `[ ]` → `[x]`, append `*(archived <date>)*` if absent, normalize
the separator to an em-dash, and record it in `gate.synced`. Atomic write.
- Remaining unchecked **Blocked by** entries are hard blockers → print each
(noting whether the slug is an active change), set
`gate.reason: "hard-blockers"`, exit 2.
- Remaining unchecked **Soft-blocked by** entries: without `--allow-soft`,
print each and exit 3; with `--allow-soft`, record `gate.softAcknowledged`.
5. Fetch `openspec instructions apply --json` (exit and JSON shape checked).
6. Emit merged output and exit 0:

```json
{
"change": "add-widget",
"type": "feat",
"gate": {
"state": "clear",
"hardBlockers": [],
"softAcknowledged": [],
"synced": []
},
"apply": {
"state": "...",
"contextFiles": [],
"progress": {},
"tasks": [],
"instruction": "..."
}
}
```

The gate is enforced twice: here as an exit code an agent cannot rationalize
past, and in schema prose that says only "run this command and obey its exit
code." Dangling slugs cannot false-pass — `blockers/dangling-ref` fails
validation in step 2.

## `cospec archive <change> [-y] [--skip-specs] [--force-incomplete] [--json]`

### Pre-flight

1. Resolve the change and schema (a legacy schema still runs steps 5–11; step 2
delegates validation).
2. Run **full** validation, including the archive-precondition family unless
`--skip-specs`. Errors → exit 1. This makes step 9's abort detection a
should-never-fire invariant, not the primary defense.
3. **Tasks gate**: parse `tasks.md`. Unchecked tasks and no `--force-incomplete`
→ exit 1 listing them. This is stricter than OpenSpec by design: `-y` alone
does not waive incomplete tasks, so automation passing `-y` cannot skip work.
4. **Self-blocker sanity**: unchecked hard blockers in this change's own file →
a WARNING (not fatal — aborted or superseded work gets archived too).
5. **Collision pre-check**: an existing `archive/` dir matching
`^\d{4}-\d{2}-\d{2}-<name>$` with today's date → exit 1 before delegating.
6. **Skip-specs decision**: pass `--skip-specs` to OpenSpec when the user passed
it, the schema declares no specs artifact, or no `specs/**/spec.md` files
exist. Light types therefore never enter the delta-merge path.
7. **Snapshot**: the archive dir basenames and, if merging, the parsed delta ops
per capability.

### Execute

8. Spawn `openspec archive <name> -y [--skip-specs] --no-color`; capture stdout,
stderr, and exit code.

### Verify (the archive verifier)

9. Compute the new archive dirs. The target is the unique new dir matching
`/^\d{4}-\d{2}-\d{2}-<name>$/` — **date-agnostic**, so it survives a midnight
rollover. Success requires all of: exit code 0; stdout matching neither
`Aborted` nor `Archive cancelled`; the source change directory gone; and the
target plus its `.openspec.yaml` present. On failure:
- clean abort (source not moved, no new dirs) → print the honest message plus
OpenSpec's captured output verbatim, indented, and the `--skip-specs`
remedy;
- half-state (moved without target, or target without moved) → print exactly
which invariant broke and instruct manual inspection.
- exit 1.
10. **Post-merge spot-check** (skipped when `--skip-specs` / no deltas): for
each snapshotted op, verify the living spec — ADDED present, REMOVED absent,
RENAMED to-name present and from-name absent, MODIFIED present. Any miss →
exit 1 with a "spec merge verification failed" message naming the misses.

### Post

11. Run `sync-blockers` in fix mode across all remaining active changes,
collecting which entries were checked and which changes became fully
unblocked.
12. Print the flywheel summary and exit 0:

```
Archived: add-widget (feat) → openspec/changes/archive/2026-07-03-add-widget/
Specs: +2 ~1 -0 →0 applied and verified
Blockers: checked off in 1 change(s): add-dashboard
Now unblocked: add-dashboard → next: cospec apply add-dashboard
```
deterministic — the generated skills only ever tell the agent to run the command
and obey its exit code.

The full user-facing contract — the exit-code table, the step-by-step order
`apply` and `archive` run in, the `--json` shapes, and the two hard archive
gates — is owned by the site:
[Apply and archive](https://cospec.aligned.team/concepts/apply-and-archive).
Read that page for "what happens when I run this command"; this page covers what
isn't there.

## Why the gate is enforced twice

The `apply` gate is enforced in two places that never get out of sync by
construction: as an exit code an agent cannot rationalize past, and in the
schema instruction prose, which says only "run this command and obey its exit
code" — never a paraphrase of the gate logic an agent could talk itself past.
Dangling blocker slugs cannot false-pass either: `blockers/dangling-ref` fails
validation before the gate is even evaluated (see
[validation.md](validation.md)).

## Blocker sync

`cospec sync-blockers [--check] [--change <id>] [--json]` is the standalone form
of archive step 11 and is wired into the pre-commit hook (a fix/check pair). See
[blocking-changes.md](blocking-changes.md) for the STALE / DANGLING /
MANUAL-CHECK / FORMAT classes and the exit rules. `fix` is the default mode; fix
idempotence (`fix(fix(x)) == fix(x)`) is a tested property.
of `archive`'s final step, and is wired into the pre-commit hook as a fix/check
pair. The single parser both `apply`'s blocker gate and `sync-blockers` share
lives in `core/blockers.ts` — see [blocking-changes.md](blocking-changes.md) for
its grammar. `fix` is the default mode; fix idempotence
(`fix(fix(x)) == fix(x)`) is a tested property.
36 changes: 10 additions & 26 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,32 +99,16 @@ commands use.
## The failure modes cospec defends against

cospec exists because three OpenSpec behaviors are hazardous when an agent is
driving.

### 1. `openspec validate` false-errors on schemas without deltas

OpenSpec has a hardcoded `CHANGE_NO_DELTAS` rule: a change with no spec deltas
fails validation. But a `ci` or `docs` change legitimately has no deltas. cospec
runs its own rule families over every change and only delegates to
`openspec validate` for changes whose schema declares a `specs` artifact and
that actually have delta files — so the one suppressed check is suppressed only
where satisfying it is definitionally wrong. See [validation.md](validation.md).

### 2. `openspec archive` exits 0 but silently aborts

When a delta cannot merge (a MODIFIED target that does not exist, a zero-op
delta), OpenSpec prints `Aborted` and **exits 0 without moving the change**. An
agent trusting the exit code would believe the change shipped. cospec's archive
verifier (see [apply-archive.md](apply-archive.md)) checks the filesystem
directly — the change directory must be gone and a dated archive entry must
exist — and reports the abort honestly. Archive preconditions are also checked
at validate time, moving the failure left.

`openspec archive` also exits 0 while silently **thinning** a spec: a MODIFIED
delta that drops `#### Scenario:` entries merges cleanly with no complaint.
cospec closes this with `archive/scenario-preservation` (below).

### 2a. The archive gate ordering (two hard pre-delegation steps)
driving — `openspec validate` false-erroring on schemas without deltas,
`openspec archive` exiting 0 while silently aborting or thinning a spec, and
OpenSpec's generated files referencing skills it never generates. The
user-facing account of all three, and why each matters, is owned by the site:
[How cospec relates to OpenSpec](https://cospec.aligned.team/concepts/how-it-relates-to-openspec).
What follows is the implementation detail behind the two archive-time defenses —
the exact gate ordering and the dangling-reference guard — which the site
intentionally doesn't carry.

### The archive gate ordering (two hard pre-delegation steps)

`cospec archive`'s steps are, in order: fast-validate → tasks gate →
**`archive/verification-incomplete`** → self-blocker sanity warning → collision
Expand Down
67 changes: 10 additions & 57 deletions docs/blocking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,12 @@ must ship before this one. It is machine-parsed: the `apply` gate reads it, and
`sync-blockers` keeps it current as dependencies archive. One parser
(`core/blockers.ts`) serves validate, apply, archive, and sync.

## The template

Every type ships the same template (the instructions differ by weight, the
format does not):

```markdown
# Dependencies

## Blocked by

<!-- Changes that MUST be archived before this change can be applied. -->
<!-- Format: - [ ] `change-slug` — what it provides -->
<!-- cospec checks the box and appends *(archived YYYY-MM-DD)* when the dependency ships. -->

None.

## Soft-blocked by

<!-- Changes that improve this one but aren't strictly required. -->
<!-- Format: - [ ] `change-slug` — what degrades without it -->

None.
```
The user-facing account — the template, hard vs. soft sections, how `apply`
gates on it, and the STALE/DANGLING/MANUAL-CHECK/FORMAT sync diagnostics — is
owned by the site:
[Blocking changes](https://cospec.aligned.team/concepts/blocking-changes). This
page keeps the exact machine grammar `core/blockers.ts` implements, since the
site describes it in prose rather than as a parseable spec.

## The grammar

Expand Down Expand Up @@ -57,49 +40,19 @@ Two sections are machine-gated. Extra sections (`## Phase Gates`,
- **Outside** the two gated sections everything is ignored by the gate, but
backticked-slug bullets missing a checkbox are linted (WARNING).

Examples:
## Sync internals

```markdown
## Blocked by

- [ ] `add-auth` — the session token this endpoint reads
- [x] `add-db-pool` — the connection pool _(archived 2026-06-30)_

## Soft-blocked by

None.
```

## Sync semantics

`cospec sync-blockers [--check] [--change <id>] [--json]` — `fix` is the
default.
`cospec sync-blockers [--check] [--change <id>] [--json]` (`fix` is the
default):

1. Build the archive index (`archive/` dirs → slug → date; duplicate slug →
latest date + warning; non-matching dirs → warning, ignored).
2. Build the active index (dirs under `changes/` except `archive/`).
3. For each active change with a `blocking-changes.md`, classify each entry:
- **STALE** — unchecked, target archived. `fix`: rewrite to the canonical
checked form with `*(archived <date>)*`, atomic write. `--check`: report.
- **DANGLING** — unchecked, target neither archived nor active. Error; never
auto-fixed.
- **MANUAL-CHECK** — checked but target not archived. Warning (manual
check-off is allowed, just surfaced).
- **FORMAT** — a backticked-slug bullet without a checkbox in a gated
section. Error. Non-canonical separators are normalized in fix mode.
3. Classify each entry per the site's diagnostic classes, then fix or report.
4. Report changes that are now fully unblocked (all Blocked-by checked, or
`None.`).
5. Exit: `--check` → 1 if any STALE / DANGLING / FORMAT; `fix` → 1 only if
DANGLING / FORMAT remain.

This runs standalone, as `cospec archive`'s post step across all remaining
changes, and in the pre-commit hook as a fix/check pair.

## How apply uses it

`cospec apply` self-heals the ledger before gating: any unchecked entry whose
slug is already archived is checked off in place (and reported in
`gate.synced`), so a just-shipped dependency never blocks the next change on a
stale box. What remains unchecked under **Blocked by** is a hard block (exit 2);
what remains under **Soft-blocked by** is a soft block (exit 3 without
`--allow-soft`). See [apply-archive.md](apply-archive.md).
36 changes: 9 additions & 27 deletions docs/harness-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,11 @@ in `canon/workflows/*.md` and rendered per harness. This is cospec's full opsx
below). cospec has no core/custom profile split: it always emits the full eleven
to every configured harness.

```
Claude Code:
.claude/commands/cospec/{propose,new,continue,ff,apply,verify,archive,bulk-archive,sync-specs,explore,onboard}.md
.claude/skills/cospec-{propose,new-change,continue-change,ff-change,apply-change,verify-change,archive-change,bulk-archive-change,sync-specs,explore,onboard}/SKILL.md
.claude/settings.json # additive permissions merge

Codex (project-level only):
.codex/skills/cospec-{same 11}/SKILL.md
.codex/rules/cospec.rules # pre-approves read-only + gate cospec calls

OpenCode:
.opencode/commands/cospec-{eleven}.md # /cospec-propose … — FULL bodies, work with .claude absent
.opencode/skills/cospec-{same 11}/SKILL.md
```

Slash syntax is substituted per harness (`/cospec:x` ↔ `/cospec-x`). Every body
calls **only** `cospec` commands — never bare `openspec` — so one permission
entry covers the whole loop.
The exact per-harness file tree each `cospec init` writes, the slash-syntax
substitution, the restart/reload notes, and the smoke-test checklist are owned
by the site: [Harness setup](https://cospec.aligned.team/guide/harness-setup).
This page covers what each generated workflow body actually does and the canon
internals behind it — content the site intentionally keeps at a higher level.

## What each workflow does

Expand Down Expand Up @@ -165,12 +152,7 @@ merged entry. If it does not parse, cospec prints the snippet and skips.
## Per-harness smoke checklist

Codex and OpenCode project-level skill loading is inferred from real repos, not
vendor docs. All enforcement lives in the CLI, so a half-loaded skill still
cannot bypass a gate — but confirm loading manually after `init`:

1. **Claude Code** — restart; `/cospec:propose` appears in the command list;
`Bash(cospec *)` is in `.claude/settings.json`.
2. **Codex** — start a session; the `cospec-*` skills are listed; a read-only
`cospec status` runs without an approval prompt (`archive` still prompts).
3. **OpenCode** — reload; `/cospec-propose` runs and drives the loop even with
`.claude/` absent (OpenCode bodies are full, not pointers).
vendor docs — that inference is why this note exists here rather than only on
the site. All enforcement lives in the CLI, so a half-loaded skill still cannot
bypass a gate. The actual checklist to run after `init` is owned by the site:
[Harness setup](https://cospec.aligned.team/guide/harness-setup#smoke-checks).
Loading