Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
1699ada
docs(cli): plan cli-surface-parity change
replygirl Sep 29, 2026
1d6f042
test(cli): add the upstream key oracle and cli-surface fixtures
replygirl Sep 29, 2026
56dc2f0
test(cli): pin cli-surface-parity differentials as failing rows
replygirl Sep 29, 2026
64b08ff
feat(cli): detect namespace folders the way OpenSpec does
replygirl Sep 29, 2026
ec3fe28
feat(validate): add the nested-change, unreadable and missing-item rules
replygirl Sep 29, 2026
4f7fa76
fix(cli): classify user schemas from the directory OpenSpec reads
replygirl Sep 29, 2026
b09823d
feat(cli): name the next step on every status entry
replygirl Sep 29, 2026
19ac050
feat(cli): add OpenSpec's status keys to the JSON documents
replygirl Sep 29, 2026
d57cfce
feat(cli): render schemas cospec does not type from OpenSpec's status
replygirl Sep 29, 2026
9e9fe98
feat(cli): accept status --schema as OpenSpec's schema override
replygirl Sep 29, 2026
bf6bcaf
fix(cli): report namespace folders and read failures in status
replygirl Sep 29, 2026
aab08f8
feat(cli): sort list rows and carry OpenSpec's list keys
replygirl Sep 29, 2026
5885681
fix(cli): answer namespace folders and read failures in list
replygirl Sep 29, 2026
2ad780a
feat(validate): resolve items and bulk scopes as OpenSpec does
replygirl Sep 29, 2026
96e08bd
feat(validate): add --report full|findings
replygirl Sep 29, 2026
f4fd0cd
feat(validate): bound bulk validation with --concurrency
replygirl Sep 29, 2026
5c52d08
feat(validate): add OpenSpec's report keys to the JSON document
replygirl Sep 29, 2026
e752033
fix(validate): report unreadable artifacts and respell relayed remedies
replygirl Sep 29, 2026
517ad54
fix(validate): match structurally-invalid targets in linear time
replygirl Sep 29, 2026
f45a31a
test(validate): prove the scenario-depth masked-view exception
replygirl Sep 29, 2026
751ebfb
feat(cli): serve the schemas and archived-changes completion sources
replygirl Sep 29, 2026
73d979c
feat(cli): complete schema names in the generated shell scripts
replygirl Sep 29, 2026
b4cc558
fix(apply): answer every early exit with one JSON document
replygirl Sep 29, 2026
5d32344
docs(cli): document cli-surface-parity behavior
replygirl Sep 29, 2026
1f2c056
docs(validate): record how the validation-parity archive ran
replygirl Sep 29, 2026
3d011e4
docs(agents): record the additive upstream-key discipline
replygirl Sep 29, 2026
0f05a1b
fix(cli): relay the fix line of a refused list and pin the no-root case
replygirl Sep 29, 2026
23a915d
test(cli): align stale rows with the shipped cli-surface behavior
replygirl Sep 29, 2026
b5e61b8
docs(cli): record implement-stage design notes
replygirl Sep 29, 2026
d57b30e
test(cli): fix a stale namespace-folder rule-id comment
replygirl Sep 29, 2026
fad5a2a
docs(cli): record cli-surface-parity evidence
replygirl Sep 29, 2026
3ecabfe
fix(cli): backtick a bare __complete pair the formatter mis-read
replygirl Sep 29, 2026
0be6cd9
test(cli): add the round-2 review rows as failing
replygirl Oct 4, 2026
7a37b62
test(cli): add the unreadable tasks.md rows as failing
replygirl Oct 4, 2026
cbbac10
fix(cli): answer an unreadable tasks.md as the binary does
replygirl Oct 4, 2026
6b3b733
fix(validate): validate a forced spec that discovery skips
replygirl Oct 4, 2026
eeb3e94
fix(cli): match schema outputs with the binary's glob semantics
replygirl Oct 4, 2026
ac4ca61
fix(bench): count a refused validate document as no report
replygirl Oct 4, 2026
7789964
fix(cli): take a custom schema's status from its own artifacts
replygirl Oct 4, 2026
d8878c3
fix(validate): relay the binary's --archived failure document
replygirl Oct 4, 2026
13efb45
fix(cli): validate and apply past an unreadable archive
replygirl Oct 5, 2026
612ef98
fix(validate): answer --json outside a root with one document
replygirl Oct 5, 2026
2c1780b
fix(cli): relay a failed list --specs as the binary's document
replygirl Oct 5, 2026
35825db
fix(validate): report an unreadable living spec as an issue
replygirl Oct 5, 2026
1196bbe
test(validate): replace the exponential reference regex with a guard
replygirl Oct 5, 2026
33a63c7
docs(cli): record the round-2 review fixes
replygirl Oct 5, 2026
111216e
test(cli): add the round-3 review rows as failing
replygirl Oct 5, 2026
0ffffe2
docs(cli): plan the round-3 review fixes
replygirl Oct 5, 2026
b216910
fix(validate): name the kind on every delegated validation
replygirl Oct 5, 2026
bfc846c
fix(validate): fail a change whose target living spec is unreadable
replygirl Oct 5, 2026
f6073bf
fix(cli): answer an unreadable planning directory with one document
replygirl Oct 5, 2026
f22e277
fix(validate): resolve a named item outside any root
replygirl Oct 5, 2026
2cf8239
fix(cli): look a change up as the binary does
replygirl Oct 5, 2026
c1b97c7
fix(cli): respell every status diagnostic the binary relays
replygirl Oct 5, 2026
2acf27d
fix(cli): keep an empty change's artifacts empty under --json
replygirl Oct 5, 2026
15ca07c
fix(cli): refuse a change status the binary refuses
replygirl Oct 5, 2026
640386b
fix(apply): relay the binary's apply instructions refusal
replygirl Oct 5, 2026
6cb7041
fix(validate): read past a directory no artifact lives in
replygirl Oct 5, 2026
2a5c3e1
test(cli): hold rows 15.4, 15.6 and 15.7 to their ledger
replygirl Oct 5, 2026
b831056
test(cli): answer apply 1foo through the binary's change lookup
replygirl Oct 5, 2026
9d14120
docs(cli): record the round-3 review fixes
replygirl Oct 5, 2026
28baaf0
fix(cli): refuse in text a change whose schema cannot load
replygirl Oct 5, 2026
e696b56
fix(cli): refuse in text a change whose metadata is refused
replygirl Oct 5, 2026
a806c41
fix(cli): catch listSchemas's errno in changeMetadataRefused
replygirl Oct 5, 2026
327c91e
test(cli): add a unit test for 13.3's errno catch, tick the rebase task
replygirl Oct 5, 2026
fb4fe46
docs(cli): close out cli-surface-parity before archive
replygirl Oct 5, 2026
f53fdab
chore(cli): archive cli-surface-parity
replygirl Oct 5, 2026
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
23 changes: 19 additions & 4 deletions .agents/shared.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,19 @@ advisory findings listed in `apps/cli/src/core/rules/views.ts`
fixture proving it reads the view it is registered under. A new rule or gate
takes the verbatim view unless no commented line can ever trigger it.

**JSON documents are additive** — a cospec `--json` document that mirrors an
OpenSpec command (`status`, `list`, `validate`, and `archive --json` next)
carries every key upstream's document does: computed natively where cospec owns
the fact (`validate`'s report keys), otherwise from one delegated call per
invocation — never one per change — merged by identity through `mergeUpstream`
(`apps/cli/src/core/upstream-keys.ts`). No cospec key is removed and no cospec
value changed; every envelope keeps `version: 1`. The key oracle
(`apps/cli/test/contract/support/key-oracle.ts`, rows in `cli-surface.test.ts`)
is the gate: it fails on a missing upstream key, an upstream value reported
differently, a changed pre-existing cospec key, and any collision outside its
named list (`NAMED_COLLISIONS`: `version`, validate's `items[].type`), each
entry stating why cospec's value wins.

**Wrapped-call discipline** — every call into the wrapped binary declares its
expected exit codes, a stdout deny-list, and an observable post-condition. Trust
filesystem/JSON post-conditions, never exit codes alone (OpenSpec aborts with
Expand All @@ -263,10 +276,12 @@ out-of-scope issues as a proposed follow-up, not a silent fix.
**Dependencies** — pin exact versions in `package.json`; regenerate the lockfile
after editing the manifest. The OpenSpec pin is load-bearing: bumping it means
running the contract suite and re-probing before updating
`EXPECTED_OPENSPEC_VERSION`. Tool pins in `mise.toml` are lockfile-backed — bump
a version and regenerate `mise.lock` in the same commit; CI's "mise lockfile
drift gate" step (`mise install` then `git diff --exit-code mise.lock`) fails
the PR otherwise.
`EXPECTED_OPENSPEC_VERSION`, and moving the exact `fast-glob` devDependency to
the version the new pin resolves (`core/glob.ts` ports the binary's glob
matching over it; `test/contract/glob.test.ts` fails until they agree). Tool
pins in `mise.toml` are lockfile-backed — bump a version and regenerate
`mise.lock` in the same commit; CI's "mise lockfile drift gate" step
(`mise install` then `git diff --exit-code mise.lock`) fails the PR otherwise.

**Tests** — every command change lands with a contract or integration test.
Contract tests run the real pinned binary; a false archive PASS is a release
Expand Down
23 changes: 19 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,19 @@ advisory findings listed in `apps/cli/src/core/rules/views.ts`
fixture proving it reads the view it is registered under. A new rule or gate
takes the verbatim view unless no commented line can ever trigger it.

**JSON documents are additive** — a cospec `--json` document that mirrors an
OpenSpec command (`status`, `list`, `validate`, and `archive --json` next)
carries every key upstream's document does: computed natively where cospec owns
the fact (`validate`'s report keys), otherwise from one delegated call per
invocation — never one per change — merged by identity through `mergeUpstream`
(`apps/cli/src/core/upstream-keys.ts`). No cospec key is removed and no cospec
value changed; every envelope keeps `version: 1`. The key oracle
(`apps/cli/test/contract/support/key-oracle.ts`, rows in `cli-surface.test.ts`)
is the gate: it fails on a missing upstream key, an upstream value reported
differently, a changed pre-existing cospec key, and any collision outside its
named list (`NAMED_COLLISIONS`: `version`, validate's `items[].type`), each
entry stating why cospec's value wins.

**Wrapped-call discipline** — every call into the wrapped binary declares its
expected exit codes, a stdout deny-list, and an observable post-condition. Trust
filesystem/JSON post-conditions, never exit codes alone (OpenSpec aborts with
Expand All @@ -267,10 +280,12 @@ out-of-scope issues as a proposed follow-up, not a silent fix.
**Dependencies** — pin exact versions in `package.json`; regenerate the lockfile
after editing the manifest. The OpenSpec pin is load-bearing: bumping it means
running the contract suite and re-probing before updating
`EXPECTED_OPENSPEC_VERSION`. Tool pins in `mise.toml` are lockfile-backed — bump
a version and regenerate `mise.lock` in the same commit; CI's "mise lockfile
drift gate" step (`mise install` then `git diff --exit-code mise.lock`) fails
the PR otherwise.
`EXPECTED_OPENSPEC_VERSION`, and moving the exact `fast-glob` devDependency to
the version the new pin resolves (`core/glob.ts` ports the binary's glob
matching over it; `test/contract/glob.test.ts` fails until they agree). Tool
pins in `mise.toml` are lockfile-backed — bump a version and regenerate
`mise.lock` in the same commit; CI's "mise lockfile drift gate" step
(`mise install` then `git diff --exit-code mise.lock`) fails the PR otherwise.

**Tests** — every command change lands with a contract or integration test.
Contract tests run the real pinned binary; a false archive PASS is a release
Expand Down
23 changes: 19 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,19 @@ advisory findings listed in `apps/cli/src/core/rules/views.ts`
fixture proving it reads the view it is registered under. A new rule or gate
takes the verbatim view unless no commented line can ever trigger it.

**JSON documents are additive** — a cospec `--json` document that mirrors an
OpenSpec command (`status`, `list`, `validate`, and `archive --json` next)
carries every key upstream's document does: computed natively where cospec owns
the fact (`validate`'s report keys), otherwise from one delegated call per
invocation — never one per change — merged by identity through `mergeUpstream`
(`apps/cli/src/core/upstream-keys.ts`). No cospec key is removed and no cospec
value changed; every envelope keeps `version: 1`. The key oracle
(`apps/cli/test/contract/support/key-oracle.ts`, rows in `cli-surface.test.ts`)
is the gate: it fails on a missing upstream key, an upstream value reported
differently, a changed pre-existing cospec key, and any collision outside its
named list (`NAMED_COLLISIONS`: `version`, validate's `items[].type`), each
entry stating why cospec's value wins.

**Wrapped-call discipline** — every call into the wrapped binary declares its
expected exit codes, a stdout deny-list, and an observable post-condition. Trust
filesystem/JSON post-conditions, never exit codes alone (OpenSpec aborts with
Expand All @@ -263,10 +276,12 @@ out-of-scope issues as a proposed follow-up, not a silent fix.
**Dependencies** — pin exact versions in `package.json`; regenerate the lockfile
after editing the manifest. The OpenSpec pin is load-bearing: bumping it means
running the contract suite and re-probing before updating
`EXPECTED_OPENSPEC_VERSION`. Tool pins in `mise.toml` are lockfile-backed — bump
a version and regenerate `mise.lock` in the same commit; CI's "mise lockfile
drift gate" step (`mise install` then `git diff --exit-code mise.lock`) fails
the PR otherwise.
`EXPECTED_OPENSPEC_VERSION`, and moving the exact `fast-glob` devDependency to
the version the new pin resolves (`core/glob.ts` ports the binary's glob
matching over it; `test/contract/glob.test.ts` fails until they agree). Tool
pins in `mise.toml` are lockfile-backed — bump a version and regenerate
`mise.lock` in the same commit; CI's "mise lockfile drift gate" step
(`mise install` then `git diff --exit-code mise.lock`) fails the PR otherwise.

**Tests** — every command change lands with a contract or integration test.
Contract tests run the real pinned binary; a false archive PASS is a release
Expand Down
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
},
"devDependencies": {
"bun-types": "1.3.14",
"fast-glob": "3.3.3",
"yaml": "2.9.0"
},
"optionalDependencies": {
Expand Down
154 changes: 124 additions & 30 deletions apps/cli/src/commands/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,26 @@ import {
type Change,
} from '../core/change.ts'
import { hasFlag } from '../core/command-table.ts'
import { answeringErrno } from '../core/errno.ts'
import {
openspecApplyInstructions,
OpenspecCallError,
type ApplyInstructionsJson,
type Root,
type StatusDiagnostic,
} from '../core/openspec.ts'
import { respellRemedies } from '../core/remedies.ts'
import { renderHuman, renderJson, type ItemReport } from '../core/report.ts'
import { resolveRoot } from '../core/root.ts'
import { renderHuman, toJson, type ItemReport } from '../core/report.ts'
import { surfaceUnmetConsequences } from '../core/rules/meta.ts'
import {
ARTIFACT_FILES,
enforcedApplyRequires,
type ArtifactId,
type CospecType,
} from '../core/rules/type-facts.ts'
import { buildValidateContext, validateChange } from './validate.ts'
import { resolveRootOrDocument } from '../core/upstream-keys.ts'
import type { ArchiveWarning } from './status.ts'
import { readValidateContext, validateChange } from './validate.ts'

// --- shared primitives (exported for status/list/archive/new) --------------

Expand Down Expand Up @@ -239,22 +242,98 @@ function printWarnings(instr: ApplyInstructionsJson): void {
for (const w of instr.warnings ?? []) process.stdout.write(`Warning: ${w}\n`)
}

function printReport(report: ItemReport, ctx: CommandContext): void {
/** The document's `warnings`, when there are any to carry. */
function warningsKey(warnings: readonly ArchiveWarning[]): { warnings?: ArchiveWarning[] } {
return warnings.length === 0 ? {} : { warnings: [...warnings] }
}

function printReport(
report: ItemReport,
ctx: CommandContext,
warnings: readonly ArchiveWarning[],
): void {
const out = ctx.flags.json
? renderJson([report])
? `${JSON.stringify({ ...toJson([report]), ...warningsKey(warnings) }, null, 2)}\n`
: renderHuman([report], { noColor: ctx.flags.noColor, title: 'cospec apply' })
process.stdout.write(out)
}

/** Legacy schema: no cospec gate — delegate to openspec and exit per its state. */
async function applyLegacy(change: Change, ctx: CommandContext, root: Root): Promise<number> {
let instr: ApplyInstructionsJson
/**
* An early exit (design D10): `prose` on stderr, or under `--json` one
* `{status: [{severity, code: "change_error", message, fix?}]}` document on
* stdout — the code the binary's `instructions apply` reports for the same
* lookups — so a `--json` caller always gets one document. Exit 1.
*/
function earlyExit(
ctx: CommandContext,
prose: string,
message: string,
fix?: string,
warnings: readonly ArchiveWarning[] = [],
): number {
if (ctx.flags.json) {
const status = [
{ severity: 'error', code: 'change_error', message, ...(fix === undefined ? {} : { fix }) },
]
process.stdout.write(`${JSON.stringify({ status, ...warningsKey(warnings) }, null, 2)}\n`)
} else process.stderr.write(prose)
return EXIT.failure
}

/**
* The binary's refusal of `instructions apply`, relayed as the answer: its
* failure document under `--json`, each message and fix spelled through the
* remedy allowlist, or `cospec apply: <message>` and its `Fix:` line in text.
* Exit 1.
*/
function relayRefusal(
ctx: CommandContext,
refused: { status: StatusDiagnostic[] } & Record<string, unknown>,
warnings: readonly ArchiveWarning[] = [],
): number {
const status = refused.status.map((d) => ({
...d,
message: respellRemedies(d.message),
...(d.fix === undefined ? {} : { fix: respellRemedies(d.fix) }),
}))
if (ctx.flags.json)
process.stdout.write(
`${JSON.stringify({ ...refused, status, ...warningsKey(warnings) }, null, 2)}\n`,
)
else
for (const d of status)
process.stderr.write(
`cospec apply: ${d.message}\n${d.fix === undefined ? '' : `Fix: ${d.fix}\n`}`,
)
return EXIT.failure
}

/**
* The apply payload the binary gives for `change`, relayed through cospec; or
* the exit code of the answer already printed when it refused the change or
* broke the call's contract.
*/
async function applyInstructions(
ctx: CommandContext,
root: Root,
change: Change,
warnings: readonly ArchiveWarning[] = [],
): Promise<ApplyInstructionsJson | number> {
let answer
try {
instr = relayApplyInstructions(await openspecApplyInstructions(root, change.id), change.id)
answer = await openspecApplyInstructions(root, change.id)
} catch (err) {
process.stderr.write(`cospec apply: ${(err as Error).message}\n`)
return EXIT.failure
const msg = err instanceof OpenspecCallError ? err.message : (err as Error).message
return earlyExit(ctx, `cospec apply: ${msg}\n`, msg, undefined, warnings)
}
if ('refused' in answer) return relayRefusal(ctx, answer.refused, warnings)
return relayApplyInstructions(answer.instructions, change.id)
}

/** Legacy schema: no cospec gate — delegate to openspec and exit per its state. */
async function applyLegacy(change: Change, ctx: CommandContext, root: Root): Promise<number> {
const instr = await applyInstructions(ctx, root, change)
if (typeof instr === 'number') return instr
if (ctx.flags.json) {
process.stdout.write(
`${JSON.stringify({ change: change.id, type: change.schema, legacy: true, apply: instr }, null, 2)}\n`,
Expand All @@ -269,10 +348,20 @@ async function applyLegacy(change: Change, ctx: CommandContext, root: Root): Pro
return instr.state === 'blocked' ? EXIT.blocked : EXIT.success
}

export async function run(ctx: CommandContext): Promise<number> {
/**
* `cospec apply`: an errno failure it lets escape (an unreadable
* `openspec/changes/`) is one `change_error` document under `--json`, the
* code the binary's `instructions apply` reports.
*/
export function run(ctx: CommandContext): Promise<number> {
return answeringErrno(ctx.flags.json, { code: 'change_error' }, () => apply(ctx))
}

async function apply(ctx: CommandContext): Promise<number> {
const { flags } = ctx
const parsedArgs = ctx.parsed!
const root = await resolveRoot(ctx)
const root = await resolveRootOrDocument(ctx, 'change_error')
if (root === undefined) return EXIT.failure
const base = root.base
const allowSoft = hasFlag(parsedArgs, '--allow-soft')
// `skip_specs` precedence (DESIGN §5, OpenSpec 1.7 parity): the one-shot CLI
Expand All @@ -287,30 +376,37 @@ export async function run(ctx: CommandContext): Promise<number> {
const name = parsedArgs.positionals[0]!

if (!existsSync(openspecDir(base))) {
process.stderr.write(`cospec: no openspec/ directory at ${base} — run 'cospec init' first\n`)
return EXIT.failure
const message = `no openspec/ directory at ${base} — run 'cospec init' first`
return earlyExit(ctx, `cospec: ${message}\n`, message)
}

const change = resolveChange(base, name)
if (change === undefined) {
process.stderr.write(`cospec apply: unknown change '${name}'\n`)
const suggestion = closest(
name,
listChanges(base).map((c) => c.id),
)
if (suggestion !== undefined) process.stderr.write(`Did you mean '${suggestion}'?\n`)
return EXIT.failure
const didYouMean = suggestion === undefined ? '' : `Did you mean '${suggestion}'?`
return earlyExit(
ctx,
`cospec apply: unknown change '${name}'\n${didYouMean === '' ? '' : `${didYouMean}\n`}`,
`unknown change '${name}'${didYouMean === '' ? '' : `. ${didYouMean}`}`,
)
}

// Step 1: legacy schemas bypass the cospec gate entirely.
const resolution = resolveSchema(base, change.schema)
if (resolution.kind === 'legacy') return applyLegacy(change, ctx, root)

// Step 2: fast validation. Errors block the gate outright.
const vctx = buildValidateContext(base)
// An unreadable archive is read as empty (`readValidateContext`): the gate
// can only err toward blocked, and the warning says why.
const { ctx: vctx, warning } = readValidateContext(base)
const warnings = warning === undefined ? [] : [warning]
if (!flags.json) for (const w of warnings) process.stderr.write(`Warning: ${w.message}\n`)
const report = await validateChange(root, change, vctx, { strict: false, fast: true })
if (!report.valid) {
printReport(report, ctx)
printReport(report, ctx, warnings)
return EXIT.failure
}

Expand All @@ -331,6 +427,7 @@ export async function run(ctx: CommandContext): Promise<number> {
{
change: change.id,
type: change.schema,
...warningsKey(warnings),
gate: { state: 'blocked', reason: 'missing-artifacts', missingArtifacts: missing },
},
null,
Expand All @@ -347,7 +444,7 @@ export async function run(ctx: CommandContext): Promise<number> {

// Step 4: blocker gate. Self-heal against the archive first (§5.1 step 4c).
const blockersPath = join(change.dir, BLOCKERS_FILE)
const archived = archiveMap(base)
const archived = warning === undefined ? archiveMap(base) : new Map<string, string>()
const active = new Set(listChanges(base).map((c) => c.id))
const original = readFileSync(blockersPath, 'utf8')
const heal = syncBlockers(original, archived, active, { fix: true })
Expand All @@ -362,6 +459,7 @@ export async function run(ctx: CommandContext): Promise<number> {
{
change: change.id,
type: change.schema,
...warningsKey(warnings),
gate: {
state: 'blocked',
reason: 'hard-blockers',
Expand Down Expand Up @@ -423,6 +521,7 @@ export async function run(ctx: CommandContext): Promise<number> {
{
change: change.id,
type: change.schema,
...warningsKey(warnings),
gate: { state: 'soft-blocked', softBlockers: gate.soft, synced: heal.synced },
},
null,
Expand All @@ -442,15 +541,9 @@ export async function run(ctx: CommandContext): Promise<number> {

const softAcknowledged = allowSoft ? gate.soft.map((s) => s.slug) : []

// Step 5: fetch the apply payload from openspec.
let instr: ApplyInstructionsJson
try {
instr = relayApplyInstructions(await openspecApplyInstructions(root, change.id), change.id)
} catch (err) {
const msg = err instanceof OpenspecCallError ? err.message : (err as Error).message
process.stderr.write(`cospec apply: ${msg}\n`)
return EXIT.failure
}
// Step 5: fetch the apply payload from openspec; its refusal is the answer.
const instr = await applyInstructions(ctx, root, change, warnings)
if (typeof instr === 'number') return instr

// Step 6: merged clear-gate output.
if (flags.json) {
Expand All @@ -459,6 +552,7 @@ export async function run(ctx: CommandContext): Promise<number> {
{
change: change.id,
type: change.schema,
...warningsKey(warnings),
gate: { state: 'clear', hardBlockers: [], softAcknowledged, synced: heal.synced },
apply: instr,
},
Expand Down
5 changes: 3 additions & 2 deletions apps/cli/src/commands/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,9 @@ const DATE_PREFIXED_RE = /^\d{4}-\d{2}-\d{2}-/
* Both accepted forms are real binary behaviour inside cospec's `>=1.0.0
* <2.0.0` range: from 1.7.0 a change whose id already carries a date prefix
* archives under that id verbatim (#1309), while older binaries re-prefix it.
* cospec's own `CHANGE_ID_RE`/`meta/name-kebab` reject a date-prefixed id, so
* the verbatim arm is defence for a change created outside cospec, not a path
* `cospec new` (`CHANGE_ID_RE`) never creates a date-prefixed id and
* `meta/name-kebab` fails one before archive moves anything, so the verbatim
* arm is defence for a change created outside cospec, not a path
* `cospec archive` can reach on its own.
*/
export function isArchiveTargetFor(changeId: string, dirName: string): boolean {
Expand Down
Loading
Loading