Establishes the committed .agents/docs/ convention (distinct from the uncommitted docs/ working-notes folder) and seeds the Ink-parity verification loop: - ink-parity-loop.md: design spec + reusable /loop prompt (audit → test-first fix → codex review → PR → CI → auto-merge, hard codex gate). - ink-parity.md: pinned Ink reference (v7.0.4, commit 40b3a75) + the intentional-divergence allowlist the audit skips. - parity-ledger.md: working ledger of audit sweeps and confirmed gaps. - AGENTS.md: "Context Engineering" section documenting the convention. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
Ink-Parity Verification Loop
Design spec + the reusable
/loopprompt for continuously verifying that vue-tui stays aligned with Ink: audit → confirm gaps → test-first fix → codex review → PR → CI → auto-merge, repeating until a clean sweep finds nothing.Related: ink-parity (allowlist + pinned Ink version), parity-ledger (working gap ledger).
Goal
Catch places where vue-tui has drifted from Ink's behavior — missing features, wrong output, mishandled input/escape sequences, divergent lifecycle — without "fixing" the differences that are deliberate design choices. Ship each confirmed gap as its own reviewable PR and merge it autonomously once it is provably correct.
Non-goals
- Chasing API-shape / naming / export-surface differences. Those are catalogued as
intentional in
ink-parity.mdand skipped. (See the allowlist.) - React-concurrent-mode features (Suspense/useTransition) — no Vue equivalent, N/A.
- Editing the
ink-parity.mdallowlist. The loop's only permitted write to that file is appending an item under "Candidate intentional divergences (needs human review)". It never edits existing allowlist entries and never promotes a candidate into the allowlist — a human does that.
Context-engineering convention
This loop relies on two committed docs under .agents/docs/ (the project's
context-engineering home, distinct from the uncommitted docs/ working-notes folder).
Philosophy borrowed from rolldown: one concept per file, files cross-link, you read the
relevant doc before working in an area, and you update the doc in the same change
that affects it.
A short "Context Engineering" section in AGENTS.md documents this convention so the
rule is discoverable from the source of truth.
Artifacts
.agents/docs/ink-parity.md (committed) — pinned reference + allowlist
- Target Ink: version + git tag + pinned commit SHA the audit diffs against.
- Intentional divergences (the allowlist): each entry is
area — what Ink does — what vue-tui does instead — why it's deliberate. The audit drops any candidate gap that matches an entry here. Curated by a human; the loop only appends a "Candidate intentional divergences (needs human review)" section, never promotes entries itself.
Seed entries (from prior parity work, Ink v7.0.4):
render() → createApp() rename · exports measureText/measureTextNatural (Ink does
not) · useExit() instead of useApp() full AppContext · AriaRole/AriaState props ·
named type re-exports (BoxProps/TextProps/…) intentionally absent.
.agents/docs/parity-ledger.md (committed) — the working ledger
Records audit sweeps and confirmed gaps so the loop survives restarts and never re-does merged work.
## Sweep history
| sweep | Ink SHA | candidates → confirmed | status |
|-------|---------|------------------------|--------|
## Confirmed gaps
| id | area | summary | priority | status | branch | PR |
|----|------|---------|----------|--------|--------|----|
status ∈ todo · in-progress · pr-open · merged · blocked.
Recording merged (a merged PR can't update itself). A fix PR commits its own row as
pr-open. The flip to merged happens at the next wake's reconciliation step: any
pr-open row whose PR is now merged is set to merged, and that edit rides along in the
next fix PR (bundled at its top). The loop therefore trails reality by one PR; the
stop-condition's final reconciliation lands the last merged flip as its own tiny
chore(parity): reconcile ledger PR. This keeps every ledger write inside a reviewed PR.
Phase A — Audit sweep
Runs only when there is no work in flight — i.e. the ledger has no todo,
in-progress, or pr-open rows (or on first run). A blocked row is not a reason to
audit: it pauses for the user (see stop condition).
- Pin & clone Ink at the tag recorded in
ink-parity.md→/tmp/ink-<sha>(a local throwaway clone, re-pulled each sweep — agents and codex read it from disk so no network is needed mid-review). Record the SHA in a new ledger sweep row. - Fan-out diff (Workflow) — parallel agents, one per Ink area, comparing real Ink
source to vue-tui source: components (Box/Text/Static/Transform/Newline), hooks
(input/focus/app/stdout/stderr), render lifecycle,
io/escape sequences + writes, text wrapping, exit semantics. Each returns candidate differences with file refs. - Allowlist filter — drop every candidate matching
ink-parity.md. - Adversarial verify — a second, independent agent tries to refute each survivor: is it actually intentional? already covered by an existing test? not really present in Ink at this SHA? Default to "not a gap" when uncertain. Only survivors are confirmed.
- Rank & record — write confirmed gaps to the ledger, priority-ordered (correctness/behavior bugs first, omissions second).
Phase B — Fix loop (one cohesive gap per PR, priority order)
First, reconcile: for every pr-open row whose PR has merged, set its row to
merged; carry those edits into this iteration's PR (bundled at the top). Then take the
highest-priority todo gap:
- Branch from
main→fix/parity-<slug>; mark ledger rowin-progress(working state). - Test-first — write a failing test reproducing the gap. Honor the project's test
discipline:
FORCE_COLORfor ANSI,CI: "false"injected, process-global tests in*.sequential.test.*. Confirm the test fails for the right reason. - Fix the code; confirm the test passes.
vp run ready(fmt → build → lint → type → test) must be fully green.- Codex review — run a codex review of the diff; address every finding.
- Push; open PR (squash; co-author line included — this is the user's own repo). The PR's
commits set this gap's ledger row to
pr-openwith branch + PR link (its flip tomergedcomes from the next wake's reconciliation above). - Wait for CI (dynamic wake on completion).
- Merge gate (hard) — merge only when both:
- CI is fully green, and
- a codex pass reports zero unaddressed findings. Green CI alone is not sufficient. If codex still has findings, address and re-push; do not merge.
- On gate pass → auto-merge (squash). The row stays
pr-openuntil reconciled next wake. - On CI red → systematic-debugging, push fix, re-wait. After 3 failed attempts on
the same gap → mark
blockedand pause + ping the user.
Stop condition
Only consider stopping once no todo, in-progress, or pr-open rows remain (all
known work merged and reconciled). If any blocked row exists, do not declare
victory — pause and ping the user to resolve it. Otherwise land a final
chore(parity): reconcile ledger PR to flip the last pr-open→merged, then run one
fresh audit sweep. If that sweep yields zero new confirmed gaps, report
parity verified against Ink <sha>, no actionable gaps and end the loop. Otherwise
continue with the new gaps.
Pacing & rails
- Dynamic
/loop(self-paced): wake when CI / background work completes; set a long fallback wake (~1200s) so a hung CI never freezes the loop. - Never touch allowlisted divergences. The loop's only permitted write to
ink-parity.mdis appending under "Candidate intentional divergences (needs human review)" — never edit or promote allowlist entries. Never commit anything underdocs/(the.agents/docs/artifacts here are the committed exception). - One gap per PR keeps every merge reviewable.
- Codex findings are a hard merge block (above).
The reusable /loop prompt
Paste this whole block after /loop (no interval → self-paced):
Verify vue-tui's alignment with Ink and close real gaps, one PR at a time, until a clean sweep finds nothing.
State lives in two committed docs — read both before doing anything:
- .agents/docs/ink-parity.md → target Ink version+SHA and the intentional-divergence ALLOWLIST. Your ONLY allowed write here is appending under "Candidate intentional divergences (needs human review)"; never edit or promote existing allowlist entries.
- .agents/docs/parity-ledger.md → sweep history + confirmed-gap table (id/area/summary/priority/status/branch/PR). status ∈ todo·in-progress·pr-open·merged·blocked.
Every wake, FIRST reconcile: for each `pr-open` row whose PR has merged, set it to `merged` (carry these edits into this iteration's PR, bundled at the top). Then do the next actionable thing:
A) If, after reconciliation, the ledger has no `todo`/`in-progress`/`pr-open` rows → run an AUDIT SWEEP (a `blocked` row is NOT a reason to audit — see STOP):
1. Clone Ink at the pinned SHA from ink-parity.md into /tmp/ink-<sha> (local throwaway clone; agents read it from disk, no network needed mid-review). Add a sweep row to the ledger.
2. Run a Workflow that fans out parallel agents to diff Ink source vs vue-tui source, one agent per area (components, hooks, render lifecycle, io/ escape sequences, text wrapping, exit semantics). Collect candidate differences with file refs.
3. Drop every candidate that matches the ink-parity.md allowlist.
4. Adversarially verify each survivor with a second agent that tries to REFUTE it (intentional? already tested? not actually in Ink at this SHA?). Default to "not a gap" when unsure.
5. Write confirmed gaps to the ledger, priority-ordered (correctness/behavior first, omissions next).
STOP only when no todo/in-progress/pr-open rows remain. If any `blocked` row exists, pause and ping me instead of declaring victory. Otherwise land a final `chore(parity): reconcile ledger` PR, then run one fresh sweep; if it finds zero new confirmed gaps → report "parity verified against Ink <sha>, no actionable gaps" and STOP the loop.
B) Otherwise take the highest-priority `todo` gap and ship it:
1. Branch from main → fix/parity-<slug>; set ledger row in-progress (working state).
2. TEST-FIRST: write a failing test reproducing the gap (FORCE_COLOR for ANSI, inject CI:"false", process-global tests in *.sequential.test.*). Confirm it fails for the right reason.
3. Fix the code; confirm the test passes.
4. Run `vp run ready` — fmt/build/lint/type/test must all be green.
5. Run a codex review of the diff; address every finding.
6. Push; open a squash PR (co-author line included). The PR's commits set this gap's row to pr-open with branch+PR link (its flip to `merged` comes from next wake's reconciliation).
7. Wait for CI (schedule a dynamic wake on completion; long fallback ~1200s).
8. HARD MERGE GATE — merge ONLY when CI is fully green AND a codex pass reports zero unaddressed findings. Green CI alone is not enough; if codex still has findings, fix, re-push, re-wait.
9. On gate pass → squash-merge. Row stays pr-open until reconciled next wake.
10. On CI red → debug systematically, push a fix, re-wait. After 3 failed attempts on the same gap, mark it `blocked` and STOP to ping me.
Rails: never touch allowlisted divergences; the only write to ink-parity.md is appending the candidate section; never commit under docs/; one gap per PR; update the ledger in the same PR as the change.