Initial Request
Phases 0–2 leave a provider-shaped hole with exactly one provider in it.
Phase 0 repaired the command→agent issue seam and froze two goldens. Phase 1 compiled the Git agent from an MDS generator host, provably byte-identical. Phase 2 split the Git agent into a provider-neutral contract (git.md, the ops, their **Input:**/**Output:** blocks, the D4/D11 invariants, the ≤40-line preamble) and GitHub mechanics generated per op into dist/skills/git/references/tracker/github/{op}.md. The shape is correct and the only occupant is GitHub.
Phase 3 fills the hole:
- A deterministic provider selection —
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>, inspectable with devflow tracker --status.
- A background Tracker agent (17th agent,
model: sonnet, read-only, no tools: key) that infers the project's tracker conventions once — from the connected MCP server plus a bounded git-history scan — and writes the global, write-once ~/.devflow/tracker.md (D11-scrubbed, chmod 600, # UNRESOLVED: sentinels for anything it could not determine, and a ## Dedup Strategy section recording the probe evidence).
- A SessionStart hook section (Section 3 of
session-start-context) that emits a silent background-setup directive — for jira/linear only, only when ~/.devflow/tracker.md is absent, only on source: startup|clear, bounded by a claim file and an attempt cap.
- An MCP contract module (
_mcp.mds → references/tracker/_mcp.md) with a mechanical D11 gate — redact-secrets.cjs --emit — because a shell && short-circuit cannot exist inside an MCP tool call, and an instruction is not a gate.
- Two provider mechanics modules: Jira (3b) and Linear (3c).
Selection and conventions are separate. That separation is what makes the silent-github path, the hook gate and the zero-change GitHub guarantee all trivial: selection is a manifest enum read by a hook and a preamble; conventions are an inferred global file that only ever exists for a non-GitHub user.
Honest user-visible-change statement
-
GitHub users — the default, and every existing install: nothing changes. No prompt, no new file, no altered byte. Regression-tested against the frozen tests/fixtures/golden/github-status-lines.txt captured at Phase 0 and never regenerated since (AC-3.1). No Jira or Linear literal reaches dist/agents/git.md, dist/commands/*.md or references/tracker/github/** (AC-3.12). Under provider github the hook's Section 3 performs zero subprocess invocations (DR-10).
-
Jira/Linear users get, deliberately and visibly:
- one new init wizard question (Advanced always; Recommended only when the mode prompt was shown interactively);
- a
--tracker <id> flag and a devflow tracker --status/--set <id> command;
- a silent background agent that writes
~/.devflow/tracker.md at a session start — never narrated, never a question;
- a
- **Tracker**: {provider} line in ### Traceability;
- and the one accepted UX regression — a confirm prompt on
devflow uninstall where a user with no other user content previously got a silent artifacts-only sweep.
-
Linear ships at rank 4 — visible first-line marker, no author filter, post-with-warning — and says so in a ## Known Unknowns section with a filed probe issue.
The uninstall consequence, stated in full
~/.devflow/tracker.md is classified as user content (OD-15), sitting in uninstall.ts:227-265 between preference-profile.md (:246-250) and learning.json (:252-256). resolveDevflowDirCleanup (:170-202) needs no change: adding a userContent entry automatically flips a user-scope interactive uninstall from 'artifacts-only' to 'prompt' (:198-201).
The rationale is the agent-models.json reclassification, verbatim: "Stale per-agent model overrides silently re-apply…" (uninstall.ts:215-217). "Silently" is the load-bearing word. Phase 3's provider-mismatch guard (P3a-S14) removes silence: a stale tracker.md whose frontmatter provider: disagrees with the resolved provider produces TRACEABILITY: DEGRADED (tracker configuration mismatch) and no tracker call.
Reversal condition, recorded: the user-content classification is conditional on the mismatch guard shipping. If P3a-S14's guard is ever dropped, tracker.md is reclassified to an install artifact in the same change — otherwise a silently-authoritative stale file survives uninstall.
.tracker.processing, .tracker.attempts and .tracker.enabled are install artifacts (installArtifactPaths, uninstall.ts:283-306) and are removed by an artifacts-only sweep, which keeps tracker.md. The two lists must stay disjoint (@d8, uninstall.ts:276-278).
Execution: SEQUENTIAL_CODE_AGENTS · context-risk: HIGH · Design artifact: .devflow/docs/design/tracker-phase-3.2026-09-05_1155.md (authoritative; local, gitignored — a 60k-truncated copy is posted as a collapsed comment).
Product Requirements
Depends on
Phase 2 (#324) merged to main. Tracking issue: #321.
Acceptance criteria
- AC-3.1 — UAC-1 holds — no prompt, no new file, no altered byte on the GitHub path — regression-tested against the frozen
github-status-lines.txt
- AC-3.2 (rewritten) — 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 — the full end-to-end "next tracker op resolves from it" is (process-only — PR checklist)
- AC-3.3 — provider
jira with no Jira MCP ⇒ a named DEGRADED at each tracker op, branch and PR still created, Tracked (pending) with the reason, never a GitHub issue as fallback
- AC-3.4 —
SHIPPED_ISSUES="PROJ-1 PROJ-2" under jira never yields COMPLETE
- AC-3.5 —
--emit prints no body on every non-zero path; nonce present and per-invocation; every posting mechanic spells the body {SCRUBBED_BODY} and names D11-OK; the bypass guard is red on create_comment(body: $DEVFLOW_BODY_RAW) — AMENDMENT [DR-01]: …and every posting mechanic also names SECRET-EXPOSED. The success framing is D11-OK <nonce> <sha256> <bytes> <n> [type:count,…], carrying the first-pass scrub result. _mcp.md mandates: echo SCRUB: N […] from the D11-OK line before posting; when N > 0 also emit SECRET-EXPOSED (rotate {type} credential — the source file still holds it). tests/guards/mcp-sink-bypass.test.ts asserts every posting mechanic names D11-OK AND SECRET-EXPOSED; the known-bad sample is a Jira posting mechanic that omits the rotation line. Both literals are added to the DEGRADED/status-line literal registry test. — AMENDMENT [DR-06]: …and every posting mechanic names the <bytes> verification: before posting, confirm the received body's byte length equals the <bytes> field of the D11-OK line; on mismatch DO NOT POST and emit TRACEABILITY: DEGRADED (redaction unavailable). Known-bad = a mechanic that names D11-OK but not <bytes>.
- AC-3.6 (repurposed — carve-out is void) — the init wizard step is reachable under interactive-Recommended and Advanced, unreachable under
--recommended/non-TTY, and --tracker suppresses the prompt on both paths
- AC-3.7 — the hostile-
tracker.md table passes for every field; the normalisation literal appears exactly once; no mechanics file re-reads provider:
- AC-3.8 — file-set parity across three providers both directions; every define non-empty; every matrix cell filled — vacuous until 3c; assert
providers.length === 3
- AC-3.9 (rewritten) — MCP tools absent or denied ⇒ the agent writes nothing, removes the claim file, and the next startup re-arms; MCP present but ambiguous ⇒ the file is written with
# UNRESOLVED: sentinels — AMENDMENT [DR-02]: on a write-less exit the agent increments {devflowDir}/.tracker.attempts before deleting the claim file; on a successful write it deletes the counter.
- AC-3.10 — the retired-wording grep is clean repo-wide
- AC-3.11 —
/implement PROJ-123 → branch {type}/PROJ-123-{slug} with the type from ## Issue Types, - **Issue**: PROJ-123, - **Tracker**: jira, and the rendered PR link line — live journey is (process-only — PR checklist)
- AC-3.12 — no Jira/Linear literal in
dist/agents/git.md, dist/commands/*.md or references/tracker/github/**
- AC-3.13 —
32767 pinned and 60000/X-RateLimit-Remaining absent in the Jira/Linear modules; Retry-After present in Jira; RATELIMITED present in Linear — AMENDMENT [DR-08]: …plus "no per-item fetch verb (getJiraIssue, get_issue, or any single-key fetch capability) appears in any provider's fetch-issues-batch reference", RED against a seeded per-item fixture.
- AC-3.14 — per-provider
find_marker names the identity capability and the first-line predicate; a marker at line 5 of a third-party comment does not suppress; markers are namespaced per comment kind
- AC-3.15 — the
tracker.md write is scrub-gated fail-closed; no userinfo URL, no email/accountId literal; denied field names dropped with ### Substitutions; file mode 0600
- AC-3.16 (amended — carve-out rows void) — the three-way ADR-007 sweep plus the per-section absent⇒default table, enumerated by name over the 10 tracker ops and the named command sites
- AC-3.17 (rewritten) — two concurrent Tracker agents ⇒ exactly one file; the loser exits silently without overwriting or reporting
- AC-3.18 — no command source and no
dist/commands/*.md (incl. release.md) reads ~/.devflow/tracker.md; no curl/wget/Authorization:/token-env read in git.md ∪ references
- AC-3.19 — each of 3a/3b/3c independently satisfies prefix-shippability with a green suite (process-only — PR checklist); its clause-(iii) arm is resolved by decision D-D below — do not read AC-3.19 as blocking 3a
- AC-3.20 (new) — provider change (re-init,
--reset, or reinstall) never leaves a silently-authoritative stale file: init renames it and the preamble emits DEGRADED (tracker configuration mismatch)
- AC-3.21 (new) —
manifest.features.tracker absent or malformed parses to {provider:'github'} and never makes parseManifest return null
- AC-3.22 (new) — hook output is independent of the developer's real
$HOME
- Prefix-shippability — After phase N: (i)
npm run build && npm test && npm run test:integration && npm pack --dry-run are green; (ii) a fresh install from the tarball drives /plan → /implement → /code-review → /resolve → /release on a GitHub project with no new prompt and no new file in git status; (iii) every artifact in the tree has at least one reachable consumer in this phase's end-state — no file, define, list element, frontmatter key or conditional arm exists solely for a later phase (ADR-003); (iv) the frozen github-status-lines.txt fixture is byte-identical to its Phase-0 capture.
- Clause (iii) per phase — The prefix-shippability invariant says "After phase N". Phase 3 is one phase. Its clause (iii) obligation is therefore evaluated at the 3c boundary, not at each of 3a/3b/3c.
Subtasks
| # |
Domain |
Owned files (with anchors) |
Steps |
Depends on |
Gate |
Handoff contents |
| 3a-1 |
TS core + CLI + uninstall |
new src/core/tracker.ts · new src/cli/commands/tracker-prompts.ts · new src/cli/commands/tracker.ts · src/cli/commands/init.ts (:222-238, :259-265, :291-292, :375-392, :438-453, :665-688, :744, :753, :924-958, :947-951, :1996) · src/cli/commands/init-seed.ts (:35-49, :52-61, :87-115, :335, :355-369) · src/core/manifest.ts (:38-72, :135-142, :229) · src/cli/commands/uninstall.ts (:227-265, :283-306) · new tests/core/tracker.test.ts · new tests/tracker-prompts.test.ts · new tests/tracker-cli.test.ts · tests/manifest.test.ts · tests/init-seed.test.ts · tests/uninstall-logic.test.ts:913-935 |
P3a-S1, S2, S3, S4, S5, S6, S15, S17 |
Phase 2 merged to main |
G3.1 |
the manifest key path literal features.tracker.provider as a shared constant · confirmation that features.tracker is absent-tolerant and never in the hard-null set at manifest.ts:151-165 · the chosen model tier (sonnet) so tracker.md's frontmatter and the hook allowlist agree · the .tracker.enabled sentinel path and its write/remove rule [DR-10] · the rearmTrackerInference helper name and the two call sites [DR-22] · the explicit pairing note that P3a-S15's rename and P3a-S17's user-content classification are void unless 3a-4's P3a-S14 mismatch guard ships in the same 3a group (the OD-15 reversal condition) |
| 3a-2 |
prompt assets + agent-enumerating tests |
new src/assets/agents/tracker.md (incl. the tracker.md schema template) · src/core/plugins.ts:63 · tests/agent-frontmatter.test.ts:35, :36-53 (16→17) · tests/agent-name-guards.test.ts (:511-524, :618-673) · tests/build.test.ts — [D21] re-verify the tests/build.test.ts repoint ranges (plan-1 :46-55/:84-91/:106-135 vs plan-2 :45-55/:84-92/:100-140, neither verified) before asserting no literal edits are needed · tests/skill-references.test.ts · new tests/tracker-agent.test.ts · new tests/tracker/hostile-values.test.ts |
P3a-S9, S16, S10 |
3a-1 |
G3.2 |
tracker.md's frontmatter name: byte-exactly (Tracker) · the claim-file path {devflowDir}/.tracker.processing and the attempt-counter path · the statement that _roster.mds gains no row and plugins.ts uses a structural, not exempted, Guard-5 pass · the schema section names verbatim, so 3a-4's ## Tracker input contract block matches |
| 3a-3 |
hooks + tests |
src/assets/scripts/hooks/session-start-context (:8-13, insert at :166) · tests/shell-hooks.test.ts (new describe beside :1670) · tests/config-disable-guards.test.ts:190-221 · the TS↔shell key-path parity constant + its test |
P3a-S7, S8 |
3a-2 (the agent file must exist first) |
G3.3 |
the allowlisted provider case literal and the byte-identical silence clause · the resolved ${DEVFLOW_DIR:-$HOME/.devflow} idiom · the named TRACKER_PROCESSING_STALE_SECS constant · the counter-increment rule at directive emission [DR-02] |
| 3a-4 |
prompt assets + scripts + tests |
new src/assets/mds/tracker/_mcp.mds · src/assets/scripts/redact-secrets.cjs (--emit, :85-88 narrowing, exit 5, amended :380-410 block) · tests/redact-secrets.test.ts (new describes) · src/assets/agents/git.mds:29 preamble additions · src/core/feature-config.ts (the tracker key) · new tests/guards/mcp-sink-bypass.test.ts · new tests/tracker/schema-scope.test.ts · new tests/tracker/byte-budget.test.ts (P3a-S18, BUDGET_GIT_MD_P3) · tests/fixtures/golden/git-agent.md (P3a-S18 — the standalone golden-regeneration commit) · docs/reference/platform-assumptions.md · P3a-S19 docs: CLAUDE.md · docs/reference/cli-reference.md · README.md · new .devflow/features/tracker-feature/KNOWLEDGE.md · CHANGELOG.md |
P3a-S11, S12, S13, S14, S18, S19 |
3a-3 |
G3.4 |
the _mcp.md generation-gating disposition · the D11-OK <nonce> <sha256> <bytes> <n> [type:count,…] framing literal verbatim [DR-01] · the <bytes> verification clause [DR-06] · the capability-probe table · the re-derived BUDGET_GIT_MD_P3 and the ≤70-line preamble ceiling [DR-13] |
| 3b |
prompt assets + tests |
new src/assets/mds/tracker/_jira.mds · its manifest rows in scripts/build-mds.ts · new tests/tracker/jira-module.test.ts |
P3b-S1, S2, S3 |
3a-4 |
G3.5 |
the file-set / define-set parity shape now that N = 2 providers · the marker-namespacing scheme (devflow:shipped / devflow:wave / devflow:traceability) · the DEGRADED literals actually emitted, checked against §14's canonical table · the per-op aggregate call budget literal [DR-09] |
| 3c |
prompt assets + tests |
new src/assets/mds/tracker/_linear.mds · manifest rows · new tests/tracker/linear-module.test.ts · new tests/provider-literals.test.ts · ## Known Unknowns + the filed probe issue |
P3c-S1 … S7 |
3b |
G3.6 |
— (terminal) |
Gates
| Gate |
Must be green before |
Assertions |
| G3.1 |
3a-2 starts |
tests/tracker-prompts.test.ts green incl. the 8-row gate matrix and the hostile-value table · manifest round-trip: a manifest with no features.tracker parses; features.tracker: "jira" (a string) self-heals to {provider:'github'}; parseManifest never returns null (AC-3.21) · uninstall disjointness test 9f extended to before.length === 6 · --reset ⇒ github · rearmTrackerInference unit cases + one call-site assertion each in init-seed.test.ts and tracker-cli.test.ts [DR-22] · the .tracker.enabled sentinel is written when the resolved provider ≠ github and removed when it is [DR-10] |
| G3.2 |
3a-3 starts |
loadShippedDefaults() ⊇ getAllAgentNames() = 17 and ['tracker'] === 'sonnet' · agent-frontmatter retitled 16→17 · agent-name-guards capitalizeFirst and the >= 50 floor green · tracker.md has no tools: key and no Agent(/subagent_type literal · _roster.mds unchanged |
| G3.3 |
3a-4 starts |
Full shell-hooks Section-3 suite green incl. hostile providers ⇒ no directive; source: resume/compact ⇒ no directive; fresh/stale claim; attempt cap at N=5; counter incremented on directive emission [DR-02]; provider github ⇒ zero subprocess invocations [DR-10]; _HAS_JQ=false parity; envelope key-set (:1010-1018 and :297 — [drift: two planners cite :701/:710, which are the UserPromptSubmit preamble assertions]) · config-disable-guards.test.ts passes under a seeded temp $HOME with provider=jira (AC-3.22) |
| G3.4 |
3b starts |
--emit prints no body on every non-zero path with a per-invocation nonce (AC-3.5) · the framing carries <n> [type:count,…] and the SOURCE module src/assets/mds/tracker/_mcp.mds names both SCRUB: and SECRET-EXPOSED [DR-01] · the same SOURCE module names the <bytes> verification [DR-06] — [E2] both clauses are asserted against the source .mds, never the generated dist/skills/git/references/tracker/_mcp.md, which does not exist at the 3a boundary because generation is keyed on a jira/linear module existing (P3a-S12 / hazard H7 / conflict C5) · the MCP bypass guard proven red on create_comment(body: $DEVFLOW_BODY_RAW) · the AC-2.7 _mcp.md guard re-scoped (H7) · hostile-tracker.md table green for every field, normalisation literal appears exactly once (AC-3.7) · the two-sided schema-heading test green with >= 11 sections [DR-21] · the DEGRADED registry test green in both directions [DR-04] · BUDGET_GIT_MD_P3 and the ≤70-line preamble ceiling green [DR-13] · P3a-S14's mismatch guard confirmed shipped (the precondition for 3a-1's user-content classification and the rename transition) |
| G3.5 |
3c starts |
File-set + define-set parity across two providers both directions, every define non-empty (AC-3.8 partial) · 32767 pinned; 60000 / X-RateLimit-Remaining absent from _jira.mds; Retry-After present (AC-3.13) · the fetch_batch single-query bullet present and no per-item fetch verb in the batch reference [DR-08] · the aggregate call budget pinned as a tested literal [DR-09] · frozen status lines unchanged |
| G3.6 |
PR opens |
Full prefix-shippability · parity across three providers · AC-3.12 · AC-3.18 · AC-3.19 (process checklist) · the _mcp.md shared-literal registry negative arm green and non-vacuous [DR-19] · the Phase-3 tests/fixtures/golden/git-agent.md regeneration commit is present and standalone (its own commit, reviewed as a text diff, no behaviour change in it) and tests/fixtures/golden/github-status-lines.txt is byte-identical to its Phase-0 capture [DR-03] |
/implement spawn notes
- Spawn one Code agent per subtask, in the order 3a-1 → 3a-2 → 3a-3 → 3a-4 → 3b → 3c. Each agent's prompt carries this artifact's §7 step block for its subtask, its row of the table above, its gate, and the handoff it receives.
- Commits are pathspec-scoped (
git commit -- <owned paths>) so per-commit provenance survives the squash-merge.
- Three commits, not six. 3a-1 … 3a-4 produce the single
3a commit group; the phase's commit boundaries are 3a, 3b, 3c.
- Spawn-input contract (
implement.mds:136-172) — stated per agent so nothing is left to inference:
- 3a-1 · 3a-2 · 3a-3 · 3a-4 · 3b —
CREATE_PR: false, PUSH: false, HANDOFF_REQUIRED: true.
- 3c —
CREATE_PR: true, PUSH: true. implement.mds:409: "The last sequential Code agent (with CREATE_PR: true) handles PR creation" — for SEQUENTIAL_CODE_AGENTS there is no separate Git-agent PR-creation step, so a terminal agent carrying CREATE_PR: false ends the phase with commits on a branch and no PR, and the Closes #325 traceability chain never fires.
- [I10] Handoff, stated explicitly on every spawn:
HANDOFF_REQUIRED: true on every subtask but the last; every Code agent reads and writes HANDOFF_FILE = .devflow/docs/handoff-{branch_slug}.md (implement.mds:151), writing its §6-row "Handoff contents" after its own gate passes. PUSH is a Code-agent input (code.md:34, :83) that implement.mds's spawn template does not carry — state it in each spawn prompt.
- The
3a-4 → 3b run seam is pre-authorised (§5.4; conflict C18 / decision D-G(a)). Taking it changes which agent is terminal: the first run's terminal agent (3a-4) keeps CREATE_PR: false, the second run's terminal agent (3c) carries CREATE_PR: true. Re-invoke /implement on the same issue and the same branch.
- Standing instruction for every Code prompt: "a golden mismatch means the source is wrong, never the fixture."
Execution strategy
SEQUENTIAL_CODE_AGENTS — 6 subtasks: 3a-1 → 3a-2 → 3a-3 → 3a-4 → 3b → 3c. Phase context risk: HIGH.
One issue, three ordered commits 3a → 3b → 3c on one branch (decision D-C). 3a alone spans five domains — TypeScript core, CLI, hooks, prompt assets, shell scripts — and is the phase's bulk, so it is executed as four sequential Code agents producing one commit group. init.ts (2,007 lines) is flagged critically unstable by the skim heatmap and 3a is its largest single consumer, which is why 3a-1's init/manifest work is its own reviewable commit inside the phase.
Why PARALLEL_CODE_AGENTS is rejected
Three independent facts, any one sufficient (PF-020's closure rule: disjointness must hold over each agent's READ/EXECUTE closure, not merely its WRITE set):
npm run build is rm -rf dist && … (package.json:20). Every Code agent's verification step deletes and rebuilds the one shared dist/. A sibling agent mid-suite observes a missing dist tree — and every dist-scanning guard in this feature deliberately fails loud on a missing directory. Parallel agents therefore manufacture nondeterministic red suites that look exactly like real regressions.
/implement has no worktree isolation — grep for worktree in implement.mds returns zero hits. All Code agents share one worktree, one git index, one dist/, one node_modules, one vitest run.
- The test corpus spans every partition. The seam test, the D11 sink-class guard, the containment test and the frozen fixture all read
git.md ∪ dist/commands/*.md ∪ dist/skills/git/references/** — the union of every proposed subtask's outputs.
The closest legitimate candidate, recorded so it is not re-litigated: 3a-1 (TS selection substrate) ∥ 3a-2 (prompt asset + plugins.ts). Write sets are genuinely disjoint including test files. It is still rejected on the shared-dist/ and no-worktree-isolation grounds. If parallelism is ever wanted here it requires per-agent git worktrees with per-worktree dist/, which /implement does not support — a tooling change, not a plan change.
PR title
feat(tracker): pluggable issue tracker with Jira and Linear providers
Implementation Plan
Design artifact (authoritative): .devflow/docs/design/tracker-phase-3.2026-09-05_1155.md — local and gitignored. A copy truncated at the 60k comment cap is posted as a collapsed comment: #325 (comment). Run with /implement .devflow/docs/design/tracker-phase-3.2026-09-05_1155.md. Tracking issue: #321.
Initial Request
Phases 0–2 leave a provider-shaped hole with exactly one provider in it.
Phase 0 repaired the command→agent issue seam and froze two goldens. Phase 1 compiled the Git agent from an MDS generator host, provably byte-identical. Phase 2 split the Git agent into a provider-neutral contract (
git.md, the ops, their**Input:**/**Output:**blocks, the D4/D11 invariants, the ≤40-line preamble) and GitHub mechanics generated per op intodist/skills/git/references/tracker/github/{op}.md. The shape is correct and the only occupant is GitHub.Phase 3 fills the hole:
manifest.features.tracker = { provider }, an enum overgithub|jira|lineardefaulting togithub, chosen atdevflow initon both wizard paths, settable non-interactively with--tracker <id>and afterwards withdevflow tracker --set <id>, inspectable withdevflow tracker --status.model: sonnet, read-only, notools:key) that infers the project's tracker conventions once — from the connected MCP server plus a bounded git-history scan — and writes the global, write-once~/.devflow/tracker.md(D11-scrubbed,chmod 600,# UNRESOLVED:sentinels for anything it could not determine, and a## Dedup Strategysection recording the probe evidence).session-start-context) that emits a silent background-setup directive — forjira/linearonly, only when~/.devflow/tracker.mdis absent, only onsource: startup|clear, bounded by a claim file and an attempt cap._mcp.mds→references/tracker/_mcp.md) with a mechanical D11 gate —redact-secrets.cjs --emit— because a shell&&short-circuit cannot exist inside an MCP tool call, and an instruction is not a gate.Selection and conventions are separate. That separation is what makes the silent-
githubpath, the hook gate and the zero-change GitHub guarantee all trivial: selection is a manifest enum read by a hook and a preamble; conventions are an inferred global file that only ever exists for a non-GitHub user.Honest user-visible-change statement
GitHub users — the default, and every existing install: nothing changes. No prompt, no new file, no altered byte. Regression-tested against the frozen
tests/fixtures/golden/github-status-lines.txtcaptured at Phase 0 and never regenerated since (AC-3.1). No Jira or Linear literal reachesdist/agents/git.md,dist/commands/*.mdorreferences/tracker/github/**(AC-3.12). Under providergithubthe hook's Section 3 performs zero subprocess invocations (DR-10).Jira/Linear users get, deliberately and visibly:
--tracker <id>flag and adevflow tracker --status/--set <id>command;~/.devflow/tracker.mdat a session start — never narrated, never a question;- **Tracker**: {provider}line in### Traceability;devflow uninstallwhere a user with no other user content previously got a silentartifacts-onlysweep.Linear ships at rank 4 — visible first-line marker, no author filter, post-with-warning — and says so in a
## Known Unknownssection with a filed probe issue.The uninstall consequence, stated in full
~/.devflow/tracker.mdis classified as user content (OD-15), sitting inuninstall.ts:227-265betweenpreference-profile.md(:246-250) andlearning.json(:252-256).resolveDevflowDirCleanup(:170-202) needs no change: adding auserContententry automatically flips a user-scope interactive uninstall from'artifacts-only'to'prompt'(:198-201).The rationale is the
agent-models.jsonreclassification, verbatim: "Stale per-agent model overrides silently re-apply…" (uninstall.ts:215-217). "Silently" is the load-bearing word. Phase 3's provider-mismatch guard (P3a-S14) removes silence: a staletracker.mdwhose frontmatterprovider:disagrees with the resolved provider producesTRACEABILITY: DEGRADED (tracker configuration mismatch)and no tracker call.Reversal condition, recorded: the user-content classification is conditional on the mismatch guard shipping. If P3a-S14's guard is ever dropped,
tracker.mdis reclassified to an install artifact in the same change — otherwise a silently-authoritative stale file survives uninstall..tracker.processing,.tracker.attemptsand.tracker.enabledare install artifacts (installArtifactPaths,uninstall.ts:283-306) and are removed by an artifacts-only sweep, which keepstracker.md. The two lists must stay disjoint (@d8,uninstall.ts:276-278).Execution:
SEQUENTIAL_CODE_AGENTS· context-risk: HIGH · Design artifact:.devflow/docs/design/tracker-phase-3.2026-09-05_1155.md(authoritative; local, gitignored — a 60k-truncated copy is posted as a collapsed comment).Product Requirements
Depends on
Phase 2 (#324) merged to
main. Tracking issue: #321.Acceptance criteria
github-status-lines.txtjiraand notracker.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 — the full end-to-end "next tracker op resolves from it" is(process-only — PR checklist)jirawith no Jira MCP ⇒ a named DEGRADED at each tracker op, branch and PR still created,Tracked (pending)with the reason, never a GitHub issue as fallbackSHIPPED_ISSUES="PROJ-1 PROJ-2"under jira never yieldsCOMPLETE--emitprints no body on every non-zero path; nonce present and per-invocation; every posting mechanic spells the body{SCRUBBED_BODY}and namesD11-OK; the bypass guard is red oncreate_comment(body: $DEVFLOW_BODY_RAW)— AMENDMENT [DR-01]: …and every posting mechanic also namesSECRET-EXPOSED. The success framing isD11-OK <nonce> <sha256> <bytes> <n> [type:count,…], carrying the first-pass scrub result._mcp.mdmandates: echoSCRUB: N […]from theD11-OKline before posting; when N > 0 also emitSECRET-EXPOSED (rotate {type} credential — the source file still holds it).tests/guards/mcp-sink-bypass.test.tsasserts every posting mechanic namesD11-OKANDSECRET-EXPOSED; the known-bad sample is a Jira posting mechanic that omits the rotation line. Both literals are added to the DEGRADED/status-line literal registry test. — AMENDMENT [DR-06]: …and every posting mechanic names the<bytes>verification: before posting, confirm the received body's byte length equals the<bytes>field of theD11-OKline; on mismatch DO NOT POST and emitTRACEABILITY: DEGRADED (redaction unavailable). Known-bad = a mechanic that namesD11-OKbut not<bytes>.--recommended/non-TTY, and--trackersuppresses the prompt on both pathstracker.mdtable passes for every field; the normalisation literal appears exactly once; no mechanics file re-readsprovider:providers.length === 3# UNRESOLVED:sentinels — AMENDMENT [DR-02]: on a write-less exit the agent increments{devflowDir}/.tracker.attemptsbefore deleting the claim file; on a successful write it deletes the counter./implement PROJ-123→ branch{type}/PROJ-123-{slug}with the type from## Issue Types,- **Issue**: PROJ-123,- **Tracker**: jira, and the rendered PR link line — live journey is(process-only — PR checklist)dist/agents/git.md,dist/commands/*.mdorreferences/tracker/github/**32767pinned and60000/X-RateLimit-Remainingabsent in the Jira/Linear modules;Retry-Afterpresent in Jira;RATELIMITEDpresent in Linear — AMENDMENT [DR-08]: …plus "no per-item fetch verb (getJiraIssue,get_issue, or any single-key fetch capability) appears in any provider'sfetch-issues-batchreference", RED against a seeded per-item fixture.find_markernames the identity capability and the first-line predicate; a marker at line 5 of a third-party comment does not suppress; markers are namespaced per comment kindtracker.mdwrite is scrub-gated fail-closed; no userinfo URL, no email/accountId literal; denied field names dropped with### Substitutions; file mode0600dist/commands/*.md(incl.release.md) reads~/.devflow/tracker.md; nocurl/wget/Authorization:/token-env read ingit.md ∪ references--reset, or reinstall) never leaves a silently-authoritative stale file: init renames it and the preamble emitsDEGRADED (tracker configuration mismatch)manifest.features.trackerabsent or malformed parses to{provider:'github'}and never makesparseManifestreturn null$HOMEnpm run build && npm test && npm run test:integration && npm pack --dry-runare green; (ii) a fresh install from the tarball drives/plan → /implement → /code-review → /resolve → /releaseon a GitHub project with no new prompt and no new file ingit status; (iii) every artifact in the tree has at least one reachable consumer in this phase's end-state — no file, define, list element, frontmatter key or conditional arm exists solely for a later phase (ADR-003); (iv) the frozengithub-status-lines.txtfixture is byte-identical to its Phase-0 capture.Subtasks
src/core/tracker.ts· newsrc/cli/commands/tracker-prompts.ts· newsrc/cli/commands/tracker.ts·src/cli/commands/init.ts(:222-238,:259-265,:291-292,:375-392,:438-453,:665-688,:744,:753,:924-958,:947-951,:1996) ·src/cli/commands/init-seed.ts(:35-49,:52-61,:87-115,:335,:355-369) ·src/core/manifest.ts(:38-72,:135-142,:229) ·src/cli/commands/uninstall.ts(:227-265,:283-306) · newtests/core/tracker.test.ts· newtests/tracker-prompts.test.ts· newtests/tracker-cli.test.ts·tests/manifest.test.ts·tests/init-seed.test.ts·tests/uninstall-logic.test.ts:913-935features.tracker.provideras a shared constant · confirmation thatfeatures.trackeris absent-tolerant and never in the hard-null set atmanifest.ts:151-165· the chosen model tier (sonnet) sotracker.md's frontmatter and the hook allowlist agree · the.tracker.enabledsentinel path and its write/remove rule [DR-10] · therearmTrackerInferencehelper name and the two call sites [DR-22] · the explicit pairing note that P3a-S15's rename and P3a-S17's user-content classification are void unless 3a-4's P3a-S14 mismatch guard ships in the same 3a group (the OD-15 reversal condition)src/assets/agents/tracker.md(incl. thetracker.mdschema template) ·src/core/plugins.ts:63·tests/agent-frontmatter.test.ts:35,:36-53(16→17) ·tests/agent-name-guards.test.ts(:511-524,:618-673) ·tests/build.test.ts—[D21]re-verify thetests/build.test.tsrepoint ranges (plan-1:46-55/:84-91/:106-135vs plan-2:45-55/:84-92/:100-140, neither verified) before asserting no literal edits are needed ·tests/skill-references.test.ts· newtests/tracker-agent.test.ts· newtests/tracker/hostile-values.test.tstracker.md's frontmattername:byte-exactly (Tracker) · the claim-file path{devflowDir}/.tracker.processingand the attempt-counter path · the statement that_roster.mdsgains no row andplugins.tsuses a structural, not exempted, Guard-5 pass · the schema section names verbatim, so 3a-4's## Tracker input contractblock matchessrc/assets/scripts/hooks/session-start-context(:8-13, insert at:166) ·tests/shell-hooks.test.ts(newdescribebeside:1670) ·tests/config-disable-guards.test.ts:190-221· the TS↔shell key-path parity constant + its testcaseliteral and the byte-identical silence clause · the resolved${DEVFLOW_DIR:-$HOME/.devflow}idiom · the namedTRACKER_PROCESSING_STALE_SECSconstant · the counter-increment rule at directive emission [DR-02]src/assets/mds/tracker/_mcp.mds·src/assets/scripts/redact-secrets.cjs(--emit,:85-88narrowing, exit5, amended:380-410block) ·tests/redact-secrets.test.ts(new describes) ·src/assets/agents/git.mds:29preamble additions ·src/core/feature-config.ts(thetrackerkey) · newtests/guards/mcp-sink-bypass.test.ts· newtests/tracker/schema-scope.test.ts· newtests/tracker/byte-budget.test.ts(P3a-S18,BUDGET_GIT_MD_P3) ·tests/fixtures/golden/git-agent.md(P3a-S18 — the standalone golden-regeneration commit) ·docs/reference/platform-assumptions.md· P3a-S19 docs:CLAUDE.md·docs/reference/cli-reference.md·README.md· new.devflow/features/tracker-feature/KNOWLEDGE.md·CHANGELOG.md_mcp.mdgeneration-gating disposition · theD11-OK <nonce> <sha256> <bytes> <n> [type:count,…]framing literal verbatim [DR-01] · the<bytes>verification clause [DR-06] · the capability-probe table · the re-derivedBUDGET_GIT_MD_P3and the ≤70-line preamble ceiling [DR-13]src/assets/mds/tracker/_jira.mds· its manifest rows inscripts/build-mds.ts· newtests/tracker/jira-module.test.tsdevflow:shipped/devflow:wave/devflow:traceability) · the DEGRADED literals actually emitted, checked against §14's canonical table · the per-op aggregate call budget literal [DR-09]src/assets/mds/tracker/_linear.mds· manifest rows · newtests/tracker/linear-module.test.ts· newtests/provider-literals.test.ts·## Known Unknowns+ the filed probe issueGates
tests/tracker-prompts.test.tsgreen incl. the 8-row gate matrix and the hostile-value table · manifest round-trip: a manifest with nofeatures.trackerparses;features.tracker: "jira"(a string) self-heals to{provider:'github'};parseManifestnever returns null (AC-3.21) · uninstall disjointness test 9f extended tobefore.length === 6·--reset ⇒ github·rearmTrackerInferenceunit cases + one call-site assertion each ininit-seed.test.tsandtracker-cli.test.ts[DR-22] · the.tracker.enabledsentinel is written when the resolved provider ≠ github and removed when it is [DR-10]loadShippedDefaults() ⊇ getAllAgentNames()= 17 and['tracker'] === 'sonnet'·agent-frontmatterretitled 16→17 ·agent-name-guardscapitalizeFirstand the>= 50floor green ·tracker.mdhas notools:key and noAgent(/subagent_typeliteral ·_roster.mdsunchangedshell-hooksSection-3 suite green incl. hostile providers ⇒ no directive;source: resume/compact⇒ no directive; fresh/stale claim; attempt cap at N=5; counter incremented on directive emission [DR-02]; providergithub⇒ zero subprocess invocations [DR-10];_HAS_JQ=falseparity; envelope key-set (:1010-1018and:297— [drift: two planners cite:701/:710, which are the UserPromptSubmit preamble assertions]) ·config-disable-guards.test.tspasses under a seeded temp$HOMEwithprovider=jira(AC-3.22)--emitprints no body on every non-zero path with a per-invocation nonce (AC-3.5) · the framing carries<n> [type:count,…]and the SOURCE modulesrc/assets/mds/tracker/_mcp.mdsnames bothSCRUB:andSECRET-EXPOSED[DR-01] · the same SOURCE module names the<bytes>verification [DR-06] — [E2] both clauses are asserted against the source.mds, never the generateddist/skills/git/references/tracker/_mcp.md, which does not exist at the 3a boundary because generation is keyed on a jira/linear module existing (P3a-S12 / hazard H7 / conflict C5) · the MCP bypass guard proven red oncreate_comment(body: $DEVFLOW_BODY_RAW)· the AC-2.7_mcp.mdguard re-scoped (H7) · hostile-tracker.mdtable green for every field, normalisation literal appears exactly once (AC-3.7) · the two-sided schema-heading test green with>= 11sections [DR-21] · the DEGRADED registry test green in both directions [DR-04] ·BUDGET_GIT_MD_P3and the ≤70-line preamble ceiling green [DR-13] · P3a-S14's mismatch guard confirmed shipped (the precondition for 3a-1's user-content classification and the rename transition)32767pinned;60000/X-RateLimit-Remainingabsent from_jira.mds;Retry-Afterpresent (AC-3.13) · thefetch_batchsingle-query bullet present and no per-item fetch verb in the batch reference [DR-08] · the aggregate call budget pinned as a tested literal [DR-09] · frozen status lines unchanged_mcp.mdshared-literal registry negative arm green and non-vacuous [DR-19] · the Phase-3tests/fixtures/golden/git-agent.mdregeneration commit is present and standalone (its own commit, reviewed as a text diff, no behaviour change in it) andtests/fixtures/golden/github-status-lines.txtis byte-identical to its Phase-0 capture [DR-03]/implementspawn notesgit commit -- <owned paths>) so per-commit provenance survives the squash-merge.3acommit group; the phase's commit boundaries are3a,3b,3c.implement.mds:136-172) — stated per agent so nothing is left to inference:CREATE_PR: false,PUSH: false,HANDOFF_REQUIRED: true.CREATE_PR: true,PUSH: true.implement.mds:409: "The last sequential Code agent (withCREATE_PR: true) handles PR creation" — forSEQUENTIAL_CODE_AGENTSthere is no separate Git-agent PR-creation step, so a terminal agent carryingCREATE_PR: falseends the phase with commits on a branch and no PR, and theCloses #325traceability chain never fires.HANDOFF_REQUIRED: trueon every subtask but the last; every Code agent reads and writesHANDOFF_FILE=.devflow/docs/handoff-{branch_slug}.md(implement.mds:151), writing its §6-row "Handoff contents" after its own gate passes.PUSHis a Code-agent input (code.md:34,:83) thatimplement.mds's spawn template does not carry — state it in each spawn prompt.3a-4 → 3brun seam is pre-authorised (§5.4; conflict C18 / decision D-G(a)). Taking it changes which agent is terminal: the first run's terminal agent (3a-4) keepsCREATE_PR: false, the second run's terminal agent (3c) carriesCREATE_PR: true. Re-invoke/implementon the same issue and the same branch.Execution strategy
SEQUENTIAL_CODE_AGENTS— 6 subtasks: 3a-1 → 3a-2 → 3a-3 → 3a-4 → 3b → 3c. Phase context risk: HIGH.One issue, three ordered commits
3a → 3b → 3con one branch (decision D-C). 3a alone spans five domains — TypeScript core, CLI, hooks, prompt assets, shell scripts — and is the phase's bulk, so it is executed as four sequential Code agents producing one commit group.init.ts(2,007 lines) is flagged critically unstable by the skim heatmap and 3a is its largest single consumer, which is why 3a-1's init/manifest work is its own reviewable commit inside the phase.Why
PARALLEL_CODE_AGENTSis rejectedThree independent facts, any one sufficient (PF-020's closure rule: disjointness must hold over each agent's READ/EXECUTE closure, not merely its WRITE set):
npm run buildisrm -rf dist && …(package.json:20). Every Code agent's verification step deletes and rebuilds the one shareddist/. A sibling agent mid-suite observes a missing dist tree — and every dist-scanning guard in this feature deliberately fails loud on a missing directory. Parallel agents therefore manufacture nondeterministic red suites that look exactly like real regressions./implementhas no worktree isolation — grep forworktreeinimplement.mdsreturns zero hits. All Code agents share one worktree, one git index, onedist/, onenode_modules, one vitest run.git.md ∪ dist/commands/*.md ∪ dist/skills/git/references/**— the union of every proposed subtask's outputs.The closest legitimate candidate, recorded so it is not re-litigated: 3a-1 (TS selection substrate) ∥ 3a-2 (prompt asset +
plugins.ts). Write sets are genuinely disjoint including test files. It is still rejected on the shared-dist/and no-worktree-isolation grounds. If parallelism is ever wanted here it requires per-agentgit worktrees with per-worktreedist/, which/implementdoes not support — a tooling change, not a plan change.PR title
feat(tracker): pluggable issue tracker with Jira and Linear providersImplementation Plan
Design artifact (authoritative):
.devflow/docs/design/tracker-phase-3.2026-09-05_1155.md— local and gitignored. A copy truncated at the 60k comment cap is posted as a collapsed comment: #325 (comment). Run with/implement .devflow/docs/design/tracker-phase-3.2026-09-05_1155.md. Tracking issue: #321.