@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
@@ -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: 440px;
1455
- max-width: 42vw;
1456
- min-width: 320px;
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
  }
@@ -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: hm[1].trim(),
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
- state.phaseTree = buildPhaseTree(projectDir, state.raw && state.raw.phases, listCached);
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)) {
@@ -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
  });