---
command: np:propose-milestones
description: Re-plan all not-yet-done milestones on an existing project. AI reads PROJECT.md + REQUIREMENTS.md + existing milestone state, proposes add/update/remove operations, user reviews per-item, subcommand applies atomically.
argument-hint: [--apply <answers.json>]
---

# np:propose-milestones

Re-plan the not-yet-done milestone pipeline of an existing project. Where `/np:new-project` creates the initial scaffold (+ optional AI-proposed milestones in Phase 3) and `/np:new-milestone` appends a single milestone manually, `/np:propose-milestones` re-examines the whole open pipeline against current Discovery content and proposes an updated sequence.

## Philosophy

<philosophy>
Projects drift. Discovery answers get refined, Strategic Decisions shift, Success Criteria sharpen, new constraints emerge — but the milestone pipeline set at project-init rarely gets revisited until someone manually runs `/np:new-milestone` for each new idea. This workflow closes that gap: it re-reads the current PROJECT.md + REQUIREMENTS.md, looks at what each not-yet-done milestone already represents, and proposes a coherent updated pipeline. Completed milestones stay untouched; milestones with real investment (slices, non-TBD CONTEXT.md) require explicit confirmation before modification.

Runtime-agnostic: the subcommand is deterministic Node code; the AI proposal happens in the host agent (Claude / Codex / …); the review goes through the askuser gateway (or text-mode inline prompts in Claude Code).
</philosophy>

## Scope Guardrail

<scope_guardrail>
This workflow ONLY touches:

- `.nubos-pilot/roadmap.yaml` (add / modify / remove milestone entries)
- `.nubos-pilot/ROADMAP.md` (regenerated by downstream render if present)
- `.nubos-pilot/milestones/M<NNN>/` (new dirs for added milestones; existing dirs move to `.nubos-pilot/archive/milestones/M<NNN>-<date>/` on remove)

It NEVER:

- writes `.nubos-pilot/PROJECT.md` — D-29 strict invariant, enforced by `_writeFile` guard
- modifies **completed** milestones (`status: done|complete|completed`) — the subcommand raises `milestone-completed-untouchable`
- modifies **milestones with slices** without explicit `confirm_force_modify: true` — the subcommand raises `milestone-has-slices`
- touches CONTEXT.md of existing milestones on update (only `name` / `goal` in roadmap.yaml change; CONTEXT.md is the user's work)
- runs when PROJECT.md still has `_TBD` placeholders (refuses in Step 2 below)
</scope_guardrail>

## Downstream Awareness

<downstream_awareness>
Each operation maps to a single atomic change inside one file-lock on `roadmap.yaml`:

- `add` → calls the same path as `np:new-milestone --apply`: appends to `roadmap.yaml`, creates `M<NNN>/` dir with CONTEXT.md / ROADMAP.md / META.json skeleton (all TBD), slice subdir.
- `update` → mutates `name` and/or `goal` of an existing milestone entry in `roadmap.yaml`. CONTEXT.md is never touched; the user's /np:discuss-phase work is preserved.
- `remove` → drops the milestone entry from `roadmap.yaml` and moves `M<NNN>/` to `.nubos-pilot/archive/milestones/M<NNN>-<YYYY-MM-DD>/` (never hard-deletes).

All ops run inside a single `withFileLock(roadmap.yaml, …)` — either every op applies or none does. New milestones always auto-number as `M<next>` (next = max existing + 1).
</downstream_awareness>

## Guard

Refuse when not in an initialized project.

```bash
if [ ! -f .nubos-pilot/PROJECT.md ]; then
  echo "Error: no .nubos-pilot/PROJECT.md found. Run /np:new-project first."
  exit 1
fi
```

The subcommand raises `project-not-initialized` anyway; the shell check gives a cleaner message before the AI analysis starts.

## Single-Call Init

```bash
LANG_DIRECTIVE=$(node .nubos-pilot/bin/np-tools.cjs lang-directive)
INIT=$(node .nubos-pilot/bin/np-tools.cjs init propose-milestones)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```

**Language (SSOT = `.nubos-pilot/config.json` → `response_language`).**
`$LANG_DIRECTIVE` is authoritative. Obey it for askuser prompt texts, AI-facing reasoning shown to the user, and any narrative prose. YAML keys, milestone IDs, and status strings stay canonical English.

**Askuser routing.** Every `node .nubos-pilot/bin/np-tools.cjs askuser …` block below is a spec, not a literal command. Pick the path once at Initialize:
- **Claude Code** (native `AskUserQuestion` tool is available): parse the JSON spec and call `AskUserQuestion` directly. `select` → `multiSelect: false`; `multiselect` → `multiSelect: true`; `confirm` → `options: [{label: "Yes"}, {label: "No"}]`; `input` → ask free-form in chat. Use a short `header` (≤12 chars).
- **`text_mode == true`** (INIT payload): skip every askuser block and render questions as plain-text numbered lists. Opt-in via `.nubos-pilot/config.json` → `workflow.text_mode`.
- **Other runtime with TTY** (Codex, Gemini, …): execute the shell `askuser` block verbatim.

Parse INIT for: `milestones[]` (each with `id`, `name`, `goal`, `status`, `classification`, `slice_count`, `context`, `touchable`, `modification_requires_confirm`), `project_md`, `requirements_md`, `project_has_tbd`, `current_state_milestone`, `next_milestone_number`, `guidance`.

## Step 1: Present current state

Render the classification table to the user so context is shared before proposing:

```
Current milestones:
  M001 — Auth          [discussed]     (pending, 0 slices, CONTEXT filled)
  M002 — Profile       [empty]         (pending, 0 slices, CONTEXT TBD)
  M003 — Feed          [active]        (pending, 0 slices, state pointer)
  M004 — Analytics     [completed]     (done — untouchable)

Legend:
  completed  — done; never modified
  active     — has slices or is current state; modify only with explicit confirm
  discussed  — CONTEXT.md has real content; modify only with explicit confirm
  empty      — no slices, CONTEXT still TBD; freely modifiable
```

## Step 2: Guard on unfilled Discovery

If INIT payload `project_has_tbd == true`, refuse with a clear hint:

```
PROJECT.md still has _TBD placeholders from /np:discuss-project.
/np:propose-milestones needs a populated Discovery to produce useful proposals.

Run /np:discuss-project first (finish the six discovery sections), then retry.
```

Exit — do not offer fallback. Bad Discovery in → garbage proposals out; there is no value in a proposal anchored on TBD content.

## Step 3: AI proposal

Read the full `project_md` and `requirements_md` from the INIT payload. For each milestone in `milestones[]` that is NOT `classification: completed`, decide independently:

1. **Keep as-is** — name + goal still align with current Discovery. State the single sentence that anchors it (e.g. "Anchored on Success Criterion: <excerpt>").
2. **Update** — name or goal need refinement given current Discovery. Propose the new name/goal and say *why* it changed (what in PROJECT.md / REQUIREMENTS.md prompted this).
3. **Remove** — the milestone no longer fits (replaced by different scope, crosses a Non-Goal now, or was redundant with another). Say *why*.

Then propose any **new** milestones (`add` operations) the current Discovery implies but are missing. Anchor each on a specific Success Criterion or Strategic Decision.

Constraints:
- Respect **Non-Goals** — never propose a milestone that crosses a declared Non-Goal.
- Honor **Strategic Decisions** — use for ordering (sequence, `depends_on` hints for later /np:plan-phase).
- Target **3–6 total** not-yet-done milestones after the proposal. Do not pad; a small project may correctly end up with 1–2.
- **Do not propose changes to milestones classified as `completed`.** They are frozen.

Render the proposal as a diff-style list in the main chat:

```
Proposed changes:

  KEEP   M001 — Auth           (still anchored on Success Criterion "secure multi-device sessions")
  UPDATE M002 — Profile        → "Profile & Preferences"
         goal: "Ship user profile + preference persistence"
         reason: REQUIREMENTS now includes REQ-07 (preferences) which belongs here
  REMOVE M003 — Feed           reason: Non-Goals explicitly excludes social feed; this crossed the line
  ADD            Analytics     goal: "Ship event tracking + admin dashboard"
                               reason: Success Criterion "measure retention" has no home
  ADD            Migrations    goal: "Ship production DB migration pipeline"
                               reason: Strategic Decision "infra before features" implies this early

Sensitive items (require your explicit confirm if you accept):
  - UPDATE M002 — classification=discussed (CONTEXT.md already has content)
```

## Step 4: User review per item

For each proposal, ask one question via `askuser`. The exact prompt depends on the op type and classification:

### Keep proposals (no change)
No prompt — just listed under "Unchanged" in the summary. The user can override via Step 5 bulk-gate.

### Update / Remove of **touchable** milestones (classification=`empty`)
```bash
ANSWER=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
  "type": "select",
  "question": "UPDATE M002 — name: Profile → Profile & Preferences?",
  "options": ["Accept", "Edit (type your own name/goal)", "Skip this item"]
}')
```

On `Edit` → two follow-up `input` prompts (new_name, new_goal) with the proposed values as defaults.

### Update / Remove of **non-touchable** milestones (classification=`active` or `discussed`)
```bash
ANSWER=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
  "type": "select",
  "question": "⚠ M002 already has CONTEXT.md content. UPDATE anyway?",
  "options": ["Accept (confirm_force_modify=true)", "Edit", "Skip this item"]
}')
```

`Accept` must set `confirm_force_modify: true` on the resulting operation — the subcommand will reject the change otherwise. Additionally, if the milestone has `slice_count > 0`:

```bash
# Extra hard-confirm for milestones with slices
ANSWER=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
  "type": "confirm",
  "question": "M002 has 3 slice(s) with tasks. Modification will still preserve CONTEXT.md and slices, but roadmap name/goal will change. Proceed?",
  "default": false
}')
```

### Add proposals
```bash
ANSWER=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
  "type": "select",
  "question": "ADD new milestone: Analytics — Ship event tracking + admin dashboard?",
  "options": ["Accept", "Edit name/goal", "Skip this item"]
}')
```

## Step 5: User bulk-override gate

After per-item review, offer two meta-options before apply:

```bash
FINAL=$(node .nubos-pilot/bin/np-tools.cjs askuser --json '{
  "type": "select",
  "question": "About to apply N operations. Last chance.",
  "options": [
    "Apply all accepted operations",
    "Show diff again (re-render proposal with my edits)",
    "Add another milestone manually before applying",
    "Abort — make no changes"
  ]
}')
```

- `Show diff again` → re-render the proposal + user-edits and loop back to Step 4.
- `Add another milestone manually` → enter the `/np:new-milestone` three-question loop inline (same prompts as the `new-project` Phase 3 bulk loop) and append the collected answers as `add` operations.
- `Abort` → exit cleanly, no writes.

## Step 6: Apply

Build the answers JSON from collected decisions:

```bash
OPS_JSON=$(mktemp -t np-propose-ops.XXXXXX)
trap 'rm -f "$OPS_JSON"' EXIT

node -e '
  const fs = require("fs");
  fs.writeFileSync(process.env.OPS_JSON, JSON.stringify({
    operations: [
      // each entry:
      // { type: "add", milestone_name, milestone_goal }
      // { type: "update", milestone_id: "M<NNN>", new_name?, new_goal?, confirm_force_modify? }
      // { type: "remove", milestone_id: "M<NNN>", confirm_force_modify? }
    ],
  }));
' OPS_JSON="$OPS_JSON"

node .nubos-pilot/bin/np-tools.cjs init propose-milestones --apply "$OPS_JSON"
```

Output JSON:

```jsonc
{
  "mode": "apply",
  "results": [
    { "type": "add",    "id": "M005", "number": 5, "name": "Analytics" },
    { "type": "update", "id": "M002", "changed": { "from_name": "Profile", "to_name": "Profile & Preferences" } },
    { "type": "remove", "id": "M003", "archived_to": ".nubos-pilot/archive/milestones/M003-2026-04-21" }
  ]
}
```

## Step 7: Summary + next steps

Render the apply result to the user with concrete next actions:

```
Applied N operations.

Updated pipeline (not-yet-done):
  M001 — Auth
  M002 — Profile & Preferences       (updated: name, goal)
  M004 — Analytics                   (new)
  M005 — Migrations                  (new)

Archived:
  M003 — Feed                        → .nubos-pilot/archive/milestones/M003-2026-04-21

Next:
  - /np:discuss-phase <N> for each new or updated milestone to refresh CONTEXT.md
  - /np:plan-phase <N> to break it into slices + tasks
```

## Optional Commit

```bash
if [ "$(node .nubos-pilot/bin/np-tools.cjs config-get workflow.commit_docs 2>/dev/null)" = "true" ]; then
  git add .nubos-pilot/
  git commit -m "chore: np:propose-milestones re-plan pipeline"
fi
```

## Errors

| Code | Trigger | User action |
|------|---------|-------------|
| `project-not-initialized` | no `PROJECT.md` in `.nubos-pilot/` | Run `/np:new-project` first |
| `roadmap-missing` / `roadmap-parse-error` | `roadmap.yaml` missing or malformed | Inspect `.nubos-pilot/roadmap.yaml` |
| `milestone-completed-untouchable` | update/remove on status=done | Cannot modify shipped milestones; add a new one instead |
| `milestone-has-slices` | update/remove on milestone with slices without `confirm_force_modify:true` | Either confirm explicitly per-item or first `/np:reset-slice` the affected slices |
| `milestone-not-found` | update/remove references non-existent M<NNN> | Re-run; use IDs shown in Step 1 |
| `answers-missing-field` | op missing required field | Fix the op and re-run |
| `invalid-operation-type` | op type is not add/update/remove | Valid types: add, update, remove |
## Definition of Done

This workflow exits successfully only when, per [`templates/COMPLETENESS.md`](../templates/COMPLETENESS.md):

- Rule 1 (Do the whole thing) — proposal covers every requirement, mapped to a milestone or an explicit `Deferred` block.
- Rule 5 (Genuinely impress) — each milestone has a one-line vision and a quantified scope, not vague themes.
- Rule 11 (Ship the complete thing) — proposal is `np:new-milestone`-ready on exit.

Any violation = workflow exits non-zero. The orchestrator does not relax these.
