| name | boss-plan |
| description | Plan a tracker backlog ticket. Grabs the next unplanned issue by priority (or a ticket ID you provide), resolves drafting through boss-plan draft extensions or portable fallbacks, attaches the plan natively to the tracker issue, then writes a summary, labels, Fibonacci estimate, and priority before moving it from the unplanned to the planned state. Interactive by default; runs fully headless when BOSS_CRON=true. |
boss-plan
Turn a vague, one-line tracker ticket into a fully-planned ticket (in the planned
state) with an implementation-ready plan attached. Use when asked to "plan a tracker ticket",
"plan the next ticket", "boss-plan", or given a ticket ID.
This skill is interactive by default — it may drive AskUserQuestion through a discovered
draft extension. Under BOSS_CRON=true it runs fully headless, dispatching a single awaited
subagent for recon + drafting (Phase 2), so it is safe to schedule unattended.
- Leave no local artifacts. At every terminal state, discard the scratch you created (gitignored dirs, seeded design docs,
mktemp files) so the worktree is clean — in all modes, headless (BOSS_CRON=true) especially.
- Dispatch zero-change work as such. Planning runs commit nothing, so a plain session finalizes
blocked behind an empty draft PR. See Sessions that change nothing in the boss skill: create_session takes quick_chat (no worktree or PR) or defer_pr (worktree, no up-front PR).
Headless mode. If BOSS_CRON=true, no human can answer AskUserQuestion, so never call it —
in the orchestrator or the subagent, at any phase. The default path: preflight → select the
ranked-queue head → dispatch ONE awaited general-purpose drafting subagent (Phase 2) → classify
its run-file sentinel → upload + write back to Linear. Make selections with reasonable defaults from
the ticket and codebase, discard local artifacts, and never block waiting for input.
On-demand references (read only when the mode calls for it)
Mode-exclusive prose lives in references/*.md, loaded only on the path that needs it. The
default headless orchestrator path reads neither — the resident body carries the whole skeleton.
| Reference | Read it when… |
|---|
references/interactive-mode.md | Interactive /boss-plan only — Phase 1 confirm loop, design-doc seed, draft resolution |
references/headless-drafting-brief.md | Passed (by path) to the Phase 2 drafting subagent — never read by the orchestrator |
references/extension-reviewers.md | Phase 3.5 — repo-local boss-plan-* extension plan-reviewers (additive; no-op when none) |
Workspace facts (do not re-discover). Load the config once in Phase 0 —
loadSkillConfig({cwd}) → config; tc = trackerConfigFor(config) — and reference these role
names generically everywhere else:
- Reach the tracker only through the resolved tracker adapter; its server, team, team-key and
workspace come from
trackerConfigFor(config) (never inline them, and never pass a project filter).
- Statuses by role: the unplanned state (start) and the planned state (end), resolved from
trackerConfigFor(config).states.{unplanned,planned} (with inProgress/inReview for the
active-backlog reads).
- Pipeline label roles resolve through
labelName(config, '<role>'), whose keys are camelCase: agentFriendly, needsHuman, agentPlan, agentQuestion, epic — the display
names they resolve to are agent-friendly, needs-human, and so on. labelName fails closed —
an unconfigured or misspelled role throws — so never hand it a display name. Never create labels.
agentFriendly and needsHuman are mutually exclusive (every plan gets exactly one).
- Content-taxonomy labels (
bug, feature, improvement, docs) are the tracker's own
display names, not fixed pipeline roles. Read the issue's existing set with the readLabels op
and merge — preserve what it returned, and add one only when it genuinely applies. Resolve each
taxonomy name with optionalLabelName(config, '<role>'), whose keys are bug, feature,
improvement, docs; if it returns null, apply the literal display name. Never create labels.
- Tracker priority numeric:
1=Urgent, 2=High, 3=Medium, 4=Low, 0=None.
- Dependency links use the tracker's
blocks/blocked by relations. A blocker is "cleared" only
when its state type is completed or canceled (PR merged / work dropped) — the
DEFAULT_CLEARED_STATE_TYPES / DEFAULT_CANCELED_STATE_TYPES rule in
toolbox/plan-deps-lib.mjs. boss-build will not start a ticket blocked by an uncleared blocker.
- Proof publishing remains independent of implementation-plan storage. Its configured publish
adapter and continue to govern proof artifacts only.
Phase 0 — Preflight
- Self-disable when this repo has no configured tracker. This runs in both interactive and
headless modes and precedes every tracker read/write. Probe the config seam and, when the repo
has no
.boss-skills.json / no configured tracker, print exactly one line and exit 0 — a clean
no-op, not an error (a /boss-plan in an unrelated repo is a no-op; a non-zero exit would surface
as a cron/agent error):
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
CONFIGURED=$(node -e 'import(require("node:url").pathToFileURL(process.env.BOSS_PLAN_TOOLBOX+"/skill-config.mjs").href).then(m=>{const c=m.loadSkillConfig({cwd:process.cwd()});process.stdout.write(m.isConfiguredForPlanning(c)?"yes":"no")}).catch(e=>{process.stderr.write("boss-plan preflight: "+(e&&e.message||e)+"\n");process.stdout.write("error")})')
# `isConfiguredForPlanning` requires the tracker identity AND the full state role map
# (`states.{unplanned,planned,inProgress,inReview}`), so a repo configured only for a stateless
# core self-disables cleanly ('no') instead of running with undefined state names.
# Distinguish a loader failure (malformed/invalid .boss-skills.json → 'error' or empty) from a
# valid "not planning-ready" ('no'): loadSkillConfig throws a `skill-config:` error on a present
# but broken config, so a broken config must abort loudly, never skip silently as a clean no-op.
if [ "$CONFIGURED" != "yes" ] && [ "$CONFIGURED" != "no" ]; then
echo "boss-plan: .boss-skills.json is present but could not be loaded (see error above) — aborting instead of skipping." >&2
exit 1
fi
if [ "$CONFIGURED" != "yes" ]; then
echo "boss-plan: no configured tracker in .boss-skills.json for this repo — nothing to plan here; skipping."
exit 0
fi
This block is the toolbox preamble. Each Bash tool call is a fresh shell, so every command
block that dereferences $BOSS_PLAN_TOOLBOX must begin with this preamble; an exported value
never survives to the next block. loadSkillConfig is synchronous and takes an options object
(loadSkillConfig({ cwd })); positional or awaited calls read as broken config.
- Warn when this installed toolbox has drifted from its source. The install is a copy, so a
repo whose helpers have moved on leaves a stale one here — silently. Probe once, now; it is an
observation that never aborts and never re-checks mid-run. The probe prints the realpath of the
toolbox, so a
bossanova/-prefixed path is symlink equivalence with the configured
, not drift. Guard the call, because an install predating the helper must
report that the drift status is unknown rather than fail — and re-derive the path first, since a
guard against an unset variable is silent in exactly the same way as a clean tree:
A line names installed helpers that differ from this repo's helper source.
Re-vendor and reinstall the skills to clear it, then continue — the run is not blocked.
Phase 1 — Select the issue
- If the user gave a ticket ID: call
get_issue with it. Respect that choice
regardless of status.
- Interactive: if it is already in the planned/in-progress/
Done/Canceled state, warn and
confirm before re-planning (see references/interactive-mode.md).
- Headless (
BOSS_CRON=true): do not ask. A cron job that names a ticket means to consider that
ticket, but the idempotence precheck below still wins: an already-planned ticket with a valid
description and canonical plan attachment exits cleanly without re-drafting. If the ticket is
Done/Canceled, log a warning and stop (re-planning finished work unattended is almost
never intended) rather than blocking.
- Otherwise: list the team's unplanned issues via the tracker adapter's list/select capability —
scoped to
trackerConfigFor(config).team and the unplanned state, limit=250. Rank the whole
queue by priority, reading the tracker's numbers correctly: Urgent(1) > High(2) > Medium(3)
Low(4) > None(0). Tie-break by oldest createdAt first. Keep this ranked list.
- Interactive: show the head of the ranked queue and run the confirm loop (plan this one /
skip this one / pick a different one / cancel) — see
references/interactive-mode.md. skip
walks down the ranked list.
- Headless (
BOSS_CRON=true): do not ask. Select the head of the ranked queue (highest
priority, oldest tie-break) and proceed straight to Phase 2. If the unplanned queue is empty,
report that and stop.
Before Phase 2 in both modes, run the idempotence precheck. Write the selected issue payload from
the Phase 1 read to .linear-plans/<ISSUE-ID>.precheck.json and invoke the deterministic guard
(planIdempotencePrecheck(...) in $BOSS_PLAN_TOOLBOX/plan-run-guards.mjs):
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
PRECHECK=".linear-plans/<ISSUE-ID>.precheck.json"
node "$BOSS_PLAN_TOOLBOX/plan-run-guards.mjs" idempotence "$PRECHECK"
If it prints action: "noop", delete the scratch file, print one line naming the ticket and the
satisfied conjuncts (planned state, valid description, canonical plan attachment), then exit 0
with zero tracker writes. If it prints action: "plan", log every reasons[] token and
continue. This precheck applies to explicitly-named tickets as well as queue-selected tickets; a
named ticket is not permission to destructively re-draft an already valid plan.
Phase 2 — Draft the plan
The plan itself — codebase recon, the review dimensions, and the polished write-up — is produced
per the Phase 3 plan requirements (the shared contract for what a plan must contain). The two
modes differ only in who drafts:
Draft-resolution (shared Fallback contract)
Resolve drafting by the Fallback contract: discovered boss-plan-* role: draft
extension → host built-in → inline prompt; tiers 2/3 suppressed only when a Tier-1 dispatch
succeeded, never merely because an extension exists. A dispatch succeeded only when its
result is valid AND the requested non-empty plan exists at the per-dispatch plan path that
dispatch alone was given, written by that dispatch — never at a path a peer could have written;
promote the first success to the real plan path. Record
extension <name>: skipped (<reason>) for every failed dispatch, including when a sibling
succeeded; when none succeeded, fall through to tier 2, then tier 3.
Interactive (default /boss-plan)
Resolve the draft/review step via the Fallback contract; the interactive
resolution and tier-3 inline drafting prompt live in references/interactive-mode.md. Then
continue to Phase 3.5 → Phase 4.
Headless (BOSS_CRON=true) — dispatch ONE awaited drafting subagent
Do not draft inline. Recon, drafting, and the self-review dimensions are bulk
context; keeping them on the main thread is exactly the cost this mode avoids. Instead:
Bulk-output discipline (no raw bulk in the orchestrator). The drafting dispatch keeps its bulk
material — the codebase recon and the drafted plan body — in the subagent's own context and
returns only the plan-file path plus a bounded metadata object; the orchestrator never pastes
the plan body or a subagent transcript back into its own context. It classifies the outcome from the
run-file sentinel only (never from returned prose) and reads the finished plan file exactly once,
for the Phase 4 secret gate.
-
Create the per-run sentinel context (the subagent writes its terminal decision here; the
orchestrator classifies from the file only). DISPATCH_FAILURE must stay byte-identical to
the module constant in bs-run-sentinel.mjs:
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
RUN_SENTINEL="$BOSS_PLAN_TOOLBOX/bs-run-sentinel.mjs"
test -f "$RUN_SENTINEL" || { echo "BLOCKED: bs-run-sentinel.mjs missing" >&2; exit 1; }
DISPATCH_FAILURE="dispatch-failure"
PLAN_PATH=".linear-plans/<ISSUE-ID>-<slug>.md" # compute the slug with plan-slug.mjs issueSlug
RUN="$(node "$RUN_SENTINEL" make-ctx boss-plan)"
RUN_ID="${RUN%%$'\t'*}"; RUN_DIR="${RUN#*$'\t'}"
export RUN_SENTINEL DISPATCH_FAILURE PLAN_PATH RUN_ID RUN_DIR
-
Before dispatch, write the byte copy of the Phase 1 get_issue description to
.linear-plans/<ISSUE-ID>.image-guard-orig.md. This is the single raw-description snapshot for
the whole run: Phase 4 reuses it, and the worker receives this path as its only description
source. Do not let the worker re-read the tracker description; signed upload URLs can rotate and
fail the parity gate.
-
Dispatch ONE awaited general-purpose subagent (subagent_type: general-purpose,
plan drafting is judgment, so **tier: opus**; **await** the dispatch —
never run_in_background). Pass it the path references/headless-drafting-brief.md (not
its text), the ticket id/title, the description snapshot path, the target PLAN_PATH, and the sentinel context
RUN_SENTINEL/RUN_DIR/RUN_ID. The brief tells it to recon, work the review dimensions, write
the plan to PLAN_PATH, write the terminal sentinel with a planPath payload, and return only
the bounded metadata object
(planPath, labels, agentFriendly, estimate, priority, openQuestions,
) — (returning content re-inflates the
caller: codex fold).
Phase 2.5 — Epic decomposition (triage = EPIC only)
When triage classifies the ticket EPIC — the honest estimate is ≥ 5, or the work spans
multiple independently-shippable
PRs with ≥ 2 genuinely separable PR-sized pieces (an honest ≤ 3 single-PR ticket is
SUBSTANTIAL, plan as one) — decompose it into a Linear parent + N fully-planned
children wired by an intra-epic blockedBy DAG, the exact shape boss-epic consumes.
Estimate is the forcing function: a single ticket may be estimated only 0/1/2/3; an honest 5
triages EPIC (unless genuinely atomic & un-splittable — then it survives as one ticket with a
recorded - Atomic-5: justification under ## Planning); an 8 is never a single-ticket estimate. The
interactive propose → confirm → create flow lives in references/interactive-mode.md; the headless
decompose-and-auto-create flow in references/headless-drafting-brief.md. The deterministic core —
validation, cycle safety, stable creation order, and the tracker-write plan — is the unit-tested
$BOSS_PLAN_TOOLBOX/plan-epic-lib.mjs (validateDecomposition, validateLayering, assertAcyclic,
topoOrderChildren, epicWiringPlan, epicParentEstimate, stableChildKey, serializeEpicSpec,
parseEpicSpec, validateSpecIdentity, specAttachmentFilename, specAttachmentTitle,
reconcileEpicChildren, EPIC_LABEL, EPIC_MIN_CHILDREN, EPIC_MAX_CHILDREN,
CHILD_MAX_ESTIMATE, SPEC_ATTACHMENT_MIME) plus this phase's own
$BOSS_PLAN_TOOLBOX/plan-epic-phase25.mjs (detectEpicParent, epicSpecRecoveryGate,
stalePlanAttachmentSweep, epicPhase25WritePlan);
never re-derive either inline.
Precondition — the source ticket MUST be unplanned. The whole epic model depends on it:
parent-repurpose-last keeps the original in unplanned until the epic is fully built, and idempotent
resume re-picks a stranded partial epic via the headless unplanned sweep (list_issues state=unplanned). Phase 1 admits an explicitly-named planned/in-progress source; if such a
non-unplanned source triages EPIC, check BOTH spec stores before falling back — one
get_issue(parent) already returns attachments[] and description, so checking both costs
zero extra calls. detectEpicParent(issue) is the whole classification, over that one payload:
it returns {isEpicParent, source, specAttachmentId, ambiguous, reasons} and owns the
store-specific presence rule, the attachment-wins-over-legacy ordering, and the two-or-more
Epic spec (…) attachments case (ambiguous: true ⇒ abort loudly per the contract's duplicate
policy, never guess which is current). An isEpicParent verdict means an existing epic parent (a
fully-built epic is flipped to planned but keeps its spec), so route to the idempotent
resume/no-op path — read the spec with parseEpicSpec on the attachment body, never on a
description that merely contains it — and never to the single-ticket fallback, which would
re-plan a finished epic as a normal buildable ticket with an implementation-plan artifact +
agent-friendly. Why the two stores are asymmetric: an Epic spec (…) attachment is created by
nothing but this phase, so a present-but-unreadable one is still proof; the description store is
the opposite because it is reporter-writable prose. A bare <!-- boss-plan-epic-spec: substring
is not evidence — a reporter can quote that string (this very sentence does), and treating the quote
as presence would classify a brand-new ticket as an unreadable epic parent and abort it loudly on
every sweep, permanently unplannable. Identity is checked on an
attachment-sourced spec ONLY: an attachment body can be copied or mis-attached from another epic,
so it must prove validateSpecIdentity(spec, <ISSUE-ID>). A legacy inline spec predates both
schemaVersion and parentId, so that check would reject it unconditionally; it is accepted on
provenance instead — it sits in the description of the ticket being resumed, which is the
strongest binding that store can offer. That is weaker than it sounds (duplicating an issue copies
its description, so a duplicate carries a spec naming the original's children); it is accepted
because the legacy store is read-only, frozen, and slated for removal, not because it is forgeproof.
A legacy parse that succeeds is trusted and as-is; only a legacy parse
reaches the gate below. — when the spec cannot be read, or an
attachment-sourced spec fails , the decision is
, which owns the ALL-of conjunct
set and names every failed one. Feed it the enumeration
below; where that op omits each child's attachments, read them per child — the one extra read this
gate may make. Its is only ever (enumerate + no-op) or :
— deliberately not even expressible in
that return type, because it would re-plan a finished or partial
epic as a normal buildable ticket. An parent can never satisfy the planned-parent
conjunct, so a corrupt spec attachment on one aborts every sweep until a human intervenes — that is deliberate
(re-decomposing would duplicate children), and the remediation is the same as the duplicate policy's:
delete the unreadable attachment, leaving the parent to re-decompose cleanly, or
repair its body. Accepted residual: the gate cannot detect a child deleted
outright — that failure is non-destructive (a partial epic is left alone, not corrupted). Only a
non-unplanned source with store present falls back to a single-ticket
plan (headless records the reason; interactive may re-ask). A non-unplanned parent
would sit in a non-queue state through the create→wire→expose window and, on a crash before the final
flip, be — recoverable only by manually re-running
that exact id. This precondition also means a well-formed epic parent never carries stale
/plan-link metadata; the strip in step 4 (below) is a defense-in-depth backstop, not
the primary guard.
The spec attachment contract. The decomposition spec is a native tracker attachment carrying
plain JSON, never a description marker:
| Field | Value |
|---|
| filename | epic-spec.json (specAttachmentFilename()) |
| MIME type | application/json (SPEC_ATTACHMENT_MIME) |
| title | Epic spec (<ISSUE-ID>) (specAttachmentTitle(<ISSUE-ID>)) — must NOT start with Implementation plan |
| body | serializeEpicSpec(spec) — plain JSON { schemaVersion, parentId, parent, children } |
| read | readPlanAttachment (the Phase 0 attachment-read op), by attachment id from get_issue |
| duplicate policy | exactly one is valid; two or more ⇒ abort loudly, never guess — a human deletes all but one, then re-runs |
| identity | validateSpecIdentity(spec, <ISSUE-ID>) — schemaVersion + parentId must match, not title alone |
Upload it with the same prepare → PUT → finalize mechanism a plan artifact uses
(references/plan-storage.md steps 1–5, uploadRequest.headers scratch-file discipline and its
immediate deletion after the PUT included, and the step-5 read-back), substituting this contract's
filename, MIME type and title. Never hand-roll a second upload path, and never claim the plan artifact's text/markdown
MIME or its Implementation plan (…) title: bs-epic-lib.mjs's normalizeTicket recognizes a plan
by exactly that prefix, so a spec attachment titled that way is mistaken for the parent's plan
artifact. Title alone is not identity — a human can create an attachment with any name — so the
schemaVersion + parentId match is what makes it trustworthy.
The planner drafts a decomposition spec
{ parentId:"<ISSUE-ID>", parent:{title,goal,keyChanges[]}, children:[{key,title,goal,keyChanges[],blockedByKeys[],estimate,priority,agentFriendly,openQuestions[]}] }
(each key is a stable title-derived slug from stableChildKey, so a fresh-worktree retry
re-derives it identically and its resume marker still matches; parentId is the source ticket's own
id and is not optional — serializeEpicSpec omits an absent id rather than inventing one, and
validateSpecIdentity then refuses the attachment forever, so an unset parentId ships an
unbindable spec), then runs this ordering discipline —
validate everything locally BEFORE the first Linear write (the atomicity guard):
-
Validate the spec. validateDecomposition + assertAcyclic. On failure: interactive
re-asks / falls back to a single SUBSTANTIAL plan; headless falls back to a single-ticket
plan and records the reason (never emit a broken epic).
-
Fully plan every child locally to IDless scratch, each a planContract-v1 plan (Phase 3), drafted with
allowEpic: false — the recursion guard:
a child is never itself decomposed (depth cap = 1). The spec never carries plan bodies, so copy
only each child plan's own agentFriendly verdict and its openQuestions list onto its spec
entry — serializeEpicSpec derives the child's agentQuestion (⇒ the agent-question label) from
a non-empty openQuestions, so a child left blank here silently loses that queue signal on
resume. Then re-run validateDecomposition on the completed spec before any
write — step 1 validated the spec before those verdicts existed, so its non-boolean-agentFriendly
guard (a malformed "false" string serializeEpicSpec would coerce to true) only bites when
validation runs again after the copy. Run the Phase 4 secret and image-parity
gates on every child plan before any write.
-
Confirm (interactive only, via AskUserQuestion: create this epic / plan as one ticket /
cancel); headless auto-creates.
-
Persist the FULL spec FIRST. The spec is an attachment now, so the old single atomic
save_issue becomes an ordered write sequence, and that sequence is
epicPhase25WritePlan({parentId, spec, unplannedState, staleAttachmentIds, labelsToStrip})
(labelsToStrip = the agent-friendly/needs-human exposure roles; it is parent-scoped,
stage 1's stripLabels and nothing else): execute its ops in emitted order — label-strip,
then spec-upload, then stale-delete, then create-children — (it emits one per SPEC child, never per missing child; executing
those unfiltered on a resume duplicates every child that already exists), exactly as step 5
executes . Each entry is , and , under the adapter's own key names; the created-id map passed to
must include the reserved entry beside every child id —
names what only
the executor can supply because it does not exist until the previous op ran (the prepare's ,
the PUT's //, the finalize's ). It owns the ordering (in
particular that every destructive delete comes strictly after the spec upload); never re-derive
that inline. It does own the stage preconditions below, which stay prose, and it emits
— a child's label set is not derivable from the spec
( persists /, never a array), so the
content labels + union below stays the caller's job:
Guards (load-bearing — the trigger bar is low + headless auto-creates): per-child estimate ceiling
CHILD_MAX_ESTIMATE = 3 (a 5/8 child is rejected ⇒ decompose further; the producer-before-consumer
soft check validateLayering warns on a read/ui child not gated by its producer), child-count cap
EPIC_MAX_CHILDREN = 12 (over ⇒ needs-human, never a single oversized ticket — the exact
monolith this avoids), minimum EPIC_MIN_CHILDREN = 2
(under ⇒ one ticket), recursion guard (allowEpic: false, no child recursion), cycle safety
(assertAcyclic rejects any blockedByKeys cycle before writes), validate-before-write (zero
Linear writes on any spec/gate failure), parent-repurpose-last (the write-atomicity guard:
children are created + wired before step 7 moves the parent unplanned → planned, so a crash or
malformed sentinel mid-create leaves the original ticket unplanned and the next sweep re-picks and
resumes it — a partial epic is never stranded), and idempotent resume (durable — survives a fresh
cron worktree where the .linear-plans/ scratch is gone): first get_issue the parent and
decode the returned body first with node "$BOSS_PLAN_TOOLBOX/plan-attachment.mjs" decode <in-file> <out-file>, then
parseEpicSpec the decoded body of its Epic spec (<ISSUE-ID>) attachment (two or more
Epic spec (…) attachments ⇒ abort loudly, never guess) to recover the FULL original
spec (parent overview + every child's full metadata) — step 4 stage 2 wrote it before any child and
no later description save touches it, so it survives even a fully-built epic. Then
validateSpecIdentity(spec, <ISSUE-ID>) it — attachment-sourced specs only, here too and not just on
the named-source branch above: the attachment was selected by title, and title alone is not identity.
A failure takes the unreadable-spec recovery gate, never a silent resume against another epic's spec. Legacy store (the one
description read that remains): a parent written by an earlier build carries the spec inline as a
<!-- boss-plan-epic-spec:… --> description marker instead, and parseEpicSpec falls back to that
form, so such an epic is still recognised and recovered. Then enumerate the already-created children with
(the op uses — on the parent does not return the children's descriptions where
the child markers live) then join them against the spec with — never by eye, never
by title — which matches each live child's marker to and
reports . (no orphans): create
exactly what names. ( holds one ):
adopt that child and rewrite description marker to — replacing
only the marker substring and , since the
save replaces the description and would otherwise wipe the child's gated plan body; repair the
child, never the spec key, because is the namespace reports under and the one every
sibling's and resolve through, so re-pointing the spec at
would strand those refs and throw mid-wire, after children already exist — create nothing for it. ( — multiple orphans, an unmarked child, duplicate live keys, or a non-array input):
take the SAFE branch — report , write nothing, create nothing, never guess; a refusal must never
be read as "no children exist" (that would duplicate the whole epic). Create only the spec keys
names —
— then finish wiring + parent repurpose.
because step 6 saves the parent overview description-only while the
parent stays unplanned (the planned flip is step 7), a crash in that window re-picks an unplanned
parent whose description is ( + child checklist),
not the reporter's raw notes; on resume —
never recompose from the transformed description (which would nest the overview or
trip image parity) — then (a crash could have landed after the parent save but before that pass, so the normal-flow
ordering — parent commit → external links → exposure — must hold on resume too, else an agent-friendly
root child is exposed without blocking overlapping active backlog work; the links are append-only, so
re-running is a safe no-op for edges already written) and finally finish the missing child exposure + the
unplanned → planned flip. A re-run
existing children and completes only what is missing from the original spec; on a
fully-built epic it is a clean no-op (never duplicates), even from a fresh worktree.
Phase 3 — Plan requirements (shared drafting spec)
Interactive or headless drafting (per references/headless-drafting-brief.md) produces
.linear-plans/<ISSUE-ID>-<slug>.md (gitignored; slug = issue id + hyphenated title; compute with
node -e 'import(require("node:url").pathToFileURL(process.argv[3]+"/plan-slug.mjs").href).then(m=>console.log(m.issueSlug(process.argv[1],process.argv[2])))' <ISSUE-ID> "<title>" "${BOSS_PLAN_TOOLBOX:?}" after running the toolbox preamble first).
The full drafting spec — body requirements and fill-in description-summary template — lives once
in references/headless-drafting-brief.md § "Step 5"/"Step 7"; both modes follow it.
The orchestrator keeps only the versioned description section contract consumed by boss-build and
bs-sweep-plan: descriptionSummary MUST carry these ## sections in order (## Why this needs a human and ## Open Questions conditional; all others always present), and stamps
- Contract: v<N> under ## Planning (v1 today):
## Summary · ## Approach · ## Key changes · ## Testing · ## Risks / unknowns ·
## Premises · ## Acceptance criteria · ## Required proof · ## Why this needs a human ·
## Open Questions · ## Planning · ## Original notes
## Premises and ## Proof harness analysis are optional. Any other heading is off-contract: drop
it, or register it in planContract.sections. The programmatic check is
validatePlanDescription(config, description) ($BOSS_PLAN_TOOLBOX/skill-config.mjs). The
epic-parent overview uses explicit validatePlanDescription(config, description, {mode:'epic-parent'})
with ## Summary, ## Child tickets, ## Planning, and ## Original notes. Unknown modes warn and fall back to child-plan.
When a ticket names a specific call site, construct, literal claim, or other mechanism that could
recur, record a repo-wide sibling-class enumeration before fixing scope. List every
site the search returns with verdict (fix or not a defect) and reason; adjudicate the class per
site rather than sweeping every match wholesale. The reason names the discriminator, such as where
the branch actually lives. A one-row "only named site found" table discharges it. An acceptance
criterion must not cap the number of changed files; scope comes from enumeration, not file count.
config-first order; the natural-reading (description, config) call throws a named
argument-order error. It returns { ok, version, missing, unknown, unsupportedVersion }; ok covers
only missing/unsupportedVersion, and unknown is enforced by the Phase 4 contract gate.
Headless open questions → agent-question. The subagent records only genuinely controversial
forks (high bar — could-have-gone-either-way calls, never routine ones) as openQuestions; a
non-empty list drives the agent-question label (Phase 4) and the plan's ## Open Questions
section. Interactive runs have a human answering each fork, so they produce none.
Phase 3.5 — Extension plan-reviewers (additive, non-fatal)
Before upload, run any repo-local boss-plan-* extension plan-reviewers
(discover --core boss-plan --role plan-reviewer) over the drafted plan — strictly additive, a
documented no-op when none are installed (the default today), in both the interactive and headless
paths. Pass that exact role: inferring --role review from this phase's name rejects every
correctly-installed extension into skipped as unknown requested role "review", which now reads
as a misinstallation in the ledger. Full protocol (discover → dispatch each as a fresh read-only
subagent → validate its envelope → fold or skip), against
docs/skills/extension-contract.md, lives in
references/extension-reviewers.md.
Phase 4 — Finalize the plan attachment and write back to the tracker
STOP — secret gate (mandatory, do not skip). This runs before finalizing the native tracker
attachment. Read the entire plan file (with special attention to the ## Original notes verbatim
block and anything pasted from the ticket or interview) and confirm it contains zero of: API
keys, tokens, passwords, connection strings, private keys, session cookies, internal
hostnames/IPs, or customer PII. If you find anything credential- or PII-shaped, redact it in
every persisted artifact with [REDACTED] or [REDACTED: reference] (e.g. [REDACTED: repo-root .env]) before attaching it. If you are unsure whether something
is sensitive, treat it as sensitive and redact it. Do not finalize the attachment until this
check passes. For credential-valued external-image query parameters, use token=REDACTED,
token=[REDACTED], or token=[REDACTED:%20vault]; these preserve the image
reference without persisting its credential. The redacted source is the safe form used by the verbatim attachment checks below;
the raw Phase 1 source is retained only in the ephemeral image-parity scratch file.
A signed uploads.linear.app URL is an explicit carve-out: do not redact the reference away.
Strip its signature query string and preserve the unsigned asset path instead, so the image-parity
gate can retain the asset identity without carrying a credential-like signature.
STOP — image-parity gate (mandatory, mechanical, do not skip). A rewritten description that
silently drops the reporter's screenshots is "worse than none" (the Phase 0 edge rule), and the
drafting LLM cannot be trusted to preserve them — so verify parity mechanically before any
Linear write. Reuse the raw snapshot Phase 2 already wrote at
.linear-plans/<ISSUE-ID>.image-guard-orig.md; do not rewrite it here. An empty or
whitespace-only original is refused (exit 1); pass --allow-empty-original only if it truly is
empty. Write the returned descriptionSummary to .linear-plans/<ISSUE-ID>.image-guard-new.md
(per-issue paths avoid
clobbering). Also write .linear-plans/<ISSUE-ID>.attachment-guard-orig.md as the same Phase 1
source with only the mandatory secret/PII redactions and upload-signature stripping applied;
do not derive it from either generated artifact. Both the returned descriptionSummary and the
final attachment must preserve this safe source under ## Original notes. Set EXPECTED_IMAGES
to the number of distinct canonical upload identities observed in the Phase 1 description — the
uploads.linear.app origin plus pathname, ignoring query strings — then run the guard:
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
ORIG=".linear-plans/<ISSUE-ID>.image-guard-orig.md"; SAFE_ORIG=".linear-plans/<ISSUE-ID>.attachment-guard-orig.md"; NEW=".linear-plans/<ISSUE-ID>.image-guard-new.md"
PLAN_FILE="${PLAN_FILE:-.linear-plans/<ISSUE-ID>-<slug>.md}"
EXPECTED_IMAGES="<distinct canonical upload identities observed in Phase 1>"
cleanup_guard_scratch() {
rm -f "$ORIG" "$SAFE_ORIG" "$NEW" "$PLAN_FILE" || echo "warning: guard scratch cleanup failed" >&2
}
# Keep scratch until all gates pass; every failing gate calls this helper before exiting.
if ! node "$BOSS_PLAN_TOOLBOX/plan-image-guard.mjs" --original "$ORIG" --rewritten "$NEW" \
--expect-images "$EXPECTED_IMAGES" --require-unsigned-uploads; then
echo "image-parity gate failed (guard message above) — no Linear write, aborting" >&2
cleanup_guard_scratch
exit 1
fi
if ! node "$BOSS_PLAN_TOOLBOX/plan-image-guard.mjs" --original "$ORIG" --rewritten "$SAFE_ORIG" \
--require-safe-source; then
echo "safe-source gate failed (guard message above) — no Linear write, aborting" >&2
cleanup_guard_scratch
exit 1
fi
if ! node "$BOSS_PLAN_TOOLBOX/plan-image-guard.mjs" --original "$SAFE_ORIG" --rewritten "$NEW" \
--require-verbatim --require-unsigned-uploads; then
echo "description safety gate failed (guard message above) — no Linear write, aborting" >&2
cleanup_guard_scratch
exit 1
fi
if ! node "$BOSS_PLAN_TOOLBOX/plan-image-guard.mjs" --original "$SAFE_ORIG" --rewritten "$PLAN_FILE" \
--require-verbatim --require-unsigned-uploads; then
echo "plan-attachment safety gate failed (guard message above) — no attachment finalize, aborting" >&2
cleanup_guard_scratch
exit 1
fi
STOP — plan-contract gate (mandatory, mechanical, do not skip). "Exactly these ## sections,
in order" was until now enforced only by the consumer, days after a malformed artifact had
already been published: descriptions missing most required sections, a whole-field self-describing
placeholder, an unsubstituted <ATTACHMENT-ID>-style token, an off-contract heading, and a plan
file ending in literal tool-call scaffolding all passed every earlier gate. Verify
mechanically, reusing the in-hand descriptionSummary and PLAN_FILE — zero extra tracker
reads. Re-derive the toolbox dir here; blocks inherit nothing:
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
ORIG=".linear-plans/<ISSUE-ID>.image-guard-orig.md"; SAFE_ORIG=".linear-plans/<ISSUE-ID>.attachment-guard-orig.md"; NEW=".linear-plans/<ISSUE-ID>.image-guard-new.md"
PLAN_FILE="${PLAN_FILE:-.linear-plans/<ISSUE-ID>-<slug>.md}"
if ! node "$BOSS_PLAN_TOOLBOX/plan-contract-guard.mjs" --description "$NEW" --plan "$PLAN_FILE"; then
echo "plan-contract gate failed (guard message above) — no Linear write, aborting" >&2
rm -f "$ORIG" "$SAFE_ORIG" "$NEW" "$PLAN_FILE" || echo "warning: contract gate scratch cleanup failed" >&2
exit 1
fi
This is a fifth failed-gate exit, so it owes the same cleanup as the four above and removes all
four scratch paths — $ORIG included, the raw Phase 1 source that may carry sensitive content.
"Discard the scratch (Phase 5 cleanup)" describes the SUCCESS path only: exit 1 means Phase 5
never runs, which is exactly why each failing gate deletes the scratch itself.
One stderr line per violation, each tagged missing-sections, unknown-section, section-order,
placeholder-residue, not-a-description, plan-file-residue, or unreadable-input; a missing
or unreadable file is itself a violation, never a pass, and an unknown-section message names both
the heading and its remedy. On non-zero exit take the SAFE branch: no Linear write, no
attachment finalize, a one-line stderr reason carrying the guard's own message, discard the scratch
as above, exit non-zero.
STOP — premise re-verification (mandatory, mechanical, do not skip). The plan artifact and
description may still be valid while the tracker premises the drafter relied on have moved. The
drafting sentinel carries premises: [{id, state}]; immediately before the single tracker save,
re-read those issue ids through the tracker adapter's getIssue capability, build a JSON object of
live states, and run:
if [ -z "${BOSS_SKILLS_HOME:-}" ]; then
for candidate in "$HOME/.claude/skills" "$HOME/.codex/skills"; do
if [ -d "$candidate/boss-plan/toolbox" ]; then BOSS_SKILLS_HOME="$candidate"; break; fi
done
fi
test -n "${BOSS_SKILLS_HOME:-}" || { echo "BLOCKED: installed boss skills not found"; exit 1; }
BOSS_PLAN_TOOLBOX="$BOSS_SKILLS_HOME/boss-plan/toolbox"
export BOSS_SKILLS_HOME BOSS_PLAN_TOOLBOX
PREMISES_FILE=".linear-plans/<ISSUE-ID>.premises.json"; LIVE_STATES_FILE=".linear-plans/<ISSUE-ID>.premise-states.json"
PREMISE_REPORT="$(node "$BOSS_PLAN_TOOLBOX/plan-run-guards.mjs" premises "$PREMISES_FILE" "$LIVE_STATES_FILE" 2>&1)"
PREMISE_RC=$?
An empty premises array skips the reads. A premise-limit or unreadable premise is a SAFE
branch before tracker writeback. A changed state does not abort: append an orchestrator-owned
- Premise drift: <ticket> was <state at recon>, is now <current state> line parsed from
PREMISE_REPORT under ## Planning before the save, and name that annotation in the Phase 6
report. This warning line is outside the description-section contract; planContract.version stays
unchanged.
- Finalize the native tracker attachment before tracker writeback (failure: no plan metadata/state write). Follow
references/plan-storage.md. Set