fix(apply): fail with exit 1 when apply instructions are blocked#1250
fix(apply): fail with exit 1 when apply instructions are blocked#1250lawsonoates wants to merge 3 commits into
Conversation
Exit with code 1 when apply instructions are blocked, add a spec-driven hint when delta specs are absent, and extend tests plus the tmp-init fixture so blocked and ready apply paths are covered.
Expect only tasks in the missing-artifacts message and assert that missing delta specs do not block apply while the default schema still requires tasks alone.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesApply instructions blocked state and spec-driven hint
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
alfred-openspec
left a comment
There was a problem hiding this comment.
PR #1250 Review: blocked apply exits non-zero
Reviewed: 2026-06-24 23:18 AEST
PR: #1250
Author: @lawsonoates
Head: 9315bd1cdb32447f15f9d735eed77a969eeec914
Issue: #1212, spec-driven fast-path workflow silently leaves specs stale
Verdict
Changes requested.
The exit-code hardening is good and should be kept, but the PR no longer fixes the root #1212 path. The final commit explicitly preserves the existing spec-driven apply contract of apply.requires: [tasks] and adds a regression test proving that a change with proposal.md, design.md, and tasks.md but no specs/ is still considered ready to apply.
That is exactly the unsafe state described in #1212: fast-path generation can stop at apply requirements, opsx:apply proceeds, and archive later has no delta specs to sync.
What changed
src/commands/workflow/instructions.tsapplyInstructionsCommand()now setsprocess.exitCode = 1when generated apply instructions areblocked.- JSON and text output both still print before the non-zero exit is set.
generateApplyInstructions()includes a spec-driven delta-spec hint only whenspecsis already inmissingArtifacts.
test/commands/artifact-workflow.test.ts- Blocked apply now expects exit code 1.
- New regression asserts missing delta specs do not block apply when tasks exist.
test/fixtures/tmp-init/openspec/changes/c1/tasks.md- Added a simple tasks fixture so existing apply JSON path is ready.
Verification
Ran in /tmp/openspec-pr1250:
pnpm install --frozen-lockfile
pnpm exec vitest run test/commands/artifact-workflow.test.tsResult: 1 file, 60 tests passed.
Review findings
Blocking: missing delta specs still pass apply for spec-driven changes
The final PR state leaves schemas/spec-driven/schema.yaml as:
apply:
requires: [tasks]generateApplyInstructions() derives missingArtifacts exclusively from that apply.requires list. Since specs is not required, this branch never runs for the #1212 failure state:
const specsHint =
missingArtifacts.includes('specs') && context.schemaName === 'spec-driven'The new test makes this explicit:
await createTestChange('missing-specs-apply', ['proposal', 'design', 'tasks']);
expect(result.exitCode).toBe(0);
expect(json.state).toBe('ready');
expect(json.missingArtifacts).toBeUndefined();That means the unsafe fast path remains green. Exit-code hardening only helps when the command is already blocked, but #1212 is about the command failing to block.
Recommended fix
Minimum acceptable fix for this PR:
- Add
specstoschemas/spec-driven/schema.yamlapply.requires, likelyrequires: [specs, tasks]. - Flip the missing-specs test to expect blocked state, exit code 1,
missingArtifacts: ['specs'], and the delta-spec hint. - Keep the existing exit-code hardening.
Follow-up still worth tracking separately:
- Archive should also guard against spec-driven changes with no delta specs before it cleanly reports “No delta specs”. That is the last line of defense if the apply fast path regresses again.
Notes
This is small and close. I would not merge as-is because it creates a false sense that #1212 is fixed while preserving the exact missing-specs apply path.
Add specs to apply.requires so fast-path workflows cannot stop at tasks alone. Update apply instruction tests to expect blocked state when delta specs are missing.
|
@alfred-openspec implemented your recommendations |
alfred-openspec
left a comment
There was a problem hiding this comment.
Thanks Lawson, this addresses the blocking issue from my last review. I re-tested the PR at f2d72fb: pnpm install --frozen-lockfile and pnpm exec vitest run test/commands/artifact-workflow.test.ts pass, and spec-driven apply now blocks with exit 1 when delta specs are missing.\n\nI still think we should add an archive-side guard as a follow-up last line of defense for #1212, but this PR is good to merge for the apply path.
Propose a deterministic CLI backstop so a spec-driven change cannot archive while silently dropping the specs it declared. Root cause (confirmed): `openspec validate` runs validateChangeDeltaSpecs unconditionally and rejects a change with no deltas, but `openspec archive` gates the same check behind `if (hasDeltaSpecs)`, so a change with no specs/ skips validation and archives clean — the proposal's no-delta error is downgraded to a non-blocking warning. Fix: - Archive runs delta-spec validation unless --skip-specs (parity with validate); a no-delta change is blocked with CHANGE_NO_DELTAS. - instructions apply exits non-zero when blocked. - spec-driven apply.requires includes specs. - Archive skill prompt aligned with the new blocking behavior. Closes Fission-AI#1212, Fission-AI#1260, Fission-AI#1222, Fission-AI#1264, Fission-AI#799. Supersedes PRs Fission-AI#1250, Fission-AI#1271, Fission-AI#1241, Fission-AI#1233. References (out of scope) Fission-AI#1246, Fission-AI#1112, Fission-AI#1252. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Propose one deterministic completeness check, enforced in the validator and shared by `validate`, CI, and `archive`, so a spec-driven change cannot archive while silently dropping the specs it declared. Two silent failure modes, both closed: - Total drop: archive gated delta-spec validation behind `if (hasDeltaSpecs)`, so a change with no specs/ skipped the CHANGE_NO_DELTAS check that `validate` already enforces. Fix: validate unless --skip-specs (validate/archive parity). - Partial drop: nothing checked the proposal's declared capabilities against delivered delta specs. Fix: deterministic validateChangeCapabilityCoverage — every capability under `## Capabilities` must have specs/<id>/spec.md with a delta section. Parsing contract validated against all 93 in-repo proposals (found 7 real proposal/spec inconsistencies; zero false extractions). Plus earlier guardrails (incorporates Fission-AI#1250): apply exits non-zero when blocked; spec-driven apply.requires includes specs. Closes Fission-AI#1212, Fission-AI#1260, Fission-AI#1222, Fission-AI#1264, Fission-AI#799. Supersedes PRs Fission-AI#1250, Fission-AI#1271, Fission-AI#1241, Fission-AI#1233. References (out of scope) Fission-AI#1246, Fission-AI#1112, Fission-AI#1252. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the spec-driven silent-spec-drop family at its real root: one architectural fault — correctness-critical decisions live in agent prompts, not deterministic CLI logic — expressed at four layers. Fix, ordered by leverage: - Deterministic CLI gate: archive validates like `validate` unless --skip-specs, schema-aware (only when the schema graph has a `specs` artifact, per Fission-AI#997). Blocks total drop (CHANGE_NO_DELTAS), partial drop (new schema-aware capability-coverage rule), and format/mode-G (present non-delta spec → "No delta sections found"). - Loop fix: spec-driven apply.requires [specs, tasks] + propose/ff walk the full schema build order (incorporates Fission-AI#1250; Fission-AI#1250's reviewer explicitly deferred this archive guard). - Keystone: the archive skill calls `openspec archive` CLI instead of agent-judged sync + raw mv, so the CLI guarantees are reachable by agents (Fission-AI#656, Fission-AI#863). - Recovery audit for already-archived drift; delta-vs-full-spec clarity. Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#164. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses-in-part Fission-AI#997 Fission-AI#194 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252 Fission-AI#913 Fission-AI#1120. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the spec-driven silent-spec-drop family at its root: one fault — correctness-critical decisions live in agent prompts, not deterministic CLI logic — expressed at four layers, fixed at the most deterministic point for each. - CLI gate (guarantee): archive validates like `validate` unless --skip-specs, schema-aware (only when the schema graph produces delta specs, per Fission-AI#997). Blocks total (CHANGE_NO_DELTAS), partial (new capability-coverage rule), and format/mode-G ("No delta sections"). - Loop fix: spec-driven apply.requires [specs, tasks] + propose/ff create every artifact transitively required by applyRequires (incorporates Fission-AI#1250; its reviewer deferred this archive guard). - Keystone: archive skill calls `openspec archive` instead of agent-judged sync + raw mv (Fission-AI#656, Fission-AI#863); also removes the sync-specs skill dependency (Fission-AI#913). - Recovery audit; delta-vs-full-spec clarity. Verified against current main (rebuilt CLI): bug reproduces; archive --json + ArchiveBlockedError exist; Fission-AI#1250 not merged; Fission-AI#194/Fission-AI#1268 closed. Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#913. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses Fission-AI#997 Fission-AI#164 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252 Fission-AI#1120 Fission-AI#827 Fission-AI#1265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the spec-driven silent-spec-drop family at its root: one fault — correctness-critical decisions live in agent prompts, not deterministic CLI logic — fixed at the most deterministic point for each of four layers. - CLI gate (guarantee): ONE shared schema-aware predicate (schemaProducesDeltaSpecs) gates the delta requirement in BOTH `validate` and `archive`, so they agree by construction and proposal-only schemas pass both while spec-driven-with-no-specs fails both (Fission-AI#997). Blocks total (CHANGE_NO_DELTAS), partial (capability coverage), and format/mode-G ("No delta sections"). `--yes` never bypasses the gate. - Loop fix: spec-driven apply.requires [specs, tasks] + propose/ff create every artifact transitively required by applyRequires (incorporates Fission-AI#1250). - Keystone: archive skill calls `openspec archive` instead of agent-judged sync + raw mv (Fission-AI#656, Fission-AI#863); removes the sync-specs skill dependency (Fission-AI#913). - Recovery audit; delta-vs-full-spec clarity. Addresses review feedback (alfred-openspec: validate needs the same schema-aware gate as archive; coderabbit: document --yes does not bypass, fenced-block tags, clarify partial detection, fail-open/--yes tests). Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#913. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses Fission-AI#997 Fission-AI#164 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252 Fission-AI#1120 Fission-AI#827 Fission-AI#1265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the spec-driven silent-spec-drop family at its root: one fault — correctness-critical decisions live in agent prompts, not deterministic CLI logic — fixed at the most deterministic point for each of four layers. CLI gate (guarantee): one shared schema-aware predicate (schemaProducesDeltaSpecs, memoized per run) gates the delta requirement in BOTH validate and archive; blocks total/partial/format drops; proposal-only schemas pass both (Fission-AI#997). Loop: spec-driven apply.requires [specs, tasks] (apply gate is non-transitive — design stays optional). Keystone: ALL archive templates (skill + command + bulk) call `openspec archive` instead of agent-judged sync + raw mv (Fission-AI#656, Fission-AI#863); never --skip-specs/--no-validate. Recovery: specced archived-drift audit (forward-only). Clarity: delta-vs-full spec + stricter Capabilities-section contract. Incorporates three review rounds (alfred-openspec, CodeRabbit) and an internal adversarial pass: validate schema-aware parity, --yes doesn't bypass, all archive surfaces reworked, non-transitive apply gate, schema-resolution memoization, capability deletion/rename + bold-label/non-kebab handling, proposal-only fixture for Fission-AI#997 tests. Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#913. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses Fission-AI#997 Fission-AI#164 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252 (complementary; land first) Fission-AI#1120 Fission-AI#827 Fission-AI#1265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the spec-driven silent-spec-drop family at its root: one fault — correctness-critical decisions live in agent prompts, not deterministic CLI logic — fixed at the most deterministic point for each of four layers. CLI gate (guarantee): one shared schema-aware predicate (schemaProducesDeltaSpecs, memoized per run) gates the delta requirement in BOTH validate and archive; blocks total/partial/format drops; proposal-only schemas pass both (Fission-AI#997). Loop: spec-driven apply.requires [specs, tasks] (apply gate is non-transitive — design stays optional). Keystone: ALL archive templates (skill + command + bulk) call `openspec archive` instead of agent-judged sync + raw mv (Fission-AI#656, Fission-AI#863); never --skip-specs/--no-validate. Recovery: specced archived-drift audit (forward-only). Clarity: delta-vs-full spec + stricter Capabilities-section contract. Incorporates three review rounds (alfred-openspec, CodeRabbit) and an internal adversarial pass: validate schema-aware parity, --yes doesn't bypass, all archive surfaces reworked, non-transitive apply gate, schema-resolution memoization, capability deletion/rename + bold-label/non-kebab handling, proposal-only fixture for Fission-AI#997 tests. Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#913. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses Fission-AI#997 Fission-AI#164 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252 (complementary; land first) Fission-AI#1120 Fission-AI#827 Fission-AI#1265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic, CLI-enforced prevention of silent spec drop in the spec-driven workflow, addressing one root fault (correctness decisions in agent prompts) across four layers: schema-aware delta gate shared by validate+archive (Fission-AI#997), the non-transitive apply-gate loop fix (incorporates Fission-AI#1250), the keystone (all archive templates call `openspec archive` — Fission-AI#656/Fission-AI#863), and a specced archived-drift audit. Hardened across three review rounds (alfred-openspec, CodeRabbit), an internal adversarial pass, and an open-PR coordination scan: - reconciles Fission-AI#977 (allow-specless) via the schema-aware gate + --skip-specs, deliberately not allowing silent specless archives under spec-driven; - coordinates with Fission-AI#902 (propose/ff spec discovery), the unify-template-generation-pipeline manifest, add-change-stacking-awareness (provides/touches markers + overlap warnings), and add-artifact- regeneration-support (complementary staleness); - rebases onto approved Fission-AI#1186/Fission-AI#1151/Fission-AI#1153/Fission-AI#1252; Fission-AI#1252 is a prerequisite. Closes Fission-AI#1212 Fission-AI#1260 Fission-AI#1222 Fission-AI#1264 Fission-AI#799 Fission-AI#656 Fission-AI#863 Fission-AI#913. Supersedes PRs Fission-AI#1250 Fission-AI#1271 Fission-AI#1241 Fission-AI#1233. Addresses Fission-AI#997 Fission-AI#977 Fission-AI#902 Fission-AI#164 Fission-AI#426 Fission-AI#911. Out of scope Fission-AI#1246 Fission-AI#1112 Fission-AI#1252(prereq) Fission-AI#1120 Fission-AI#827 Fission-AI#1265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-to-end Add test/cli-e2e/issue-1212-spec-drop.test.ts, a regression that drives the real built CLI against a spec-driven project reproducing Fission-AI#1212: - PR Fission-AI#1250: `instructions apply --json` on a change with no delta specs exits 1 with state=blocked, missingArtifacts includes "specs", and the spec-driven "Delta specs must exist…" hint. (This PR contains all of Fission-AI#1250 plus the archive guard its reviewer deferred.) - Fission-AI#1212 safeguard: `archive --json --yes` refuses — the change is not moved and no main spec is silently created. - Fission-AI#1212 happy path: once the delta spec exists, apply is unblocked and archive syncs the main spec. - Fission-AI#1212 recovery: `validate --archived` detects an already-archived change whose declared capability never reached openspec/specs/. Docs: proposal now records the e2e proof for Fission-AI#1212/Fission-AI#1250. Full e2e suite green (32), full suite 1786 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Fix for #1212.
Hardens the apply path so missing required artifacts are treated as a hard failure instead of a soft warning the agent can ignore.
Changes
openspec instructions applynow sets exit code 1 when apply is blocked (missing required artifacts per the schema'sapply.requires)specsis among the missing artifacts, pointing authors to create delta specs underchanges/<name>/specs/before implementationspecsis inapply.requires)tmp-initfixture withtasks.mdso the apply JSON e2e test exercises a ready change, not a blocked oneBehavior
Before: Apply could report
Blockedbut still exit 0, making it easy for agents/scripts to proceed anyway.After: Blocked apply exits 1 and includes clearer guidance when delta specs are missing.
Testing
pnpm exec vitest run test/commands/artifact-workflow.test.tspnpm test(full suite)Notes
I was unable to reproduce the exact failure described in #1212. On a greenfield playground project, the
propose → apply → archivepath created delta specs during propose, and archive synced them intoopenspec/specs/as expected.A follow-up could add
specsto the defaultapply.requiresif others can reproduce the skip-delta-specs behaviour.Summary by CodeRabbit
applycommand now returns a non-zero exit status when apply instructions are blocked, improving CI/script reliability.