agentleFS
phahadekphahadek/claude-orchestrator/skills/milestone-seed/SKILL.md

milestone-seed

Skill1 starsChanged 3 months agoMITView on GitHub

Stand up a NEW milestone: ground in the prior milestone's real state and the live dispatch switches, audit the Future Scope deferral list, frame and lock a charter with the operator, create the milestone board, and seed it with decision-shaped πŸ“ Design (and πŸ”Ž Investigation) tasks grounded in real code. Use when the user says "let's start planning milestone X", "seed milestone X", "let's seed M15", or opens a session to stand up the next milestone. Ends with the board SEEDED AND REGISTERED (a `milestones` row + grooming-manifest entry) but NOT ACTIVATED β€” that belongs to /milestone-wrap. Distinct from /groom and /design, which work an EXISTING milestone's backlog.

  • Reads credentials

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 10 are distinctive, and 2 are very specific to this project.

  • β‰ˆ0

    Common Β· estimate

    Many files like this have a rule of this kind.

  • β‰ˆ10

    Distinctive Β· estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ground in the record, never in memory: Read the actual prior board, the actual charter, the actual code.

    line 44
  2. Distinctive:

    Never reason from memory about what the last milestone did.

    line 98
  3. Distinctive:

    Look at what the prior board's leftovers are made of: If its open tail is mostly small bugs and polish unrelated to its charter, that is …

    line 110
  4. Distinctive:

    Read the project's Future Scope page in full.

    line 121
  5. Very specific:

    Only the edited blocks' rich_text is PATCHed.

    line 154
  6. Distinctive:

    Verify by re-reading and diffing against your intended body.

    line 163
  7. Distinctive:

    A loose or rolling milestone is a legitimate shape: Not every milestone is a tightly-themed build-out.

    line 174
  8. Distinctive:

    Non-goals are as load-bearing as goals: Write them as things a future session can check itself against.

    line 180
  9. Distinctive:

    Lock it as prose, and get explicit sign-off: Only then create anything.

    line 182
  10. Very specific:

    New tasks always start at πŸ”² Backlog: Never create one at Ready.

    line 287
  11. Distinctive:

    Ask: *"what would an implementer of this milestone hit that no task owns?"* File the gaps.

    line 301
  12. Distinctive:

    Nothing is created before the charter is signed off: Not the board, not a task.

    line 336

β‰ˆ β€œCommon” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. β€œVery specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Milestone Seed

Standing up a milestone is framing work with a persistence tail. The framing β€” what this milestone is for, and what it deliberately is not β€” is the hard part and the part that decays if it lives only in chat. The tail is mechanical: a board, a charter, a seed set, four homes updated.

/milestone-seed ──▢ board + charter + seed tasks + milestones row (REGISTERED, NOT ACTIVE)
                          β”‚
                          β”œβ”€β”€β–Ά /design  (works the πŸ“ Design tasks)
                          β”œβ”€β”€β–Ά /groom   (brings πŸ”² Backlog to πŸ—‚οΈ Ready)
                          └──▢ /milestone-wrap  (activates it, when the prior one closes)

You do not activate the milestone. /milestone-wrap Steps 4–5 own marking the next milestone active β€” repointing projects.auto_launch_milestone_id, the Notion "Active Task Board" callout, the phase line. This skill registers the milestone (a milestones row + grooming-manifest entry, via POST /api/projects/<id>/milestones in Step 4) but stops short of activating it. A registered-but-unactivated milestone is inactive because auto_launch_milestone_id does not point at it; activation (the repoint) belongs to /milestone-wrap. That boundary is what makes it safe to seed while the prior milestone is still running.

Not /groom or /design. Those work an existing milestone's backlog. This creates the milestone. Don't conflate them.

Doctrine

  • Ground in the record, never in memory. Read the actual prior board, the actual charter, the actual code. An "obvious gap" is very often a doc imprecision β€” falsify before you file.
  • Frame before you build. Nothing is created until the operator has signed off on a charter in prose. A board created before the framing lands is a board you will fight.
  • The live switches outrank the docs. context.md describes intent; the DB describes reality. Read both, and when they disagree, say so.
  • Two gates, not one. Approving the seed set and approving the task bodies are separate operator decisions. See Step 5.
  • Don't hand the work back. Find the prior charter, the deferral list, the live switches yourself. Ask the operator for decisions, not for locations.

Step 0 β€” Resolve the project and establish the live switches

Read config/projects/<dir>/context.md for the board IDs, master Notion page, project id-space, and per-project conventions. Then read the live state β€” not the doc's description of it:

Open the orchestrator DB read-only with the bundled better-sqlite3 and read three tables. The DB path and the runtime directory to run from are host facts β€” take them from the project's context.md (Β§ Inspecting live state), or $ORCHESTRATOR_DB_PATH where it is set; this skill deliberately does not hardcode either.

// run from the runtime backend dir named in context.md; { readonly: true }, never a write
const db = require('better-sqlite3')(process.env.ORCHESTRATOR_DB_PATH, { readonly: true });
db.prepare('SELECT id,name,canonical_short_id,wrapped_at FROM milestones WHERE project_id=? ORDER BY display_order DESC LIMIT 4').all('<projectId>');
db.prepare('SELECT id,auto_launch_enabled,auto_launch_milestone_id FROM projects').all();

Put a non-trivial query in a .cjs file rather than a node -e one-liner: in node -e a double-quoted SQL literal is parsed by SQLite as an identifier, so it silently becomes a column reference. Single-quote SQL literals, as above.

Report any disagreement between context.md and the DB to the operator. On the M15 seed, context.md said auto_launch_milestone_id pointed at M13 and described the switch as deliberately unflipped; it had since been repointed to M14.

Two things you must know before filing anything:

  1. Where does auto_launch_milestone_id point? That is the only milestone AutoLauncher polls; filing on that milestone's board is a live action.
  2. A brand-new board is inactive. It has no milestones row (those come from ProjectService.insertMilestone, an explicit API action β€” there is no auto-discovery from Notion), and auto_launch_milestone_id does not point at it. State this to the operator β€” it is the safety property that makes seeding during an active milestone fine, and it should not be nervously re-derived every time.

Step 1 β€” Ground in the prior milestone

Never reason from memory about what the last milestone did.

  • Enumerate its board fully β€” node ~/.claude/scripts/notion-query.mjs <db-id> --env <repo>/packages/backend/.env --json. Summarize by Status Γ— Type. What is still open tells you what is about to spill into the new milestone.
  • Read its charter (the board's own description) and the project's context page.
  • Read the code any candidate theme touches, before asserting a gap exists.

⚠️ notion-query.mjs --json keys the title under Task Name / _title, never title. A scan written as r.title reads undefined on every row and reports a clean "no match" rather than erroring.

Look at what the prior board's leftovers are made of. If its open tail is mostly small bugs and polish unrelated to its charter, that is evidence about what the next milestone should be β€” the board is telling you what it is actually producing.


Step 2 β€” Audit the Future Scope deferral list (mandatory)

Deferrals are the raw material for the next milestone. Do this before framing the charter, not after β€” on the M15 seed, one entry became the entire feature track.

Read the project's Future Scope page in full. For every entry, reach one of:

DispositionWhen
RetireSuperseded by shipped code, or its stated premise is falsified
ReframeThe need is real but its stated blocker or rationale is stale
ConsolidateSeveral entries share one trigger β€” collapse them into one
PromoteIt becomes a track or a task in the new milestone
KeepStill valid β€” but only with a checkable promotion condition

Four rules that make this worth doing:

  1. Falsify by measurement, not memory. An entry's argument is a claim. On the M15 seed, one entry justified itself with "421 firings in 14 days"; the live count was 6 in 14 days against a higher session count β€” a ~70Γ— collapse that killed the entry. Two more died because the code they asked for had shipped.
  2. Every keep states a condition you can check. "Revisit if it becomes a problem" is malformed. "Promote when a rate-limit 403 is observed under sustained load" is not. Without this the page rots into an un-triageable list nobody can act on.
  3. Consolidate entries that share one bet. Six distribution deferrals on the M15 audit were a single "if this goes outward-facing" decision β€” a third of the page's length, and one choice, not six.
  4. The audit is a discovery channel, not cleanup. Measuring to test one entry is how the M15 seed found a 220-message/month session re-drive loop that nobody had ever looked at. Two seed tasks came out of it. Expect this; leave room for it.

Writing the result back. The page is a flat list, so a notion.pageEdit staged intent is the audited path β€” it fails closed on a stale base and leaves an audit trail:

  • The apply is per block. Stage one content_update per entry block: each old_str must sit inside exactly one block's plain text. A match spanning blocks, or landing on a block with children, is refused (NotionPageEditUnpatchableTargetError), and a whole-page old_str no longer works.
  • Only the edited blocks' rich_text is PATCHed. Untouched blocks keep their formatting; only blocks you edit flatten (bold, inline code and link URLs in them are lost). Write plain text in the replacement (**bold** is not parsed) and tell the operator the edited entries lose inline formatting.
  • Build each old_str from the block's plain_text β€” which strips bold, inline code and link URLs. The notion-page.mjs export does not match this (it preserves them), so copy from plain text, not from the export.
  • page_id must be notion:-prefixed. A bare uuid dies at apply time with Invalid task ID (no colon) β€” applyPageEdit runs it through toExternalId.
  • Verify by re-reading and diffing against your intended body. Do not trust the 200.

Step 3 β€” Frame the charter, and get sign-off before creating anything

This is the step that matters. Do it as dialogue, not a document drop.

  • Name the gap. Classically the substrate-vs-agency gap: what the prior milestone built versus what this one must run. Reflect your understanding back so the operator can reframe it β€” they will, and that is the point.
  • A loose or rolling milestone is a legitimate shape. Not every milestone is a tightly-themed build-out. "Mostly bug-fixing and small changes" has direct precedent (M8 Ongoing Maintenance, M9) and both closed cleanly. But it still needs a premise and non-goals β€” convergence is machine-tracked (gate readiness, config-seed, the convergence panel), and a milestone with no boundary cannot converge. Bound it by kind of work rather than by a task list.
  • Non-goals are as load-bearing as goals. Write them as things a future session can check itself against. The most useful ones name a temptation and refuse it.
  • Lock it as prose, and get explicit sign-off. Only then create anything.

Persist it in the right home:

ContentHome
The charter (premise, principle, tracks, non-goals)The milestone board's own description
One concise sentenceThe project's 🏁 Project Milestones table
DeferralsπŸ”­ Future Scope
Universal procedureconfig/procedures.md

There is no Key Decisions Log on the master page β€” it was retired 2026-07-26. Do not re-add one.


Step 4 β€” Create the board

A milestone board is a Notion database. Its parent is the prior milestone board's parent (read it: GET /v1/databases/<prior-board-id> β†’ parent), unless the project's context.md overrides it. Do not assume the master Project Context page β€” some projects keep boards under a milestones hub page.

The schema must mirror the prior board's Status / Type / Priority options exactly β€” same option names, same colors β€” or the orchestrator's board reader breaks. Fetch the prior board first and copy its option set rather than retyping it.

Task Name  TITLE
Type       SELECT(πŸ’» Code:purple, πŸ“ Design:pink, πŸ“‹ Planning:blue, πŸ”§ Operational:orange,
                  πŸ”Ž Investigation:yellow, πŸ§ͺ Testing:green, 🚦 Gate:red, πŸ“ Docs:gray,
                  🎨 Assets:brown, πŸ› οΈ Tooling:default)
Status     SELECT(πŸ”² Backlog:gray, πŸ—‚οΈ Ready:gray, πŸ”„ In Progress:yellow, πŸ‘€ In Review:orange,
                  βœ… Done:green, 🚫 Blocked:red, ⏭️ Deferred:gray)
Notes      RICH_TEXT
Priority   SELECT(πŸ”΄ High:red, 🟑 Medium:yellow, 🟒 Low:green, P2:brown)
Depends On RICH_TEXT

Verify the mirror functionally, not visually. Read the new board back through notion-query.mjs and confirm the property keys match the prior board's (Task Name, Status, Type, Priority, Depends On, Notes). Eyeballing the creation response is not the same check β€” the reader is what has to work.

Record both ids of the new board: the Notion database id (the id in the board URL) and the data-source id (collection://…). The data-source id is only what notion-create-pages needs as a parent; the database id is what the milestone is registered with. Proposing the context.md entry is deferred to /wrap (see Step 7).

Register the milestone. Once the board and its ids are confirmed, create the milestones row and the grooming-manifest entry in one call:

POST /api/projects/<projectId>/milestones
Authorization: Bearer $ORCHESTRATOR_DEVICE_TOKEN
{"name": "<milestone display name>", "sourceId": "<new board's Notion database id>"}

sourceId is the Notion database id, never the data-source id: every registered milestone carries the database id, and the grooming-manifest board key mirrors milestones.source_id. Registering with the wrong id forces a later PATCH /api/milestones/{id} plus a hand edit of grooming.json.

displayOrder defaults to max(display_order of the project's milestones) + 1 (0 for the first), so the newest milestone sorts last and its grooming-manifest neighbour is the prior milestone. Pass an explicit displayOrder only to insert out of order.

This is ProjectService.createMilestone β€” it inserts the milestones row and registers the canonicalShortId/board/neighbour entry into config/projects/<dir>/grooming.json (a no-op if that project has no manifest file; idempotent if the canonicalShortId is already registered). It does not activate the milestone β€” auto_launch_milestone_id is untouched and does not point at it, so the milestone stays inactive until /milestone-wrap repoints it.

Verify by reading it back, through a different path than the one you wrote it with: GET /api/projects/<projectId>/milestones and confirm the new row is present with the expected canonicalShortId and a sourceId equal to the Notion database id, in the same shape as the prior milestones' sourceId.


Step 5 β€” Seed the tasks (two gates)

Seed πŸ“ Design tasks with decision-shaped Open Questions grounded in real code, plus πŸ”Ž Investigation tasks for live symptoms the grounding surfaced. Read config/task-writing.md at the moment you author β€” it is reference content, not a skim-once summary.

One operator gate, then file directly. The operator approves the seed set β€” how many tasks, what each covers, what its open questions are. Task bodies are never drafted in chat: file them at πŸ”² Backlog directly; the operator reviews them on the board and grooming is the review gate. Filing on an unregistered board is inert and fully reversible.

Authoring rules specific to seeding:

  • Every Open Question is a decision between named, real options with file:line anchors β€” so /groom can validate-and-promote and /design invents nothing. Add a Code anchors note to each Context so the grounding travels with the task.
  • Open-question count is a diagnostic, not a target. Fewer than ~2 β†’ fold the task into a sibling. More than ~6 β†’ split it. Never drop a genuine question to hit a tidy count β€” split, don't trim; disposition, don't drop.
  • Hard-block generously. Under-declaring races worktrees; over-declaring costs a wait. Depends On is pipe-delimited page IDs, so create the blocker first and wire the dependents after.
  • New tasks always start at πŸ”² Backlog. Never create one at Ready. (Enforced by the check-task-status.mjs hook.)
  • Do not create milestone scaffolding. The Manual Verification Gate, the config-seed task, testing and docs tasks are deliberately deferred to grooming. Creating them at seed time is premature.
  • State the milestone key space at every call site. gate_item.milestone / seed_item.milestone store the display name; ops_journal.milestone stores the UUID. Two shipped bugs came from resolving one and querying the other β€” neither errored; both matched zero rows.

Step 6 β€” Run a completeness critic on the milestone

Ask: "what would an implementer of this milestone hit that no task owns?" File the gaps.

Distinguish a genuine design gap (file it) from milestone scaffolding (the gate, config-seed, testing, docs, migration ops β€” deferred to grooming, see above). And when a verified "gap" turns out to be a doc imprecision, clarify the doc β€” do not file hardening work or mutate code to force a distinction the design deliberately keeps separate.


Step 7 β€” Update the homes, then stop

  • The project's context.md β€” board link, data-source id, and a short note that the milestone is seeded and registered (has a milestones row + grooming-manifest entry) but not activated, so a future session knows filing on it is still inert until /milestone-wrap repoints auto_launch_milestone_id. Include the charter's most load-bearing non-goal. context.md edits require explicit operator permission β€” never write them during the seed; propose the edit at /wrap.
  • The project's 🏁 Project Milestones table β€” add one row (one concise sentence). It lives wherever this project keeps it (the master Project Context page, or a hub page β€” per context.md). Do not change the "Active Task Board" callout or the phase line; the prior milestone is still active and /milestone-wrap owns that flip.

Anchor a update_content edit on plain prose or a heading, never a markdown table row β€” Notion's own serialization differs from the export (bare domains auto-linkify, straight quotes may curl, tildes escape). On a match failure, re-fetch and copy the exact serialized text.

Then report and stop. Do not offer to register, or activate the milestone.


Rules (hard)

  1. Nothing is created before the charter is signed off. Not the board, not a task.
  2. Never activate the milestone. No auto_launch_milestone_id repoint. That is /milestone-wrap. (Registering it β€” the milestones row + grooming-manifest entry, Step 4 β€” is this skill's job and does not by itself activate anything.)
  3. Never create a task at πŸ—‚οΈ Ready.
  4. Never file on the prior milestone's board to park an idea without telling the operator you are filing there β€” it is a live action on the active board.
  5. Falsify before you file. An apparent defect is often a doc imprecision. Read the code.
  6. Verify every write by reading it back, through a different path than the one you wrote it with where possible.
  7. Plain-text questions to the operator, never AskUserQuestion β€” it is denied by a PreToolUse hook in this environment.
  8. Inspect repos with git -C <path>, never cd <path> && git β€” the latter trips a built-in hook-safety prompt that allowlisting cannot suppress. One command per Bash call.

Provenance

Graduated from config/procedures.md Β§ Milestone-seed sessions on 2026-08-08, after the procedure had been exercised on M12 (2026-07-18), M13, M14 (2026-07-26) and M15 (2026-08-07). Steps 0, 2, and the two-gate rule in Step 5 are the M15 run's additions; the rest is the procedures.md text, reorganized.

Promoted to the repo β€” this skill now has its source at skills/milestone-seed/ and is in the SKILLS vendoring list in scripts/sync-guidelines-load.mjs, so /sync-guidelines both updates and backs it up. It was local-only (one copy, one disk, unversioned) from 2026-08-08 until that promotion.

It was not split into reference/ files at promotion time, though the original plan in procedures.md called for it: at 334 lines this file is smaller than skills/gate/SKILL.md (348) which carries no reference/ directory, so the repo's own convention does not call for a split at this size. Reconsider if it grows past ~450 lines, the point where /groom and /design split.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.