Skip to content

feat(tracker): pluggable issue tracker with Jira and Linear providers - #344

Merged
dean0x merged 152 commits into
mainfrom
feat/325-tracker-phase-3-init-selected-provider
Sep 19, 2026
Merged

dean0x merged 152 commits into
mainfrom
feat/325-tracker-phase-3-init-selected-provider

Conversation

@dean0x

@dean0x dean0x commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Problem Being Solved

Phases 0–2 left a provider-shaped hole with one provider in it. Devflow users whose issues live in Jira or Linear got GitHub-only traceability: issue creation, Closes refs, back-links, tech-debt tracking and release evidence all assumed gh. Selecting another tracker was not possible, and the one always-loaded gate that every shipped-issue back-link passed through required issue references to be digits only — so even a hypothetical Jira user would have had the operation silently drop every PROJ-1.

This phase fills the hole with two things that are deliberately never conflated: selection (a manifest enum read by a hook and a prompt preamble — nothing infers it) and conventions (a global file inferred once by a background agent, existing only for a non-GitHub user).

Key Changes

3a — selection substrate, the agent, the hook, the tool-call substrate.

  • src/core/tracker.ts (registry, strict parseTrackerId, tolerant normalizeTrackerFeature, the rearmTrackerInference / applyTrackerSentinel / renameStaleTrackerConventions lifecycle trio) + tracker-prompts.ts + tracker.ts CLI; init.ts wiring on both wizard paths behind one shared predicate, --tracker <id>, devflow tracker --status/--set, manifest.features.tracker with a silent self-heal that is deliberately kept out of the hard-null set, seed/--reset gating, and uninstall classification (tracker.md is user content; the three .tracker.* files are install artifacts, and the two lists stay disjoint).
  • The background Tracker agent (17th agent, hook-spawned, no roster row) + plugins.ts registration.
  • session-start-context Section 3: sentinel gate, manifest read, a positive jira|linear allowlist before any interpolation, claim file, source-gating, attempt cap of 5 — and zero subprocess invocations under github, proven by a stub-jq invocation counter.
  • redact-secrets.cjs --emit (nonce framing, double scrub, exit code 5, no body on any non-zero path) + _mcp.mds, the provider-independent tool-call contract behind a registry gate + the Git-agent preamble's resolution order, mismatch guard and - **Tracker**: line.
  • D-F, landed as a fix during the phase: devflow tracker --status re-arms the attempt counter. Asking why nothing is being learned is exactly the moment to hand back another five tries; both re-arm paths are documented.

3b — Jira mechanics. Ten generated references, all tool calls selected by capability description rather than by tool name, 32767 / Retry-After / no pre-emptive rung, a single-query batch, the ≤50 × ≤2 = ≤100 aggregate budget, namespaced first-line markers, a pointer sentence where GitHub posts a collapsed block. It also surfaced D-OVERLAY-PROVIDER-SHAPE, a production defect in the installer: tracker/_mcp.md lands directly in tracker/, and planOverlayUnits bucketed it as a provider unit named tracker — a unit whose atomic swap renames tracker/ itself over every provider directory beside it, with a staging sibling outside the subtree the prune converges. The github files survived by promotion order alone. Classification is now by SHAPE (tracker/{provider} exactly), with the flat set carrying the directory it lands in and one staging name per directory.

3c — Linear at rank 4, and the wording that was still GitHub's.

  • Ten generated references under references/tracker/linear/. Rank 4 is stated rung by rung (OD-12): no viewer/"me" tool and a binary-payload attachment create on a stock official server make three of the four dedup rungs unreachable, so devflow posts with a warningdedup unavailable — duplicate possible on every run — and the absent identity capability is never a reason to suppress. The marker carries both halves rank 4 needs: first-line equality against a quoter, and the project URL against a coincidence.
  • HTTP 400 RATELIMITED named with its error-text form, because the generic 4xx rule would keep the fan-out running into the window the rung exists to stop. Two separately anchored ref forms after ASCII-upper normalisation.
  • ## Known Unknowns in the module (module-level prose, emitted nowhere by design — a column-0 ## inside a generated reference truncates its own section), surfaced for users in docs/cli-reference.md with the rank-4 statement, with Probe Linear's real comment-body and rate-limit behaviour (32767 is borrowed from Jira, unmeasured) #343 as the owner and artifact for the borrowed 32767.
  • tests/provider-literals.test.ts — the 5 × 3 literal matrix over sources and generated trees, plus the cross-provider [DR-08] batch negative.
  • The [DR-19] arm the contract never had: a separate MCP_SHARED_LITERAL_REGISTRY, because the sibling registry's ownership arm fixes its owners as exactly the three cross-cutting documents.
  • Two GitHub shapes in always-loaded, provider-blind positions, neutralised. The ## Operations table's three tracker cells (and the two op descriptions that duplicate them) now say "tracker issue"; backlink-shipped-issues step 0 defers to the resolved provider's anchored grammar, with GitHub's ^#?[1-9][0-9]{0,8}$ moved into the github mechanics beside the same drop rule. Net effect: the always-loaded prompt is 25 characters and one line shorter, so every per-spawn budget row gained headroom while gaining a provider. src/assets/skills/git/SKILL.md's X-RateLimit-Remaining threshold was kept, deliberately, and recorded as such.
  • ★ The relocated gate ADMITS ONE MORE SPELLING THAN main's did, and the normalisation is what neutralises it. main's always-loaded step 0 required entries to be digits only, so #42 was dropped; §14.1 fixes the github grammar as ^#?[1-9][0-9]{0,8}$, which admits it — and the operation interpolates the admitted entry into gh issue view {number} and gh issue comment {number}. A # at word start opens a shell comment, so the widened set had to come with a rule: strip exactly one leading # once, before the loop, and interpolate only the stripped digits (#4242, the same normalisation fetch-issue and fetch-issues-batch already state). GAP-18 calls this pre-flight the shell-injection guard for an interpolated ref, and that is the sense in which it is not relaxed — the admitted set is wider by one normalised form, and the normalised form is the only one that reaches a command. Pinned per file in tests/provider-literals.test.ts (five rows, each with a per-row known-bad probe), because the tree-level matrix cannot say which file states it.
  • ★ AC-3.1 ⇔ AC-3.11: the Output templates' # is a RENDERING, and a preamble rule says so at zero template bytes. The templates read - **Issue**: #{number}, - **Number**: #{number} and ### Issue #{number}:, and the first of them is line 48 of the frozen github-status-lines.txt — so AC-3.1 makes them uneditable for the life of this phase while AC-3.11 wants - **Issue**: PROJ-123 under Jira. The two criteria meet on the same bytes, and §14.1 is the arbiter: #-prefixing is a property of the github rendering of ISSUE_REF, not of the field. The resolution is therefore one bullet in ## Tracker input contractevery rendered issue ref takes ## Reference Rendering's form, never #-prefixed — the Output templates' # is github's rendering, not a literal — pinned as claim 6 of tests/tracker/schema-scope.test.ts, with a companion arm asserting the three slots still carry the # (if they lost it the rule would be a rule about nothing, and the frozen fixture would already have broken). The branch half of AC-3.11 ({type}/{KEY}-{slug}, type exact-matched against ## Issue Types) is deliberately not restated in the always-loaded file — the byte budget refuses a second copy and GAP-37 forbids one — and is pinned per provider instead.
  • GAP-48 (optional per-op capability attestation line, P3a-S14) deferred: no reachable consumer at the 3c boundary and no budget headroom; owner dean0x; revisit with chore(prompt-surface): put devflow on a diet — condense every installed surface without losing detail #342 (prompt diet).
  • Five standalone golden-regeneration commits in the phase, each fixture-only and reviewable as a text diff: 2bf24b3 (3a's preamble), bc36a48 (3c's neutral wording), 5db2639 (3c's reference wording), 9e8cd83 (the alignment fixes) and f131c5c (scoping the rendering rule to issue refs). github-status-lines.txt is untouched in all five, verified by cmp against main.

Breaking Changes

None for GitHub users (AC-3.1, AC-3.12). No prompt, no new file, no altered byte on the GitHub path; under provider github the new session-start gate forks nothing, and the frozen status-line fixture is byte-identical to its Phase-0 capture.

One deliberate UX change. A jira/linear user with no other user content now sees a confirm prompt on devflow uninstall where they previously got a silent artifacts-only sweep — because ~/.devflow/tracker.md is classified as user content. Documented in CHANGELOG.md ### Changed. The reversal condition is recorded at the code site and in the feature KB: that classification is safe only because the reader-side mismatch guard removes the silence a stale file would otherwise have. If that guard is ever dropped or softened, tracker.md moves back to installArtifactPaths in the same change.

Reviewer Focus Areas

  1. init.ts blast radius — both wizard paths behind one shared predicate, re-init covered, and the manifest's hard-null set untouched.
  2. The redact-secrets.cjs boundary amendment — the normative single-stdout block amended rather than bypassed; exactly two stdout write sites; "no body on any non-zero exit" proven on every path; the parseArgs/frameEmit/scrubTwice decomposition present; the framing carries the first-pass <n> [type:count,…], and _mcp.md mandates both SECRET-EXPOSED and the <bytes> check.
  3. The hook's allowlist-before-interpolation — the provider case runs before any interpolation and is a positive allowlist (jira|linear), never != github; the silence clause is byte-identical to Section 2's; the GitHub path forks zero subprocesses.
  4. The agent-before-hook commit order and the >= 50 subagent_type floor.
  5. No Jira/Linear literal reaches dist/agents/git.md, dist/commands/*.md or references/tracker/github/**, and the frozen github-status-lines.txt is byte-identical.
  6. The DEGRADED literals against §14.2's canonical table — both registry directions, including the retired-synonym absence assertion.
  7. Linear's honest rank-4 statement, its ## Known Unknowns, and the filed probe issue with a named owner (Probe Linear's real comment-body and rate-limit behaviour (32767 is borrowed from Jira, unmeasured) #343).
  8. D-OVERLAY-PROVIDER-SHAPE — production code outside the "prompt assets + tests" domain of the subtask that fixed it, taken because the manifest that subtask shipped would otherwise have made the install delete provider directories. Review the OverlayUnitRef type change (its cross-cutting arm gained dir) and the one-staging-name-per-directory rule.

Test Plan

Suite: 144 files / 5,174 tests, all passing — 143 / 5,095 in the main run plus eager-memory-refresh (1 / 79) run separately, because it spawns real claude sessions and its wall time swamps the signal. Up from 141 / 5,009 at the phase branch point. npm run build green (MDS 19 compiled, 0 deferred, 0 errors, 0 warnings), npm pack --dry-run carries all 34 generated references and every shipped .mds source.

RED-first evidence (R1). Every guard in this phase was observed red before its subject existed or its claim became true, and each one's failure text is recorded in the branch's handoff notes. The 3c highlights:

  • linear-module.test.ts against the pre-module tree: fail-loud at module scope, 0 tests collected, not a silent skip.
  • Registering the provider turned six gate arms red at once, each a claim that had just become false: the deferral probe naming one gated sub-directory (expected [] to deeply equal [ '…/_mcp.mds' ] — with two tool-call providers, dropping the first left the second holding the gate open), the provider-subdir set equality, the bypass regex firing on prose (text: a status-shaped rule…), PROVIDER_OWNED_PATHS reporting 45 sites, the every-provider-has-a-ceiling arm naming linear, and reasonSpellings gaining unsupported by linear.
  • The [DR-19] negative arm proven red against a real restatement seeded into a generated Linear reference, then restored — not only against its own inline probe.
  • The provider-literal matrix red in both directions against swapped literals; the ref-grammar table red against an unanchored alternation that admits a payload every shipped form rejects.
  • The Linear loaded-set ceiling: an earlier, wordier draft of the git.md neutralisation breached the Jira row (51 ch of headroom, +54 needed) — the response was to condense the clause, never to move a ceiling.

AC-3.17's pinning is prompt-text assertions + the claim-staleness seam + hook-side fresh-claim suppression — not a concurrency test. The plan's AC table says "concurrency test" and nothing in this branch spawns two agents at once. What is actually asserted: the agent's own prompt text (claims atomically, the loser exits silently and never overwrites, wx EEXIST is not a lock wait, the heartbeat touch, and the delete-as-final-act — tests/tracker-agent.test.ts); the two-sided staleness bound, where the hook's TRACKER_PROCESSING_STALE_SECS=600 and the agent's **600 seconds** literal are compared in tests/seams/tracker-claim-staleness.test.ts, which reports an unstated bound rather than reading it as agreement; and the hook's suppression of a fresh claim, so a live agent owns its batch. Together these cover the criterion's content; they do not cover the race itself, and calling them a concurrency test would overstate them.

Prefix-shippability at this boundary. Clause (i) run and green (build && test && pack --dry-run, git status --porcelain empty). Clause (iii) run: every Phase-3 artifact was swept for a reachable consumer in the end-state tree — all 30 provider defines map to a generated file and back, both directions, with no orphans. Clause (iv) verified by cmp against main: identical. Clause (ii) — the tarball install into a scratch $HOME and the five-command walk-through — is manual and is a checklist item below, together with npm run test:integration, which spawns real claude sessions and is the verification gate's to run.

Byte budget, printed rather than quoted (tests/tracker/byte-budget.test.ts):

Row measured ceiling headroom
git.md 58,782 ch / 59,245 B 58,870 88
GitHub path loaded set 80,837 80,944 107
Jira path loaded set 88,615 88,660 45
Linear path loaded set 90,984 91,000 (new) 16 — the binding gate

Two further gates are asserted but deliberately not quoted here, because no row of that table prints them: git.md outside the preamble against the unraised Phase-2 allowance, and the preamble's line count against PREAMBLE_MAX_LINES. Their figures live in the assertions and in the failure messages, not in prose.

No existing ceiling was raised. The one new entry is derived from its own measurement, its delta over the GitHub ceiling is held to what the provider actually adds, and both per-provider gates are now generated from one table so a fourth provider adds a row rather than a copied pair of assertions.

Read the Linear row, not the git.md row, before adding always-loaded text. It contains git.md, so a character added to the agent is charged to all four gates and the smallest margin decides — the git.md ceiling's apparent slack is unspendable while the Linear row is the binding one.

Figures in this section had drifted before the alignment pass measured them, so every "measured"/"headroom" number in byte-budget.test.ts and in the numeric-floors.json entries is re-derived from the printed table rather than carried forward. Per-pass decompositions are deliberately not recorded — here or in the test comments. They split each pass into characters added against characters recovered, and three successive re-derivations of those components disagreed with one another (PF-057), because no printed row produces them. The decisions they carried are kept instead, in words: the rendering rule was funded by deleting backlink-shipped-issues' restatement of the D4 rate-limit rung — a GitHub header named in a provider-blind always-loaded line, where the always-loaded contract already states the rung provider-neutrally and defers the signal to the resolved provider's reference — and the second alignment pass then scoped that rule to issue refs, because the Output templates carry **PR**: #{number} slots as well and PR hosting stays on GitHub under every provider, so an unqualified "every rendered ref" told a jira spawn to render PR numbers unprefixed.

Process-only acceptance criteria

  • AC-3.19 — prefix-shippability run at each of 3a / 3b / 3c with a green suite
  • AC-3.2 (live) — in a Jira project with provider jira and no tracker.md, a session start emits the directive once, the background Tracker agent writes the file, and the next tracker op resolves from it, with zero user-visible narration and zero questions
  • AC-3.11 (live) — /implement PROJ-123 produces branch {type}/PROJ-123-{slug}, - **Issue**: PROJ-123, - **Tracker**: jira, and the rendered PR link line
  • Clause (ii) — tarball installed into a scratch $HOME and the five-command walk-through driven on a GitHub project

Related Issues

Closes #325

Part of #321 · Probe follow-up: #343

Tracker Phase 3, subtask 3a-1 (part 1 of 2) — the selection substrate.
Adds `manifest.features.tracker = { provider }`, an enum over
github|jira|linear defaulting to github, chosen at `devflow init` on both
wizard paths, settable non-interactively with `--tracker <id>` and
afterwards with `devflow tracker --set <id>`.

GitHub users — the default, and every existing install — see no prompt,
no new file and no changed behaviour.

- `src/core/tracker.ts` (new): the provider registry, the strict boundary
  parser `parseTrackerId`, the tolerant sink normaliser
  `normalizeTrackerFeature`, the shared `features.tracker.provider` key
  path, and the three ~/.devflow file-lifecycle owners —
  `rearmTrackerInference` [DR-22], `applyTrackerSentinel` [DR-10] and
  `renameStaleTrackerConventions` (P3a-S15). Parsing REJECTS, never
  repairs: `jira-cloud` errors rather than becoming `jira`, so a typo can
  never select mechanics the user did not name.
- `src/cli/commands/tracker-prompts.ts` (new): the four-part wizard-step
  contract, mirroring compliance-prompts.ts. `shouldRunTrackerStep` gates
  BOTH wizard paths on `modePromptShown`, never on the mode name, so
  `--recommended` and the non-TTY fallback keep their promptless
  contracts (PF-029). `runTrackerStep` never exits and never throws
  (PF-014) — the caller owns cancellation.
- `src/cli/commands/tracker.ts` (new): `devflow tracker --status/--set`.
  `--status` reports the selection and tracker.md's provenance; `--set`
  moves a stale conventions file aside, persists through the already
  generic `syncManifestFeature`, re-arms inference, and converges the
  presence sentinel.
- `init.ts`: the `--tracker <id>` option, a boundary parse before any
  prompt, the wizard step on both paths behind the one shared predicate,
  the Recommended summary row and the mandatory Advanced outcome line,
  and the single post-resolution lifecycle block.
- `manifest.ts`: `features.tracker` is absent-tolerant and is NEVER in
  the hard-null validation set — a pre-tracker manifest, i.e. every
  existing install, must keep parsing (AC-3.21).
- `init-seed.ts`: seed, registry default, and the mandatory defensive
  spread so the module-level default is never handed out by reference.

Refs #325
Tracker Phase 3, subtask 3a-1 (part 2 of 2) — the uninstall
classification and the @d8 disjointness boundary.

`~/.devflow/tracker.md` is USER CONTENT: it is inferred once per machine
and then hand-editable, and its ABSENCE is the trigger that re-runs
inference, so removing it on the decline/cancel/non-interactive/
--keep-docs paths would silently discard work the user may have
corrected by hand. It is enumerated between preference-profile.md and
learning.json; `resolveDevflowDirCleanup` needed no change — a
`userContent` entry automatically flips a user-scope interactive
uninstall from 'artifacts-only' to 'prompt'.

That flip is the one accepted UX regression of Phase 3: a user with no
other user content previously got a silent artifacts-only sweep and now
sees a confirm prompt.

The three runtime files beside it — `.tracker.processing` (the agent's
atomic claim), `.tracker.attempts` (the inference attempt counter) and
`.tracker.enabled` (the presence sentinel) — are INSTALL ARTIFACTS, so
an artifacts-only sweep takes all three and keeps tracker.md. The two
lists stay disjoint (@d8); test 9f's non-vacuity floor rises 5 → 6 and
9c's residue equality now pins tracker.md as surviving while the three
artifacts are asserted gone.

REVERSAL CONDITION, recorded at the code site: this classification is
conditional on the provider-mismatch guard shipping in the same 3a
group. agent-models.json was reclassified to an artifact precisely
because stale overrides re-apply *silently*; a stale tracker.md is only
safe to preserve because a frontmatter provider that disagrees with the
resolved provider produces a named DEGRADED and no tracker call. If that
guard is ever dropped, reclassify tracker.md to an install artifact in
the same change.

Refs #325
The 17th agent. Spawned only by the session-start setup directive, it probes
what the configured tracker can do, infers repository conventions from a bounded
history scan, and writes ~/.devflow/tracker.md exactly once — or writes nothing.
A defaults-only file would satisfy the existence gate forever and destroy the
retry trigger, so a session that cannot reach any tracker capability writes
nothing, records the attempt, and leaves the next session to try.

It carries the ~/.devflow/tracker.md schema template (the writer half of the
schema; the reader half follows), a scrub-gated create-exclusive write, and the
attempt-counter final-act rule the session-start cap depends on: without an
incrementer in the one place that knows a run produced nothing, that cap never
engages.

Registered in the commands-less devflow-core-skills plugin beside `learning`,
which is what makes the Guard-5 reverse check pass structurally rather than by
exemption. No _roster.mds row: that file is set-equal to the agentType values in
dist/commands/, and a hook-spawned agent appears in none of them.

Declares no `tools:` key deliberately — tracker server names are user-configured
and cannot be enumerated at authoring time, so an allowlist would fail at
runtime in an unwatched background run rather than at build time. A read-only
boundary section compensates, and is mechanically pinned.

The agent is provider-agnostic: the validated token arrives in the spawn
directive, so naming a provider here would be a second resolution site as well
as provider-module vocabulary landing before its module.

Guards, in the same commit because the prompt's rules are otherwise unobservable
(the agent runs unattended and its summary is never read):

  tests/tracker-agent.test.ts     identity, model tier matched against
                                  loadShippedDefaults(), the absent tools: key,
                                  the read-only boundary, the claim/heartbeat/
                                  final-act lifecycle including the
                                  counter-before-claim ordering, the write
                                  chain, the name-not-copy rule for
                                  references/learn-conventions.md, and the
                                  template's ordered heading list
  tests/tracker/hostile-values    the field x payload matrix, with shapes read
                                  out of the agent's own schema table rather
                                  than restated, so a validator that drifts
                                  laxer fails here instead of being proven by a
                                  private copy
  tests/helpers.ts                the shared heading list and table parsers —
                                  the writer/reader equality test that follows
                                  cannot catch drift in its own oracle

numeric-floors.json agent-roster-count 16 -> 17 with its resolver assertion, in
this same commit: a floor may only rise, and the registration is what moves it.

Refs #325
describeTrackerValue's sanitiser class was written with RAW control bytes
(a literal NUL, US and DEL inside /[...]/). The NUL made grep classify
src/core/tracker.ts as binary — it printed "Binary file matches" and
skipped the lines — so every grep-based guard over src/core/ silently
stopped covering this file while still exiting 0, and `git diff` rendered
any change to the class as "Binary files differ". The escaped spelling is
behaviour-identical and greppable (grep -c "^export" now reports 23
instead of nothing).

Adds tests/guards/no-control-bytes.test.ts: a named collector over every
shipped source file under src/ (.ts/.md/.mds/.cjs/.js/.json plus the
extension-less shell hooks), forbidding 0x00-0x08, 0x0b, 0x0c, 0x0e-0x1f
and 0x7f while allowing tab/LF/CR, with an inline known-bad probe per
forbidden range asserted inside the same it (R1(b)) and scope sentinels
resolved through agentsDir()/scriptsDir().

RED against the pre-fix tree:
  src/core/tracker.ts:160: raw 0x00: expected [ '...' ] to deeply equal []

Refs #325
… start

Section 3 of session-start-context: when this machine's manifest names a
non-GitHub issue tracker and no ~/.devflow/tracker.md has been inferred
for it yet, inject a silent "--- TRACKER SETUP ---" directive instructing
the main model to spawn the background Tracker agent.

[DR-10] The gate is two shell builtins and nothing else, so a GitHub user
pays one stat and ZERO forks per session. The .tracker.enabled sentinel is
what makes that possible: tracker.md is written only for jira/linear, so a
bare "does tracker.md exist" early exit would never fire on the default
provider and every SessionStart would fall through to the manifest read.
Proved differentially at runtime with an additive PATH shim that records
every exec of jq/node/date/stat (PF-045 — nothing is subtracted), with a
positive control on the jira path so the zero is not a counter that never
moves.

Five gates, cheapest first: the sentinel + tracker.md existence tests
(builtins), the OD-14 attempt cap (read builtin), source in
{startup, clear}, claim-file freshness, then the provider allowlist.
The provider case is a POSITIVE allowlist (jira|linear) that runs before
any interpolation — never != github — so a hand-edited manifest value
carrying quotes or newlines cannot reach additionalContext; a 21-row
hostile table asserts the payload appears nowhere in stdout or stderr.

[DR-02] The hook increments .tracker.attempts when it EMITS, so a crashed
agent that never reaches its own increment still burns an attempt. The
counter's shape is one decimal-integer line, parsed defensively: read's
exit status is not consulted (a missing trailing newline is not a read
failure), a non-digit value self-heals to 0 and is rewritten well-formed,
and a 7+ digit value is treated as AT the cap because [ "$N" -ge 5 ] past
intmax_t fails OPEN.

TRACKER_PROCESSING_STALE_SECS=600 is its own named literal, never shared
with Learning's 900: one constant would make a change to either feature
silently reclassify the other's live runs as crashed.

The resolved-root idiom ${DEVFLOW_DIR:-$HOME/.devflow} is captured ABOVE
the project-scoped DEVFLOW_DIR assignment that shadows the inherited env
value, with a comment at both sites.

Also:
- tests/seams/tracker-key-path.test.ts — the TS-to-shell seam for
  TRACKER_PROVIDER_KEY_PATH: exactly one live reader in the hook, the
  allowlist read out of the hook rather than restated, and 14 manifest
  shapes x 2 json-parse backends asserted against a hand-pinned oracle
  (the two readers legitimately return different tokens for a malformed
  shape and must still reach the same verdict).
- AC-3.22: every session-start-context invocation in
  config-disable-guards.test.ts now runs under a seeded temp $HOME with
  DEVFLOW_DIR='' — two of its emptiness assertions previously inherited
  the developer's real HOME and went red locally for any maintainer who
  had run `devflow init --tracker jira`, while CI stayed green.
- numeric-floors.json: new ceiling tracker-section-max-chars (800,
  measured 722) over the directive template.

Refs #325
The first step of the Git agent's provider resolution order (OD-9): the
`tracker` key in a project's `.devflow/config.json`, which is the only
signal that can say "this repo, specifically".

Three states, none collapsible. Absent means NO override and requests
ref-grammar corroboration -- deliberately not the same as a chosen
`github`, which short-circuits it. Valid is a byte-exact registry id.
Invalid carries the raw value so `DEGRADED (unknown tracker provider)`
can name it; that reason exists only because "invalid" is distinct from
"absent" (DR-26), and the manifest value's malformed case self-heals
silently instead.

parseTrackerOverride delegates membership to parseTrackerId rather than
copying reviewPublication's closed-domain ternary: that ternary heals any
invalid value to 'auto', which is right for a publication mode and wrong
for a provider -- a repaired provider is GAP-10's laundering path, and
repair is forbidden here (14.9-6, reject-never-repair).

The field is carried through coerceConfig VERBATIM because updateFeature
is a read-modify-write over the whole config: a key it dropped would be a
key `devflow knowledge --disable` DELETES, silently reverting a Jira repo
to the manifest provider through an unrelated toggle. That round-trip is
the reachable consumer the key has at this boundary (ADR-003), and it is
pinned as its own test.

BooleanFeature gains `-?`: a mapped type over an optional property yields
`K | undefined`, so a bare mapping stopped compiling the moment the
optional field joined the interface.

Refs #325
GAP-04. The file-sink gate is a shell `&&` chain; a tracker reached
through a tool call has no `--body-file` and no shell operator between
the scrub and the post, so the `&&` cannot exist and the gate silently
degrades to an instruction. `--emit` restores a mechanical one: the
scrubbed bytes are obtainable only from behind a framing line this script
alone can produce.

  D11-OK <nonce> <sha256> <bytes> <n> [type:count,...]
  <the scrubbed body>

Each field answers a specific channel failure. The nonce is
per-invocation and required (14.9-3): composed bodies carry untrusted
issue text, so a fixed literal would be forgeable by anyone who can write
a comment. <bytes> is the UTF-8 length (DR-06) so a consumer can detect a
harness-TRUNCATED result -- truncation leaves line 1 intact, so a bare
"no framing line means do not post" gate passes while the body is
partial. <n> [type:count,...] is the FIRST pass's count (DR-01); the
second pass is always zero by construction, so without it the only signal
that a real credential was present is computed and discarded and the user
is never told to rotate it.

The second scrub pass returning `SCRUB: 0 []` IS the gate. New exit code
5 covers a non-zero second pass and an unavailable nonce, distinct from 4
so a caller can tell "the scrub did not hold" from "the script broke".

The normative single-stdout-boundary block is AMENDED, not bypassed:
main's return widened to carry an emit triple and the write became
two-branch. Still exactly two stdout write sites, asserted with a seeded
third. No body accompanies any non-zero exit as a property of the RESULT
type rather than a rule each arm must remember -- a failing arm carries
`body: ''`, so the boundary writes unconditionally and cannot leak one by
forgetting to suppress it. All six failure paths assert it anyway.

Decomposed per DR-14 into parseArgs / scrubTwice / frameEmit with main as
a dispatcher, and the helpers exported behind `require.main === module`:
parseArgs is otherwise observable only end-to-end, and the nonce-failure
arm is not observable at all -- no argv and no fixture can make
randomBytes fail, so injection is what makes it assertable.

The flag is parsed BEFORE the positionals are bound. Previously argv[2]
and argv[3] bound positionally with no flag handling, so `--emit` became
a FILENAME and died at statSync reporting a missing input.

GAP-54: the four placeholder skips are anchored. They exist to keep author
fixtures readable, but a value that merely CONTAINED a placeholder
disarmed rule 8 -- the only generic `key = value` rule -- and
provider-rendered bodies are exactly what newly flows into a comment
sink. The `[REDACTED:` guard stays a contains-check, or the second pass
could never return zero and the gate would refuse every body.

The `<in> <out>` mode is byte-for-byte unchanged: it still scrubs once,
writes the same file and prints only the SCRUB line, asserted as its own
regression scope.

Refs #325
GAP-02 + hazard H7. The contract that governs every tracker sink reached
through a tool call has to be AUTHORED now -- its first consumer is a
per-operation mechanics file landing later in this same phase, and a
contract written after its callers is a contract the callers were written
without (clause (iii) is read per phase, decision D-D). It must NOT be
GENERATED now: Phase 2's own AC-2.7 guard asserts its absence after a
GitHub-only build, and every GitHub user would otherwise be billed for a
reference nothing they can reach ever loads.

So the gate is DERIVED, never declared. mcpContractIsGenerated asks
whether any registered module lands in a provider directory whose
mechanics need the contract; resolveVariantModules appends the contract
row only then. A boolean on the module would have been a flag someone has
to remember to flip -- the next subtask opens the gate by registering its
provider and by nothing else. Both arms are proven now, against an
injected registry, rather than discovered when the gate opens.

The emitted basename keeps its leading underscore, and that needed a
NARROW allowance rather than a relaxed charset:
validateContractOutputName demands the prefix where validateOutputName
forbids it, because `tracker/_mcp.md` sits beside the provider
DIRECTORIES and `tracker/mcp.md` would read as a fourth provider.
Relaxing the shared rule would have admitted `_anything.md` as a command
or agent basename too -- a widening across three destinations to buy a
property one needs (ADR-025). A test proves the name expands TODAY: the
row alone would have expanded fine while absent and refused with
`invalid-op-name` the moment 3b opened the gate, a build break planted
one subtask ahead. The section-marker regex takes the mirrored optional
underscore; it is not a containment gate -- the name it captures is
checked against the caller's registry, and what may become a PATH is
gated by the two name validators.

The build gains a THIRD discovery bucket. A gated module is `deferred`,
not refused (refusal is right for an UNREGISTERED module and wrong here)
and not silent (silence makes authored-but-gated indistinguishable from
lost). Counting it as a partial would have been worst: a partial declares
no output-dir:, and this declares one -- so the partial count is
`total - hosts - deferred` and the build names each deferred module with
its reason.

The contract itself states its rules in terms of CAPABILITIES, so it
needs no provider literal and no transport acronym -- and therefore no
provider-scope allowlist entry at all. An exemption was deliberately not
taken; the guard instead proves the module is IN scope, since an
unscanned file is an exemption nobody wrote down.

AC-2.7 is re-scoped, not deleted: the absence is still asserted, but it
now means "the gate is shut" rather than "the contract is unwritten", and
those are different claims a bare not.exists cannot tell apart. Three
arms pin all three facts -- the source is authored, the gate is shut, and
the gate opens for the right registry (PF-064).

mcp-sink-bypass.test.ts is the compiler for prose that has none: the
four clauses asserted against the SOURCE .mds per [E2], each driven by
dropping it in turn; the bypass regex red on seven real bypass shapes
including `create_comment(body: $DEVFLOW_BODY_RAW)`; and a forward arm
whose live corpus is EMPTY at this boundary and ASSERTED empty, so a
green run is never read as evidence about provider files that do not
exist. Its [DR-01] and [DR-06] known-bads are seeded mechanics omitting
exactly one clause each.

Two predicates were inert on first write and are fixed with the reason
recorded: `\bRAW\b` cannot match `$DEVFLOW_BODY_RAW` (the underscore is a
word character) and `\badd[_-]?comment\b` cannot match
`addCommentToJiraIssue`. Both boundaries that made them inert were
against exactly the spellings they exist to catch.

Refs #325
P3a-S13 + P3a-S14. Phase 2's preamble resolved the provider MANIFEST-ONLY
and read no configuration file; this makes the slot real.

Resolution order, first hit wins: the per-repo `tracker` key -> repo
ref-grammar corroboration -> the manifest -> `github`. The corroboration
rule is stated with its prohibition attached, because the obvious rule is
the wrong one: "GitHub remote plus an authenticated CLI implies github"
holds for essentially every non-github user, since devflow deliberately
keeps PR hosting on GitHub whatever the tracker is -- it would disable
the feature for exactly the users it serves (OD-9). The only signal is
whose issue grammar the repo's history speaks, at >=3 occurrences AND
>=60% share.

The mismatch guard is the reader-side invariant: frontmatter `provider:`
!= the resolved provider emits `DEGRADED (tracker configuration
mismatch)` and makes NO tracker call. It covers every path init cannot
see -- uninstall then reinstall, a hand edit, a dotfile-repo sync -- and
it is the precondition for 3a-1 preserving the file as user content on
uninstall (OD-15's reversal condition, now satisfied). A stale file is
safe to keep only because it can no longer be silently authoritative.

Absent and sentinel are kept as DIFFERENT outcomes: an absent section
takes its documented neutral default, a consumed section holding
`# UNRESOLVED:` DEGRADES and is never shape-validated as a value. A
default is safe exactly where the field was never needed and unsafe
where the writer looked and could not tell.

★ THE FROZEN FIXTURE CAUGHT A REAL DEFECT, and it is why AC-3.1 exists.
The first draft added `- **Tracker**:` to setup-task's `### Traceability`
template, which altered github-status-lines.txt -- one added line. But
14.2 says the GitHub path emits NO tracker status line at all, so an
unconditional template line was wrong on the merits, not merely
inconvenient. The rendering rule now lives in the preamble, conditional
on a non-github provider; the template is byte-unchanged and the frozen
fixture is byte-identical (cmp-verified). A GitHub user's rendered output
is unchanged, which is the whole of AC-3.1.

Two further defects my own text introduced, both caught by existing
guards and both fixed at the source rather than by widening the guard:
a `gh` code span in cross-cutting text (P2-S4 forbids naming the CLI in
provider-independent prose) and a mid-line `## Operation:
learn-conventions` literal, which the op-roster scan read as an
operation named with a trailing backtick and which broke three unrelated
op-scoped guards.

BYTE BUDGET [DR-13]. Measured: git.md 55_664 -> 58_776, preamble 29 ->
34 lines, worst-case spawn 77_719 -> 80_831.

  - PREAMBLE_MAX_LINES STAYS AT 40. 14.10 proposed raising it to 70 as
    "the honest number"; the re-derivation says 34. A `<= 70` assertion
    would be strictly weaker than the one already in place and would buy
    nothing, so it is not added.
  - BUDGET_GIT_MD_P3 = 58_870 is a NEW ceiling entry, not a raise:
    55_750 + the MEASURED 3_120 preamble growth, headroom 94. The
    Phase-2 constant stays pinned and becomes the declared base.
  - ★ The revision is spendable on the preamble ONLY, mechanically: the
    portion of git.md outside the preamble is BYTE-IDENTICAL across this
    change (52_279 ch both sides), so a companion gate holds it to the
    UNRAISED Phase-2 allowance (BUDGET_GIT_MD - PREAMBLE_CHARS_P2), with
    Phase 2's own 86 ch of headroom. Growth in an operation section
    still goes red against Phase 2's number.
  - BUDGET_LOADED_SET_P3 is COMPUTED, not typed: the Phase-0 total plus
    the git.md revision and nothing else. It carries no literal, so it
    is unregisterable and unwalkable -- budget-git-md-p3 is the single
    ratcheted number governing both gates.

DEVIATION TO REVIEW: 14.10 says "only the git.md component is further
revised", and BUDGET_LOADED_SET contains git.md, so the plan's
arithmetic could not hold both. [DR-13(c)]'s _resolution.md escape was
measured and rejected -- moving text into a per-op-summed reference is
NET ZERO on that gate, and the only classification that would reduce it
treats a containment control as an optional load (PF-027).

schema-scope.test.ts ships the reader half's guards: the [DR-21]
two-sided heading equality (both directions, distinct why-messages, both
bound to the shared oracle rather than to each other so a heading
dropped from BOTH files still fails), AC-3.16's three-way ADR-007 sweep
enumerated over all ten ops with release.md named, AC-3.18's four
negative greps, the retired headings, and the [DR-04] DEGRADED registry
in both directions.

The registry's forward arm is scoped by SUBTASK and the scoping is
asserted, not implied: half of 14.2's rows are emitted by per-provider
mechanics that land in 3b/3c, and "expected red" is indistinguishable
from a regression. A partition assertion makes DEFERRED_REASONS the only
way out of the forward arm, and a mirror arm proves each deferral is
real -- it immediately caught two rows already emitted at this boundary.

⚠ 14.2 TABLE GAP FOUND: manage-debt.md emits `DEGRADED (tech-debt
archive failed for #…)`, which appears nowhere in the canonical table.
Recorded in PRE_PHASE3_REASONS with its provenance rather than papered
over; the appendix needs the row or the literal needs retiring, and
neither is this subtask's call.

The git-agent.md golden is intentionally left red by this commit -- its
regeneration is the standalone fixture-only commit that follows [DR-03].

Refs #325
Fixture-only, standalone, no behaviour change [DR-03]. Regenerated with
`npm run test:golden:update -- git-agent`; `--unfreeze` was NOT passed
and `github-status-lines.txt` is byte-untouched, as 14.6 requires
through Phase 3.

The diff is 15 insertions / 10 deletions and every line of it is inside
the `## Tracker provider resolution` … `## Comment-sink scrub (D11)`
block — nothing outside the preamble moved, which is the same fact the
byte budget's companion gate asserts numerically (git.md outside the
preamble is byte-identical at 52_279 ch).

The equality baselines move in THIS commit, with the fixture, never
afterwards to clear a red assertion:
  GIT_AGENT_BYTES  56_075 -> 59_239
  GIT_MD_CHARS     55_664 -> 58_776
  GIT_MD_LINES        913 -> 918
  TOTAL_CHARS      65_187 -> 68_299
  TOTAL_LINES       1_218 -> 1_223

TOTAL_* are measured literals rather than a sum of the parts, so they
have to be re-pinned explicitly — a `TOTAL = A + B + C` assertion
against `A + B + C` restates its own definition and holds even when all
three parts have drifted.

Refs #325
…er agent

Three amendments 3a-3 flagged, each closing a gap between the Tracker agent's
prose and the session-start gate that spawns it:

- Step 0 now names the claim-staleness bound (600 seconds) instead of saying
  only "Fresh"/"Stale". The hook classifies the same claim file with
  TRACKER_PROCESSING_STALE_SECS; an agent that guesses a different threshold
  either exits silently against a claim the hook considers stale (burning an
  OD-14 attempt per session) or re-claims one the hook considers fresh. Both
  failures are silent. tests/seams/tracker-claim-staleness.test.ts is the only
  place the two sides are compared: it reads the assignment out of the hook and
  the bold literal out of the agent, and reports an unstated bound rather than
  reading it as agreement.

- The Environment section names the directive's third prompt field and tells the
  agent to prefer `Devflow directory:` over re-deriving the path. The field had
  no reader, which is residue under ADR-003; the two values agree today only by
  coincidence of spelling.

- The attempt counter's format is pinned as one decimal-integer line. The gate
  reads it with the shell's `read` builtin and self-heals any non-digit byte to
  0, so a count in another format is not a smaller count — it is no count at
  all, and the cap it was meant to advance stays open.

Refs #325
… D11 emit gate

The 3a docs sweep (P3a-S19), over the branch's final tree rather than any one
commit, greped by artifact name.

CLAUDE.md gains a Tracker paragraph in the same register as Compliance —
selection is a manifest enum, conventions are an inferred global file, and the
two are deliberately separate. Agent count 16 → 17 in the source tree comment
and the shared-agent roster, with the roster now naming which two agents are
hook-spawned and therefore have no _roster.mds row. tracker joins the sonnet
tier in Model Strategy; the new ~/.devflow tracker files and the per-repo
tracker config key join the file-tree listings; the Two-Mode Init paragraph
gains the wizard-step predicate and --tracker <id>; and the D11 sentence now
names both scrub modes, because a tool-call sink has no shell boundary to chain
on and an instruction is not a gate.

README gains a user-facing tracker bullet beside Compliance and 16 → 17.

docs/cli-reference.md gains an --tracker <id> init row and an Issue Tracker
section: the commands, the exact-match rule, a table of when the wizard asks,
the learned-conventions file, and the attempt cap. Two notes are stated
explicitly because they are the parts users get wrong — ~/.devflow/tracker.md
and the per-repo override are both per-developer, not team-shared, and only
two of the three commands re-arm the cap. `devflow tracker --status` does NOT
re-arm: it returns before the re-arm call, which is the shipped behaviour and
diverges from decision D-F's literal "--set/--status" wording. Documented as
shipped; flagged for the orchestrator rather than changed here.

docs/reference/platform-assumptions.md is completed. The Bash-result
truncation row is no longer `# UNMEASURED`: the limit is documented upstream as
BASH_MAX_OUTPUT_LENGTH (default 30,000 characters) and truncation preserves the
head AND the tail while eliding the middle — which strengthens the case for the
<bytes> check rather than weakening it, since a body with a hole in it still
presents an intact framing line and an intact tail. Two rows 3a-3 asked for are
added: the SessionStart `source` domain, and the unobservability of background-
agent liveness that makes the 600-second claim bound necessary. Then the MCP
surfaces table (five properties none of which can be checked from inside this
repo), a capability → observable-symptom table that names its DEGRADED literals'
authority instead of copying it, and the three standing prohibitions — never
serialise ~/.claude.json, no wildcard mcp__* pre-approval, no
--dangerously-skip-permissions.

CHANGELOG gains five ### Added entries (selection and the CLI surface, the
background agent and its hook gate, the preamble resolution order and mismatch
guard, --emit, the tool-call contract) and one ### Changed entry: the accepted
uninstall confirm-prompt regression, with its reversal condition recorded. Every
entry states that GitHub users see no change. Written under the existing
[Unreleased] heading only.

file-organization.md is touched only where it pins the agent count.

Refs #325
The tracker paragraph attributed the manifest-group-vs-config-gated placement
rule to ADR-001, copying the anchor from src/cli/commands/tracker.ts's header.
ADR-001 as currently rendered in .devflow/learning/decisions.md is about the
feature-knowledge-v2 clean break and says nothing about feature-state
placement, so the citation does not hold and CLAUDE.md is injected into every
session (PF-025 — a wrong anchor there teaches every agent a wrong anchor).

The distinction stands on its own merits and is now stated without an anchor.
The pre-existing citations in tracker.ts's header and in CLAUDE.md's own
Working Memory and proxy paragraphs are left untouched: they are outside this
subtask's scope and correcting an anchor is a ledger decision, not a docs edit.
Recorded in the tracker-feature knowledge base for whoever settles it.

Refs #325
The provider dimension of the tracker work had no knowledge base. tracker-
references owns the Phase-2 contract/mechanics split; this one owns how a
provider is chosen, how conventions are inferred, and how a reader resolves
and refuses.

Covers the selection substrate (the strict-vs-tolerant parser split and why
features.tracker must stay out of readManifest's hard-null set, the eleven
init.ts edit sites including the hud-only manifest write the plan did not
list, the wizard gating predicate, the three single-owner file lifecycles),
the Tracker agent (claim protocol, the capability-by-description rule, the
transient-vs-permanent split, the 11-section schema and its shared oracle,
the write chain), hook Section 3 (five gates with their fork costs, the
positive allowlist, the counter's three-state parse, and the method by which
the zero-fork property was actually proven), the MCP substrate (the subdir-
keyed generation gate, the two narrow name widenings, the --emit framing and
exit codes, the <bytes> refusal), the reader half (resolution order, the OD-9
prohibition, the mismatch guard, why the status line lives in the preamble),
the three-list DEGRADED registry including the tech-debt-archive literal's
provenance, and the byte budget with the loaded-set decision 3b must make
before it starts.

Jira (3b) and Linear (3c) are present as named, empty slots with the contract
each must satisfy, per the plan's docs-sweep partition.

Records D-TRACKER-PAIR [DR-25] — the core/CLI pair mirrors the compliance.ts
pair and ADR-013's split is why both names exist — and records that DR-15's
restate-under-an-allowlist fallback was NOT taken.

tracker-references' status paragraph said Phase 2 was not landed; it is on
main as ecfc141, and the two KBs now cross-reference each other.

Refs #325
Gate-2-approved decision D-F says the N=5 attempt cap is re-armed by `devflow
init` AND by `devflow tracker --set/--status`, with both paths documented in
cli-reference.md. Only `--set` re-armed: the `--status` branch returned after
printing provenance, above the re-arm call.

The decision is right and the code was the half that was wrong. `--status` is
the command a user whose inference has hit the cap actually reaches for — it is
how they find out nothing is being learned — so it has to be the command that
hands back another five tries. Without it the only escape from the cap is
deleting an undocumented dotfile by hand, and a re-arm reachable only from a
provider change is unreachable for the user who is already on the provider they
want.

The `--status` branch now calls rearmTrackerInference before its return, with
the same Result handling as the `--set` path: a failed re-arm warns and the
status report is still printed (avoids PF-009). Re-arming is still the only
write `--status` makes; the selection is untouched.

Tests. The call-site assertion no longer counts calls across the whole file —
two is a number that says nothing about where the two calls are — it splits the
action body at the Set separator and pins exactly one call in each branch, so a
re-arm that drifts out of either one goes red. Added behind it a behavioural
case that drives the compiled CLI as a subprocess against a seeded mkdtemp HOME
(PF-060), because the Commander .action() body is not unit-reachable: a counter
written at the cap, `tracker --status`, then the counter asserted gone and the
manifest provider asserted unchanged. The counter is read back before the run
so its absence afterwards cannot be the state the temp dir started in (PF-018).

Docs. cli-reference.md's re-arm table keeps its three rows and flips the
`--status` row to Yes; the usage block's `--status` comment names the re-arm,
since that is the first place a reader looks. CLAUDE.md's tracker paragraph and
the tracker-feature knowledge base now name all three re-arm paths and record
why an inspection command carries a write.

Refs #325
src/cli/commands/tracker.ts's header attributed the manifest-group-vs-config-
gated placement rule to ADR-001. ADR-001 as rendered in
.devflow/learning/decisions.md is about the feature-knowledge-v2 clean break —
deleting the old-install cleanup machinery — and its body says nothing about the
manifest or the feature config being the source of truth for feature state. The
citation does not hold.

Per PF-065 a citation is an assertion about another document's contents, not a
tag, and a real anchor attached to a claim its body does not make is worse than
silence: nothing verifies it, it reads as researched, and the next writer copies
it. This header was the origin of the copy that CLAUDE.md's tracker paragraph
carried until c3626e7.

The rule itself is real and load-bearing, so only the anchor goes: the header
states the manifest's authority in plain words with no ledger reference. The
knowledge base entry that flagged the anchor as unverified now records the
settled end state — the distinction stands on its own merits, and no ADR records
it.

Scoped to the tracker sources this phase introduced. CLAUDE.md's Working Memory
and proxy paragraphs cite ADR-001 the same way and are older than this branch;
per PF-065 that sweep is per-hit classification, not find-and-replace, and it is
reported to the orchestrator rather than folded in here.

Refs #325
Ten generated references under `references/tracker/jira/`, one per tracker
operation, and the registration that opens the tool-call contract's generation
gate. Registering `_jira.mds` with `subdir: 'tracker/jira'` is the only edit
that opens it — there is no flag and no second declaration.

Both provider rows now read ONE exported roster (`TRACKER_OPS`), so file-set
parity across providers is a compile-time property rather than two hand-listed
arrays that have to keep agreeing. `TRACKER_GITHUB_OPS` stays as a named alias
for the guards that mean "the GitHub path's ops" rather than "the roster".

Mechanics: tool calls only, selected by capability description; `32767` body
cap; `Retry-After` honoured verbatim with no pre-emptive rung, because Jira
publishes no remaining-request count; a single-query batch fetch; a four-rung
dedup ladder whose marker is the comment's FIRST LINE, namespaced per comment
kind so the three kinds cannot suppress each other; a stated aggregate call
budget; and a `### Posting gate` in each posting op that NAMES the tool-call
contract without restating it.

Four findings the registration surfaced, fixed here rather than worked around:

- `planOverlayUnits` bucketed `tracker/_mcp.md` as a provider directory named
  `tracker`, so the unit's atomic swap was a rename of `tracker/` itself over
  every provider directory beside it, with a staging sibling outside the
  subtree the prune converges. D-OVERLAY-PROVIDER-SHAPE classifies by shape;
  everything else is a flat set carrying the directory it lands in.
- The overlay suite's stale-prune arms used `jira` as a provider the manifest
  does not list. It became one, inverting those arms silently. The fixture
  provider is now `probe-provider`, and a guard asserts it stays unreal.
- The DEGRADED registry admitted only template spellings, while the appendix
  fixes the per-provider cell as the instantiated form. Both are now admitted,
  with `{provider}` tokens derived from the registry and `{capability}` tokens
  from the contract's own table, so both vocabularies stay closed.
- The reason collector captured a hard-wrapped reason with its newline intact,
  matching no registry entry and describing no defect. It collapses whitespace.

`_mcp.mds` gains two prose corrections its own consumers now depend on: Bash
results preserve the head and tail and elide the middle, which makes the
`<bytes>` check more necessary rather than less, and the file that names this
contract is a per-operation mechanics file, not the always-loaded preamble.

Refs #325
The GitHub-scoped loaded-set row answers "what does a tracker spawn cost on the
GitHub path?", and this phase does not change that answer: no GitHub mechanics
file names the tool-call contract, so its term stays 0 by construction and the
row still measures 80,831 against its unraised 80,944.

A provider that DOES load the contract is therefore priced on its own row,
`BUDGET_LOADED_SET_JIRA = 88_660` — measured 88,609, headroom 51. Folding it
into the GitHub number would have billed every GitHub user for bytes they never
receive, and would have done it by raising a ratcheted ceiling.

Three arms keep the row from being a free number: the delta over the GitHub
ceiling is held to what this provider actually adds (the contract, plus the
difference between the two providers' per-op terms); the ceiling must still sit
above the measurement it was derived from; and a named arm fails any registered
tool-call provider that has no ceiling of its own, which is the shape the next
provider must satisfy. The four-shape table gains a row per provider, the
contract's own size as a recorded row, and a derived shape count.

The gate went red once during authoring — a 197-character rewrite of the
contract's truncation clause breached it — and the clause was condensed back to
47 characters of growth rather than the ceiling being moved. That is the
response the failure message prescribes, recorded so it is the precedent.

Refs #325
Fills the named `## Provider: Jira (3b)` slot: why parity is structural rather
than asserted, the provider facts and the ones that must be absent, the dedup
ladder and the first-line namespaced markers, the aggregate call budget as a
product, the single-query batch and the two shape classes its guard forbids,
where the query-safety rule lives and why it lives in exactly one operation,
the ref grammar and how AC-3.4 falls out of it, and the loaded-set table with
its new ceiling.

Three other sections moved to their end state rather than being left describing
a tree that no longer exists: the Overview status line, the DEGRADED registry
(the deferred half is now empty, and two placeholder vocabularies are derived
rather than listed), and the byte-budget decision, which is now a shipped fact.

The 3b/3c handoff list is rewritten as what registering a provider actually
drags with it — eleven numbered rows, each stated as the end state this commit
reached, so the next provider can read it as a checklist. Two of the rows are
findings rather than plan items: the installer's overlay unit shape, and the
overlay suite's fixture provider that stopped being fictional.

Refs #325
One `### Added` entry under the existing `## [Unreleased]`. No version header
is added or touched.

Covers what a Jira user gets and, as deliberately, what they do not: the
provider facts that are present (`32767`, `Retry-After` honoured verbatim) and
GitHub's that are absent, the single-query batch, the four-rung dedup ladder and
why the marker is the comment's first line, the per-kind namespaces, the call
budget as a product, the pointer sentence that replaces a collapsed block, and
the rule that Jira being unreachable still cuts the branch and opens the PR and
never falls back to creating a GitHub issue. It closes on the guarantee that
matters to everyone else: GitHub users see nothing change.

Refs #325
Registers `src/assets/mds/tracker/_linear.mds` against `tracker/linear`,
reading the same exported `TRACKER_OPS` roster as the other two providers, so
file-set parity across three providers stays a compile-time property.

The provider ships at rank 4 (OD-12) and says so: a stock official server
exposes no viewer/"me" tool and its attachment create takes a binary payload,
so three of the dedup ladder's four rungs are unreachable. The mechanics
therefore post with a warning, bind the marker to the comment's first line, and
carry a second discriminator a coincidence would not reproduce. The borrowed
`32767` cap and the rank are recorded in a module-level `## Known Unknowns`
section with the filed probe issue (#343); the section sits above the first
section marker, so the build emits it nowhere and no generated reference
acquires a column-0 `## ` that would truncate its own op section (PF-063).

Registration drags eleven things with it, and they land here because the guards
they move go red the moment the provider is registered:

- both manifest floors 24 -> 34 together (install and tarball sinks)
- `capability-hoist-block-floor` 39 -> 49
- `PROVIDER_OWNED_PATHS` gains two entries, per (path, token)
- a NEW `budget-loaded-set-linear` ceiling (91_000, measured 90_951) with its
  derivation; the two per-provider budget gates are now generated from one
  `PRICED_PROVIDERS` table rather than written out per provider
- `unsupported by linear` admitted by the DEGRADED registry's derived tokens
- the deferral probe drops every gated sub-directory rather than naming one
- the parity scan is now over the registry's provider rows, flipped to three
  columns with a matrix-cell arm (AC-3.8, §8.11)

The per-item fetch shape table moves to tests/helpers.ts: the same [DR-08]
claim is made per provider and across providers, and two copies of the table
would be two authorities on what a per-item fetch looks like.
AC-3.13 as a MATRIX rather than three per-provider pins. Every claim in it is a
claim about the difference between providers, and a difference cannot be
asserted from inside one of its sides: `32767` present in a tool-call module is
a weak fact, while `32767` present in both tool-call providers and absent from
GitHub is what §14.2 actually resolves.

Five rows, each naming which providers state it and why it is a provider fact:
`60000` and `X-RateLimit-Remaining` GitHub-only, `32767` on both tool-call
providers, `Retry-After` Jira-only, `RATELIMITED` Linear-only. The absence half
is pinned as hard as the presence half — a module naming another provider's
signal has copied a backpressure model across a boundary where it does not
hold — and the `present` list shape puts every unlisted provider in the absent
set by construction.

Both sides are read. A source-only pin is satisfied by a literal in
module-level prose, which the build emits nowhere, and `_linear.mds`'s
`## Known Unknowns` names the borrowed cap exactly there.

[DR-08] rides along as the cross-provider batch negative, driven by the shared
shape table, plus the positive bound arm scoped to the tool-call providers: a
`gh` invocation takes a list the always-loaded step already trimmed, while a
composed query must carry the bound itself.
…chanics

[DR-19]'s remaining arm. The three cross-cutting documents already have a
shared-literal registry; `tracker/_mcp.md` sits in the identical position and
had none, so the rule its own prose states — a per-operation file may INVOKE a
rule here and never restate its substance, and on any conflict the contract
wins — was unenforced for exactly the twenty provider files written from
scratch in 3b and 3c. The containment oracle cannot see it: it accounts for
lines that existed pre-split, not for a newly authored restatement.

A SEPARATE registry rather than rows added to the sibling one, because that
registry's ownership arm asserts its owners are exactly GIT_CROSS_CUTTING_DOCS
and the contract is a different module kind behind a different gate. Folding it
in would have meant relaxing that arm to admit a fourth owner instead of
classifying the case (ADR-025).

Five entries, each justified: the framing line's composition, the definition of
where the gated bytes come from, the transformation prohibition, the
selection-by-description rule, and a capability-table row. What is deliberately
NOT in it is the set of literals every posting mechanic must name — `D11-OK`,
`<bytes>`, `SCRUB: N`, `SECRET-EXPOSED` — and a named arm asserts their absence
from the registry AND their presence in every posting mechanic, so this guard
and the bypass guard cannot be satisfied only one at a time.

Three arms plus two probes: the gate-is-open subject check, the positive arm
over the contract and its three siblings, the negative arm over every
registered provider's tree (asserted by provenance, not by a count), and a
seeded restatement of the `{SCRUBBED_BODY}` rule driven through the same
collector — with every other entry asserted unreported over the same corpus, so
the probe proves the collector discriminates rather than matching anything.
…der-neutrally

Two GitHub shapes left in provider-blind, always-loaded positions. Phase 2
reserved both for Phase 3 because neither could be decided with one provider
registered; with three they are wrong for two of them.

**The entry gate.** `backlink-shipped-issues` step 0 required every
`SHIPPED_ISSUES` entry to be "digits only" — which made the operation
unreachable under jira and linear, since `PROJ-1` is not digits and the step is
the gate every entry passes. It now defers to the resolved provider's anchored
reference grammar, stated and enforced by that provider's mechanics, and the
github grammar `^#?[1-9][0-9]{0,8}$` MOVED INTO the github reference with the
same drop rule and the canonical per-ref DEGRADED reason. The gate is not
weakened, it is relocated to where enforcement lives: the operation always
loads its provider reference, and an absent reference already means
`tracker mechanics unavailable` with no tracker call, so the fail-closed
property holds. The metacharacter rationale went with it — all three provider
mechanics now say the anchored form is what keeps a reference out of a query or
a command, and git.md's input contract already states the
shape-gate-at-the-sink rule once.

**The `## Operations` table.** "Fetch GitHub issue", "Fetch multiple GitHub
issues" and "Create or enrich a GitHub issue" are tracker operations whose
provider is resolved per spawn. Rewritten to "tracker issue(s)", together with
the two op-body descriptions that duplicate those cells — left alone the table
would have contradicted the operation two hundred lines below it. GitHub
literals that are NOT a tracker fact are untouched: `create-release` and
`fetch-review-threads` name GitHub because PR and release hosting stay there
under every provider.

Net effect on the always-loaded file: 58,776 -> 58,751 characters and one line
shorter, so every budget row gained headroom rather than spending it. Six
baseline lines are booked in CONTAINMENT_EXEMPTIONS with their rationales —
these are rewrites, not moves.

`tests/tracker/hostile-values.test.ts` gains the two describes it has been
carrying a deferral note for, because this commit is what gave them a subject:
`refs per provider` needed an anchored grammar for EVERY provider, and GitHub's
only exists in the mechanics from here on. Register row 25's eight payloads are
driven against all three grammars, each pinned form asserted verbatim in that
provider's shipped tree, with an unanchored-alternation probe showing what
§14.1's "never `^A|B$`" is about. The JQL/filter describe models §14.9-10's
escape-then-drop rule as written and scopes the drop claim to the four
characters that can end a quoted literal — a query sink is not a shell, and
dropping inert text would cost search results while protecting nothing.

The git-agent golden goes red here and is regenerated in the next commit
alone [DR-03]. `github-status-lines.txt` is byte-identical to its Phase-0
capture: every edit is outside the ranges it samples.
… wording

Fixture-only, reviewed as a text diff, no behaviour change in it [DR-03]. This
is the Phase-3 regeneration §14.6 allows once, and the diff is exactly the six
text moves the previous commit made: three `## Operations` cells, two op
descriptions, and `backlink-shipped-issues` step 0 losing its digits-only
clause and its now-duplicated metacharacter sentence.

The three equality baselines against this fixture move with it, in this commit,
because that is what makes them baselines rather than budgets — re-pinning one
later is how it stops being evidence of anything: `GIT_AGENT_BYTES`
59_239 -> 59_214, `GIT_MD_CHARS` 58_776 -> 58_751, `GIT_MD_LINES` 918 -> 917,
and the two `TOTAL_*` preloaded-set sums.

`tests/fixtures/golden/github-status-lines.txt` is untouched and `cmp`-identical
to `main`: the ranges it samples are the per-issue steps inside the generated
references and the D4/D11 blocks, none of which this change reaches.
The two facts the Linear mechanics inherit rather than measure, written for the
person they affect rather than for a reviewer of the module: the comment cap is
borrowed from Jira, and on a stock workspace devflow cannot ask the tracker who
it is, so it posts a back-link with a warning instead of suppressing one it
cannot verify. Both outcomes are described by what the user sees — a truncation
with a pointer, or a DEGRADED line on every run — and the filed probe issue is
linked so the borrowed number has a visible owner.

Placed inside `## Issue Tracker`, after the attempt cap, because a reader who
has just selected `linear` is exactly who needs it. No transport acronym: this
is a page a user reads, and which protocol the tracker is reached over is not
something they can act on.
…er wording

`## Provider: Linear (3c)` filled, and the two KBs moved to their end state
rather than gaining a third forward-looking slot.

**tracker-feature.** The Linear section states OD-12 rung by rung — three of
the four dedup rungs are unreachable on a stock server, and each one's reason is
written down because an unexplained rank reads as a misconfiguration. It records
the marker's two halves (first-line binding against a quoter, project URL
against a coincidence) and why the URL is part of the MARKER rather than a
footer; the `400 RATELIMITED` signal and why the code has to be named against a
status-shaped D4 rule; the two separately anchored ref forms; the call budget as
the common path rather than the edge; the loaded-set row with 47 ch of headroom
and why this provider's max_op is the largest of the three; and why
`## Known Unknowns` is module-level prose the build emits nowhere (PF-063).

The `3b / 3c handoff contract` is now `what registering a provider drags with
it` — the checklist a FOURTH provider reads. Rows moved to their end state:
five reference modules and 19 discovered hosts, floors at 34/34/49, owned paths
two per provider, all four hostile-value describes live, and a new row 12 for
the `_mcp.md` shared-literal registry with the distinction that keeps it from
fighting the bypass guard. The Phase-2 reservation is replaced by the decision
it was waiting for, including the `X-RateLimit-Remaining` threshold KEPT in
`SKILL.md` with its reason — so that literal's survival reads as a decision.

Two gotchas earned the hard way: the bypass regex reads any `word:` as an
argument assignment, so prose like "read the text: a rule…" is reported as an
ungated posting mechanic (write mechanics, not prose that looks like a call);
and a provider's `max_op` is what the budget prices, so a cross-cutting
paragraph costs a ceiling only in whichever op file is already the biggest.

**tracker-references.** The `## Operations` table reservation and the DR-19 MCP
deferral are both resolved, and the AC-2.7 reachability row is re-stated at 34
files with the template instantiated per registered provider. It also records
where the github grammar was placed and why: above the range
`STATUS_LINE_REFERENCE_FILES` samples in that file, which is what kept the
frozen fixture byte-identical.
Three `### Added` entries under the existing `## [Unreleased]`, written for the
user rather than for a reviewer of the module.

The first is what Linear can do and, deliberately at equal length, what it
cannot: the rank-4 dedup reality stated as an outcome the user sees — a
DEGRADED line on every run and a possible duplicate back-link — rather than as
a capability table. The second is the borrowed cap and the filed probe issue,
because a number adopted from another provider and presented as a measurement
is the thing a changelog should not do. The third is the provider-neutral
wording and the digits-only entry gate, and it names the gate as a live defect
that made the operation unreachable under two of three providers rather than
as a tidy-up.

No version header touched.
A leftover draft paragraph restated the surviving comment on the same
line, describing one assignment twice. No behavior change.
…tStagingTree

subdir was computed twice (sourceDir derivation, then the relPath
ternary) for the same unit on every loop iteration. One local reused
at both sites. No behavior change.
…te site

init.ts's HUD-only manifest write and tests/helpers.ts's makeManifest default
both spelled { provider: 'github' } while DEFAULT_TRACKER_PROVIDER — documented
as the value every malformed input self-heals to, and already used by
init-seed.ts for the same default — sat unimported. The literal typechecks
because it is a member of the domain, which is exactly why a moved default would
pass both sites unnoticed.

Issues: typescript-10 (folds consistency-05, architecture-13)
provider-scope's header claimed FOUR negatives, each "a NAMED collector
with a known-bad probe", over five describes of which one — no generated
GitHub mechanics file names `_mcp.md` — was an inline text.includes with
no collector and no seed. The header now names five and says how each is
driven, and that arm gets collectOpsNamingContract plus a probe that
seeds one op and shows the collector reports it, so its empty result is
evidence rather than a matcher that recognises nothing. Its opening line
describes the property the file enforces today instead of a phase.

mcp-sink-bypass said FOUR CLAUSES at four sites over a five-row
CONTRACT_CLAUSES whose probe already asserts `>= 5`; the prose now reads
the registry rather than counting it, and names the `SCRUB: N` echo the
enumeration had dropped.

literal-agent-paths asked for "a build"; requireBuiltCli refuses a STALE
dist/cli.js as loudly as an absent one, so the header and the GREEN arm
now say a CURRENT build and name that arm as the staleness canary.

Three `.find(...)!` sites become requireForeignToken / requireCorpusEntry,
which name the token or the file that vanished.

testing-16, typescript-14
applies ADR-003, avoids PF-018, avoids PF-064
D-TRACKER-OWNER said "devflow init and devflow tracker --set each call them
exactly once" while rearmTrackerInference has a third caller — devflow tracker
--status (D-F), documented correctly in that function's own docstring — so the
module header and a function inside it disagreed. It now separates the two
owners: the sentinel has two callers, the counter three.

promoteCrossCuttingUnit still asserted "these documents land directly in
references/" immediately above the destDir line that made it false; it now names
references/{unit.dir}, which is the references root when dir is empty.

Issues: architecture-08
applies ADR-003, avoids PF-025
The Phase-2 git.md ceiling was introduced by "no longer the live gate" in
both byte-budget.test.ts and its manifest entry — narration of a change
rather than the fact a reader needs. Both now say what is true: the
Phase-2 value is the DECLARED BASE the live gate (budget-git-md-p3) is
re-derived from, and it stays pinned because a ceiling is only ever
re-derived downward. No ceiling value moves.

GITHUB_API_MD_CHARS' rationale narrated two superseded measurements
(17,259 and 17,539) against a constant that is neither, so the prose
could not tell a reader whether the current value was the expected
landing or a fourth unrecorded one. It now states why the excluded term
needs an anchor at all and leaves the assertion as the only figure
(PF-057), and drops the batch-plan narration of edits still to come.

measureOptional's docblock named two of the three rows its caller passes;
decision-markers.md is now named with them.

testing-16, documentation-05
applies ADR-003, avoids PF-057
schema-scope's non-vacuity arm re-asserted `>= 11` on the shared oracle,
which schema-oracle.test.ts now enforces at the oracle's construction as
exactly TRACKER_SCHEMA_SECTION_COUNT DISTINCT headings — the redundant
assertion goes. The two remaining floors count distinct headings instead
of raw ones: the reader block legitimately spells `## Reference Rendering`
twice, so the raw count carried a heading of slack and a list that dropped
one section and repeated another cleared it.

mds-variants' docblock called the shipped registry GitHub-only over a
registry carrying github, jira and linear; it now states what scenario 6
asserts. The contract-gate narration and its `★` arm described which
phase would turn the gate on — the gate is derived from the registry, so
the prose says that instead.

`.find(...)!` / `.get(...)!` / `harvestFence(...)!` in the registry arm and
the command→agent seam become requireHarvest, requireOpSection and a
bumpFence counter, each naming the row, op or agent that went missing.

testing-16, typescript-14
applies ADR-003, avoids PF-018
The Tracker agent stages its scrubbed file as ~/.devflow/.tracker-staged.XXXXXX
and removes it from a trap on EXIT INT TERM. A SIGKILL outruns the trap, so a
stage outlives its run — and the artifacts-only sweep removes exact paths, so it
walked past every orphan while reporting the directory swept.

installArtifactPaths now carries the prefix as a prefix entry, and
resolveInstallArtifactPaths is the one place a prefix becomes paths: the removal
loop and the dry-run preview read the same resolver, so a path removed without
being previewed is unreachable. The stage is an install artifact, not user
content — it is a scrubbed copy that was never placed — so @d8 gains the
matching check that no user-content name falls UNDER a prefix entry.

TRACKER_STAGED_PREFIX is cross-pinned against the agent's mktemp template, the
way the other basenames are pinned against the hook.

Issues: HANDOVER from batch A1
avoids PF-013, PF-018, PF-025
Each of the 28 parity rows spawned the driver through execFileSync, which
holds the event loop for its whole duration — a suite of synchronous spawns
runs end to end however it is scheduled. The rows already stage their own
`case-N` devflow directory, so the fixtures were concurrency-safe already;
only the spawn was not.

The reader returns a promise and the describe is concurrent. Measured over
three interleaved rounds on one machine: 4169/4135/4125 ms serial against
3736/3813/3607 ms concurrent, and 4.53 s for the original synchronous file.

The async reader brings a vacuity mode with it, so the row asserts the token
is a string: with the `await` removed, 48 of 52 assertions stayed GREEN before
that guard and 28 of 28 parity rows fail with it. A second probe collapsing
every row onto one shared directory turns rows red, so the per-row staging is
load-bearing rather than decorative.

partially resolves performance-02
avoids PF-018
ADR-019's body is the typed Claude Code flag registry; it carries no
one-definition-seam corollary about prompt DI seams, so citing it as the
authority for this module's import discipline asserted a claim the anchor
does not make. The engineering fact stands on its own.

Issue: consistency-04
avoids PF-065
ADR-003 has two numbered corollaries and no enumerated clauses, so
"clause iii" indexed nothing — the anchor is right, the sub-clause was not.

The §14.3 heading test's >= 11 floor is redundant: the count and the
distinctness are settled at the oracle's construction and driven over every
admitting mutation by tests/tracker/schema-oracle.test.ts, so the equality
is the whole claim here.

No mechanics claim names an empty op list, so the collector's "anywhere in
the tree" branch could not run. The op list is now a non-empty tuple: an
empty one would report nothing while reading no provider bytes at all, and
the type refuses to spell it rather than a branch having to notice it.

Issues: consistency-04, M2 handovers (a) and (b)
avoids PF-065, avoids PF-018, applies ADR-003
resolveTrackerInitState is exported and pure and had no test at all: the
--tracker flag's only gate before a machine-wide provider change was covered
by a regex count of its call site. Driven by calling it — every registry ID,
github as a real override (D-E), and a near-miss whose rejection message is
compared against the strict parser's own rather than re-typed.

The wizard gate's mode argument is pinned too. runTrackerStepAt must forward
its caller's mode; a hardcoded 'advanced' returns true on every TTY, so a
user who chose Recommended is asked a question that path never asks — and
the 'recommended' call site still reads correctly, so nothing there shows it.

Both rows were shown red against seeded mutations before landing.

Issue: testing-04
avoids PF-018, avoids PF-029
The Set branch drives four owners in one order — rename, persist, re-arm,
sentinel — and every one of them was covered by a regex over the source. Run
as a subprocess against a seeded mkdtemp HOME, mirroring the --status arm,
and asserting the whole end-state of the devflow dir in both sentinel
directions plus the rejected-ID arm that must touch nothing.

Each owner was dropped in turn and each arm shown red before landing.

Issue: testing-04
avoids PF-018, applies PF-015, PF-060 (isolated HOME)
The tracker selection lifecycle had no executable coverage and
.tracker.enabled appeared in no e2e file at all. Three arms on the existing
isolated-HOME harness:

T1 -- a real jira install plus init --reset: the conventions file moves
aside, the sentinel is removed and the manifest collapses to github. The
rename only fires because the lifecycle is handed the real prior manifest
rather than the --reset-gated seed, which the reviewer focus area claimed
was covered.

T2 -- the manifest path made a directory so the atomic write genuinely
fails: init still exits 0, warns, and converges no tracker artifact.

T3 -- a prior linear selection survives a --hud-only install, asserted by
running init rather than re-evaluating the ?? locally.

Each arm was shown red against the corresponding seeded mutation.

Issue: testing-04
avoids PF-018, applies PF-015, PF-060 (isolated HOME)
vitest parallelises across files, not within one, so the session-start-context
Section 3 arms held tests/shell-hooks.test.ts on the suite's critical path.
Running them as a sibling file drops the pair's reported duration from 26.35s /
25.46s to 15.55s / 17.00s over two runs each, with no in-file concurrency.

runHook and HOOKS_DIR become tests/shell-hooks-helpers.ts so both files drive
hooks through one implementation.

The moved block is byte-identical and the pair still reports 306 tests. Two
mutations (a spawn arm's model= expectation, a source-reading arm's staleness
literal) were confirmed RED in the new file and restored, so the arms are
executing rather than passing vacuously — avoids PF-018.

numeric-floors.json's tracker-section-max-chars entry follows its pattern to the
new file; floor, pattern and occurrences are untouched.

applies ADR-003

Issue: performance-02
collectMissingMechanicsClaims never read corpus.tree — only label, vocab,
and read. jiraTree() had exactly one caller (the JIRA_CORPUS literal) and
is now unreachable; linearTree() keeps its other callers. Update the
ProviderCorpus docblock to describe its actual three members.
ADR-003 and ADR-019 are prose decisions with no "clause iii" or numbered
corollary structure — those labels were copied between neighbouring
comments rather than read from the ledger. Per PF-065 this is per-hit
classification, not find-and-replace:

- mds-variants.test.ts (x2), dist-agents.test.ts:499: ADR-003 citations
  whose claims the decision body actually supports — keep the anchor,
  drop the fabricated "clause iii" label.
- dist-agents.test.ts:384: bare "(clause iii)" names no anchor and its
  claim (reviewed accretion, not end-state residue) isn't ADR-003's
  subject — dropped outright.
- prompt-io.ts, compliance-prompts.ts, attribution-prompts.ts,
  build.test.ts: ADR-019's body is the typed flag registry with no
  one-definition-seam or source-vs-compiled corollary — stated the
  engineering fact plainly instead of the borrowed anchor.
The 600s literal it names moved with tracker Section 3 into
tests/shell-hooks-tracker.test.ts; the comment still named the old
tests/shell-hooks.test.ts.
Import the shared runHook from shell-hooks-helpers.ts instead of keeping
a byte-identical local copy with a stale "mirrors shell-hooks.test.ts:1495"
comment (that file's Section 3 moved to shell-hooks-tracker.test.ts).
…ation

The assertion message led with "GAP-17: four sections originally had no
stated default" — a fixed finding ID from the fix that no longer
describes the current schema table. State the invariant the assertion
actually checks.
budget-git-md-p3, budget-loaded-set-jira and budget-loaded-set-linear
described pre-resolve measurements (58_782/68_305/88_615/90_984 and
headrooms 88/45/16) that no longer match the tree. Re-measured from
tests/tracker/byte-budget.test.ts's printed table (never hand-derived,
per PF-057) and rewritten to state the formula in words plus the current
figure, dropping the hand-summed intermediate breakdown so the fixture
doesn't carry a second, independently-drifting record of one
measurement. Also drops linear's now-false "thinnest of the three rows"
claim — jira is currently tighter (headroom 27 vs 39) — in favour of the
evergreen rule: whichever provider row has the least headroom is the
binding constraint, so check both. ceiling/pattern/occurrences unchanged.
…ation

The _HAS_JQ=false parity arm spawns the hook 8 times serially, re-seeding
one HOME between shapes — the ordering IS the assertion, so it cannot be
parallelised. It measures ~1.45s standalone but timed out against the
suite's 5s default once the file began running alongside 148 others.

Give it an explicit 20s per-test timeout. The assertions are untouched,
and discrimination was re-proved rather than assumed: mutating the node
fallback's default branch in json-helper.cjs (get-field-file emitting
'jira' in place of the caller's default) takes the arm RED on the
absent-key shape with "node backend emitted for {}". avoids PF-018
…sured duration

The ISSUE_PR_LINK forwarding arm calls buildCommittedTree() — a copy of the
committed sources built into a temp root — and pays the whole cost, because the
helper memoises the build and this arm is the first of the two callers to claim
it. It measures 3.72s standalone but timed out against the suite's 5s default
five times once the file ran alongside 148 others.

Give it an explicit 20s per-test timeout (4x measured is 14.9s; floored at 20s
for contention headroom). The assertions are untouched, the build is unchanged,
and nothing is cached across tests. avoids PF-018

The sibling probe arm awaits the same memoised promise but measures 1ms — tests
within a file run serially in declaration order, so it never pays the build.
@dean0x

dean0x commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Resolution Summary

Full summary withheld (public repository).

Metric Value
Total Issues 133
Fixed 91
False Positive 0
By Design 3
Deferred 7
Blocked 0
Escalated 0
Duplicates Collapsed 21

Full report: .devflow/docs/reviews/feat-325-tracker-phase-3-init-selected-provider/2026-09-17_1253/resolution-summary.md (not committed; ask the author)
Posted by devflow

@dean0x
dean0x merged commit 9630f31 into main Sep 19, 2026
2 checks passed
@dean0x
dean0x deleted the feat/325-tracker-phase-3-init-selected-provider branch September 19, 2026 17:53
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.

Tracker Phase 3 — init-selected provider, background Tracker agent, Jira + Linear

1 participant