Quest CLI 0.8.0 (tagged, never published)
Version frozen 2026-09-17, tagged and never published. Its content ships
under 0.9.0 instead, together with the work that landed after the freeze; see
that entry for why the number moved. The v0.8.0 tag is left pointing at
9d91fc35 and is not re-pointed.
Breaks lockstep with @opum-ai/lore, forced by QCLI-328's rule that a
breaking Changed entry needs at least a minor bump, and the entry directly
below is exactly that.
Corrected 2026-09-18 (QCLI-345). This paragraph originally read
"lore-cli's dev remains at 0.7.0 and nothing in lore changes". That was
false when it was written, and it is corrected here rather than quietly
dropped because it was the stated justification for shipping quest-only.
Asked to measure their own range, lore-cli reported dev at 64 commits past
tag v0.7.0, carrying a new lore types command, a new profile.strict_types
config key, and committed-schema drift detection inside lore check.
How it got in is worth more than the correction: the claim came from reading
lore's dev package.json (0.7.0) as though it were a running version. It is
not, and the conventions are opposite in the two repositories -- lore bumps
at release time, so that field names the LAST shipped version; quest bumps at
freeze time, so ours names the NEXT, unpublished one. Reading a sibling's
field with this repository's convention in mind produces a confident wrong
answer. A version field's meaning is a per-repo convention; the commit range
is the only probe that means the same thing in both.
The date above is when this version was frozen and the bump landed, not
when it reached npm; publication is a separate, manually authorized step,
and nothing in this file should be read as evidence that it happened.
Changed (breaking)
-
A removal that matches nothing now fails loud (exit 6) instead of
returningtask.updated/milestone.updatedwith an unchanged list. Nine
flags across two commands:task edit's--remove-label,--remove-plan,
--remove-note,--remove-comment,--remove-assignee,
--remove-reference,--remove-modified-fileand--remove-dependency,
plusmilestone edit --remove-task. The error names the record id and
echoes every unmatched value JSON-delimited, and NOTHING is removed -- not
even the values in the same flag that did match.Quest shipped two removal vocabularies with opposite miss behaviour in the
same command.--remove-ac 9on a two-item list exits 6;--remove-note 1
exited 0 and removed nothing, because removal matches on exact TEXT. An
agent that had learned the loud one reasonably tried the same shape on the
quiet one and was told its edit succeeded. Reported by opum-cli-e2e via
opum-agent as one flag; reproducing it found eight, then nine across two
commands, becausemilestone edit --remove-taskhas its own merge and never
touches the shared helper.The payload could not have carried this instead, and that is the argument
that decided the shape. The natural defensive check on the returned record
-- "is the value I asked to remove absent from it?" -- returns TRUE on the
whitespace case, because a value differing by a trailing space was never in
the list under any outcome. It confirms the failure AS a success. A defect
that defeats the diligent caller and the careless one identically is worse
than one that only catches the careless, so an additiveunmatchedcount
would have left the hard case exactly as silent for everyone not reading a
field they have no reason to expect. Hence the JSON delimiters: a trailing
space sits inside the quotes and a tab renders as\t.Both downstream consumers chose loud independently, and lore-cli did so
against its own convenience: a silent no-op on the only path they reach (a
read-then-remove race) is worse for them, because lore would then report
"removed" for an edit that removed nothing, propagating the defect into
their report rather than stopping at Quest's boundary. They asked for the
record id in the message for the same reason -- their per-task error field
carries one line into a multi-task report.The ordinal-addressed family (
--remove-ac/--check-ac/--uncheck-acand
the--*-dodequivalents) is unchanged and keeps its own message. Accepting
an ordinal on the by-value flags was considered and DROPPED, not deferred: a
value that is both a valid ordinal and valid item text would have two
meanings, so a note whose text is literally"1"would get a removal that
silently addressed a different item -- a wrong object returning success,
strictly worse than the no-op it replaced.Who this can break, stated with its bound rather than as a clean bill of
health. A caller that passes removal values speculatively -- a
remove-if-present idiom, or an idempotent retry that re-sends an edit whose
removal already applied -- now gets exit 6 where it got exit 0. A consumer
sweep across the fleet checkouts on this machine found exactly one
cross-repo caller, lore-cli'ssrc/adapters/quest.tsemitting
--remove-label, and both of its call sites compute the removal from a
fresh read and short-circuit when the label is already absent, so neither is
speculative; lore-cli confirmed from their own side that no third producer
and no retry path exists. That finding is bounded to the fleet checkouts
on this machine and is NOT a claim about consumers generally. It cannot
see removals assembled into atask edit-batchops file outside those
repositories, and it cannot see any consumer outside them -- the mbpm2
project included, who are a real integration consumer of the published CLIs
and have not been asked (QCLI-297).What the short-circuit does NOT cover, volunteered by lore-cli against
their own interest. Both of those guarded call sites are a
read-modify-write with no--if-revision, which is their open LCLI-522. If
the label disappears between lore's read and lore's write, quest previously
returnedtask.updatedwith an unchanged list and the race stayed silent;
under this release it becomes exit 6. So this change converts a silent
lore race into a loud failure -- an improvement, and neither side asked to
hold the release for it, but it means lore 0.7.0 paired with this version
has a failure mode that neither lore 0.7.0 + quest 0.7.1 nor a matched newer
pair will necessarily show, because it needs concurrency to surface. A
serial qualification run cannot see it; opum-cli-e2e has been asked to
exercise that path concurrently. The pair being qualified matters here, not
just the two version numbers.
Added
-
Every
task.listenvelope carries an additive top-levelscope
({branch, otherRefsRead, unseenTaskIds?}), so a listing names the object
it answered about instead of leaving the reader to assume it covered the
repository.quest task listreads the checked-out ref's.quest/and
nothing else, while the rule that sends a session to it -- confirm nothing
is still open before reporting clear -- asks about the repository. The two
diverge for exactly the tasks most likely to be forgotten, the ones still on
an unmerged branch, and the failure direction is the bad one: an empty list
reads as the check PASSING rather than as the check NOT RUNNING. Reported by
opum-doc after an empty In Progress listing ondevmissed a task that was
In Progress with an open pull request; they caught it only because an
independent source disagreed with the empty list, and neither the exit code
nor the output would ever have shown it.branchis named on every listing. The cross-ref set difference that fills
unseenTaskIdsruns only when the listing came back empty -- the only case
where naming what was not read changes the reader's conclusion -- and
otherRefsReadsays whether it ran. Three limits, all deliberate: it
reports EXISTENCE only and never status, so a named id may already be Done
on the ref that carries it (DEC-6); it readsrefs/headsand
refs/remotesand never fetches, so rungit fetch --prunefirst if local
refs may be stale; and it is a local-git detector, which means it shares a
blind spot with any other local-ref sweep and does not replace asking
GitHub (QCLI-316).Detect it by key presence, not by version, and keep doing so after the
next release. The success envelope is an open key set, soscopeis
exactly the additive change a consumer is required to tolerate, and
"scope" in envelopeis the semantically correct probe rather than a
fallback. A version gate cannot work here in any case:devdeclares 0.7.1
and so does the published build that lacks the key (QCLI-296). -
agents.skill_sourceaccepts a third value,"none", meaning this
workspace generates no quest skill file and none ships from anywhere else.
Set it withquest init --skill-source none, or on an existing workspace
withquest init --reconfigure --skill-source none. It behaves identically
to"plugin"on every file operation -- absence is the healthy state, a
leftover.claude/skills/quest/SKILL.mdreports as drift, and--force
removes it only when byte-exact -- and differs only in what it asserts.That difference is the entire point. Reported by the mbpm2 project, a
Codex-only consumer, as an integration blocker:quest agents --update-instructions --target codexwrites AGENTS.md and the
Claude-provider skill file, because the skill file is target-independent by
design and governed byagents.skill_source. The only way to stop it was
--skill-source plugin, which asserts the opum-quest Claude Code plugin
ships the skill -- a plugin they do not have and are not using. The opt-out
required declaring something false."none"says only that no skill file is
generated, and its drift message names no plugin.The
--target codexpath is unchanged and is not deprecated: Codex is
retired as a runtime this fleet builds on, not as product surface, and this
is an external user invoking a published flag.Two things a reader should not over-read here. First, the file-writing
behaviour under"repo"-- the default, and what every existing workspace
has -- is byte-identical to before; nothing changes for a consumer who does
not set the new value. Second, a workspace that had already worked around
this with"plugin"is still correct and needs no migration;"none"is a
more accurate declaration of the same intent, not a replacement for a broken
one (QCLI-309). -
quest help initnow states that--reconfigureaccepts--skill-source,
and bothinitusage lines name all three values. The omission was the
cheaper half of the report above:--reconfigurehas accepted and persisted
--skill-sourcesince QCLI-236, but theinitsummary described it as
being for "name/task-id-prefix on an existing workspace", so a documentation
gap turned a one-command fix into a blocker. The reporter could not
reasonably have derived it (QCLI-309).
Fixed
-
Draft, milestone and decision ids are now allocated refs-aware, like task
ids have been since QCLI-279.quest draft create,quest milestone createandquest decision createused to compute "next available" from
the checked-out tree's.quest/alone, so an unmerged sibling branch or a
detached checkout behind a moved branch could mint an id that already
existed elsewhere. QCLI-279 fixed exactly one of the three allocators and
deliberately left these two pending a real reproduction; that reproduction
arrived on 2026-09-14, when two sessions independently mintedDEC-3from
branches neither of which carried the other's record. Recovered by hand, no
data lost, and it would not have been caught by a less careful actor.The obvious fix -- point QCLI-279's existing helper at the other
subdirectories -- is right for drafts and silently wrong for milestones and
decisions, which is the only interesting thing about this change. That
helper finds ids by reading FILE NAMES, which works because tasks and drafts
are stored one record per file. Milestones and decisions share a single
.quest/planning.json; a filename scan over that path matches one file
calledplanning.json, matches no id marker, and returns 0 -- and 0 is
indistinguishable from "no other ref carries a higher id". So the planning
half reads blob CONTENT per ref instead, via the revision-pinned read the
Git port already exposed.That is measured, not argued: a mutant implementing the obvious fix produces
a verdict set byte-identical to a mutant with no fix at all -- five of nine
tests red, the same five. The wrong mechanism and the absent one are the
same observation.Unchanged in every other respect. Allocation still consults only refs that
already exist locally and never fetches; only a prefix's own family can
advance its counter, so a decision cannot advance the milestone counter
though both live in one document; and a.questdirectory with no Git
repository behind it still allocates from the working tree alone. An
unreadable or unparseable planning document on some unrelated branch is
skipped while the remaining refs are still consulted -- a coarser guard
would fall back to the local-only view, which is the un-fixed behaviour
wearing a passing test. Ids minted before this change are not repaired by
upgrading. -
quest task edit --acceptance-criteria(and--definition-of-done) given a
JSON array of bare strings silently unticked every checked box: exit 0,
empty stderr,kind=task.updated. The lossless{index, text, checked}
element form already existed, but appeared nowhere in the CLI's output
except the usage error for an object array missingindex, and
quest manifest --jsonadvertised onlyjson-array-- so the discoverable
path was the destructive one. Three independent agent callers reached for
the string form within hours on 2026-09-15, none knowing the object form
existed, all three having read the manifest; one caller reaching for the
destructive path is a caller mistake, three is the shape of the surface.The replacement is now refused with exit 6 (validation, not usage: the two
neighbouring conflicts are decided by the flag combination alone, this one
by the record's state) if and only if a currently-checked position is
replaced by a BARE STRING. The test is on what the replacement carries, not
on the outcome -- an entry sayingchecked: falsehas stated its intent and
is honoured, a bare string has said nothing -- so a deliberate reset is
still expressible and this needed no--forceflag. The refusal names that
spelling at the one moment the caller is certainly reading.manifestnow
advertises the element shapes (string | {index,text,checked}) additively,
withvaluestilljson-arrayso a consumer switching on it is unaffected
(QCLI-313). -
quest instructions task-executionsaid checkbox edits are
"index-addressed". The flags take the 1-basedposition, and
quest help task editalready said so at length, including "indexis not
what these flags take" -- so two documentation surfaces of the same CLI
contradicted each other, and the wrong one is the surface the agent protocol
sends a caller to first. Reported by opum-doc, who hit it as four silent
successes: a loop over0..4gets one error oni=0and checks positions
1-4, leaving the LAST criterion unchecked under a final summary claiming all
of them met. They caught it by countingchecked: trueagainst the criterion
count, not from any exit code (QCLI-315). -
README.mdno longer states a version of its own, and a pack-time gate
keeps it that way. It ships inside the tarball, and this repository's
release path never read it --git log -- README.mdreturned one commit,
ever -- so@opum-ai/quest@0.7.0and@0.7.1both advertise 0.6.0 on their
npm pages, permanently, because version pages are immutable. The gate reads
README.mdback out of a realnpm packtarball rather than the
working-tree copy: a check on the repo file passes while the packed one is
stale, and the packed one is what the registry serves. Implements the
shipped-README contract accepted into opum-doc as
docs/reference/shipped-readme-version-assertions.md(ODOC-201), taking its
"absent" arm, and in a stronger form than the shared clause -- no
version-shaped token anywhere in the packed README, because the worst site
here (**Status: 0.6.0 released.**) carries no package name on its line and
the shared clause would have missed the defect it exists for (QCLI-307).