Skip to content

feat: first-class goals — review-gated in-flight objectives - #676

Merged
plind-junior merged 4 commits into
vouchdev:testfrom
dripsmvcp:feat/goals
Jul 31, 2026
Merged

feat: first-class goals — review-gated in-flight objectives#676
plind-junior merged 4 commits into
vouchdev:testfrom
dripsmvcp:feat/goals

Conversation

@dripsmvcp

Copy link
Copy Markdown
Contributor

Summary

vouch models claims, pages, entities and relations — everything the project knows. it has no representation of what the project is trying to do right now, so an agent re-orienting after a compaction recovers facts and decisions but not in-flight intent. this adds goal as a first-class, review-gated artifact.

What lands

  • Goal + GoalStatus (open / done / abandoned / blocked) in models.py, with the validation gap: Claim.text, Page.title, Entity.name accept empty/whitespace at the model layer; only propose_* enforces non-empty #155 empty-title validator on the model itself so every write path is closed at once.
  • GOAL added to ProposalKind; kb.propose_goal routes through proposals.propose_* like any other proposal — there is no direct-write entry point.
  • kb.list_goals (defaults to open, oldest-first, viewer-scoped) and kb.set_goal_status, backed by lifecycle.set_goal_status.
  • open goals resurface in vouch digest and in the SessionStart recall digest.
  • storage.py gains goals/ and pure put/get/update/list — no business logic; the status rules live in lifecycle.py and proposals.py.

How the review gate is preserved

two invariants, both tested:

approval is pinned to open. _payload_block_reason refuses a GOAL proposal whose payload carries any other status. approving one would put a transition on disk that never passed the lifecycle path and so never reached the audit log — a parallel write path in disguise.

lifecycle.set_goal_status is the only mutation path. it appends a goal.status event to audit.log.jsonl and a row to the goal's own append-only history. test_only_lifecycle_mutates_a_stored_goal scans the package and asserts no second caller of store.update_goal exists.

One deviation from the issue, called out

the issue's acceptance criteria ask for a decided/ record per transition. decided/ holds decided Proposal yaml keyed by proposal id and is read back through list_proposals; writing a non-proposal there would corrupt that read. the transition record instead lands in the two places that already carry authoritative history — the append-only audit log, and the goal's own history list, which is diffable in PRs exactly like decided/ is. happy to change shape if you'd rather transitions be proposals themselves.

Test Plan

  • regression/feature tests added: tests/test_goals.py (18 tests) — propose→approve→transition, the propose-only invariant, the pinned-open refusal, unknown/no-op status refusal, oldest-first viewer-scoped listing, both digest surfaces, and four-site registration
  • test_capabilities parity (_CLI_MIRRORS entries added) and test_hot_memory_universal_coverage both green
  • full CI gate run locally
$ .venv/bin/python -m pytest tests/ --ignore=tests/embeddings -p no:warnings
2646 passed, 1 skipped in 160.25s (0:02:40)

$ .venv/bin/python -m mypy src
Success: no issues found in 119 source files

$ .venv/bin/python -m ruff check src tests
All checks passed!

Closes #427

vouch modelled everything a project knows and nothing about what it is
doing. an agent re-orienting after a compaction could recall facts and
decisions but not intent — "mid-migration to typed config", "release
blocked on the audit-race fix" — which only ever lived as prose in a
session summary, unqueryable and status-less.

adds `Goal` as a first-class artifact carrying GoalStatus open / done /
abandoned / blocked, proposed and approved through exactly the same
machinery as every other write. `propose_goal` files a pending proposal;
approve() writes diffable yaml under `.vouch/goals/`.

two invariants carry the review gate through:

- approval is pinned to `open`. a payload claiming any other status is
  refused at the precheck, because approving it would put a transition
  on disk that never passed the lifecycle path and so never reached the
  audit log.
- `lifecycle.set_goal_status` is the only mutation path. it appends a
  `goal.status` event to audit.log.jsonl and a row to the goal's own
  append-only history. a test asserts no second caller of
  store.update_goal exists anywhere in the package.

open goals resurface oldest-first — the objective open longest is the
one most likely to be stale — in `vouch digest` and in the SessionStart
recall digest, viewer-scoped like every other retrieval surface.

registered on all four surfaces plus the hot-memory coverage map and
the cli-mirror table. tests/test_goals.py covers propose→approve→
transition, the no-bypass invariants, and both digest surfaces.

closes vouchdev#427
@dripsmvcp
dripsmvcp requested a review from plind-junior as a code owner July 30, 2026 20:08
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance cli command line interface mcp mcp, jsonl, and http surfaces storage kb storage, migrations, schemas, and proposals retrieval context, search, synthesis, and evaluation tests tests and fixtures size: L 500-999 changed non-doc lines labels Jul 30, 2026
@plind-junior

Copy link
Copy Markdown
Member

I was missing this feature while comparing vouch with ditto

…rfaces

two ci gates were red.

`schemas/` is generated from the pydantic models and checked for drift on
every pr; adding `ProposalKind.GOAL` changed `proposal.schema.json` and
the regenerate step was missed. `python scripts/gen_schemas.py`, one
enum member added.

the diff-coverage gate wants 100% of changed python. what was uncovered
was the cli, mcp and jsonl bodies — registered and asserted-registered,
never actually called — plus the write gates, which are the part of this
feature worth pinning since they are what keeps a goal from reaching disk
without passing the review gate.

added: a cli round trip (propose, empty listing, approve, list, move,
--status all); the mcp tools round-tripping and surfacing every error as
the ValueError an mcp host can render; the jsonl envelopes; a goal
payload the model rejects at propose time and another corrupted after
filing; a goal whose cited claim vanishes between propose and approve;
put_goal on dangling refs and on a duplicate slug; update_goal on a goal
that is gone; list_goals on a kb bootstrapped before goals existed; and
the digest's elision line.
@github-actions github-actions Bot added schemas json schemas and generated schema assets size: XL 1000 or more changed non-doc lines and removed size: L 500-999 changed non-doc lines labels Jul 30, 2026
@plind-junior
plind-junior merged commit c0899c7 into vouchdev:test Jul 31, 2026
17 checks passed
minion1227 added a commit to minion1227/vouch that referenced this pull request Jul 31, 2026
`METHOD_SCOPES` was exhaustive over `capabilities.METHODS` when this branch
was written. four methods have merged to `test` since — kb.capture_correction
(vouchdev#679), and kb.list_goals / kb.propose_goal / kb.set_goal_status (vouchdev#676) —
leaving the table stale by four and all three matrix jobs red on
`test_every_method_is_classified`.

that failure is the guard working. by this branch's own rule an unclassified
method is denied to a scoped caller, so merging as-is would have silently
locked every scoped credential out of goal writes and correction capture. the
right failure direction, but still a regression, and invisible from the diff.

the classifications:

- kb.list_goals -> kb:read. a listing that cannot change durable state, beside
  kb.list_sessions.
- kb.propose_goal -> kb:propose. files a PENDING goal a human approves.
- kb.capture_correction -> kb:propose. routes exclusively through
  propose_quoted_claim and has no import of approve.
- kb.set_goal_status -> kb:approve, not kb:propose. this is the one that
  departs from the suggestion on the closing comment, and deliberately: it is
  a lifecycle op living in lifecycle.py beside supersede/archive/confirm, it
  mutates an already approved goal in place, and server.py documents it as
  "the only write path for goal status" — status moves never go through a
  second proposal. filing it under kb:propose would let a propose-only
  credential change durable state with no review, which is the exact boundary
  this module exists to hold.

no other change: the scope machinery, the two safety rules and the tests are
as reviewed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli command line interface docs documentation, specs, examples, and repo guidance mcp mcp, jsonl, and http surfaces retrieval context, search, synthesis, and evaluation schemas json schemas and generated schema assets size: XL 1000 or more changed non-doc lines storage kb storage, migrations, schemas, and proposals tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: first-class goals — review-gated in-flight objectives

2 participants