@hanzlaa/rcode 4.9.0 → 4.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.9.0",
3
+ "version": "4.9.1",
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": {
@@ -243,7 +243,22 @@ function cmdBrain(args, { PROJECT_ROOT, RCODE_DIR }) {
243
243
  const os = require('os');
244
244
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rcode-brain-'));
245
245
  const branch = s.branch || cfg.defaults?.branch || 'main';
246
- const sparsePaths = Array.isArray(s.paths) ? s.paths : [];
246
+ // #1029 `sparse-checkout set --no-cone` treats each path as a
247
+ // .gitignore-style pattern, not a literal path pin. A bare filename
248
+ // (no '/', no wildcard) matches that filename at any depth in the repo
249
+ // tree, over-fetching every same-named file repo-wide. Anchor bare
250
+ // filenames to the repo root with a leading '/' so they pin the
251
+ // root-level file only. Patterns that already start with '/', contain a
252
+ // '/', or use wildcards (already scoped or intentionally recursive) are
253
+ // left untouched.
254
+ function anchorBareFilename(p) {
255
+ const str = String(p || '');
256
+ if (str.startsWith('/')) return str;
257
+ if (/[*?[]/.test(str)) return str;
258
+ if (str.includes('/')) return str;
259
+ return `/${str}`;
260
+ }
261
+ const sparsePaths = (Array.isArray(s.paths) ? s.paths : []).map(anchorBareFilename);
247
262
 
248
263
  // Cache key = sha1(repo + branch + sparsePaths joined). Changing any of
249
264
  // those gets a fresh cache slot. Different projects pulling the same
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Gitignore — extracted from rcode-tools.cjs (issue #204). Pure mechanical
3
- * move, no behavior change (including the pre-existing `slice_end` typo in
4
- * spliceBlock, left untouched — out of scope for this extraction).
2
+ * Gitignore — extracted from rcode-tools.cjs (issue #204).
5
3
  */
6
4
 
7
5
  const fs = require('fs');
@@ -102,8 +100,8 @@ function cmdGitignore(args, { PROJECT_ROOT, RCODE_DIR }) {
102
100
  let sliceStart = start;
103
101
  if (sliceStart > 0 && existing[sliceStart - 1] === '\n') sliceStart -= 1;
104
102
  let sliceEnd = endIdx + END.length;
105
- if (existing[slice_end] === '\n') slice_end += 1;
106
- return existing.slice(0, sliceStart) + newBlock + existing.slice(slice_end);
103
+ if (existing[sliceEnd] === '\n') sliceEnd += 1;
104
+ return existing.slice(0, sliceStart) + newBlock + existing.slice(sliceEnd);
107
105
  }
108
106
 
109
107
  if (!fs.existsSync(gitignorePath)) {
@@ -743,7 +743,10 @@ function parseSimpleYamlInline(text) {
743
743
  /**
744
744
  * Read prompt_nudge from .rcode/config.yaml.
745
745
  * Returns 'every' | 'once-per-intent' | 'when-stale' | 'off'.
746
- * Defaults to 'every' when key is absent, file is missing, or value is unknown.
746
+ * Defaults to 'once-per-intent' when key is absent, file is missing, or value
747
+ * is unknown — #953: 'every' re-nudged on every matching prompt during an
748
+ * active session, which read as noise once the keyword matcher's false
749
+ * positives compounded it. 'every' is still available as an opt-in.
747
750
  */
748
751
  function readPromptNudgeToggle(cwd) {
749
752
  const VALID = new Set(['every', 'once-per-intent', 'when-stale', 'off']);
@@ -752,9 +755,9 @@ function readPromptNudgeToggle(cwd) {
752
755
  const text = fs.readFileSync(cfgPath, 'utf8');
753
756
  const parsed = parseSimpleYamlInline(text);
754
757
  const val = (parsed.prompt_nudge || '').trim().toLowerCase();
755
- return VALID.has(val) ? val : 'every';
758
+ return VALID.has(val) ? val : 'once-per-intent';
756
759
  } catch {
757
- return 'every';
760
+ return 'once-per-intent';
758
761
  }
759
762
  }
760
763
 
@@ -791,6 +794,20 @@ function isStateStaleFallbackTrue(cwd) {
791
794
  }
792
795
  }
793
796
 
797
+ /**
798
+ * Word-boundary keyword match — #953: plain `lower.includes(kw)` matched
799
+ * generic keywords like "bug" and "crash" inside unrelated words ("debugger",
800
+ * "crashing"), over-firing the debug nudge. Boundaries are checked against
801
+ * Unicode letters/numbers (not just ASCII \w) so Arabic/Urdu keyword phrases
802
+ * still match correctly.
803
+ */
804
+ function keywordMatches(lower, kw) {
805
+ const kwLower = kw.toLowerCase();
806
+ const escaped = kwLower.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
807
+ const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, 'u');
808
+ return re.test(lower);
809
+ }
810
+
794
811
  /**
795
812
  * prompt-router: Nudge toward rcode commands for memory consistency (#892).
796
813
  * Reads stdin synchronously (NOT async — rejects bad JSON). Keyword-matches INTENT_TABLE,
@@ -868,12 +885,12 @@ function promptRouter() {
868
885
  process.exit(0);
869
886
  }
870
887
 
871
- // ── Keyword match (first-match-wins, case-insensitive) ───────────────
888
+ // ── Keyword match (first-match-wins, case-insensitive, word-boundary)
872
889
  const lower = prompt.toLowerCase();
873
890
  let matched = null;
874
891
  for (const entry of INTENT_TABLE) {
875
892
  for (const kw of entry.keywords) {
876
- if (lower.includes(kw.toLowerCase())) {
893
+ if (keywordMatches(lower, kw)) {
877
894
  matched = entry;
878
895
  break;
879
896
  }
@@ -4547,9 +4547,11 @@ function cmdGenerateClaudeMd(rawArgs) {
4547
4547
  const force = args.includes('--force');
4548
4548
  const claudeMdPath = path.join(PROJECT_ROOT, 'CLAUDE.md');
4549
4549
  const agentsMdPath = path.join(PROJECT_ROOT, 'AGENTS.md');
4550
+ const claudeExisted = fs.existsSync(claudeMdPath);
4551
+ const agentsExisted = fs.existsSync(agentsMdPath);
4550
4552
 
4551
- if (fs.existsSync(claudeMdPath) && !force) {
4552
- throw new Error(`CLAUDE.md already exists at ${claudeMdPath}. Use --force to overwrite.`);
4553
+ if (claudeExisted && agentsExisted && !force) {
4554
+ throw new Error(`CLAUDE.md and AGENTS.md already exist at ${PROJECT_ROOT}. Use --force to overwrite.`);
4553
4555
  }
4554
4556
 
4555
4557
  // Resolve project name from package.json or directory.
@@ -4647,26 +4649,33 @@ Before handling planning, exploration, auditing, refactoring, or multi-step buil
4647
4649
  **This file is part of the project. Treat it as load-bearing.**
4648
4650
  `;
4649
4651
 
4650
- const claudeExisted = fs.existsSync(claudeMdPath);
4651
- fs.writeFileSync(claudeMdPath, content);
4652
+ // Each file's own existence gates only its own write — a project with an
4653
+ // existing CLAUDE.md must still get a missing AGENTS.md backfilled, and
4654
+ // vice versa (#1025).
4655
+ const wroteClaude = !claudeExisted || force;
4656
+ if (wroteClaude) {
4657
+ fs.writeFileSync(claudeMdPath, content);
4658
+ }
4652
4659
 
4653
4660
  // Mirror the same rules to AGENTS.md (the cross-tool standard Codex, Cursor,
4654
4661
  // Windsurf, Antigravity, and Gemini read). Skip when it already exists without
4655
4662
  // --force so an install-appended "## rcode Agents (installed)" roster survives.
4656
- const agentsExisted = fs.existsSync(agentsMdPath);
4657
4663
  const wroteAgents = !agentsExisted || force;
4658
4664
  if (wroteAgents) {
4659
4665
  fs.writeFileSync(agentsMdPath, content);
4660
4666
  }
4661
4667
 
4668
+ const writtenPaths = [];
4669
+ if (wroteClaude) writtenPaths.push(path.relative(PROJECT_ROOT, claudeMdPath));
4670
+ if (wroteAgents) writtenPaths.push(path.relative(PROJECT_ROOT, agentsMdPath));
4671
+
4662
4672
  return {
4663
4673
  ok: true,
4664
4674
  path: path.relative(PROJECT_ROOT, claudeMdPath),
4665
- paths: wroteAgents
4666
- ? [path.relative(PROJECT_ROOT, claudeMdPath), path.relative(PROJECT_ROOT, agentsMdPath)]
4667
- : [path.relative(PROJECT_ROOT, claudeMdPath)],
4675
+ paths: writtenPaths,
4668
4676
  project_name: projectName,
4669
- overwritten: force && claudeExisted,
4677
+ overwritten: force && (claudeExisted || agentsExisted),
4678
+ claude_md_skipped: !wroteClaude,
4670
4679
  agents_md_skipped: !wroteAgents,
4671
4680
  };
4672
4681
  }
@@ -5283,18 +5292,55 @@ function cmdPhasePlanIndex(rawArgs) {
5283
5292
  const stem = file.replace(/-SPRINT\.md$/i, '');
5284
5293
  const text = fs.readFileSync(path.join(phaseDir, file), 'utf8');
5285
5294
  const { frontmatter, body } = parseFrontmatter(text);
5295
+ let block = '';
5296
+ if (text.startsWith('---\n')) {
5297
+ const end = text.indexOf('\n---\n', 4);
5298
+ if (end !== -1) block = text.slice(4, end);
5299
+ }
5286
5300
  const id = frontmatter.sprint || frontmatter.plan || stem;
5287
- const wave = parseInt(frontmatter.wave || '1', 10) || 1;
5288
- const autonomous = String(frontmatter.autonomous || '').toLowerCase() === 'true';
5301
+ // `wave:` may be an explicit scalar key, or absent — in which case it's
5302
+ // derived from `depends_on` below (block-list or inline form, issue #951).
5303
+ const hasExplicitWave = /^wave\s*:\s*\d+/m.test(block);
5304
+ const wave = hasExplicitWave ? (parseInt(frontmatter.wave, 10) || 1) : null;
5305
+ const dependsOn = fmListField(block, 'depends_on');
5306
+ const hasAutonomousKey = /^autonomous\s*:/m.test(block);
5307
+ const autonomous = hasAutonomousKey
5308
+ ? String(frontmatter.autonomous || '').toLowerCase() === 'true'
5309
+ : /<automated>/i.test(body);
5289
5310
  const gapClosure = String(frontmatter.gap_closure || frontmatter.type || '').toLowerCase() === 'gap_closure';
5290
5311
  const objMatch = body.match(/^##\s+(?:Objective|Goal)\s*\n+([^\n]+)/mi);
5291
5312
  const objective = objMatch ? objMatch[1].trim() : (frontmatter.goal || '').replace(/^["']|["']$/g, '');
5292
- const taskCount = (body.match(/^[-*]\s+\[[ xX]\]/gm) || []).length;
5293
- const filesModified = (body.match(/^\s*-\s*path:\s*["']?([^"'\n]+)/gm) || []).length;
5313
+ const checkboxCount = (body.match(/^[-*]\s+\[[ xX]\]/gm) || []).length;
5314
+ const storyHeaderCount = (body.match(/^###\s+Story\s+\S+/gm) || []).length;
5315
+ const taskCount = checkboxCount > 0 ? checkboxCount : storyHeaderCount;
5316
+ const filesModifiedList = fmListField(block, 'files_modified');
5317
+ const filesModified = filesModifiedList.length > 0
5318
+ ? filesModifiedList.length
5319
+ : (body.match(/^\s*-\s*path:\s*["']?([^"'\n]+)/gm) || []).length;
5294
5320
  const hasSummary = summarySet.has(stem);
5295
5321
  if (/checkpoint/i.test(body)) hasCheckpoints = true;
5296
- return { id, wave, autonomous, gap_closure: gapClosure, objective, task_count: taskCount, files_modified: filesModified, has_summary: hasSummary, file: path.relative(PROJECT_ROOT, path.join(phaseDir, file)) };
5322
+ return { id, wave, dependsOn, autonomous, gap_closure: gapClosure, objective, task_count: taskCount, files_modified: filesModified, has_summary: hasSummary, file: path.relative(PROJECT_ROOT, path.join(phaseDir, file)) };
5297
5323
  });
5324
+
5325
+ // Resolve waves left undetermined (no explicit `wave:` key) from depends_on:
5326
+ // wave(p) = 1 + max(wave of each same-phase dependency), or 1 if none.
5327
+ const idToPlan = new Map(plans.map((p) => [p.id, p]));
5328
+ function resolveWave(p, seen) {
5329
+ if (p.wave !== null) return p.wave;
5330
+ if (seen.has(p.id)) { p.wave = 1; return 1; }
5331
+ seen.add(p.id);
5332
+ let maxDepWave = 0;
5333
+ for (const depId of p.dependsOn) {
5334
+ const dep = idToPlan.get(depId);
5335
+ if (!dep) continue;
5336
+ maxDepWave = Math.max(maxDepWave, resolveWave(dep, seen));
5337
+ }
5338
+ p.wave = maxDepWave > 0 ? maxDepWave + 1 : 1;
5339
+ return p.wave;
5340
+ }
5341
+ for (const p of plans) resolveWave(p, new Set());
5342
+ for (const p of plans) delete p.dependsOn;
5343
+
5298
5344
  const waves = {};
5299
5345
  for (const p of plans) {
5300
5346
  const k = String(p.wave);
@@ -11,7 +11,7 @@
11
11
  },
12
12
  {
13
13
  "intent": "debug",
14
- "keywords": ["bug", "getting an error", "throwing an error", "error in the", "fix the error", "debug this", "crash", "failure", "broken", "not working", "fails", "exception", "traceback", "kharab", "masla", "ye kaam nahi kar raha", "yeh kaam nahi kar raha", "error a raha hai", "theek karo", "khud se crash", "خطأ", "مشكلة", "لا يعمل", "أصلح الخطأ", "تعطل البرنامج"],
14
+ "keywords": ["getting an error", "throwing an error", "error in the", "fix the error", "debug this", "crash", "not working", "exception", "traceback", "kharab", "masla", "ye kaam nahi kar raha", "yeh kaam nahi kar raha", "error a raha hai", "theek karo", "khud se crash", "خطأ", "مشكلة", "لا يعمل", "أصلح الخطأ", "تعطل البرنامج"],
15
15
  "command": "/rcode-debug"
16
16
  },
17
17
  {
@@ -387,6 +387,38 @@ Scope: {one-line scope summary}
387
387
  Routing to: {chosen command}
388
388
  Reason: {one-line why}
389
389
  ```
390
+
391
+ **herdr hint (cached read only — no live check from here).** When the chosen command is
392
+ `/rcode-execute` or `/rcode-add-phase` (routes that fan out substantial multi-file
393
+ execution work), read the cached availability flag left by `/rcode-execute`'s own
394
+ availability check:
395
+
396
+ ```bash
397
+ HERDR_AVAILABLE=$(node .rcode/bin/rcode-tools.cjs config-get workflow._herdr_available 2>/dev/null || echo "false")
398
+ HERDR_AVAILABLE=${HERDR_AVAILABLE:-false}
399
+ ```
400
+
401
+ This is a plain `config-get` read — the same sanctioned category as `config-get mode`
402
+ used elsewhere in this workflow (see `<guardrails>`). It reuses the exact cache
403
+ `/rcode-execute` writes to (`workflow._herdr_checked` / `workflow._herdr_available`) —
404
+ do NOT duplicate the `command -v herdr` probe or call `config-set` here. `/rcode-do`
405
+ has no sanctioned way to run that shell probe or persist a value itself, so if the
406
+ cache hasn't been populated yet (`_herdr_checked` still false), say nothing — the
407
+ first `/rcode-execute` run downstream will perform and cache the check itself.
408
+
409
+ If `HERDR_AVAILABLE == "true"`, append one extra line to the banner above:
410
+
411
+ ```
412
+ Note: herdr is available on this machine — {chosen command} will offer a
413
+ multi-agent orchestration option if the work fans out into independent
414
+ plans; you'll be asked to confirm before anything runs via herdr.
415
+ ```
416
+
417
+ Do NOT turn this into an `AskUserQuestion` prompt and do NOT ask the user to
418
+ decide here — `/rcode-do` only routes, it never presents execution-mode choices.
419
+ The actual offer (with the full tradeoff explanation and required confirmation)
420
+ is `/rcode-execute`'s `three_options` step, which fires after dispatch regardless
421
+ of whether this hint line was shown.
390
422
  </step>
391
423
 
392
424
  <step name="dispatch">
@@ -426,7 +458,7 @@ If the chosen command expects a phase number and one wasn't provided in the text
426
458
  <guardrails>
427
459
  **Hard prohibitions during /rcode-do execution (issue #458, refined by #1007):**
428
460
 
429
- The steps above (`parse_args`, `check_project`, `auto_init_check`, `greenfield_guard`, `explicit_intent_check`, `persona_shortcut`) legitimately call Bash for structured state/config lookups (`rcode-tools.cjs state load`, `progress init`, `config-get mode`, `classify-question`, milestone/PRD/epic detection via `ls`/`grep`) and Read for the specific persona/capability-table lookup in `persona_shortcut` step 2. That is routing plumbing, not investigation, and is allowed. What's prohibited is using those same tools to figure out the route by inspecting application code, or to do the routed work itself:
461
+ The steps above (`parse_args`, `check_project`, `auto_init_check`, `greenfield_guard`, `explicit_intent_check`, `persona_shortcut`, `display`) legitimately call Bash for structured state/config lookups (`rcode-tools.cjs state load`, `progress init`, `config-get mode`, `config-get workflow._herdr_available`, `classify-question`, milestone/PRD/epic detection via `ls`/`grep`) and Read for the specific persona/capability-table lookup in `persona_shortcut` step 2. That is routing plumbing, not investigation, and is allowed. `config-get workflow._herdr_available` is a read of a cache another workflow (`/rcode-execute`) already populated — `/rcode-do` MUST NOT run `command -v herdr` itself or call `config-set` to populate that cache; it only reads whatever is already there. What's prohibited is using those same tools to figure out the route by inspecting application code, or to do the routed work itself:
430
462
 
431
463
  - MUST NOT use Bash/Read/Grep/Glob to explore or read application source code to guess what a vague request means. The state/config lookups named above are the only sanctioned uses — anything beyond them (grepping `src/`, reading a feature file to understand behavior, etc.) means the dispatcher contract has failed — STOP and use the no-route exit instead.
432
464
  - MUST NOT call Write or Edit. The dispatcher never modifies files.
@@ -98,7 +98,41 @@ CONFIG_MODE=$(node .rcode/bin/rcode-tools.cjs config-get mode 2>/dev/null || ech
98
98
 
99
99
  **If `CONFIG_MODE == "yolo"` or `$ARGUMENTS` contains `--auto`:** Skip the menu. Auto-select **A) Autonomous run** and print one line: `▶ Auto-selecting Autonomous run (yolo mode). /rcode-settings set mode guided to change.`
100
100
 
101
- Otherwise, offer three modes via AskUserQuestion. Each option names the tradeoff explicitly:
101
+ **herdr availability check (cached).** Before offering options, determine whether a 4th
102
+ option (**D) herdr multi-agent orchestration**) should be shown. This uses the same
103
+ cached-boolean idiom as `workflow._auto_chain_active` above — read with a safe default,
104
+ write back once resolved, and skip the live check on subsequent runs:
105
+
106
+ ```bash
107
+ HERDR_NAMED_EXPLICITLY=$([[ "$ARGUMENTS" =~ herdr ]] && echo true || echo false)
108
+
109
+ HERDR_CHECKED=$(node .rcode/bin/rcode-tools.cjs config-get workflow._herdr_checked 2>/dev/null || echo "false")
110
+ HERDR_CHECKED=${HERDR_CHECKED:-false} # config-get exits 0 with empty output when key absent
111
+
112
+ if [[ "$HERDR_CHECKED" != "true" || "$HERDR_NAMED_EXPLICITLY" == "true" ]]; then
113
+ # Live re-check: first time ever, OR user named herdr explicitly this run
114
+ # (herdr may have been installed since the last negative cache hit).
115
+ if command -v herdr >/dev/null 2>&1; then
116
+ HERDR_AVAILABLE=true
117
+ else
118
+ HERDR_AVAILABLE=false
119
+ fi
120
+ node .rcode/bin/rcode-tools.cjs config-set workflow._herdr_available "$HERDR_AVAILABLE" 2>/dev/null
121
+ node .rcode/bin/rcode-tools.cjs config-set workflow._herdr_checked true 2>/dev/null
122
+ else
123
+ # Cached negative (or positive) result from a prior run — skip the check entirely.
124
+ HERDR_AVAILABLE=$(node .rcode/bin/rcode-tools.cjs config-get workflow._herdr_available 2>/dev/null || echo "false")
125
+ HERDR_AVAILABLE=${HERDR_AVAILABLE:-false}
126
+ fi
127
+ ```
128
+
129
+ Do **not** re-run `command -v herdr` on every invocation once `_herdr_checked` is `true` —
130
+ that defeats the point of caching. The only exception is `HERDR_NAMED_EXPLICITLY`: if the
131
+ user's request names herdr by name (e.g. "use herdr for this", "orchestrate via herdr"),
132
+ always re-check live regardless of the cached value, since herdr may have been installed
133
+ since the negative result was cached.
134
+
135
+ Otherwise, offer modes via AskUserQuestion. Each option names the tradeoff explicitly:
102
136
 
103
137
  **A) Autonomous run** — Spawn subagent per plan in sequence/parallel per
104
138
  wave rules. Checkpoints still pause for user. Fastest wall-clock.
@@ -112,9 +146,31 @@ Otherwise, offer three modes via AskUserQuestion. Each option names the tradeoff
112
146
  later waves in a separate session. Good for staged rollout / review
113
147
  gates.
114
148
 
149
+ **D) herdr multi-agent orchestration** (only shown if `HERDR_AVAILABLE == "true"`) —
150
+ Fan this phase's plans out to parallel `herdr` panes/tabs, each running its own
151
+ Claude agent in an isolated git worktree, then merge their work back. Be specific
152
+ about the tradeoff, not a one-liner: separate terminal panes you can watch
153
+ independently, genuinely parallel wall-clock (not wave-sequenced), noticeably
154
+ higher token cost than A/B/C since each pane runs a full agent session, and a
155
+ merge step at the end. Best fit is plans that are truly independent (no shared
156
+ `files_modified`, no cross-plan sequencing) — for plans with overlaps or a single
157
+ linear wave, herdr adds coordination overhead for no benefit; prefer A or C instead.
158
+ Selecting this option invokes the `rcode-herdr-orchestration` skill — it does not
159
+ replace this workflow's execution, it's a different way to run the same plans.
160
+
161
+ **Never auto-select D, even in yolo/`--auto` mode.** Orchestrating via herdr requires
162
+ explicit user confirmation on this turn — if `CONFIG_MODE == "yolo"` skipped the menu
163
+ above, herdr is simply not offered this run; the autonomous-run auto-selection must
164
+ never silently switch into herdr mode.
165
+
115
166
  Include a recommendation line: "My recommendation: {letter} because {reason
116
167
  in one clause}." Then ask which option to proceed with — do NOT silently
117
- pick one.
168
+ pick one. If the user selects D, confirm once more in plain language what
169
+ will happen (parallel panes, worktrees, higher cost) before invoking
170
+ `Skill(skill="rcode-herdr-orchestration", ...)` — do not invoke it on the same
171
+ turn as the AskUserQuestion answer without that confirmation being part of
172
+ the answer itself (i.e. selecting the option IS the confirmation only if its
173
+ label made the tradeoff explicit, which it does above).
118
174
  </three_options>
119
175
 
120
176
  <output_format>
@@ -42,6 +42,12 @@ Run detection in parallel:
42
42
  test -f .rcode/config.yaml && echo "rcode-configured: yes" || echo "rcode-configured: no"
43
43
  test -f .rcode/state.json && echo "state-present: yes" || echo "state-present: no"
44
44
  test -f .rcode/JOURNEY.md && echo "rihla-present: yes" || echo "rihla-present: no"
45
+ # #1028: config.yaml is written by the installer itself, so it exists on the
46
+ # very first /rcode-init run after a fresh install — not just on a genuine
47
+ # "returning" session. state.json's installer-seeded _seeded_stub marker
48
+ # (cleared once a real init/new-project run completes) is what actually
49
+ # distinguishes "normal first brownfield run" from "prior init was interrupted".
50
+ grep -q '"_seeded_stub"[[:space:]]*:[[:space:]]*true' .rcode/state.json 2>/dev/null && echo "seeded-stub: yes" || echo "seeded-stub: no"
45
51
 
46
52
  # Project presence
47
53
  # Use git rev-parse instead of test -d .git — in git worktrees .git is a FILE not a dir
@@ -83,12 +89,20 @@ If `state === "returning"` and `--reset` not passed:
83
89
  Or run with --reset to reconfigure.
84
90
  ```
85
91
 
86
- - If `rihla-present: no` — JOURNEY.md is missing from a partial prior init. Do NOT stop. Print a recovery notice and continue to Steps 4 and 4b to write the missing baseline:
92
+ - If `rihla-present: no` and `seeded-stub: yes` this is the normal, expected first `/rcode-init` run on a brownfield project: `config.yaml` was seeded by the installer, but no real init has happened yet. Nothing was broken; do NOT use "recovery" language. Print a plain first-run notice and continue to Steps 4 and 4b to write the baseline:
93
+
94
+ ```
95
+ ✓ rcode is configured. Writing the JOURNEY.md baseline now...
96
+ ```
97
+
98
+ Skip Steps 2 and 3 (config already exists). Jump directly to Step 4.
99
+
100
+ - If `rihla-present: no` and `seeded-stub: no` — a genuine prior init/new-project run completed (the stub marker was cleared) but JOURNEY.md is missing, meaning that prior run was interrupted before writing it. This is an actual recovery case. Print a recovery notice and continue to Steps 4 and 4b to write the missing baseline:
87
101
 
88
102
  ```
89
103
  ✓ rcode is already configured here.
90
104
 
91
- JOURNEY.md baseline is missing — completing the scan step now...
105
+ JOURNEY.md baseline is missing from a prior interrupted init — completing it now...
92
106
  ```
93
107
 
94
108
  Skip Steps 2 and 3 (config already exists). Jump directly to Step 4.
@@ -290,12 +304,12 @@ test -f .rcode/sources.yaml && node .rcode/bin/rcode-tools.cjs context refresh >
290
304
  `generate-claude-md` (the command routing rule + project rules block every agent needs at session start) previously only ran via the `/rcode-new-project` roadmap flow — a project set up with `/rcode-init` alone (the common "add rcode to an existing codebase" path) never got it, so agents had no ambient instruction to check `do.md` before acting ad-hoc. Close that gap here, unconditionally (not just on `fresh`):
291
305
 
292
306
  ```bash
293
- if [ ! -f CLAUDE.md ]; then
307
+ if [ ! -f CLAUDE.md ] || [ ! -f AGENTS.md ]; then
294
308
  node .rcode/bin/rcode-tools.cjs generate-claude-md
295
309
  fi
296
310
  ```
297
311
 
298
- Never pass `--force` here — an existing `CLAUDE.md` is the user's own file (or was already generated by a prior init/new-project run) and must not be overwritten. Silent no-op when `CLAUDE.md` already exists.
312
+ Never pass `--force` here — an existing `CLAUDE.md` or `AGENTS.md` is the user's own file (or was already generated by a prior init/new-project run) and must not be overwritten. `cmdGenerateClaudeMd` gates each file's write on that file's own existence, so this backfills whichever one is missing (#1025). Silent no-op when both files already exist.
299
313
 
300
314
  ## Step 5 — Suggest the next step
301
315
 
@@ -14,10 +14,12 @@ Valid rcode subagent types (use exact names — do not fall back to 'general-pur
14
14
 
15
15
  ## Step 0 — Usage check
16
16
 
17
- If `$ARGUMENTS` is empty or contains only `--help` or `-h`:
17
+ If `$ARGUMENTS` contains `--help` or `-h`:
18
18
  - Print the usage block below
19
19
  - STOP — do not proceed
20
20
 
21
+ If `$ARGUMENTS` is empty, do NOT stop — proceed to Step 1, which defaults focus to `tech+arch`.
22
+
21
23
  **Usage:**
22
24
  ```
23
25
  /rcode-scan [--focus tech|arch|quality|concerns|tech+arch] [--refresh] [--reset]
@@ -339,6 +341,25 @@ This file tracks structural changes between scans. Each entry is auto-written by
339
341
 
340
342
  This file is **read by future `/rcode-scan --refresh` runs** as additional anchor context — the memory bank is self-improving across scans.
341
343
 
344
+ ## Step 6.6: Propagate scan findings into PROJECT.md / STATE.md stubs
345
+
346
+ After the RETURNED banner, check whether `.planning/PROJECT.md` and `.planning/STATE.md` are still install stubs (contain the `<!-- INSTALL STUB` banner). If real, non-stub content already exists, skip this step entirely — never overwrite user-authored planning docs.
347
+
348
+ ```bash
349
+ grep -q '<!-- INSTALL STUB' .planning/PROJECT.md 2>/dev/null && echo "PROJECT_IS_STUB"
350
+ grep -q '<!-- INSTALL STUB' .planning/STATE.md 2>/dev/null && echo "STATE_IS_STUB"
351
+ ```
352
+
353
+ If `PROJECT.md` is a stub: replace its `**One-line:**` sentence with a brief, grounded one-line summary derived from the scan output just written (e.g. from STACK.md/ARCHITECTURE.md's opening lines — language, framework, primary purpose). Keep the rest of the stub structure (Vision/Stack sections) intact; do not attempt a full rewrite. Use Read then Edit — never blind-overwrite.
354
+
355
+ If `STATE.md` is a stub: update `**Current phase:**` line to read:
356
+ ```
357
+ **Current phase:** none — existing project scanned, see .planning/codebase/ — no phases planned yet
358
+ ```
359
+ and add one line under `## Next Action` noting the scan already ran: `Codebase scanned via /rcode-scan — run /rcode-new-project or /rcode-add-phase to plan phases.`
360
+
361
+ This is a minimal, targeted edit — not a full PROJECT.md content-generation system. The goal is closing the "user opens STATE.md and sees zero acknowledgment a scan already ran" gap.
362
+
342
363
  ## Step 7: Final cue (orchestrator-level, after RETURNED banner)
343
364
 
344
365
  The RETURNED banner above is Dalil's voice. After it, the orchestrator may add ONE neutral cue line if the user might want a deeper scan:
@@ -63,11 +63,14 @@ export function Sidebar({ activeView, projectName }) {
63
63
  const name = project.name || projectName || 'No project';
64
64
  const user = (project.user && project.user.name) ? project.user : null;
65
65
 
66
- // Full store subscription for live health badge counts.
66
+ // Full store subscription for live health badge counts. Derived from
67
+ // activeSessions (the /api/sessions poll) — same source of truth as the
68
+ // OrchPanel footer and NotifyCenter — not the static store.blockers list
69
+ // (PROJECT.md-backed, unrelated to live orchestrator state; see #965).
67
70
  // Re-renders on every setState (sessions poll every 4 s, state refresh every 30 s).
68
- const { activeSessions, blockers } = useStore();
69
- const sessionCount = (activeSessions || []).filter(s => s.status === 'running').length;
70
- const blockerCount = (blockers || []).length;
71
+ const { activeSessions } = useStore();
72
+ const sessionCount = (activeSessions || []).filter(s => s.status === 'running' && !s.waiting).length;
73
+ const blockerCount = (activeSessions || []).filter(s => s.status === 'blocked' || s.waiting).length;
71
74
 
72
75
  return html`
73
76
  <aside class="sidebar" id="sidebar">
@@ -93,7 +96,7 @@ export function Sidebar({ activeView, projectName }) {
93
96
  </span>
94
97
  <span
95
98
  class=${'health-badge' + (blockerCount > 0 ? ' health-badge--alert' : ' health-badge--zero')}
96
- title=${blockerCount + ' blocker' + (blockerCount === 1 ? '' : 's')}
99
+ title=${blockerCount + ' blocked orchestration session' + (blockerCount === 1 ? '' : 's')}
97
100
  >
98
101
  <${Icon} name="alert-triangle" size=${12} />
99
102
  ${blockerCount} blocked
@@ -91,7 +91,7 @@ function PhaseDetail({ phase: p, S }) {
91
91
  <div class="attr-grid">
92
92
  <${AttrItem} label="Status" value=${html`<${Chip} status=${p.status}/>`}/>
93
93
  <${AttrItem} label="Sprints" value=${sps.length}/>
94
- <${AttrItem} label="Tasks Done" value=${done + '/' + stories.length}/>
94
+ <${AttrItem} label="Tasks Done" value=${stories.length ? (done + '/' + stories.length) : 'no tasks tracked'}/>
95
95
  <${AttrItem} label="Progress" value=${pct(done, stories.length)}/>
96
96
  ${p.completed_at ? html`<${AttrItem} label="Completed" value=${humanDate(p.completed_at)}/>` : null}
97
97
  </div>
@@ -2651,6 +2651,8 @@ footer {
2651
2651
  overflow: hidden;
2652
2652
  text-overflow: ellipsis;
2653
2653
  white-space: nowrap;
2654
+ min-width: 0;
2655
+ flex-shrink: 1;
2654
2656
  }
2655
2657
  .orch-term-dock-live {
2656
2658
  display: inline-flex;
@@ -2661,6 +2663,7 @@ footer {
2661
2663
  color: var(--green);
2662
2664
  font-family: var(--font-mono);
2663
2665
  white-space: nowrap;
2666
+ flex-shrink: 0;
2664
2667
  }
2665
2668
  .orch-term-dock-live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); }
2666
2669
  .orch-term-dock-stop {
@@ -2675,6 +2678,7 @@ footer {
2675
2678
  cursor: pointer;
2676
2679
  font-family: var(--font-sans);
2677
2680
  margin-left: var(--space-2);
2681
+ flex-shrink: 0;
2678
2682
  }
2679
2683
  .orch-term-dock-stop:hover { opacity: 0.85; }
2680
2684
  .orch-term-dock-body {
@@ -269,6 +269,9 @@ function buildDashboard(state) {
269
269
  : (Array.isArray(raw.phases) ? raw.phases : []);
270
270
 
271
271
  // ---- phases (superset: rich phaseTree + contract range/state) ----
272
+ // Sorted numerically by phase number — state.json array order reflects
273
+ // insertion order (when a phase entry was appended), not phase sequence,
274
+ // so an inserted-late phase like 13 would otherwise render after 38/39/42/43.
272
275
  const phases = tree.map(p => {
273
276
  const started = p.started || p.created || null;
274
277
  const completed = p.completed || p.completed_at || null;
@@ -276,7 +279,7 @@ function buildDashboard(state) {
276
279
  ? [fmtShort(started), fmtShort(completed)].filter(Boolean).join(' – ')
277
280
  : '';
278
281
  return { ...p, name: p.name || p.slug || String(p.id || ''), range, state: toState(p.status) };
279
- });
282
+ }).sort((a, b) => (parseFloat(a.id ?? a.number) || 0) - (parseFloat(b.id ?? b.number) || 0));
280
283
 
281
284
  // ---- progress (prefer story counts; fall back to phase-level counts) ----
282
285
  let completed = 0, total = 0, inProg = 0;