@ai-dev-methodologies/rlp-desk 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { execFile } from 'node:child_process';
4
5
  import { promisify } from 'node:util';
@@ -13,6 +14,7 @@ import {
13
14
  import {
14
15
  appendCampaignAnalytics,
15
16
  generateCampaignReport,
17
+ generateSVReport,
16
18
  prepareCampaignAnalytics,
17
19
  } from '../reporting/campaign-reporting.mjs';
18
20
  import {
@@ -56,9 +58,12 @@ function buildPaths(rootDir, slug) {
56
58
  prdFile: path.join(deskRoot, 'plans', `prd-${slug}.md`),
57
59
  testSpecFile: path.join(deskRoot, 'plans', `test-spec-${slug}.md`),
58
60
  analyticsFile: path.join(campaignLogDir, 'campaign.jsonl'),
61
+ analyticsDir: path.join(os.homedir(), '.claude', 'ralph-desk', 'analytics', slug),
59
62
  reportFile: path.join(campaignLogDir, 'campaign-report.md'),
60
63
  statusFile: path.join(campaignLogDir, 'runtime', 'status.json'),
61
- };
64
+ flywheelPromptFile: path.join(deskRoot, 'prompts', `${slug}.flywheel.prompt.md`),
65
+ flywheelSignalFile: path.join(deskRoot, 'memos', `${slug}-flywheel-signal.json`),
66
+ };
62
67
  }
63
68
 
64
69
  async function exists(targetPath) {
@@ -394,6 +399,25 @@ async function runFinalSequentialVerify({
394
399
  };
395
400
  }
396
401
 
402
+ function buildFlywheelTriggerCmd({ flywheelPromptFile, flywheelModel, rootDir }) {
403
+ return `cd ${JSON.stringify(rootDir)} && DISABLE_OMC=1 claude --model ${flywheelModel} --no-mcp -p "$(cat ${JSON.stringify(flywheelPromptFile)})"`;
404
+ }
405
+
406
+ async function dispatchFlywheel({ paths, sendKeys, flywheelPaneId, flywheelModel, rootDir }) {
407
+ const triggerCmd = buildFlywheelTriggerCmd({
408
+ flywheelPromptFile: paths.flywheelPromptFile,
409
+ flywheelModel,
410
+ rootDir,
411
+ });
412
+ await sendKeys(flywheelPaneId, triggerCmd);
413
+ }
414
+
415
+ export function shouldRunFlywheel(flywheelMode, state) {
416
+ if (flywheelMode === 'off') return false;
417
+ if (flywheelMode === 'on-fail' && (state.consecutive_failures ?? 0) > 0) return true;
418
+ return false;
419
+ }
420
+
397
421
  export async function run(slug, options = {}) {
398
422
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
399
423
  const paths = buildPaths(rootDir, slug);
@@ -433,6 +457,10 @@ export async function run(slug, options = {}) {
433
457
  });
434
458
  state.session_name = session.sessionName;
435
459
  state.leader_pane_id = session.leaderPaneId;
460
+ state.flywheel_pane_id = await createPane({
461
+ targetPaneId: session.leaderPaneId,
462
+ layout: 'horizontal',
463
+ });
436
464
  state.worker_pane_id = await createPane({
437
465
  targetPaneId: session.leaderPaneId,
438
466
  layout: 'horizontal',
@@ -462,6 +490,22 @@ export async function run(slug, options = {}) {
462
490
  state.phase = 'complete';
463
491
  await writeSentinel(paths.completeSentinel, 'complete', 'ALL');
464
492
  await writeStatus(paths, state, options.onStatusChange, options.now);
493
+ let svSummary;
494
+ if (options.withSelfVerification) {
495
+ try {
496
+ const sv = await generateSVReport({
497
+ slug,
498
+ logsDir: path.dirname(paths.reportFile),
499
+ prdFile: paths.prdFile,
500
+ testSpecFile: paths.testSpecFile,
501
+ analyticsFile: paths.analyticsFile,
502
+ outputDir: paths.analyticsDir,
503
+ });
504
+ svSummary = sv.summary;
505
+ } catch (err) {
506
+ svSummary = `SV report generation failed: ${err.message}`;
507
+ }
508
+ }
465
509
  await generateCampaignReport({
466
510
  slug,
467
511
  reportFile: paths.reportFile,
@@ -469,6 +513,7 @@ export async function run(slug, options = {}) {
469
513
  statusFile: paths.statusFile,
470
514
  analyticsFile: paths.analyticsFile,
471
515
  now: resolveNow(options.now),
516
+ svSummary,
472
517
  });
473
518
  return {
474
519
  status: 'complete',
@@ -489,6 +534,30 @@ export async function run(slug, options = {}) {
489
534
  };
490
535
  }
491
536
 
537
+ // Flywheel direction review (runs BEFORE Worker)
538
+ if (shouldRunFlywheel(options.flywheel ?? 'off', state)) {
539
+ state.phase = 'flywheel';
540
+ await writeStatus(paths, state, options.onStatusChange, options.now);
541
+
542
+ await dispatchFlywheel({
543
+ paths,
544
+ sendKeys,
545
+ flywheelPaneId: state.flywheel_pane_id ?? state.verifier_pane_id,
546
+ flywheelModel: options.flywheelModel ?? 'opus',
547
+ rootDir,
548
+ });
549
+
550
+ const flywheelSignal = await pollForSignal(paths.flywheelSignalFile, {
551
+ mode: 'claude',
552
+ paneId: state.flywheel_pane_id ?? state.verifier_pane_id,
553
+ });
554
+
555
+ state.last_flywheel_decision = flywheelSignal.decision;
556
+ // Campaign memory already updated by flywheel agent
557
+ // Clean signal file for next iteration
558
+ await fs.unlink(paths.flywheelSignalFile).catch(() => {});
559
+ }
560
+
492
561
  state.phase = 'worker';
493
562
  await writeStatus(paths, state, options.onStatusChange, options.now);
494
563
  await dispatchWorker({
@@ -565,6 +634,22 @@ export async function run(slug, options = {}) {
565
634
  await writeSentinel(paths.blockedSentinel, 'blocked', usId);
566
635
  await appendIterationAnalytics(paths, state, usId, 'blocked', options);
567
636
  await writeStatus(paths, state, options.onStatusChange, options.now);
637
+ let svSummary;
638
+ if (options.withSelfVerification) {
639
+ try {
640
+ const sv = await generateSVReport({
641
+ slug,
642
+ logsDir: path.dirname(paths.reportFile),
643
+ prdFile: paths.prdFile,
644
+ testSpecFile: paths.testSpecFile,
645
+ analyticsFile: paths.analyticsFile,
646
+ outputDir: paths.analyticsDir,
647
+ });
648
+ svSummary = sv.summary;
649
+ } catch (err) {
650
+ svSummary = `SV report generation failed: ${err.message}`;
651
+ }
652
+ }
568
653
  await generateCampaignReport({
569
654
  slug,
570
655
  reportFile: paths.reportFile,
@@ -572,6 +657,7 @@ export async function run(slug, options = {}) {
572
657
  statusFile: paths.statusFile,
573
658
  analyticsFile: paths.analyticsFile,
574
659
  now: resolveNow(options.now),
660
+ svSummary,
575
661
  });
576
662
  return {
577
663
  status: 'blocked',
@@ -587,6 +673,22 @@ export async function run(slug, options = {}) {
587
673
  state.phase = 'blocked';
588
674
  await writeSentinel(paths.blockedSentinel, 'blocked', usId);
589
675
  await writeStatus(paths, state, options.onStatusChange, options.now);
676
+ let svSummary;
677
+ if (options.withSelfVerification) {
678
+ try {
679
+ const sv = await generateSVReport({
680
+ slug,
681
+ logsDir: path.dirname(paths.reportFile),
682
+ prdFile: paths.prdFile,
683
+ testSpecFile: paths.testSpecFile,
684
+ analyticsFile: paths.analyticsFile,
685
+ outputDir: paths.analyticsDir,
686
+ });
687
+ svSummary = sv.summary;
688
+ } catch (err) {
689
+ svSummary = `SV report generation failed: ${err.message}`;
690
+ }
691
+ }
590
692
  await generateCampaignReport({
591
693
  slug,
592
694
  reportFile: paths.reportFile,
@@ -594,6 +696,7 @@ export async function run(slug, options = {}) {
594
696
  statusFile: paths.statusFile,
595
697
  analyticsFile: paths.analyticsFile,
596
698
  now: resolveNow(options.now),
699
+ svSummary,
597
700
  });
598
701
  return {
599
702
  status: 'blocked',