@hanzlaa/rcode 4.4.3 → 4.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ pnpm dlx @hanzlaa/rcode install
23
23
  [![CI](https://github.com/hanzlahabib/rcode/actions/workflows/test.yml/badge.svg)](https://github.com/hanzlahabib/rcode/actions/workflows/test.yml)
24
24
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
25
 
26
- Status: `@hanzlaa/rcode` v4.4.3 on npm. 45 agents · 117 commands · 129 workflows · **1 runtime dependency**. Test status tracked by CI badge above. Actively dogfooded on real projects every week.
26
+ Status: `@hanzlaa/rcode` v4.4.4 on npm. 45 agents · 117 commands · 129 workflows · **1 runtime dependency**. Test status tracked by CI badge above. Actively dogfooded on real projects every week.
27
27
 
28
28
  ---
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.4.3",
3
+ "version": "4.4.4",
4
4
  "description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -2278,12 +2278,16 @@ function cmdState(subArgs) {
2278
2278
  }
2279
2279
 
2280
2280
  writeState(state);
2281
+ // #942 — surface the milestone close nudge for inserted phases too.
2282
+ const insHealth = milestoneCloseNudge();
2281
2283
  return {
2282
2284
  ok: true,
2283
2285
  phase_number: phaseNumber,
2284
2286
  name: phaseName,
2285
2287
  slug: slug,
2286
2288
  directory: path.join(PLANNING_DIR, 'phases', `${phaseNumber}-${slug}`),
2289
+ milestone_health: insHealth.milestone_health,
2290
+ ...(insHealth.nudge ? { nudge: insHealth.nudge } : {}),
2287
2291
  };
2288
2292
  }
2289
2293
 
@@ -3734,22 +3738,30 @@ function cmdPhase(subArgs) {
3734
3738
  // value at the scales we operate. Applies to phases, sprints, epics, stories,
3735
3739
  // tasks, decisions across all artifacts (dirs, ROADMAP, state.json, banners).
3736
3740
 
3737
- // #583 sanity guard: prevent phantom phase numbers caused by stale high-number
3738
- // entries in ROADMAP.md or phases/ (e.g. a prior phantom "## Phase 1009" left
3739
- // in ROADMAP triggers the next add to produce 1010). If computed next is more
3740
- // than 50 above the count of currently tracked phases, the maxNum source is
3741
- // suspect. Abort and require an explicit --number N to override.
3742
- const trackedCount = state.phases.filter(p => {
3743
- const n = parseInt(String(p.number || ''), 10);
3744
- return !Number.isNaN(n) && n > 0;
3745
- }).length;
3746
- if (next > trackedCount + 50) {
3741
+ // #583 / #944 sanity guard: prevent phantom phase numbers caused by stale
3742
+ // high-number entries in ROADMAP.md or phases/ (e.g. a prior phantom
3743
+ // "## Phase 1009" left in ROADMAP triggers the next add to produce 1010).
3744
+ //
3745
+ // The guard must NOT misfire on an INTENTIONAL high-base numbering scheme
3746
+ // (e.g. a milestone that deliberately numbers phases 1031, 1032, …). The
3747
+ // discriminant: is the high number an actual TRACKED phase in state.json,
3748
+ // or only a ROADMAP/dir entry that state has never seen?
3749
+ // - next === maxTracked + 1 → contiguous with real tracked phases →
3750
+ // intentional, allow regardless of absolute magnitude.
3751
+ // - maxNum (overall) sits far ABOVE maxTracked → a non-tracked phantom
3752
+ // is driving the number → suspect, abort.
3753
+ const trackedNums = state.phases
3754
+ .map(p => parseInt(String(p.number || ''), 10))
3755
+ .filter(n => !Number.isNaN(n) && n > 0);
3756
+ const trackedCount = trackedNums.length;
3757
+ const maxTracked = trackedNums.length ? Math.max(...trackedNums) : 0;
3758
+ if (maxNum > maxTracked && (maxNum - maxTracked) > 50) {
3747
3759
  throw new Error(
3748
- `Computed phase number ${next} is unexpectedly large ` +
3749
- `(only ${trackedCount} phases tracked in state.json). ` +
3750
- `ROADMAP.md or the phases/ directory may contain a stale high-number entry. ` +
3760
+ `Computed phase number ${next} is driven by a non-tracked entry ` +
3761
+ `(highest in ROADMAP/phases = ${maxNum}, highest in state.json = ${maxTracked}). ` +
3762
+ `ROADMAP.md or the phases/ directory likely contains a stale high-number entry. ` +
3751
3763
  `Inspect with: node rcode-tools.cjs phases list\n` +
3752
- `Then retry with an explicit number: rcode-tools.cjs phase add "${phaseName}" --number ${trackedCount + 1}`
3764
+ `Then retry with an explicit number: rcode-tools.cjs phase add "${phaseName}" --number ${maxTracked + 1}`
3753
3765
  );
3754
3766
  }
3755
3767
 
@@ -3822,12 +3834,17 @@ function cmdPhase(subArgs) {
3822
3834
  }
3823
3835
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
3824
3836
 
3837
+ // #942 — surface the milestone close nudge from the CLI itself so it can't
3838
+ // be bypassed by adding phases outside the add-phase workflow.
3839
+ const { milestone_health, nudge } = milestoneCloseNudge();
3825
3840
  return {
3826
3841
  ok: true,
3827
3842
  phase_number: number,
3828
3843
  name: phaseName,
3829
3844
  slug,
3830
3845
  directory: path.relative(PROJECT_ROOT, directory),
3846
+ milestone_health,
3847
+ ...(nudge ? { nudge } : {}),
3831
3848
  };
3832
3849
  }
3833
3850
 
@@ -3866,6 +3883,19 @@ function cmdPhase(subArgs) {
3866
3883
  .sort((a, b) => parseInt(String(a.number), 10) - parseInt(String(b.number), 10))[0] || null;
3867
3884
 
3868
3885
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
3886
+
3887
+ // #943 — when no open phases remain, the milestone is effectively finished.
3888
+ // Surface the close/next guidance from this chokepoint so finishing the
3889
+ // last phase via execute/verify/dev-story doesn't strand the user (the
3890
+ // guidance previously only appeared in /rcode-status or progress insights).
3891
+ const doneStatuses = new Set(['complete', 'completed', 'verified', 'shipped']);
3892
+ const openRemaining = state.phases.filter(p => !doneStatuses.has(p.status)).length;
3893
+ let nudge = null;
3894
+ if (openRemaining === 0 && state.phases.length > 0) {
3895
+ nudge = 'All phases are complete — this milestone is finished. ' +
3896
+ 'Run /rcode-complete-milestone to archive it, then /rcode-new-milestone to start the next.';
3897
+ }
3898
+
3869
3899
  return {
3870
3900
  ok: true,
3871
3901
  phase: phaseRef,
@@ -3874,6 +3904,8 @@ function cmdPhase(subArgs) {
3874
3904
  next_phase: next ? next.number : null,
3875
3905
  next_phase_name: next ? (next.name || null) : null,
3876
3906
  is_last_phase: !next,
3907
+ open_phases_remaining: openRemaining,
3908
+ ...(nudge ? { nudge } : {}),
3877
3909
  warnings: [],
3878
3910
  has_warnings: false,
3879
3911
  };
@@ -4175,7 +4207,13 @@ function cmdPhase(subArgs) {
4175
4207
  if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
4176
4208
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
4177
4209
 
4178
- return { ok: true, count: created.length, phases: created, roadmap_skipped: roadmapSkipped };
4210
+ // #942 same milestone close nudge for the bulk-draft path.
4211
+ const bulkHealth = milestoneCloseNudge();
4212
+ return {
4213
+ ok: true, count: created.length, phases: created, roadmap_skipped: roadmapSkipped,
4214
+ milestone_health: bulkHealth.milestone_health,
4215
+ ...(bulkHealth.nudge ? { nudge: bulkHealth.nudge } : {}),
4216
+ };
4179
4217
  }
4180
4218
 
4181
4219
  // =====================================================================
@@ -7226,6 +7264,33 @@ function cmdMilestoneHealth() {
7226
7264
  };
7227
7265
  }
7228
7266
 
7267
+ // #942 — build a milestone-health summary + human-readable nudge for any
7268
+ // phase-adding code path (single add, bulk draft, plan, insert) so the
7269
+ // "milestone has too many open phases" guidance can't be bypassed by adding
7270
+ // phases outside the add-phase workflow. Returns { milestone_health, nudge }.
7271
+ function milestoneCloseNudge() {
7272
+ let h;
7273
+ try { h = cmdMilestoneHealth(); } catch { return { milestone_health: null, nudge: null }; }
7274
+ if (!h || !h.ok) return { milestone_health: null, nudge: null };
7275
+ const summary = {
7276
+ open_phases: h.open_phases,
7277
+ recommendation: h.recommendation,
7278
+ threshold_should: h.threshold_should,
7279
+ threshold_consider: h.threshold_consider,
7280
+ };
7281
+ let nudge = null;
7282
+ if (h.recommendation === 'should-close') {
7283
+ nudge = `Milestone "${h.milestone || 'current'}" has ${h.open_phases} open phases ` +
7284
+ `(≥${h.threshold_should}). Consider /rcode-complete-milestone to archive done ` +
7285
+ `phases, then /rcode-new-milestone for ongoing work — before adding more.`;
7286
+ } else if (h.recommendation === 'consider-closing') {
7287
+ nudge = `Milestone "${h.milestone || 'current'}" has ${h.open_phases} open phases ` +
7288
+ `(≥${h.threshold_consider}). Getting large — /rcode-complete-milestone + ` +
7289
+ `/rcode-new-milestone will keep the roadmap navigable.`;
7290
+ }
7291
+ return { milestone_health: summary, nudge };
7292
+ }
7293
+
7229
7294
  function cmdStateSnapshot() {
7230
7295
  const statePath = path.join(RCODE_DIR, 'state.json');
7231
7296
  if (!fs.existsSync(statePath)) return { ok: true, state: null };
@@ -871,7 +871,16 @@ The CLI handles:
871
871
  - Updating REQUIREMENTS.md traceability
872
872
  - Scanning for verification debt (returns `warnings` array)
873
873
 
874
- Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`, `warnings`, `has_warnings`.
874
+ Extract from result: `next_phase`, `next_phase_name`, `is_last_phase`, `warnings`, `has_warnings`, `open_phases_remaining`, `nudge`.
875
+
876
+ **If `nudge` is present (#943 — no open phases remain, milestone finished):**
877
+ Surface it verbatim so the user is guided forward instead of stranded:
878
+ ```
879
+ ✓ Milestone complete — all phases done.
880
+ {nudge}
881
+ ```
882
+ Do not auto-advance past a finished milestone; let the user choose
883
+ `/rcode-complete-milestone` or `/rcode-new-milestone`.
875
884
 
876
885
  **If has_warnings is true:**
877
886
  ```
@@ -83,6 +83,14 @@ Next steps:
83
83
  Or continue with current work and return to this phase later.
84
84
  ```
85
85
 
86
+ ## Step 3.5 — Surface milestone-health nudge (#942)
87
+
88
+ `state insert-phase` returns a `nudge` field when the milestone has too many
89
+ open phases (≥8 = consider, ≥12 = should-close). If `RESULT.nudge` is present,
90
+ print it verbatim so the user is guided toward `/rcode-complete-milestone` +
91
+ `/rcode-new-milestone` instead of silently accumulating phases. If absent, say
92
+ nothing.
93
+
86
94
  ## Anti-patterns
87
95
 
88
96
  - Don't insert before Phase 1 (decimal 0.1 makes no sense)
@@ -906,6 +906,23 @@ node ".rcode/bin/rcode-tools.cjs" state planned-phase --phase "${PHASE_NUMBER}"
906
906
 
907
907
  This updates STATUS to "Ready to execute", sets the correct plan count, and timestamps Last Activity.
908
908
 
909
+ ## 13c. Milestone-health nudge (#942)
910
+
911
+ After recording completion, check whether the milestone has accumulated too many
912
+ open phases — so planning the Nth phase of a sprawling milestone guides the user
913
+ toward closing it instead of silently growing the roadmap:
914
+
915
+ ```bash
916
+ HEALTH=$(node ".rcode/bin/rcode-tools.cjs" milestone-health 2>/dev/null)
917
+ REC=$(echo "$HEALTH" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).recommendation||'')}catch{console.log('')}})")
918
+ OPEN=$(echo "$HEALTH" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).open_phases||0)}catch{console.log(0)}})")
919
+ ```
920
+
921
+ - If `REC` is `should-close` (≥12 open): surface a hard nudge recommending
922
+ `/rcode-complete-milestone` then `/rcode-new-milestone`.
923
+ - If `REC` is `consider-closing` (8–11 open): softer nudge.
924
+ - If `healthy`: say nothing.
925
+
909
926
  ## 14. Present Final Status
910
927
 
911
928
  Route to `<offer_next>` OR `auto_advance` depending on flags/config.