Skip to content

Move plan record state into GitHub fields - #15

Merged
DocksDocks merged 6 commits into
mainfrom
chore/plan-v3-issue-fields
Aug 21, 2026
Merged

Move plan record state into GitHub fields#15
DocksDocks merged 6 commits into
mainfrom
chore/plan-v3-issue-fields

Conversation

@DocksDocks

Copy link
Copy Markdown
Owner

What & why

The plan record carried a frontmatter block that duplicated fields GitHub already owns. Title, status, owner, and both timestamps had two writers and one arbiter, so a closed issue could still display plan:ongoing — which it did, live, on issue #12 after its merge.

The body now opens with <!-- plan-contract: v3 --> and carries only the human-authored plan. Title, assignee, and timestamps are read from the issue. Status is read from the phase label while the issue is open, and completion is derived from state and stateReason once it closes, so no field has two writers. archive stopped writing status and became a verifier: it accepts a closing commit only when an associated pull request merged into the default branch, which closes the direct-push loophole.

Legacy deleted, not deprecated

Both live records were converted first, so no reader needs a frontmatter parser. plan.mjs is 926 lines and its classifier has two outcomes: marker, or unreadable with no parser attempted. The 120 archived markdown files under docs/plans/finished/ stay byte-untouched in git history, and nothing parses them.

Two defects the migration found

Both were found by driving this tool through its own cutover, not by review.

  • check counted every occurrence of the version marker, so a plan about this contract could not quote its own marker. It now counts standalone marker lines.
  • Applying an export taken before an intervening body write silently reverted recorded step statuses. It cost verified state twice. An export now records the digest of the body it copied, and an edit whose file came from a superseded body is refused, naming both revisions. A first design compared the issue updatedAt and was discarded: a phase-only status change is a label write, which advances that timestamp without touching the body, so it would have refused safe edits. The suite pins that false positive as a regression fixture. A damaged provenance file stops the edit rather than silently disabling the guard.

Verification

24 acceptance rows, run against this repository and the installed CLI. Live: #12 reads finished from CLOSED/COMPLETED while carrying only plan; archive 12 accepted commit adbb40a through merged PR #13 and stripped the stale phase label; status 12 ongoing refused on a closed issue; blocked still demands a reason. Suites: plan-cli.mjs exit 0 with 199 assertion sites, plan-skill-phases.mjs exit 0 with docs/PLAN.md byte-identical to the shipped template fence. Gate: node scripts/ci.mjs --plugin plan-lifecycle exit 0.

Also deletes the two dead labels, plan:finished and plan-scheduled.

Out of scope

plan-lifecycle is bumped to 0.6.0 across both manifests and the versioned catalog, but releasing from main needs this to merge first and is deliberately a follow-up plan. No commit here creates itself; the lifecycle still creates zero commits and never pushes.

Closes #14

The plan record carried a frontmatter block that duplicated fields GitHub
already owns. Title, status, owner, and both timestamps had two writers and
one arbiter, so a closed issue could still display `plan:ongoing`.

The body now opens with `<!-- plan-contract: v3 -->` and holds only the
human-authored plan. Title, assignee, and timestamps are read from the issue.
Status is read from the phase label while the issue is open, and completion is
derived from `state` and `stateReason` once it closes, so no field has two
writers. Archive stopped writing status and became a verifier: it accepts a
closing commit only when an associated pull request merged into the default
branch, which closes the direct-push loophole.

The v1 and v2 paths are deleted rather than deprecated. Both live records were
converted first, so no reader needs a frontmatter parser; `plan.mjs` is 926
lines with a two-outcome classifier. The 120 archived markdown files stay
untouched in git history and nothing parses them.

Two defects surfaced while driving the tool through its own migration:

`check` counted every occurrence of the version marker, so a plan about this
contract could not quote its own marker. It now counts standalone marker lines.

Applying an export taken before an intervening body write silently reverted
recorded step statuses; it cost verified state twice here. An export now
records the digest of the body it copied, and an edit whose file came from a
superseded body is refused. A first design compared the issue timestamp and was
discarded: a phase-only status change is a label write, which moves that
timestamp without touching the body, so it would have refused safe edits. A
damaged provenance file now stops the edit instead of silently disabling the
guard.

Also deletes the two dead labels, `plan:finished` and `plan-scheduled`.

Closes #14
The v3 cutover edited `plugins/docks/` and `scripts/`, so the selected-plugin
gate was the wrong scope: it never lints `scripts/` and never backfills the
docks skill hashes. Remote CI failed three checks the local run could not see.

Deletes a dead `node:stream/promises` import that predates this branch and was
only latent because no lane linted it, orders the new `node:crypto` import as
organize-imports requires, and backfills the scaffold reference hash.

The stale-export guard also contradicted the contract's "no hashes" sentence.
That sentence now says what it means: the record carries no hash, and the one
digest that exists detects a stale export copy without authorizing anything.
Root AGENTS.md and the shipped template carry byte-identical wording.
… guard

Round-1 code review found the archive verifier and the export guard weaker than
the record claims.

The closing-proof query returned manually linked pull requests, so a collaborator
could link any merged pull request and pass the verifier; it now passes
`excludeUserLinked:true`, which keeps keyword closers. Verified against the live
API: microsoft/vscode#331368 returns its manual link in the plain connection and
cli/cli#14073 keeps its keyword closer under exclusion. The reviewer's paired
`includeClosedPrs` claim is false and the default stays: cli/cli#14073 returns a
merged closer without it, and cli/cli#14156 shows it only adds the
closed-unmerged references this verifier must reject.

The commit fallback took the last commit closer rather than the current closure,
so an issue closed by a commit, reopened, then closed by hand kept stale proof.
It now reads only the latest ClosedEvent.

A missing sidecar disabled the export guard, which made a copy or a deleted
sidecar a silent bypass. Every edit now requires provenance, matching the
documented export, edit, check, delete flow. The refreshed digest is staged
before the remote write, so a local failure costs one re-export instead of
leaving a digest that names a body GitHub already replaced.

Check 2 gained the inverse blocked rule: only a blocked plan may open Open
questions with a Blocked line.

Each fix is mutation-checked; reverting any one of the five fails
scripts/tests/plan-cli.mjs. The phase pins now hold whole clauses instead of the
words 'byte' and 'unreadable'.
…roof

Round-2 code review found both round-1 repairs over-reaching.

checkPlan guessed `drafting` whenever it ran without issue context, so a
legitimate blocked plan's export failed `check --file` on the very line the
contract requires. Phase lives in the labels, so a body alone now derives no
status and skips the phase-label rule, both `Blocked:` rules, and the
filled-Research rule; `check <issue>` still enforces all four.

archivePlan branched exclusively on a non-empty closing-reference connection, so
one open keyword reference suppressed a valid closing-commit proof. It now seeks
an eligible keyword closer first and consults the commit proof whenever that
search finds none. Every refusal message is unchanged.

Five compositions gained fixtures: a blocked body under `check --file`, a
placeholder-Research body under `check --file`, an ineligible keyword reference
beside a valid commit proof, a latest closer that is a pull request, and a
merged closer on the second page of closing references. Each was proven
load-bearing by reverting the behaviour and re-running the suite.

Acceptance row a28 was issue-bound, since file bytes carry no phase, and a30
covers the file-only path it wrongly claimed.
…port

Round-3 review found the fixture wrote the issue body straight to a file while
acceptance row a30 claims the file came from `plan.mjs export`, so a broken
export path would still have satisfied the row.

The fixture now runs `export`, asserts the exported bytes equal the issue body,
and checks that exported path.
…stic

The self-test asserted the manager skill links `references/plan-contract.md`
but reported a failure as a missing "v2 plan contract reference", which is the
last retired-version wording in the plugin payload.
@DocksDocks
DocksDocks merged commit 2cbd9b1 into main Aug 21, 2026
5 checks passed
@DocksDocks
DocksDocks deleted the chore/plan-v3-issue-fields branch August 21, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move plan record state into GitHub fields

1 participant