@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.
- package/AGENTS.md +1 -1
- package/CONTRIBUTING.md +1 -0
- package/README.md +38 -9
- package/cli/install.js +14 -0
- package/dist/rcode.js +48 -48
- package/package.json +1 -1
- package/rcode/bin/rcode-hooks.cjs +347 -9
- package/rcode/commands/lens-audit.md +8 -6
- package/rcode/skills/SKILLS_INDEX.md +3 -2
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/SKILL.md +12 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/backlog-building.md +13 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +17 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/orchestrator-rhythm.md +9 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/wave-design.md +21 -0
- package/rcode/skills/core/rcode-lazy/SKILL.md +149 -0
- package/rcode/templates/settings-hooks.json +12 -1
- package/rcode/workflows/do.md +1 -0
- package/rcode/workflows/enable-hooks.md +3 -2
- package/rcode/workflows/lens-audit.md +49 -8
- package/server/dashboard.js +6 -2
- package/server/lib/api.js +35 -20
- package/server/lib/html/client/components/OrchPanel.js +43 -2
- package/server/lib/html/client/components/dashboard/InProgress.js +5 -4
- package/server/lib/html/client/components/shared.js +14 -0
- package/server/lib/html/client/orchestrator.js +13 -0
- package/server/lib/html/client/views/KanbanView.js +33 -4
- package/server/lib/html/css.js +57 -3
- package/server/lib/scanner.js +40 -4
- package/server/orchestrator.js +42 -0
- package/rcode/skills/core/rcode-init/scripts/__pycache__/rcode_init.cpython-38.pyc +0 -0
- 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
|
}
|
package/rcode/workflows/do.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
37
|
-
/rcode-lens-audit <N> # run lens N (1-
|
|
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–
|
|
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.
|
|
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–
|
|
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.
|
package/server/dashboard.js
CHANGED
|
@@ -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
|
-
|
|
133
|
-
if (!
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
35
|
-
...${pressable(() =>
|
|
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=${
|
|
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
|
-
|
|
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 ----
|