Tracker Publish
Publish approved story groups to an external tracker. This skill prepares the handoff contract used by the standalone orchestrator; it does not start the orchestrator.
Operating Model (overview)
This optional add-on mirrors approved Claude Harness story groups into an external tracker (Linear/Jira), then lets a standalone orchestrator schedule unblocked groups and launch Claude Code in isolated workspaces. The default scaffold remains local-only — use tracker orchestration only when a project wants Linear/Jira to act as the visible work queue and human review surface.
Claude Harness stays the source of truth for planning and verification (/brd → /spec → /design → /auto --group <id>). The tracker is only a control plane: issue status gates whether a group may run, blocker links mirror the local dependency graph, and comments hold proof/PR links. Human review and final merge stay outside the autonomous loop. The orchestrator is external — it polls the tracker, claims eligible unblocked group issues, creates one workspace per group, launches claude --print, reads .claude/state/tracker-runs/<group>/result.json, updates the tracker, and leaves completed work in Human Review.
Recommended tracker states: Published/Todo (created, not eligible) → Ready for Agent (eligible when blockers are terminal/pass) → In Progress (claimed) → Human Review (branch/PR/proof ready) → Done (after human merge). Plus Blocked, Canceled/Cancelled/Duplicate as terminal states.
Safety rules:
- Do not dispatch work unless the issue carries the configured ready label/state.
- Never mark tracker issues
Doneautomatically. - Do not bypass local story readiness, design approval, sprint contracts, or evaluator gates.
- Keep tracker API keys in environment variables, not repo files.
- Use isolated workspaces for unattended runs; prefer group-level execution (the external orchestrator schedules groups;
/autocreates the internal agent teams).
Related files: .claude/templates/tracker-config.template.json, .claude/templates/tracker-workflow.template.md, .claude/state/tracker-map.json, .claude/state/tracker-runs/, and the symphony_clone/ orchestrator.
Granularity
The --granularity flag chooses what becomes a tracker issue:
| Mode | Issue maps to | When to use |
|---|---|---|
group (default) |
One tracker issue per dependency group from dependency-graph.md. The group issue body lists every ready story in that group, and the harness command is /auto --group <id> (or whatever HARNESS_COMMAND_TEMPLATE resolves to). |
Default for /build projects and any work where a group is reviewed as a single PR. The agent team inside /auto handles per-story decomposition. |
story |
One tracker issue per ready story. Each story issue carries Story: E1-S1 plus its group ID. Group-level blockers are mirrored as blocked_by links between story issues. The orchestrator runs each story individually via the per-issue mode override (default mode-vibe for trivial stories; the publisher writes mode-lite or mode-vibe based on story metadata). |
When the human reviewer wants one PR per story (smaller diffs, faster review cycle), when stories are independently shippable, or when you want different Claude commands per story (for example, mode-vibe for a docs story, /auto for an API story). |
single |
One tracker issue for a single brownfield story (no epic/dependency-graph prerequisites). Built by scripts/single-story-map.js into the same map shape publish-to-linear.js consumes, then published with the unchanged publisher. |
Used by /feature's single-story lane, where the change is one bounded story and the full /build artifact set (epics, dependency-graph, component-map, features.json) does not exist. |
Picking the right granularity matters more than people think:
groupgranularity is cheaper to orchestrate (fewer issues, fewer PRs, fewer Linear comments) and lets/autoexploit intra-group parallelism via agent teams. It produces large PRs that need careful human review.storygranularity gives you small PRs but loses the intra-group parallelism gain — each story runs as its own orchestrator workspace and the orchestrator pays per-issue overhead (clone, branch, tracker round-trips). On a Max-licence-only setup, this can be slower wall-clock for the same total work.
If you do not specify --granularity, default to group. Switch to story only if the human explicitly asks for per-story PRs.
--granularityis prose-driven, not a parsed CLI flag:publish-to-linear.jstakes no--granularityargument. The granularity is realized by which map this skill writes before invoking the publisher —group/storyfrom the dependency graph,singleviascripts/single-story-map.js. The publisher just creates one Linear issue pergroupsentry it finds.
Prerequisites
The following files must exist and be approved by the human:
specs/stories/epics.mdspecs/stories/dependency-graph.mdspecs/stories/E*-S*.mdfeatures.jsonspecs/design/component-map.mdspecs/design/api-contracts.md.claude/tracker-config.json
If design artifacts are missing, stop and ask the human to run /design first. The orchestrator needs component-map.md so /auto can coordinate agent teams without file ownership conflicts.
--granularity singleexception: the single-story lane needs only.claude/tracker-config.jsonand the story's acceptance criteria — none ofepics.md,dependency-graph.md,component-map.md, orfeatures.jsonis required./featurebuilds the one-entry map viascripts/single-story-map.js.
Output Contract
Write or update:
.claude/state/tracker-map.json
.claude/state/tracker-runs/
tracker-map.json shape (group granularity):
{
"provider": "linear",
"granularity": "group",
"published_at": "2026-05-01T00:00:00.000Z",
"groups": {
"A": {
"tracker_key": "ENG-101",
"tracker_id": "tracker-internal-id",
"url": "https://linear.app/example/issue/ENG-101",
"stories": ["E1-S1", "E1-S2"],
"depends_on_groups": []
}
},
"stories": {
"E1-S1": {
"group": "A",
"tracker_key": "ENG-101"
}
}
}
tracker-map.json shape (story granularity):
{
"provider": "linear",
"granularity": "story",
"published_at": "2026-05-01T00:00:00.000Z",
"groups": {
"A": {
"stories": ["E1-S1", "E1-S2"],
"depends_on_groups": []
}
},
"stories": {
"E1-S1": {
"group": "A",
"tracker_key": "ENG-101",
"tracker_id": "tracker-internal-id-1",
"url": "https://linear.app/example/issue/ENG-101",
"harness_command": "/build --lite --group A",
"mode_label": "mode-lite",
"blocked_by_stories": []
},
"E1-S2": {
"group": "A",
"tracker_key": "ENG-102",
"tracker_id": "tracker-internal-id-2",
"url": "https://linear.app/example/issue/ENG-102",
"harness_command": "/build --lite --group A",
"mode_label": "mode-lite",
"blocked_by_stories": ["E1-S1"]
}
}
}
Under story granularity, the groups section is informational — every actual tracker issue lives under stories. The orchestrator looks up the story by tracker key and runs the harness command resolved for that story.
Publishing Rules
Common rules (both granularities)
- Do not publish stories marked
needs_breakdown. - Do not dispatch any run from this skill.
- Apply configured labels such as
harness-group,agent-ready. - Always include the per-issue
mode-*label when a non-default harness command is intended (for examplemode-litefor a/build --lite-eligible scope). - Keep
.claude/state/tracker-map.jsonauthoritative for the local repo. If you re-publish, update entries in place rather than creating duplicates.
Group granularity (--granularity group, default)
Read
dependency-graph.mdand create one tracker issue per dependency group.Include every ready story in the group issue body.
Include acceptance criteria, feature IDs, owned files from
component-map.md, and the harness command:/auto --group AMirror group dependencies as tracker blocker relationships when the provider supports them.
Apply
group-A(or equivalent group ID) label.
Story granularity (--granularity story)
- Read every ready story file under
specs/stories/and create one tracker issue per story (skipneeds_breakdownand stories not in any group). - Include the story's acceptance criteria, owned files from
component-map.md, and feature IDs fromfeatures.jsonfiltered to that story. - The harness command for each story is derived as follows:
- If the story file declares a
Mode:field (e.g.Mode: lite), use that. - Otherwise, if the entire group fits
/build --liteeligibility (≤5 stories, single group, no DB/auth — see.claude/skills/build/references/lite-lane.md), use/build --lite --group <group-id>for every story in that group. - Otherwise default to
/auto --group <group-id>and let/autorecognise the single-story scope.
- If the story file declares a
- Apply the matching
mode-*label (e.g.mode-lite) when the harness command is not the default/auto. - Mirror in-group story dependencies (
Depends On:in story files) as tracker blocker relationships between story issues. Cross-group dependencies are inherited from the group's blocker links. - Apply
story-E1-S1(or the story ID as a slug) label so humans can filter the tracker view by story.
Group Issue Body Template (--granularity group)
## Harness Group
- Group: A
- Harness command: /auto --group A
- Stories: E1-S1, E1-S2
- Depends on groups: none
## Acceptance Criteria
### E1-S1 — Story title
- Criterion 1
- Criterion 2
## Feature IDs
- F001
- F002
## Expected Proof
- Branch or PR URL
- Unit/lint/typecheck result
- Evaluator report
- Security review
- Updated `features.json` pass/fail state
Story Issue Body Template (--granularity story)
## Harness Story
- Story: E1-S1
- Group: A
- Harness command: /build --lite --group A
- Depends on stories: none
## Acceptance Criteria
- Criterion 1
- Criterion 2
- Criterion 3
## Owned Files (from component-map.md)
- src/research/duckduckgo.py
- tests/test_duckduckgo.py
## Feature IDs
- F001
## Expected Proof
- Branch or PR URL
- Unit/lint/typecheck result for the owned files
- Updated `features.json` entry for this story
Remote Publish — How Tracker Issues Actually Get Created
/tracker-publish writes the local handoff contract (.claude/state/tracker-map.json + .claude/state/tracker-runs/group-*.md). It does not call Linear directly from Claude's tool surface. The actual remote create happens via one of three transports, picked in this order:
- Linear MCP server — if a Linear MCP is configured in the session, prefer the MCP tool (
linear:issue:createor equivalent). MCP gives you OAuth and avoids storing the API key. - Bundled publisher script —
node .claude/skills/tracker-publish/scripts/publish-to-linear.js. Self-contained Node script that:- auto-loads
<project-root>/.env(without overriding existing shell env) - reads
tracker-map.json+tracker-config.json - looks up the Linear project by slug, picks the matching team (by
team_key) - resolves the configured
ready_stateto a state ID - creates each missing label, then
issueCreates one issue per pending group - writes the real
tracker_key/tracker_id/urlback intotracker-map.json - idempotent: groups whose
tracker_keyalready looks real are skipped
- auto-loads
- Manual
linear-cli— if a user has the third-party CLI installed and wants to drive it themselves.
Invoking the bundled script
# from the project root
node .claude/skills/tracker-publish/scripts/publish-to-linear.js
# dry run (no remote calls, no file writes)
node .claude/skills/tracker-publish/scripts/publish-to-linear.js --dry-run
# override env file location (e.g. when .env lives elsewhere)
node .claude/skills/tracker-publish/scripts/publish-to-linear.js --env-file /path/to/.env
Provider-aware routing: --provider jira
When provider is jira, the skill runs node .claude/skills/tracker-publish/scripts/publish-to-jira.js instead of the Linear publisher. The Jira publisher uses the same tracker-map.json shape and --granularity semantics — one Jira issue is created per groups entry. Each issue body is formatted as Atlassian Document Format (ADF) and, after creation, the issue is transitioned into the configured ready state. A group whose ready-state transition cannot be found is created but left in its default status; the run summary flags it as a warning (not an error).
Auth: set JIRA_EMAIL and JIRA_API_TOKEN in the project .env or the shell. The script uses HTTP Basic auth (email:token base-64 encoded) — no OAuth flow required.
Config fields (in .claude/tracker-config.json under tracker):
| Field | Required | Notes |
|---|---|---|
base_url |
yes | e.g. https://your-org.atlassian.net |
project_key |
yes | e.g. ENG |
issue_type |
no | defaults to Task |
ready_state |
no | workflow transition name for the ready state (e.g. Ready for Agent); also read from the tracker-map's config_snapshot.ready_state; defaults to To Do. A group whose ready-state transition isn't found is created and left in its default status (the summary flags it). |
# from the project root
node .claude/skills/tracker-publish/scripts/publish-to-jira.js
# dry run (no remote calls, no file writes)
node .claude/skills/tracker-publish/scripts/publish-to-jira.js --dry-run
Prerequisites:
LINEAR_API_KEYin<project-root>/.env(the file is git-ignored by the scaffold) or in the shell.tracker.project_slugin.claude/tracker-config.jsonset to a real Linear project slug — the placeholderreplace-with-linear-project-slugis rejected.tracker.team_key(e.g.ENG) matching the team attached to that project. If absent, the script falls back to the first team on the project and warns.- Workflow state names in
config_snapshot(ready_state,running_state, etc.) must exist in the team — the script will print the available states if a name is wrong.
What the skill should do when MCP isn't available
After writing the local artifacts, check for Linear MCP. If none:
- Detect whether
LINEAR_API_KEYis reachable (project.envor shell). - If yes, surface the exact command to run (the
node …/publish-to-linear.jsline above) and wait for the user to invoke it. - If no, surface a "missing prerequisites" block explaining how to set the key and what to do next.
Do not assume the user has shelled in the API key — many users set it only in .env and never export it. Always prefer the script's built-in .env loader over a shell-export requirement.
Human Review Gate
After publishing, present:
- groups created or updated
- tracker issue URLs (real ones if the bundled script ran, otherwise
pending-remote-publishplaceholders with the follow-up command) - dependency/blocker mapping
- next command for the standalone orchestrator
Ask the human to confirm the tracker workflow before unattended orchestration begins.