@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
package/server/lib/html/css.js
CHANGED
|
@@ -770,6 +770,46 @@ section .body {
|
|
|
770
770
|
}
|
|
771
771
|
.task-detail-row strong { color: var(--text-muted); font-weight: 500; min-width: 64px; flex-shrink: 0; }
|
|
772
772
|
.task-detail-cmds { margin-top: var(--space-3); }
|
|
773
|
+
/* ── Per-task actions + result (#905) ───────────────────────────── */
|
|
774
|
+
.task-actions {
|
|
775
|
+
margin-top: var(--space-3);
|
|
776
|
+
padding-top: var(--space-3);
|
|
777
|
+
border-top: 1px solid var(--border-subtle);
|
|
778
|
+
}
|
|
779
|
+
.task-actions-title {
|
|
780
|
+
color: var(--text-muted);
|
|
781
|
+
font-weight: 500;
|
|
782
|
+
margin-bottom: var(--space-2);
|
|
783
|
+
}
|
|
784
|
+
.task-actions-list {
|
|
785
|
+
margin: 0;
|
|
786
|
+
padding-left: var(--space-5);
|
|
787
|
+
display: flex;
|
|
788
|
+
flex-direction: column;
|
|
789
|
+
gap: var(--space-1);
|
|
790
|
+
}
|
|
791
|
+
.task-action-step {
|
|
792
|
+
color: var(--text-secondary);
|
|
793
|
+
line-height: 1.45;
|
|
794
|
+
}
|
|
795
|
+
.task-outcome {
|
|
796
|
+
display: flex;
|
|
797
|
+
align-items: baseline;
|
|
798
|
+
gap: var(--space-2);
|
|
799
|
+
margin-top: var(--space-3);
|
|
800
|
+
padding: var(--space-2) var(--space-3);
|
|
801
|
+
background: var(--bg-elev-2);
|
|
802
|
+
border-left: 2px solid var(--accent-green);
|
|
803
|
+
border-radius: var(--radius-4);
|
|
804
|
+
}
|
|
805
|
+
.task-outcome-label {
|
|
806
|
+
color: var(--accent-green);
|
|
807
|
+
font-weight: 600;
|
|
808
|
+
text-transform: uppercase;
|
|
809
|
+
letter-spacing: 0.04em;
|
|
810
|
+
flex-shrink: 0;
|
|
811
|
+
}
|
|
812
|
+
.task-outcome-text { color: var(--text-secondary); line-height: 1.45; }
|
|
773
813
|
.task-expand-icon {
|
|
774
814
|
font-size: 8px;
|
|
775
815
|
color: var(--text-muted);
|
|
@@ -1451,9 +1491,9 @@ footer {
|
|
|
1451
1491
|
top: 0;
|
|
1452
1492
|
right: 0;
|
|
1453
1493
|
bottom: 0;
|
|
1454
|
-
width:
|
|
1455
|
-
max-width:
|
|
1456
|
-
min-width:
|
|
1494
|
+
width: var(--orch-w, 720px);
|
|
1495
|
+
max-width: 70vw;
|
|
1496
|
+
min-width: 360px;
|
|
1457
1497
|
background: var(--bg-elev-1);
|
|
1458
1498
|
border-left: 1px solid var(--border-subtle);
|
|
1459
1499
|
display: flex;
|
|
@@ -1463,6 +1503,20 @@ footer {
|
|
|
1463
1503
|
transition: transform var(--t-menu) var(--ease);
|
|
1464
1504
|
box-shadow: -8px 0 32px rgba(0,0,0,0.4);
|
|
1465
1505
|
}
|
|
1506
|
+
.orch-panel-resize {
|
|
1507
|
+
position: absolute;
|
|
1508
|
+
left: 0;
|
|
1509
|
+
top: 0;
|
|
1510
|
+
bottom: 0;
|
|
1511
|
+
width: 6px;
|
|
1512
|
+
cursor: ew-resize;
|
|
1513
|
+
z-index: 10;
|
|
1514
|
+
}
|
|
1515
|
+
.orch-panel-resize:hover,
|
|
1516
|
+
.orch-panel-resize.dragging {
|
|
1517
|
+
background: var(--accent, #5865f2);
|
|
1518
|
+
opacity: 0.35;
|
|
1519
|
+
}
|
|
1466
1520
|
.orch-panel.open {
|
|
1467
1521
|
transform: translateX(0);
|
|
1468
1522
|
}
|
package/server/lib/scanner.js
CHANGED
|
@@ -80,6 +80,23 @@ function parseYamlList(text, key) {
|
|
|
80
80
|
return [];
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Parse a SPRINT.md <action> block into an ordered list of action steps.
|
|
85
|
+
* Action blocks are numbered ("1. …\n2. …"); each step may span several lines.
|
|
86
|
+
* Whitespace within a step is collapsed so each renders as one readable line.
|
|
87
|
+
* Falls back to non-empty lines when the block is not numbered. Returns [].
|
|
88
|
+
*/
|
|
89
|
+
function parseActionSteps(raw) {
|
|
90
|
+
if (!raw) return [];
|
|
91
|
+
const t = raw.trim();
|
|
92
|
+
if (!t) return [];
|
|
93
|
+
const matches = [...t.matchAll(/(?:^|\n)\s*\d+\.\s+([\s\S]*?)(?=\n\s*\d+\.\s|$)/g)];
|
|
94
|
+
if (matches.length) {
|
|
95
|
+
return matches.map(m => m[1].replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
return t.split('\n').map(l => l.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
/**
|
|
84
101
|
* Derive the phase → sprint → story tree from the .planning/phases/ filesystem,
|
|
85
102
|
* which is the committed source of truth. state.json sprint/story records are
|
|
@@ -93,9 +110,10 @@ function parseYamlList(text, key) {
|
|
|
93
110
|
* @param {function} [listCached] per-scan dir lister from makeDirLister()
|
|
94
111
|
* @returns {Array|null} phases with a populated `sprints` array each
|
|
95
112
|
*/
|
|
96
|
-
function buildPhaseTree(projectDir, rawPhases, listCached) {
|
|
113
|
+
function buildPhaseTree(projectDir, rawPhases, listCached, overrides) {
|
|
97
114
|
if (!Array.isArray(rawPhases)) return null;
|
|
98
115
|
const list = listCached || makeDirLister();
|
|
116
|
+
const ov = (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) ? overrides : {};
|
|
99
117
|
const phasesDir = path.join(projectDir, '.planning', 'phases');
|
|
100
118
|
const allEntries = list(phasesDir);
|
|
101
119
|
if (allEntries === null) return rawPhases;
|
|
@@ -144,7 +162,16 @@ function buildPhaseTree(projectDir, rawPhases, listCached) {
|
|
|
144
162
|
title: titleM ? titleM[1].trim() : `Task ${stories.length + 1}`,
|
|
145
163
|
status: phaseComplete ? 'done' : 'todo',
|
|
146
164
|
};
|
|
165
|
+
if (ov[story.id]) story.status = ov[story.id].status;
|
|
147
166
|
if (acM && acM[1].trim()) story.acceptance = acM[1].trim();
|
|
167
|
+
// Per-task execution transparency (#905): the ordered <action> steps
|
|
168
|
+
// (what the task does) + the <done> outcome (the result). Read-only —
|
|
169
|
+
// parsed from the same SPRINT.md text already loaded above.
|
|
170
|
+
const actionM = tm[2].match(/<action>\s*([\s\S]*?)\s*<\/action>/);
|
|
171
|
+
const doneM = tm[2].match(/<done>\s*([\s\S]*?)\s*<\/done>/);
|
|
172
|
+
const steps = actionM ? parseActionSteps(actionM[1]) : [];
|
|
173
|
+
if (steps.length) story.actions = steps;
|
|
174
|
+
if (doneM && doneM[1].trim()) story.outcome = doneM[1].replace(/\s+/g, ' ').trim();
|
|
148
175
|
stories.push(story);
|
|
149
176
|
}
|
|
150
177
|
// Fallback for pre-<task> SPRINT.md format (phases 20-30 era):
|
|
@@ -153,10 +180,11 @@ function buildPhaseTree(projectDir, rawPhases, listCached) {
|
|
|
153
180
|
const headRe = /^#{2,4}\s+(?:Story|Task)\s+([^\s—–-]+)\s*[—–-]\s*(.+?)\s*$/gm;
|
|
154
181
|
let hm;
|
|
155
182
|
while ((hm = headRe.exec(text))) {
|
|
183
|
+
const hId = hm[1].trim();
|
|
156
184
|
stories.push({
|
|
157
|
-
id:
|
|
185
|
+
id: hId,
|
|
158
186
|
title: hm[2].trim(),
|
|
159
|
-
status: phaseComplete ? 'done' : 'todo',
|
|
187
|
+
status: ov[hId] ? ov[hId].status : (phaseComplete ? 'done' : 'todo'),
|
|
160
188
|
});
|
|
161
189
|
}
|
|
162
190
|
}
|
|
@@ -566,7 +594,14 @@ function scanStateUncached(rcodeDir) {
|
|
|
566
594
|
} catch { /* ignore */ }
|
|
567
595
|
}
|
|
568
596
|
|
|
569
|
-
|
|
597
|
+
let boardOverrides = {};
|
|
598
|
+
try {
|
|
599
|
+
const ovRaw = fs.readFileSync(path.join(rcodeDir, 'board-overrides.json'), 'utf8');
|
|
600
|
+
const ovParsed = JSON.parse(ovRaw);
|
|
601
|
+
if (ovParsed && typeof ovParsed === 'object' && !Array.isArray(ovParsed)) boardOverrides = ovParsed;
|
|
602
|
+
} catch { boardOverrides = {}; }
|
|
603
|
+
|
|
604
|
+
state.phaseTree = buildPhaseTree(projectDir, state.raw && state.raw.phases, listCached, boardOverrides);
|
|
570
605
|
|
|
571
606
|
// Derive the redesign dashboard contract (DATA-CONTRACT.md). Attached to the
|
|
572
607
|
// scan so GET /api/state returns the exact shape and client.js seeds it into
|
|
@@ -607,6 +642,7 @@ function scanSignature(rcodeDir, projectDir) {
|
|
|
607
642
|
statOne(path.join(rcodeDir, 'config.yaml'));
|
|
608
643
|
statOne(path.join(rcodeDir, 'HANDOFF.json'));
|
|
609
644
|
statOne(path.join(rcodeDir, 'context', 'active.md'));
|
|
645
|
+
statOne(path.join(rcodeDir, 'board-overrides.json'));
|
|
610
646
|
(function walk(dir, depth) {
|
|
611
647
|
if (depth > SIG_WALK_MAX_DEPTH) return;
|
|
612
648
|
for (const e of listDir(dir)) {
|
package/server/orchestrator.js
CHANGED
|
@@ -280,6 +280,22 @@ function appendRejection(entry) {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
// ── Board overlay ─────────────────────────────────────────────────────────────
|
|
284
|
+
// Writes a single entry to .rcode/board-overrides.json. The scanner reads this
|
|
285
|
+
// file on every scan and applies it on top of derived story statuses.
|
|
286
|
+
function setTaskOverride(storyId, status, runner) {
|
|
287
|
+
const overridesPath = path.join(PROJECT_ROOT, '.rcode', 'board-overrides.json');
|
|
288
|
+
let overrides = {};
|
|
289
|
+
try {
|
|
290
|
+
const raw = fs.readFileSync(overridesPath, 'utf8');
|
|
291
|
+
const parsed = JSON.parse(raw);
|
|
292
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) overrides = parsed;
|
|
293
|
+
} catch { overrides = {}; }
|
|
294
|
+
overrides[storyId] = { status, runner: runner || null, updatedAt: new Date().toISOString() };
|
|
295
|
+
fs.mkdirSync(path.dirname(overridesPath), { recursive: true });
|
|
296
|
+
fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2));
|
|
297
|
+
}
|
|
298
|
+
|
|
283
299
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
284
300
|
|
|
285
301
|
function json(res, code, body) {
|
|
@@ -557,6 +573,14 @@ async function handleRun(req, res) {
|
|
|
557
573
|
const status = signal ? 'stopped' : (exitCode === 0 ? 'done' : 'exited');
|
|
558
574
|
setStatus(s, status);
|
|
559
575
|
persistRun(storyId, s, status);
|
|
576
|
+
if (status === 'done'
|
|
577
|
+
&& !storyId.startsWith('cmd-')
|
|
578
|
+
&& !storyId.startsWith('sprint-')
|
|
579
|
+
&& !storyId.startsWith('phase-')) {
|
|
580
|
+
try { setTaskOverride(storyId, 'done', null); } catch (err) {
|
|
581
|
+
console.error('[orchestrator] board-overrides write failed:', err.message);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
560
584
|
});
|
|
561
585
|
|
|
562
586
|
// CLIs with no interactive initial-prompt flag (see registry) get the
|
|
@@ -622,6 +646,23 @@ function handleRejections(res) {
|
|
|
622
646
|
json(res, 200, { rejections: readRejections() });
|
|
623
647
|
}
|
|
624
648
|
|
|
649
|
+
const TASK_STATUS_ENUM = new Set(['todo', 'in_progress', 'blocked', 'done']);
|
|
650
|
+
|
|
651
|
+
async function handleTaskStatus(req, res) {
|
|
652
|
+
const body = await parseBody(req);
|
|
653
|
+
const storyId = String(body.storyId || '').trim();
|
|
654
|
+
if (!validStoryId(storyId)) { json(res, 400, { error: 'invalid storyId' }); return; }
|
|
655
|
+
const status = String(body.status || '').trim();
|
|
656
|
+
if (!TASK_STATUS_ENUM.has(status)) { json(res, 400, { error: 'invalid status — must be one of todo,in_progress,blocked,done' }); return; }
|
|
657
|
+
try {
|
|
658
|
+
setTaskOverride(storyId, status, null);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error('[orchestrator] handleTaskStatus write failed:', err.message);
|
|
661
|
+
json(res, 500, { error: 'could not write board-overrides' }); return;
|
|
662
|
+
}
|
|
663
|
+
json(res, 200, { ok: true });
|
|
664
|
+
}
|
|
665
|
+
|
|
625
666
|
// ── WebSocket data plane ───────────────────────────────────────────────────────
|
|
626
667
|
|
|
627
668
|
function attachWebSocket(ws, storyId) {
|
|
@@ -704,6 +745,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
704
745
|
if (method === 'POST' && pathOnly === '/api/clean-sessions') { await handleCleanSessions(req, res); return; }
|
|
705
746
|
if (method === 'POST' && pathOnly === '/api/reject') { await handleReject(req, res); return; }
|
|
706
747
|
if (method === 'GET' && pathOnly === '/api/rejections') { handleRejections(res); return; }
|
|
748
|
+
if (method === 'POST' && pathOnly === '/api/task-status') { await handleTaskStatus(req, res); return; }
|
|
707
749
|
|
|
708
750
|
res.writeHead(404); res.end('Not found');
|
|
709
751
|
});
|
|
Binary file
|