@hanzlaa/rcode 4.3.2 → 4.3.3

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.
Files changed (31) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -0
  3. package/README.md +38 -9
  4. package/cli/install.js +14 -0
  5. package/dist/rcode.js +48 -48
  6. package/package.json +1 -1
  7. package/rcode/bin/rcode-hooks.cjs +347 -9
  8. package/rcode/commands/lens-audit.md +8 -6
  9. package/rcode/skills/SKILLS_INDEX.md +3 -2
  10. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/SKILL.md +12 -0
  11. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/backlog-building.md +13 -0
  12. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +17 -0
  13. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/orchestrator-rhythm.md +9 -0
  14. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/wave-design.md +21 -0
  15. package/rcode/skills/core/rcode-lazy/SKILL.md +149 -0
  16. package/rcode/templates/settings-hooks.json +12 -1
  17. package/rcode/workflows/do.md +1 -0
  18. package/rcode/workflows/enable-hooks.md +3 -2
  19. package/rcode/workflows/lens-audit.md +49 -8
  20. package/server/dashboard.js +6 -2
  21. package/server/lib/api.js +35 -20
  22. package/server/lib/html/client/components/OrchPanel.js +43 -2
  23. package/server/lib/html/client/components/dashboard/InProgress.js +5 -4
  24. package/server/lib/html/client/components/shared.js +14 -0
  25. package/server/lib/html/client/orchestrator.js +13 -0
  26. package/server/lib/html/client/views/KanbanView.js +33 -4
  27. package/server/lib/html/css.js +57 -3
  28. package/server/lib/scanner.js +40 -4
  29. package/server/orchestrator.js +42 -0
  30. package/rcode/skills/core/rcode-init/scripts/__pycache__/rcode_init.cpython-38.pyc +0 -0
  31. package/rcode/skills/core/rcode-init/scripts/tests/__pycache__/test_rcode_init.cpython-38.pyc +0 -0
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: rcode-lazy
3
+ description: >
4
+ Always-on "lazy senior dev" lens that forces the simplest solution that
5
+ actually works — shortest, most minimal — before any code is written.
6
+ Question whether the task needs to exist (YAGNI), reach for stdlib before
7
+ custom code, native platform features before dependencies, one line before
8
+ fifty. Supports intensity levels: lite, full (default), ultra. Activates when
9
+ the user says "lazy mode", "be lazy", "simplest solution", "minimal
10
+ solution", "yagni", "do less", "shortest path", "kam code likho", or
11
+ complains about over-engineering, bloat, boilerplate, or unnecessary
12
+ dependencies. Do NOT use for: simplifying code that already exists (use
13
+ rcode-trim), shipping in small atomic steps (use rcode-incremental).
14
+ rcode-lazy is the generative reflex that prevents bloat; rcode-trim removes
15
+ it after the fact.
16
+ argument-hint: "[lite|full|ultra]"
17
+ triggers:
18
+ - "lazy mode"
19
+ - "be lazy"
20
+ - "simplest solution"
21
+ - "yagni"
22
+ - "do less"
23
+ - "kam code likho"
24
+ user-invocable: true
25
+ ---
26
+ @.rcode/references/karpathy-guidelines.md
27
+
28
+
29
+ # Lazy
30
+
31
+ A lazy senior developer writes less because they have maintained more. Lazy
32
+ means efficient, not careless. The best code is the code never written.
33
+
34
+
35
+ ## What this actually is (read this first)
36
+
37
+ The floor of this skill is one sentence: **"Follow YAGNI, prefer one-liners
38
+ where they stay correct."** A model already understands that. Benchmarks have
39
+ shown a one-line prompt matches a full lazy-mode skill on toy tasks — so this
40
+ skill does not claim to teach the model anything new, and ships no LOC numbers
41
+ it cannot reproduce on real work.
42
+
43
+ What it adds over that one line is only three things, and that is the whole
44
+ value:
45
+
46
+ 1. **Persistence** — the reflex stays on without you retyping it each turn.
47
+ 2. **Intensity** — `lite | full | ultra` dials how hard it pushes back.
48
+ 3. **Guardrails** — an explicit list of what must *never* be simplified away.
49
+
50
+ This is the generative arm of Karpathy **P2 (Simplicity)** and **P3
51
+ (Surgical)**, which rcode already injects into every skill. rcode-lazy makes
52
+ those two principles loud and adjustable instead of background defaults.
53
+
54
+
55
+ ## Persistence
56
+
57
+ ACTIVE EVERY RESPONSE once invoked, until "stop lazy" / "normal mode" or
58
+ session end. Still active when unsure. Default level **full**; switch with
59
+ `/rcode-lazy lite|full|ultra`.
60
+
61
+
62
+ ## The ladder
63
+
64
+ Stop at the first rung that holds — take the highest rung that works, don't
65
+ keep climbing:
66
+
67
+ 1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI / Karpathy P1)
68
+ 2. **Stdlib does it?** Use it.
69
+ 3. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, a DB constraint over app code.
70
+ 4. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
71
+ 5. **Can it be one line?** One line.
72
+ 6. **Only then:** the minimum code that works.
73
+
74
+
75
+ ## Rules
76
+
77
+ - No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
78
+ - No scaffolding "for later" — later can scaffold for itself.
79
+ - Deletion over addition. Fewest files, shortest working diff.
80
+ - Two stdlib options the same size? Take the one that is correct on edge cases. Lazy means less code, not a flimsier algorithm.
81
+ - Mark deliberate shortcuts with an `rcode-lazy:` comment naming the ceiling and the upgrade path: `// rcode-lazy: global lock, per-account locks if throughput matters`.
82
+ - Complex request you can't fully default? Ship the lazy version and flag it in the same response: "Did X; Y covers it. Need full X? Say so." Don't stall.
83
+
84
+
85
+ ## When NOT to be lazy (guardrails)
86
+
87
+ Never simplify away: input validation at trust boundaries, error handling that
88
+ prevents data loss, security measures, accessibility basics, anything the user
89
+ explicitly requested. User insists on the full version → build it, no
90
+ re-arguing.
91
+
92
+ Non-trivial logic (a branch, a loop, a parser, a money/security path) leaves
93
+ ONE runnable check behind — the smallest thing that fails if the logic breaks
94
+ (Karpathy P4: a verifiable success criterion). Trivial one-liners need no test.
95
+
96
+
97
+ ## Intensity
98
+
99
+ | Level | What changes |
100
+ |-------|--------------|
101
+ | **lite** | Build what's asked, but name the lazier alternative in one line. User picks. |
102
+ | **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation. Default. |
103
+ | **ultra** | YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath. |
104
+
105
+
106
+ ## Output Format
107
+
108
+ Code first. Then at most three short lines: what was skipped, when to add it.
109
+ If the explanation is longer than the code, delete the explanation. Prose the
110
+ user explicitly asked for (a report, a walkthrough) is not debt — give it in
111
+ full; the rule is only against unrequested prose.
112
+
113
+ Pattern:
114
+
115
+ ```
116
+ [code]
117
+ → skipped: [X], add when [Y].
118
+ ```
119
+
120
+
121
+ ## Workflow
122
+
123
+ 1. Read the request; note the intensity level (default **full**).
124
+ 2. Walk the ladder top-down; stop at the first rung that holds.
125
+ 3. Write the minimum code that works; mark deliberate shortcuts with `rcode-lazy:`.
126
+ 4. Add ONE runnable check if the logic is non-trivial.
127
+ 5. Output code first, then the `→ skipped:` line.
128
+
129
+
130
+ ## Examples
131
+
132
+ ### Happy path
133
+ **User:** "Add a cache for these API responses." (full)
134
+ **Result:** `@lru_cache(maxsize=1000)` on the fetch function.
135
+ `→ skipped: custom cache class, add when lru_cache measurably falls short.`
136
+
137
+ ### Edge case (ultra challenges the requirement)
138
+ **User:** "Add a cache for these API responses." (ultra)
139
+ **Result:** "No cache until a profiler says so. When it does: `@lru_cache`. A hand-rolled TTL cache class is a bug farm with a hit rate."
140
+
141
+ ### Negative boundary (route elsewhere)
142
+ **User:** "This 300-line file is bloated, clean it up."
143
+ **Result:** Existing code → not rcode-lazy. Route to `rcode-trim`.
144
+
145
+
146
+ ## Memory Bank Hooks
147
+
148
+ - **Reads:** the active request; current intensity level if set this session.
149
+ - **Writes:** nothing — it is a generative lens, not a document producer.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "pre-edit hook is currently advisory (logs warning, does not block). Full session-read tracking requires PostToolUse Read hook to write state file. Implemented in follow-up.",
2
+ "_comment": "pre-edit hook is currently advisory (logs warning, does not block). Full session-read tracking requires PostToolUse Read hook to write state file. Implemented in follow-up. prompt-router (UserPromptSubmit) is advisory — emits additionalContext to nudge toward the right rcode command; never blocks.",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -70,6 +70,17 @@
70
70
  }
71
71
  ]
72
72
  }
73
+ ],
74
+ "UserPromptSubmit": [
75
+ {
76
+ "matcher": "",
77
+ "hooks": [
78
+ {
79
+ "type": "command",
80
+ "command": "node .rcode/bin/rcode-hooks.cjs prompt-router"
81
+ }
82
+ ]
83
+ }
73
84
  ]
74
85
  }
75
86
  }
@@ -290,6 +290,7 @@ Evaluate `$QUESTION` against these routing rules. Apply the **first matching** r
290
290
  | Validate an idea, "working backwards", "press release", "PRFAQ", "is this worth building" | `/rcode-prfaq` | Stress-test concept before committing sprint capacity |
291
291
  | Brainstorm, generate ideas, "explore options", "what could we do" | `/rcode-brainstorm` | Structured ideation before planning |
292
292
  | Audit code quality, "review changes", "karpathy", "check my diff", "too complex" | `/rcode-review --karpathy` | 4-principle code audit against recent diff |
293
+ | Make it simpler, "be lazy", "lazy mode", "simplest solution", "yagni", "over-engineered", "kam code likho" | `/rcode-lazy` | Generative simplicity lens — YAGNI before code is written (rcode-trim removes bloat after the fact) |
293
294
  | Walk through a change, "checkpoint", "explain this diff", "human review" | `/rcode-checkpoint-preview` | Human-in-the-loop diff walkthrough |
294
295
  | Exploring, researching, comparing, or "how does X work" | `/rcode-research-phase` | Domain research before planning |
295
296
  | Scope unclear, conflicting UIs/options, "which one", "better UX", "still have confusion", "how should X look", brainstorming vision | `/rcode-discuss-phase` | Decisions not yet locked — gather before planning |
@@ -1,7 +1,7 @@
1
1
  # Workflow: rcode-enable-hooks
2
2
 
3
3
  <purpose>
4
- Merge rcode opt-in hooks from settings-hooks.json into .claude/settings.json. Creates settings.json if missing. Enables all 8 guardrails: pre-edit (read-before-edit check), pre-workflow (command hint), post-commit (format validation), bash-guard (blocks dangerous commands), pre-compact (refreshes HANDOFF.json before context compaction), stop-verify (syntax-checks files changed during the response), cost-track (logs measured token usage to .rcode/telemetry/cost.jsonl), and compact-nudge (advises /rcode-trim or /clear after many edits).
4
+ Merge rcode opt-in hooks from settings-hooks.json into .claude/settings.json. Creates settings.json if missing. Enables all 9 guardrails: pre-edit (read-before-edit check), pre-workflow (command hint), post-commit (format validation), bash-guard (blocks dangerous commands), pre-compact (refreshes HANDOFF.json before context compaction), stop-verify (syntax-checks files changed during the response), cost-track (logs measured token usage to .rcode/telemetry/cost.jsonl), compact-nudge (advises /rcode-trim or /clear after many edits), and prompt-router (proactive nudge toward the right rcode command for long-term memory consistency).
5
5
  </purpose>
6
6
 
7
7
 
@@ -44,7 +44,7 @@ If `.claude/` directory does not exist, create it first.
44
44
 
45
45
  ## Step 3 — Merge hooks
46
46
 
47
- For each hook type (`PreToolUse`, `PostToolUse`):
47
+ For each hook type (`PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`, `UserPromptSubmit`):
48
48
 
49
49
  - If the hook type does not exist in the current settings.json, add it.
50
50
  - If it exists, append the new matchers and hook commands (avoid duplicates by checking for exact command matches).
@@ -86,6 +86,7 @@ Enabled guardrails:
86
86
  • stop-verify: Syntax-checks files changed during the response
87
87
  • cost-track: Logs measured token usage to .rcode/telemetry/cost.jsonl
88
88
  • compact-nudge: Advises /rcode-trim or /clear after many edits
89
+ • prompt-router: Nudges toward the matching /rcode-* command and /rcode-memory-update so work lands in .rcode/state.json (toggle via prompt_nudge in .rcode/config.yaml: every|once-per-intent|when-stale|off)
89
90
 
90
91
  To disable, remove the hooks section from .claude/settings.json or edit .rcode/templates/settings-hooks.json and re-run.
91
92
  ```
@@ -1,7 +1,7 @@
1
1
  # Workflow: rcode-lens-audit
2
2
 
3
3
  <purpose>
4
- Run a structured 15-lens code audit against the current project. Each lens
4
+ Run a structured 16-lens code audit against the current project. Each lens
5
5
  delegates to its mapped primary skill via Task() subagent dispatch — the skill
6
6
  provides domain expertise; this workflow handles orchestration, aggregation,
7
7
  and GH issue body generation. Never fixes anything; audit-first, fix-second.
@@ -26,6 +26,7 @@ and GH issue body generation. Never fixes anything; audit-first, fix-second.
26
26
  | 13 | Observability | `rcode-observability-auditor` | — |
27
27
  | 14 | Naming | `rcode-codebase-mapper` | `rcode-reviewer` |
28
28
  | 15 | Coverage | `rcode-nyquist-auditor` | `rcode-fatima` |
29
+ | 16 | YAGNI / Over-engineering | `rcode-reviewer` | `rcode-lazy` |
29
30
 
30
31
  ## Step 0 — Usage check
31
32
 
@@ -33,8 +34,8 @@ If `$ARGUMENTS` is `--help` or `-h`:
33
34
 
34
35
  ```
35
36
  /rcode-lens-audit # interactive — asks which lens
36
- /rcode-lens-audit all # run all 15 lenses sequentially
37
- /rcode-lens-audit <N> # run lens N (1-15) only
37
+ /rcode-lens-audit all # run all 16 lenses sequentially
38
+ /rcode-lens-audit <N> # run lens N (1-16) only
38
39
  /rcode-lens-audit <name> # run by name, e.g. "security", "performance"
39
40
 
40
41
  Lenses and their primary skills:
@@ -53,6 +54,7 @@ Lenses and their primary skills:
53
54
  13. observability — rcode-observability-auditor
54
55
  14. naming — rcode-codebase-mapper + rcode-reviewer
55
56
  15. coverage — rcode-nyquist-auditor + rcode-fatima
57
+ 16. yagni — rcode-reviewer + rcode-lazy (speculative abstractions, unused config, deps stdlib covers)
56
58
  ```
57
59
 
58
60
  STOP after printing help.
@@ -83,15 +85,15 @@ rcode-tools not found. Run: npx @hanzlaa/rcode install .
83
85
  ```
84
86
 
85
87
  Parse `$ARGUMENTS`:
86
- - `all` → `LENSES=(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)`
87
- - digit 1–15 → `LENSES=(<N>)`
88
+ - `all` → `LENSES=(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16)`
89
+ - digit 1–16 → `LENSES=(<N>)`
88
90
  - known lens name → map to number → `LENSES=(<N>)`
89
91
  - empty → continue to Step 2 (interactive picker)
90
92
 
91
93
  Name → number mapping:
92
94
  `security=1, performance=2, testability=3, extensibility=4, dep-health=5,`
93
95
  `error-recovery=6, state-machine=7, i18n=8, documentation=9, cross-platform=10,`
94
- `karpathy=11, sxo=12, observability=13, naming=14, coverage=15`
96
+ `karpathy=11, sxo=12, observability=13, naming=14, coverage=15, yagni=16`
95
97
 
96
98
  ## Step 2 — Interactive picker (when no argument given)
97
99
 
@@ -117,7 +119,8 @@ Options:
117
119
  13. observability — rcode-observability-auditor (unguarded calls, silent fails)
118
120
  14. naming — rcode-codebase-mapper (naming drift, PLAN.md vs SPRINT.md)
119
121
  15. coverage — rcode-nyquist-auditor (parity gaps, untested commands)
120
- 16. all run all 15 lenses
122
+ 16. yagni rcode-reviewer (speculative abstractions, single-impl interfaces, deps stdlib covers)
123
+ 17. all — run all 16 lenses
121
124
  0. cancel
122
125
  ```
123
126
 
@@ -612,6 +615,44 @@ FINDINGS[coverage] = merge(PRIMARY, SECONDARY)
612
615
 
613
616
  ---
614
617
 
618
+ ### Lens 16 — YAGNI / Over-engineering
619
+
620
+ ```
621
+ PRIMARY = Task(
622
+ subagent_type="rcode-code-reviewer",
623
+ model="{lens_model}",
624
+ prompt="YAGNI / over-engineering audit — do NOT fix code. {CONTEXT}
625
+
626
+ Run Lens 16 (YAGNI) audit against recent changes (HEAD~20..HEAD), using the
627
+ rcode-lazy ladder as the rubric (@.rcode/skills/core/rcode-lazy/SKILL.md).
628
+ This lens is narrower than Lens 11 (Karpathy): hunt ONLY for code that exists
629
+ but should not, or is bigger than the job needs. Flag:
630
+
631
+ - Speculative features / config / params for needs that do not exist yet
632
+ - Abstractions with a single implementation: an interface, factory, or
633
+ strategy with exactly one concrete user — inline it
634
+ - Wrapper layers that add a hop for one caller
635
+ - 'For later' scaffolding: extension points, plugin registries, generic
636
+ handlers with one case
637
+ - A new dependency for what stdlib or a native platform feature already does
638
+ (e.g. a date-picker lib vs <input type=\"date\">, a deep-clone lib vs
639
+ structuredClone)
640
+ - Config for a value that never changes (hardcode it)
641
+ - Hand-rolled code reproducing an already-installed dependency
642
+
643
+ Do NOT flag: validation at trust boundaries, error handling that prevents
644
+ data loss, security, accessibility, or anything explicitly requested — those
645
+ are never YAGNI violations even when verbose.
646
+
647
+ Return: file:line — YAGNI smell — what to delete/inline + the simpler form [critical|warn|info]
648
+ If clean: PASS"
649
+ )
650
+
651
+ FINDINGS[yagni] = PRIMARY
652
+ ```
653
+
654
+ ---
655
+
615
656
  ## Step 5 — Compile findings per lens
616
657
 
617
658
  For each lens that was run, print:
@@ -689,7 +730,7 @@ Next: file the GH issues above, then run /rcode-audit fix to address them.
689
730
  ## On Error
690
731
 
691
732
  - **rcode-tools not found**: print `Run: npx @hanzlaa/rcode install .` and STOP.
692
- - **Lens N out of range (not 1–15)**: print valid range and STOP.
733
+ - **Lens N out of range (not 1–16)**: print valid range and STOP.
693
734
  - **Subagent skill not installed**: note `(skill not available — skipping)`, continue.
694
735
  - **Scope dirs empty**: note `(no source files in scope)` per lens, still run dispatch.
695
736
  - **Karpathy dispatch fails** (Lens 11): note failure, continue with remaining lenses.
@@ -38,6 +38,10 @@ const { renderHtml } = require('./lib/html/shell');
38
38
  const PORT = parseInt(process.env.PORT || '7717', 10);
39
39
  const RCODE_DIR = process.env.RCODE_DIR || path.join(process.cwd(), '.rcode');
40
40
  const PROJECT_ROOT = path.dirname(RCODE_DIR);
41
+ // Fallback root for agent prompts when rcode is installed as a package (not run
42
+ // from inside the rcode source repo). server/dashboard.js lives at <pkg>/server/,
43
+ // so <pkg> is one level up.
44
+ const PACKAGE_ROOT = path.join(__dirname, '..');
41
45
 
42
46
  // Shared orchestrator token — passed to the orchestrator via env and embedded
43
47
  // in the HTML. Persisted to ~/.rcode/orch-token so it stays STABLE across
@@ -93,12 +97,12 @@ function handleRequest(req, res) {
93
97
  }
94
98
 
95
99
  if (url === '/api/agents') {
96
- handleApiAgents(req, res, PROJECT_ROOT);
100
+ handleApiAgents(req, res, PROJECT_ROOT, PACKAGE_ROOT);
97
101
  return;
98
102
  }
99
103
 
100
104
  if (url.startsWith('/api/file')) {
101
- handleApiFile(req, res, PROJECT_ROOT);
105
+ handleApiFile(req, res, PROJECT_ROOT, PACKAGE_ROOT);
102
106
  return;
103
107
  }
104
108
 
package/server/lib/api.js CHANGED
@@ -120,7 +120,7 @@ function handleApiFiles(req, res, projectRoot) {
120
120
  res.end(JSON.stringify(groups));
121
121
  }
122
122
 
123
- function handleApiFile(req, res, projectRoot) {
123
+ function handleApiFile(req, res, projectRoot, packageRoot) {
124
124
  const url = req.url || '';
125
125
  const params = new URLSearchParams(url.split('?')[1] || '');
126
126
  const relPath = params.get('path') || '';
@@ -128,24 +128,33 @@ function handleApiFile(req, res, projectRoot) {
128
128
  res.writeHead(400); res.end('Missing path parameter'); return;
129
129
  }
130
130
  // Fix #321: decode URL-encoded characters before resolving
131
- const decoded = decodeURIComponent(relPath);
132
- const resolved = path.resolve(projectRoot, decoded.replace(/^\//, ''));
133
- if (!resolved.startsWith(projectRoot + path.sep) && resolved !== projectRoot) {
134
- res.writeHead(403); res.end('Forbidden'); return;
131
+ const decoded = decodeURIComponent(relPath).replace(/^\//, '');
132
+
133
+ if (!decoded.endsWith('.md')) {
134
+ res.writeHead(403); res.end('Forbidden: only .md files'); return;
135
135
  }
136
- // Dereference symlinks so a symlink outside projectRoot cannot bypass the guard
137
- let realResolved;
138
- try { realResolved = fs.realpathSync(resolved); }
139
- catch { res.writeHead(404); res.end('File not found'); return; }
140
- if (!realResolved.startsWith(projectRoot + path.sep) && realResolved !== projectRoot) {
141
- res.writeHead(403); res.end('Forbidden'); return;
136
+
137
+ // Helper: try to read `decoded` relative to `root`; returns content string or null.
138
+ function tryRead(root) {
139
+ const resolved = path.resolve(root, decoded);
140
+ if (!resolved.startsWith(root + path.sep) && resolved !== root) return null;
141
+ let real;
142
+ try { real = fs.realpathSync(resolved); } catch { return null; }
143
+ if (!real.startsWith(root + path.sep) && real !== root) return null;
144
+ try { return fs.readFileSync(resolved, 'utf8'); } catch { return null; }
142
145
  }
143
- if (!resolved.endsWith('.md')) {
144
- res.writeHead(403); res.end('Forbidden: only .md files'); return;
146
+
147
+ // For rcode/agents/ paths fall back to the package install dir when the file
148
+ // doesn't exist under the project root (rcode installed as a dependency).
149
+ const isAgentPath = decoded.startsWith('rcode/agents/');
150
+ let content = tryRead(projectRoot);
151
+ if (content === null && isAgentPath && packageRoot && packageRoot !== projectRoot) {
152
+ content = tryRead(packageRoot);
153
+ }
154
+
155
+ if (content === null) {
156
+ res.writeHead(404); res.end('File not found'); return;
145
157
  }
146
- let content;
147
- try { content = fs.readFileSync(resolved, 'utf8'); }
148
- catch { res.writeHead(404); res.end('File not found'); return; }
149
158
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
150
159
  res.end(content);
151
160
  }
@@ -242,11 +251,17 @@ function parseAgentFrontmatter(raw) {
242
251
  // returns one small frontmatter summary per agent .md file. Full prompt
243
252
  // bodies are NOT included; the client fetches those lazily per agent via the
244
253
  // existing /api/file handler when a card is opened.
245
- function handleApiAgents(req, res, projectRoot) {
246
- const agentsDir = path.join(projectRoot, 'rcode', 'agents');
254
+ function handleApiAgents(req, res, projectRoot, packageRoot) {
255
+ // Prefer the project's own rcode/agents/ dir; fall back to the package install
256
+ // dir so the drawer works in any project where rcode is an npm dependency.
257
+ let agentsDir = path.join(projectRoot, 'rcode', 'agents');
247
258
  let entries = [];
248
- try { entries = fs.readdirSync(agentsDir, { withFileTypes: true }); }
249
- catch { /* no agents dir return an empty roster */ }
259
+ try { entries = fs.readdirSync(agentsDir, { withFileTypes: true }); } catch { /* ignore */ }
260
+ const mdEntries = entries.filter(e => e.isFile() && e.name.endsWith('.md'));
261
+ if (mdEntries.length === 0 && packageRoot && packageRoot !== projectRoot) {
262
+ agentsDir = path.join(packageRoot, 'rcode', 'agents');
263
+ try { entries = fs.readdirSync(agentsDir, { withFileTypes: true }); } catch { entries = []; }
264
+ }
250
265
  const agents = [];
251
266
  for (const e of entries) {
252
267
  if (!e.isFile() || !e.name.endsWith('.md')) continue;
@@ -50,6 +50,9 @@ function closeStream(storyId) {
50
50
 
51
51
  // ── Component ─────────────────────────────────────────────────────────────────
52
52
 
53
+ const STORAGE_KEY = 'rcode-orch-panel-w';
54
+ const MIN_W = 360;
55
+
53
56
  export function OrchPanel() {
54
57
  const { orchPanel, activeSessions } = useStore();
55
58
  const open = !!(orchPanel && orchPanel.open);
@@ -60,7 +63,15 @@ export function OrchPanel() {
60
63
  // sessionsMap: { [storyId]: { title, lines, fileOps, status } }
61
64
  const [sessionsMap, setSessionsMap] = useState({});
62
65
  const [activeTab, setActiveTab ] = useState(null);
63
- const bodyRef = useRef(null);
66
+ const bodyRef = useRef(null);
67
+ const panelRef = useRef(null);
68
+
69
+ // Restore saved width on open
70
+ useEffect(() => {
71
+ if (!open || !panelRef.current) return;
72
+ const saved = localStorage.getItem(STORAGE_KEY);
73
+ if (saved) panelRef.current.style.setProperty('--orch-w', saved + 'px');
74
+ }, [open]);
64
75
 
65
76
  // Scroll to bottom whenever lines change for the active tab
66
77
  useEffect(() => {
@@ -167,6 +178,35 @@ export function OrchPanel() {
167
178
  setState({ orchPanel: null });
168
179
  }, []);
169
180
 
181
+ // Horizontal resize via the left-edge drag handle
182
+ function handleResizeDown(e) {
183
+ e.preventDefault();
184
+ const startX = e.clientX;
185
+ const el = panelRef.current;
186
+ if (!el) return;
187
+ const startW = el.getBoundingClientRect().width;
188
+ const handle = e.currentTarget;
189
+ handle.classList.add('dragging');
190
+
191
+ function onMove(ev) {
192
+ const delta = startX - ev.clientX;
193
+ const maxW = Math.floor(window.innerWidth * 0.7);
194
+ const w = Math.min(maxW, Math.max(MIN_W, startW + delta));
195
+ el.style.setProperty('--orch-w', w + 'px');
196
+ }
197
+
198
+ function onUp(ev) {
199
+ document.removeEventListener('mousemove', onMove);
200
+ document.removeEventListener('mouseup', onUp);
201
+ handle.classList.remove('dragging');
202
+ const w = Math.round(el.getBoundingClientRect().width);
203
+ localStorage.setItem(STORAGE_KEY, w);
204
+ }
205
+
206
+ document.addEventListener('mousemove', onMove);
207
+ document.addEventListener('mouseup', onUp);
208
+ }
209
+
170
210
  // Open (or focus) a session tab and attach its live stream. Used both for
171
211
  // locally-opened tabs and for sessions discovered via the orchestrator API.
172
212
  function handleTabClick(storyId) {
@@ -221,7 +261,8 @@ export function OrchPanel() {
221
261
  const panelCls = 'orch-panel' + (open ? ' open' : '');
222
262
 
223
263
  return html`
224
- <div class=${panelCls}>
264
+ <div class=${panelCls} ref=${panelRef}>
265
+ <div class="orch-panel-resize" onMouseDown=${handleResizeDown}></div>
225
266
  <div class="orch-panel-header">
226
267
  <div class="orch-panel-title">
227
268
  <span class=${'orch-status-dot' + (runningCount > 0 ? ' up' : '')}></span>
@@ -7,7 +7,8 @@
7
7
  * 1. Live orchestrator sessions (store.activeSessions, status==='running') —
8
8
  * pulsing dot, title from storyId (or the command for cmd-* runs),
9
9
  * elapsed time since startTime; clicking opens the session's orchestrator
10
- * panel (existing openOrchPanel mechanism).
10
+ * panel opens the interactive xterm terminal (openTermPanel) since
11
+ * these rows are always live sessions you may want to talk to.
11
12
  * 2. Scanned tasks from `tasks.inProgress[{ title, pct }]` (pct null → the
12
13
  * percent pill is omitted).
13
14
  *
@@ -19,7 +20,7 @@
19
20
  import { html } from '../../preact.js';
20
21
  import { useStore } from '../../store.js';
21
22
  import { orchElapsed, rowLink } from '../../util.js';
22
- import { openOrchPanel } from '../../orchestrator.js';
23
+ import { openTermPanel } from '../../orchestrator.js';
23
24
  import { pressable } from '../shared.js';
24
25
  import { TaskPipeline } from '../TaskPipeline.js';
25
26
 
@@ -31,8 +32,8 @@ function sessionTitle(s) {
31
32
 
32
33
  function LiveRow({ session: s }) {
33
34
  return html`
34
- <li class="ip-row ip-live-row" title=${'Open session ' + s.storyId}
35
- ...${pressable(() => openOrchPanel(s.storyId))}>
35
+ <li class="ip-row ip-live-row" title=${'Open terminal for ' + s.storyId}
36
+ ...${pressable(() => openTermPanel(s.storyId, sessionTitle(s)))}>
36
37
  <span class="live-dot"></span>
37
38
  <span class="ip-title ip-live-title">${sessionTitle(s)}</span>
38
39
  <span class="ip-live-elapsed">${orchElapsed(s.startTime)}</span>
@@ -354,6 +354,20 @@ export function TaskCard({ task: t }) {
354
354
  ` : null}
355
355
  ${t.acceptance ? html`<div class="task-detail-row"><strong>Acceptance:</strong> ${t.acceptance}</div>` : null}
356
356
  ${t.assignee ? html`<div class="task-detail-row"><strong>Assignee:</strong> ${t.assignee}</div>` : null}
357
+ ${(t.actions && t.actions.length) ? html`
358
+ <div class="task-actions">
359
+ <div class="task-actions-title">Actions performed</div>
360
+ <ol class="task-actions-list">
361
+ ${t.actions.map((step, i) => html`<li key=${i} class="task-action-step">${step}</li>`)}
362
+ </ol>
363
+ </div>
364
+ ` : null}
365
+ ${t.outcome ? html`
366
+ <div class="task-outcome">
367
+ <span class="task-outcome-label">Result</span>
368
+ <span class="task-outcome-text">${t.outcome}</span>
369
+ </div>
370
+ ` : null}
357
371
  ${taskCmds.length ? html`
358
372
  <div class="task-detail-cmds">
359
373
  ${taskCmds.map(([cmd, desc]) => html`<${CmdHint} key=${cmd} cmd=${cmd} desc=${desc}/>`)}
@@ -178,6 +178,19 @@ export function fetchRejections() {
178
178
  .catch(() => []);
179
179
  }
180
180
 
181
+ /**
182
+ * POST /api/task-status — persist a kanban column move for storyId.
183
+ * status is the target column id (todo | in_progress | blocked | done).
184
+ */
185
+ export function setTaskStatus(storyId, status) {
186
+ const tok = orchToken();
187
+ return fetch(ORCH_HTTP + '/api/task-status', {
188
+ method: 'POST',
189
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
190
+ body: JSON.stringify({ storyId, status }),
191
+ }).then(r => r.json()).catch(() => ({}));
192
+ }
193
+
181
194
  /**
182
195
  * POST /api/clean-sessions — remove ended sessions.
183
196
  * olderThanDays = 0 removes all ended sessions; > 0 keeps recent ones.
@@ -10,7 +10,7 @@
10
10
  import { html, useState, useCallback } from '../preact.js';
11
11
  import { useStore, refresh } from '../store.js';
12
12
  import { allTasks, currentPhaseName } from '../util.js';
13
- import { stopStory, openOrchPanel } from '../orchestrator.js';
13
+ import { stopStory, openOrchPanel, openTermPanel, setTaskStatus } from '../orchestrator.js';
14
14
  import { openRunnerPicker } from '../components/RunnerPicker.js';
15
15
  import { showToast } from '../components/shared.js';
16
16
 
@@ -56,6 +56,12 @@ function KanbanCard({ task, col, live, orchDown, onDragStart, onDragEnd }) {
56
56
  e.stopPropagation();
57
57
  stopStory(sid);
58
58
  }
59
+ // Live session → full interactive xterm terminal (you can type to the agent).
60
+ function handleTerm(e) {
61
+ e.stopPropagation();
62
+ openTermPanel(sid, sid);
63
+ }
64
+ // Ended session → lightweight read-only log view (no live TUI to mangle).
59
65
  function handleView(e) {
60
66
  e.stopPropagation();
61
67
  openOrchPanel(sid);
@@ -92,7 +98,7 @@ function KanbanCard({ task, col, live, orchDown, onDragStart, onDragEnd }) {
92
98
  onClick=${handleRun}>▶ Run</button>
93
99
  ` : isRunning ? html`
94
100
  <button class="kanban-stop-btn" onClick=${handleStop}>■ Stop</button>
95
- <button class="kanban-view-btn" onClick=${handleView}>↗ View</button>
101
+ <button class="kanban-view-btn" onClick=${handleTerm}>↗ Terminal</button>
96
102
  ` : html`
97
103
  <button class="kanban-view-btn" onClick=${handleView}>↗ Logs</button>
98
104
  `}
@@ -188,9 +194,32 @@ export function KanbanView() {
188
194
 
189
195
  function handleDrop(e, colId) {
190
196
  if (!dragging || !dragging.id) return;
191
- setVisualMoves(prev => ({ ...prev, [dragging.id]: colId }));
197
+ const taskId = dragging.id;
198
+ const prevCol = effCol(dragging, runningByStory);
199
+ const isRunning = !!(runningByStory && runningByStory[taskId]);
200
+
201
+ // Optimistic visual move
202
+ setVisualMoves(prev => ({ ...prev, [taskId]: colId }));
203
+
204
+ const anchor = document.querySelector('[data-story-id="' + taskId + '"]') || e.currentTarget;
205
+
206
+ if (colId === 'in_progress' && (prevCol === 'todo' || prevCol === 'blocked') && !isRunning) {
207
+ openRunnerPicker(anchor, {
208
+ kind: 'session', storyId: taskId, cmd: '/rcode-dev-story ' + taskId, title: taskId,
209
+ });
210
+ setTaskStatus(taskId, 'in_progress').then(() => {
211
+ refresh();
212
+ setVisualMoves(prev => { const n = { ...prev }; delete n[taskId]; return n; });
213
+ });
214
+ } else {
215
+ setTaskStatus(taskId, colId).then(() => {
216
+ refresh();
217
+ setVisualMoves(prev => { const n = { ...prev }; delete n[taskId]; return n; });
218
+ });
219
+ showToast('Moved to ' + (COLS.find(c => c.id === colId)?.label || colId));
220
+ }
221
+
192
222
  setDragging(null);
193
- showToast('Moved (visual only — not persisted)'); // visual only — not persisted
194
223
  }
195
224
 
196
225
  // ---- Manual refresh ----