Skip to content

Quest CLI 0.8.0 (tagged, never published)

Choose a tag to compare

@jeremy-newhouse jeremy-newhouse released this 27 Sep 18:57
· 217 commits to dev since this release
9d91fc3

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
    returning task.updated/milestone.updated with 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-file and --remove-dependency,
    plus milestone 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 9 on 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, because milestone edit --remove-task has 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 additive unmatched count
    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-ac and
    the --*-dod equivalents) 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's src/adapters/quest.ts emitting
    --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 a task edit-batch ops 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
    returned task.updated with 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.list envelope carries an additive top-level scope
    ({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 list reads 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 on dev missed 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.

    branch is named on every listing. The cross-ref set difference that fills
    unseenTaskIds runs only when the listing came back empty -- the only case
    where naming what was not read changes the reader's conclusion -- and
    otherRefsRead says 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 reads refs/heads and
    refs/remotes and never fetches, so run git fetch --prune first 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, so scope is
    exactly the additive change a consumer is required to tolerate, and
    "scope" in envelope is the semantically correct probe rather than a
    fallback. A version gate cannot work here in any case: dev declares 0.7.1
    and so does the published build that lacks the key (QCLI-296).

  • agents.skill_source accepts a third value, "none", meaning this
    workspace generates no quest skill file and none ships from anywhere else.
    Set it with quest init --skill-source none, or on an existing workspace
    with quest 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.md reports 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 codex writes AGENTS.md and the
    Claude-provider skill file, because the skill file is target-independent by
    design and governed by agents.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 codex path 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 init now states that --reconfigure accepts --skill-source,
    and both init usage lines name all three values. The omission was the
    cheaper half of the report above: --reconfigure has accepted and persisted
    --skill-source since QCLI-236, but the init summary 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 create and quest decision create used 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 minted DEC-3 from
    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
    called planning.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 .quest directory 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 missing index, and
    quest manifest --json advertised only json-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 saying checked: false has stated its intent and
    is honoured, a bare string has said nothing -- so a deliberate reset is
    still expressible and this needed no --force flag. The refusal names that
    spelling at the one moment the caller is certainly reading. manifest now
    advertises the element shapes (string | {index,text,checked}) additively,
    with value still json-array so a consumer switching on it is unaffected
    (QCLI-313).

  • quest instructions task-execution said checkbox edits are
    "index-addressed". The flags take the 1-based position, and
    quest help task edit already said so at length, including "index is 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 over 0..4 gets one error on i=0 and checks positions
    1-4, leaving the LAST criterion unchecked under a final summary claiming all
    of them met. They caught it by counting checked: true against the criterion
    count, not from any exit code (QCLI-315).

  • README.md no 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.md returned one commit,
    ever -- so @opum-ai/quest@0.7.0 and @0.7.1 both advertise 0.6.0 on their
    npm pages, permanently, because version pages are immutable. The gate reads
    README.md back out of a real npm pack tarball 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).