openclaw-node-harness 2.0.3 → 2.1.0

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 (118) hide show
  1. package/README.md +646 -3
  2. package/bin/hyperagent.mjs +419 -0
  3. package/bin/mesh-agent.js +603 -81
  4. package/bin/mesh-bridge.js +340 -11
  5. package/bin/mesh-deploy-listener.js +119 -97
  6. package/bin/mesh-deploy.js +8 -0
  7. package/bin/mesh-task-daemon.js +1005 -40
  8. package/bin/mesh.js +423 -6
  9. package/config/claude-settings.json +95 -0
  10. package/config/daemon.json.template +2 -1
  11. package/config/git-hooks/pre-commit +13 -0
  12. package/config/git-hooks/pre-push +12 -0
  13. package/config/harness-rules.json +174 -0
  14. package/config/plan-templates/team-bugfix.yaml +52 -0
  15. package/config/plan-templates/team-deploy.yaml +50 -0
  16. package/config/plan-templates/team-feature.yaml +71 -0
  17. package/config/roles/qa-engineer.yaml +36 -0
  18. package/config/roles/solidity-dev.yaml +51 -0
  19. package/config/roles/tech-architect.yaml +36 -0
  20. package/config/rules/framework/solidity.md +22 -0
  21. package/config/rules/framework/typescript.md +21 -0
  22. package/config/rules/framework/unity.md +21 -0
  23. package/config/rules/universal/design-docs.md +18 -0
  24. package/config/rules/universal/git-hygiene.md +18 -0
  25. package/config/rules/universal/security.md +19 -0
  26. package/config/rules/universal/test-standards.md +19 -0
  27. package/identity/DELEGATION.md +6 -6
  28. package/install.sh +300 -8
  29. package/lib/circling-parser.js +119 -0
  30. package/lib/hyperagent-store.mjs +652 -0
  31. package/lib/kanban-io.js +59 -10
  32. package/lib/mcp-knowledge/bench.mjs +118 -0
  33. package/lib/mcp-knowledge/core.mjs +528 -0
  34. package/lib/mcp-knowledge/package.json +25 -0
  35. package/lib/mcp-knowledge/server.mjs +245 -0
  36. package/lib/mcp-knowledge/test.mjs +802 -0
  37. package/lib/memory-budget.mjs +261 -0
  38. package/lib/mesh-collab.js +354 -4
  39. package/lib/mesh-harness.js +427 -0
  40. package/lib/mesh-plans.js +13 -5
  41. package/lib/mesh-registry.js +11 -2
  42. package/lib/mesh-tasks.js +67 -0
  43. package/lib/plan-templates.js +226 -0
  44. package/lib/pre-compression-flush.mjs +320 -0
  45. package/lib/role-loader.js +292 -0
  46. package/lib/rule-loader.js +358 -0
  47. package/lib/session-store.mjs +458 -0
  48. package/lib/transcript-parser.mjs +292 -0
  49. package/mission-control/drizzle/soul_schema_update.sql +29 -0
  50. package/mission-control/drizzle.config.ts +1 -4
  51. package/mission-control/package-lock.json +1571 -83
  52. package/mission-control/package.json +6 -2
  53. package/mission-control/scripts/gen-chronology.js +3 -3
  54. package/mission-control/scripts/import-pipeline-v2.js +0 -16
  55. package/mission-control/scripts/import-pipeline.js +0 -15
  56. package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
  57. package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
  58. package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
  59. package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
  60. package/mission-control/src/app/api/cowork/events/route.ts +65 -0
  61. package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
  62. package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
  63. package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
  64. package/mission-control/src/app/api/diagnostics/route.ts +97 -0
  65. package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
  66. package/mission-control/src/app/api/mesh/events/route.ts +95 -19
  67. package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
  68. package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
  69. package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
  70. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +1 -1
  71. package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
  72. package/mission-control/src/app/api/tasks/route.ts +21 -30
  73. package/mission-control/src/app/cowork/page.tsx +261 -0
  74. package/mission-control/src/app/diagnostics/page.tsx +385 -0
  75. package/mission-control/src/app/graph/page.tsx +26 -0
  76. package/mission-control/src/app/memory/page.tsx +1 -1
  77. package/mission-control/src/app/obsidian/page.tsx +36 -6
  78. package/mission-control/src/app/roadmap/page.tsx +24 -0
  79. package/mission-control/src/app/souls/page.tsx +2 -2
  80. package/mission-control/src/components/board/execution-config.tsx +431 -0
  81. package/mission-control/src/components/board/kanban-board.tsx +75 -9
  82. package/mission-control/src/components/board/kanban-column.tsx +135 -19
  83. package/mission-control/src/components/board/task-card.tsx +55 -2
  84. package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
  85. package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
  86. package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
  87. package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
  88. package/mission-control/src/components/cowork/role-picker.tsx +102 -0
  89. package/mission-control/src/components/cowork/session-card.tsx +284 -0
  90. package/mission-control/src/components/layout/sidebar.tsx +39 -2
  91. package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
  92. package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
  93. package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
  94. package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
  95. package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
  96. package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
  97. package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
  98. package/mission-control/src/lib/config.ts +58 -0
  99. package/mission-control/src/lib/db/index.ts +69 -0
  100. package/mission-control/src/lib/db/schema.ts +61 -3
  101. package/mission-control/src/lib/hooks.ts +309 -0
  102. package/mission-control/src/lib/memory/entities.ts +3 -2
  103. package/mission-control/src/lib/nats.ts +66 -1
  104. package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
  105. package/mission-control/src/lib/parsers/transcript.ts +4 -4
  106. package/mission-control/src/lib/scheduler.ts +12 -11
  107. package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
  108. package/mission-control/src/lib/sync/tasks.ts +23 -1
  109. package/mission-control/src/lib/task-id.ts +32 -0
  110. package/mission-control/src/lib/tts/index.ts +33 -9
  111. package/mission-control/tsconfig.json +2 -1
  112. package/mission-control/vitest.config.ts +14 -0
  113. package/package.json +15 -2
  114. package/services/service-manifest.json +1 -1
  115. package/skills/cc-godmode/references/agents.md +8 -8
  116. package/workspace-bin/memory-daemon.mjs +199 -5
  117. package/workspace-bin/session-search.mjs +204 -0
  118. package/workspace-bin/web-fetch.mjs +65 -0
package/bin/mesh.js CHANGED
@@ -397,6 +397,10 @@ async function cmdSubmit(args) {
397
397
  success_criteria: task.success_criteria || [],
398
398
  scope: task.scope || [],
399
399
  priority: task.auto_priority || 0,
400
+ llm_provider: task.provider || task.llm_provider || null,
401
+ llm_model: task.model || task.llm_model || null,
402
+ preferred_nodes: task.preferred_nodes || [],
403
+ exclude_nodes: task.exclude_nodes || [],
400
404
  });
401
405
  console.log(`Submitted: ${result.data.task_id} [${result.data.status}]`);
402
406
  // Mark as 'submitted' — NOT 'running'. The card reflects actual mesh state.
@@ -451,12 +455,18 @@ async function cmdSubmit(args) {
451
455
  scope: task.scope || [],
452
456
  priority: task.priority || 0,
453
457
  tags: task.tags || [],
458
+ llm_provider: task.provider || task.llm_provider || null,
459
+ llm_model: task.model || task.llm_model || null,
460
+ preferred_nodes: task.preferred_nodes || [],
461
+ exclude_nodes: task.exclude_nodes || [],
462
+ collaboration: task.collaboration || undefined,
454
463
  });
455
464
 
456
465
  console.log(`Submitted: ${result.data.task_id} "${result.data.title}"`);
457
466
  console.log(` Status: ${result.data.status}`);
458
467
  console.log(` Budget: ${result.data.budget_minutes}m`);
459
468
  console.log(` Metric: ${result.data.metric || 'none'}`);
469
+ if (result.data.llm_provider) console.log(` Provider: ${result.data.llm_provider}`);
460
470
  await nc.close();
461
471
  }
462
472
 
@@ -464,6 +474,62 @@ async function cmdSubmit(args) {
464
474
  * mesh tasks [--status <filter>] — list mesh tasks.
465
475
  */
466
476
  async function cmdTasks(args) {
477
+ const subCmd = args[0];
478
+
479
+ // Subcommands: approve, reject
480
+ if (subCmd === 'approve') {
481
+ const taskId = args[1];
482
+ if (!taskId) { console.error('Usage: mesh tasks approve <task-id>'); process.exit(1); }
483
+ const nc = await natsConnect();
484
+ try {
485
+ const result = await natsRequest(nc, 'mesh.tasks.approve', { task_id: taskId });
486
+ console.log(`Task approved: ${result.task_id} → ${result.status}`);
487
+ } finally { await nc.close(); }
488
+ return;
489
+ }
490
+
491
+ if (subCmd === 'reject') {
492
+ const taskId = args[1];
493
+ if (!taskId) { console.error('Usage: mesh tasks reject <task-id> [--reason "..."]'); process.exit(1); }
494
+ let reason = 'Rejected by reviewer';
495
+ for (let i = 2; i < args.length; i++) {
496
+ if (args[i] === '--reason' && args[i + 1]) { reason = args[++i]; }
497
+ }
498
+ const nc = await natsConnect();
499
+ try {
500
+ const result = await natsRequest(nc, 'mesh.tasks.reject', { task_id: taskId, reason });
501
+ console.log(`Task rejected: ${result.task_id} → re-queued`);
502
+ console.log(` Reason: ${reason}`);
503
+ } finally { await nc.close(); }
504
+ return;
505
+ }
506
+
507
+ if (subCmd === 'review') {
508
+ // List only pending_review tasks
509
+ const nc = await natsConnect();
510
+ const result = await natsRequest(nc, 'mesh.tasks.list', { status: 'pending_review' });
511
+ const tasks = result.data || [];
512
+ if (tasks.length === 0) {
513
+ console.log('No tasks pending review.');
514
+ await nc.close();
515
+ return;
516
+ }
517
+ console.log(`Tasks pending review (${tasks.length}):\n`);
518
+ for (const t of tasks) {
519
+ console.log(` ${t.task_id} "${t.title}"`);
520
+ if (t.result?.summary) console.log(` Result: ${t.result.summary.slice(0, 200)}`);
521
+ if (t.result?.harness?.warnings?.length) {
522
+ console.log(` Harness warnings: ${t.result.harness.warnings.length}`);
523
+ }
524
+ console.log(` Approve: mesh tasks approve ${t.task_id}`);
525
+ console.log(` Reject: mesh tasks reject ${t.task_id} --reason "..."`);
526
+ console.log('');
527
+ }
528
+ await nc.close();
529
+ return;
530
+ }
531
+
532
+ // Default: list all tasks
467
533
  const nc = await natsConnect();
468
534
  const filter = {};
469
535
  const statusIdx = args.indexOf('--status');
@@ -484,7 +550,8 @@ async function cmdTasks(args) {
484
550
  : t.started_at
485
551
  ? ((Date.now() - new Date(t.started_at)) / 1000).toFixed(0) + 's (running)'
486
552
  : '-';
487
- console.log(` ${t.task_id} [${t.status}] "${t.title}"`);
553
+ const reviewTag = t.status === 'pending_review' ? ' ⏳' : '';
554
+ console.log(` ${t.task_id} [${t.status}]${reviewTag} "${t.title}"`);
488
555
  console.log(` Owner: ${t.owner || '-'} Elapsed: ${elapsed} Attempts: ${t.attempts.length}`);
489
556
  if (t.metric) console.log(` Metric: ${t.metric}`);
490
557
  if (t.result?.summary) console.log(` Result: ${t.result.summary.slice(0, 120)}`);
@@ -598,7 +665,11 @@ async function cmdRepair(args) {
598
665
  */
599
666
  async function cmdDeploy(args) {
600
667
  const { execSync } = require('child_process');
601
- const repoDir = process.env.OPENCLAW_REPO_DIR || path.join(os.homedir(), 'openclaw');
668
+ // Prefer openclaw-node (git repo) over openclaw (runtime)
669
+ const defaultRepo = fs.existsSync(path.join(os.homedir(), 'openclaw-node', '.git'))
670
+ ? path.join(os.homedir(), 'openclaw-node')
671
+ : path.join(os.homedir(), 'openclaw');
672
+ const repoDir = process.env.OPENCLAW_REPO_DIR || defaultRepo;
602
673
  const force = args.includes('--force');
603
674
 
604
675
  // Parse --component flags
@@ -658,7 +729,7 @@ async function cmdDeploy(args) {
658
729
  await nc.flush();
659
730
  console.log('Deploy trigger sent.\n');
660
731
 
661
- // Poll for results (15s timeout)
732
+ // Poll for results (10s timeout)
662
733
  console.log('Waiting for node responses...');
663
734
  const deadline = Date.now() + 15000;
664
735
  const seen = new Set();
@@ -668,6 +739,7 @@ async function cmdDeploy(args) {
668
739
  const resultsKv = await js.views.kv('MESH_DEPLOY_RESULTS');
669
740
 
670
741
  while (Date.now() < deadline) {
742
+ // Check all nodes
671
743
  const allAliasNodes = [...new Set(Object.values(NODE_ALIASES))];
672
744
  const checkNodes = targetNodes.length > 0 ? targetNodes : allAliasNodes;
673
745
 
@@ -706,6 +778,345 @@ async function cmdDeploy(args) {
706
778
  /**
707
779
  * mesh help — show usage.
708
780
  */
781
+ // ── Plan Commands ──────────────────────────────────
782
+
783
+ async function cmdPlan(args) {
784
+ const { loadTemplate, listTemplates, validateTemplate, instantiateTemplate } = require('../lib/plan-templates');
785
+ const TEMPLATES_DIR = process.env.OPENCLAW_TEMPLATES_DIR || path.join(process.env.HOME, '.openclaw', 'plan-templates');
786
+ const FALLBACK_DIR = path.join(__dirname, '..', 'config', 'plan-templates');
787
+
788
+ const sub = args[0];
789
+
790
+ switch (sub) {
791
+ case 'templates': {
792
+ // List available templates
793
+ const templates = [
794
+ ...listTemplates(TEMPLATES_DIR),
795
+ ...listTemplates(FALLBACK_DIR),
796
+ ];
797
+ // Deduplicate by id
798
+ const seen = new Set();
799
+ const unique = templates.filter(t => { if (seen.has(t.id)) return false; seen.add(t.id); return true; });
800
+
801
+ if (unique.length === 0) {
802
+ console.log('No plan templates found.');
803
+ console.log(`Checked: ${TEMPLATES_DIR}`);
804
+ return;
805
+ }
806
+
807
+ console.log('Available plan templates:\n');
808
+ for (const t of unique) {
809
+ console.log(` ${t.id.padEnd(20)} ${t.description}`);
810
+ }
811
+ return;
812
+ }
813
+
814
+ case 'create': {
815
+ // Parse --template, --context, --parent-task, and --set flags
816
+ let templateId = null;
817
+ let context = '';
818
+ let parentTaskId = null;
819
+ const overrides = []; // [{path: 'implement.delegation.mode', value: 'collab_mesh'}, ...]
820
+
821
+ for (let i = 1; i < args.length; i++) {
822
+ if (args[i] === '--template' && args[i + 1]) { templateId = args[++i]; continue; }
823
+ if (args[i] === '--context' && args[i + 1]) { context = args[++i]; continue; }
824
+ if (args[i] === '--parent-task' && args[i + 1]) { parentTaskId = args[++i]; continue; }
825
+ if (args[i] === '--set' && args[i + 1]) {
826
+ // Format: subtask_id.field.path=value
827
+ const raw = args[++i];
828
+ const eqIdx = raw.indexOf('=');
829
+ if (eqIdx === -1) {
830
+ console.error(`Invalid --set format: "${raw}" (expected subtask_id.field=value)`);
831
+ process.exit(1);
832
+ }
833
+ overrides.push({ path: raw.slice(0, eqIdx), value: raw.slice(eqIdx + 1) });
834
+ continue;
835
+ }
836
+ }
837
+
838
+ if (!templateId) {
839
+ console.error('Usage: mesh plan create --template <id> --context "<description>" [--parent-task <task-id>] [--set subtask.field=value]');
840
+ process.exit(1);
841
+ }
842
+
843
+ // Find template file
844
+ let templatePath = null;
845
+ for (const dir of [TEMPLATES_DIR, FALLBACK_DIR]) {
846
+ const candidate = path.join(dir, `${templateId}.yaml`);
847
+ if (fs.existsSync(candidate)) { templatePath = candidate; break; }
848
+ const candidateYml = path.join(dir, `${templateId}.yml`);
849
+ if (fs.existsSync(candidateYml)) { templatePath = candidateYml; break; }
850
+ }
851
+
852
+ if (!templatePath) {
853
+ console.error(`Template not found: ${templateId}`);
854
+ console.error(`Run "mesh plan templates" to see available templates.`);
855
+ process.exit(1);
856
+ }
857
+
858
+ const template = loadTemplate(templatePath);
859
+ const validation = validateTemplate(template);
860
+ if (!validation.valid) {
861
+ console.error('Template validation failed:');
862
+ validation.errors.forEach(e => console.error(` - ${e}`));
863
+ process.exit(1);
864
+ }
865
+
866
+ const plan = instantiateTemplate(template, context, { parent_task_id: parentTaskId });
867
+
868
+ // Apply --set overrides to instantiated plan subtasks
869
+ // Format: subtask_id.field.nested=value (e.g., implement.delegation.mode=collab_mesh)
870
+ for (const { path: setPath, value } of overrides) {
871
+ const parts = setPath.split('.');
872
+ const subtaskId = parts[0];
873
+ const st = plan.subtasks.find(s => s.subtask_id === subtaskId);
874
+ if (!st) {
875
+ console.error(`--set: unknown subtask "${subtaskId}". Available: ${plan.subtasks.map(s => s.subtask_id).join(', ')}`);
876
+ process.exit(1);
877
+ }
878
+ // Walk the nested path and set the value
879
+ let target = st;
880
+ for (let j = 1; j < parts.length - 1; j++) {
881
+ if (target[parts[j]] === undefined || target[parts[j]] === null) target[parts[j]] = {};
882
+ target = target[parts[j]];
883
+ }
884
+ const finalKey = parts[parts.length - 1];
885
+ // Auto-coerce numbers and booleans
886
+ let coerced = value;
887
+ if (value === 'true') coerced = true;
888
+ else if (value === 'false') coerced = false;
889
+ else if (/^\d+$/.test(value)) coerced = parseInt(value, 10);
890
+ target[finalKey] = coerced;
891
+ }
892
+
893
+ // Submit to mesh via NATS
894
+ const nc = await connect({ servers: NATS_URL, timeout: 5000 });
895
+ try {
896
+ const reply = await nc.request(
897
+ 'mesh.plans.create',
898
+ sc.encode(JSON.stringify(plan)),
899
+ { timeout: 10000 }
900
+ );
901
+ const result = JSON.parse(sc.decode(reply.data));
902
+ console.log(`Plan created: ${result.plan_id}`);
903
+ console.log(` Subtasks: ${result.subtasks.length}`);
904
+ console.log(` Waves: ${result.estimated_waves}`);
905
+ console.log(` Budget: ${result.total_budget_minutes}min`);
906
+ console.log(` Status: ${result.status}`);
907
+ if (result.requires_approval) {
908
+ console.log(`\n Approve with: mesh plan approve ${result.plan_id}`);
909
+ }
910
+ } finally {
911
+ await nc.close();
912
+ }
913
+ return;
914
+ }
915
+
916
+ case 'list': {
917
+ let statusFilter = null;
918
+ for (let i = 1; i < args.length; i++) {
919
+ if (args[i] === '--status' && args[i + 1]) { statusFilter = args[++i]; }
920
+ }
921
+
922
+ const nc = await connect({ servers: NATS_URL, timeout: 5000 });
923
+ try {
924
+ const payload = statusFilter ? { status: statusFilter } : {};
925
+ const reply = await nc.request(
926
+ 'mesh.plans.list',
927
+ sc.encode(JSON.stringify(payload)),
928
+ { timeout: 10000 }
929
+ );
930
+ const plans = JSON.parse(sc.decode(reply.data));
931
+ if (plans.length === 0) {
932
+ console.log('No plans found.');
933
+ return;
934
+ }
935
+
936
+ console.log(`Plans (${plans.length}):\n`);
937
+ for (const p of plans) {
938
+ const status = p.status.padEnd(12);
939
+ const subtasks = `${p.total_subtasks} subtasks`;
940
+ console.log(` ${p.plan_id} ${status} ${subtasks} "${p.title}"`);
941
+ }
942
+ } finally {
943
+ await nc.close();
944
+ }
945
+ return;
946
+ }
947
+
948
+ case 'show': {
949
+ const planId = args[1];
950
+ if (!planId) {
951
+ console.error('Usage: mesh plan show <plan-id>');
952
+ process.exit(1);
953
+ }
954
+
955
+ const nc = await connect({ servers: NATS_URL, timeout: 5000 });
956
+ try {
957
+ const reply = await nc.request(
958
+ 'mesh.plans.get',
959
+ sc.encode(JSON.stringify({ plan_id: planId })),
960
+ { timeout: 10000 }
961
+ );
962
+ const plan = JSON.parse(sc.decode(reply.data));
963
+ if (!plan || plan.error) {
964
+ console.error(plan?.error || `Plan not found: ${planId}`);
965
+ process.exit(1);
966
+ }
967
+
968
+ // Header
969
+ console.log(`\nPlan: ${plan.plan_id}`);
970
+ console.log(` Title: ${plan.title}`);
971
+ console.log(` Status: ${plan.status}`);
972
+ console.log(` Policy: ${plan.failure_policy || 'continue_best_effort'}`);
973
+ console.log(` Approval: ${plan.requires_approval ? 'required' : 'auto'}`);
974
+ if (plan.created_at) console.log(` Created: ${plan.created_at}`);
975
+ if (plan.parent_task_id) console.log(` Parent: ${plan.parent_task_id}`);
976
+
977
+ // Compute waves from subtask dependencies
978
+ const subtasks = plan.subtasks || [];
979
+ const waves = new Map();
980
+ for (const st of subtasks) {
981
+ const wave = st.wave ?? 0;
982
+ if (!waves.has(wave)) waves.set(wave, []);
983
+ waves.get(wave).push(st);
984
+ }
985
+
986
+ // If no wave field, group by dependency depth
987
+ if (waves.size <= 1 && subtasks.length > 1) {
988
+ waves.clear();
989
+ const idToSt = new Map(subtasks.map(s => [s.subtask_id, s]));
990
+ const depths = new Map();
991
+ function getDepth(id) {
992
+ if (depths.has(id)) return depths.get(id);
993
+ const st = idToSt.get(id);
994
+ if (!st || !st.depends_on || st.depends_on.length === 0) { depths.set(id, 0); return 0; }
995
+ const d = 1 + Math.max(...st.depends_on.map(dep => getDepth(dep)));
996
+ depths.set(id, d);
997
+ return d;
998
+ }
999
+ for (const st of subtasks) getDepth(st.subtask_id);
1000
+ for (const st of subtasks) {
1001
+ const w = depths.get(st.subtask_id) || 0;
1002
+ if (!waves.has(w)) waves.set(w, []);
1003
+ waves.get(w).push(st);
1004
+ }
1005
+ }
1006
+
1007
+ // Render subtask tree
1008
+ const sortedWaves = [...waves.keys()].sort((a, b) => a - b);
1009
+ for (const w of sortedWaves) {
1010
+ console.log(`\n ── Wave ${w} ${'─'.repeat(50)}`);
1011
+ for (const st of waves.get(w)) {
1012
+ const status = (st.status || 'pending').toUpperCase();
1013
+ const critical = st.critical ? ' [CRITICAL]' : '';
1014
+ const mode = st.delegation?.mode || 'auto';
1015
+ const reason = st.delegation?.reason ? ` (${st.delegation.reason})` : '';
1016
+ const budget = st.budget_minutes ? ` ${st.budget_minutes}min` : '';
1017
+ const metric = st.metric ? ` metric:"${st.metric}"` : '';
1018
+ const deps = st.depends_on?.length ? ` deps:[${st.depends_on.join(',')}]` : '';
1019
+
1020
+ console.log(` ${status.padEnd(10)} ${st.subtask_id}${critical}`);
1021
+ console.log(` "${st.title}"`);
1022
+ console.log(` route:${mode}${reason}${budget}${metric}${deps}`);
1023
+
1024
+ if (st.result) {
1025
+ const success = st.result.success ? '✓' : '✗';
1026
+ const summary = st.result.summary || '';
1027
+ console.log(` result: ${success} ${summary}`);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ // Summary
1033
+ const completed = subtasks.filter(s => s.status === 'completed').length;
1034
+ const failed = subtasks.filter(s => s.status === 'failed').length;
1035
+ const blocked = subtasks.filter(s => s.status === 'blocked').length;
1036
+ const pending = subtasks.filter(s => s.status === 'pending' || s.status === 'queued').length;
1037
+ const running = subtasks.filter(s => s.status === 'running').length;
1038
+ console.log(`\n Summary: ${subtasks.length} subtasks — ${completed} done, ${running} running, ${pending} pending, ${failed} failed, ${blocked} blocked`);
1039
+
1040
+ } finally {
1041
+ await nc.close();
1042
+ }
1043
+ return;
1044
+ }
1045
+
1046
+ case 'approve': {
1047
+ const planId = args[1];
1048
+ if (!planId) {
1049
+ console.error('Usage: mesh plan approve <plan-id>');
1050
+ process.exit(1);
1051
+ }
1052
+
1053
+ const nc = await connect({ servers: NATS_URL, timeout: 5000 });
1054
+ try {
1055
+ const reply = await nc.request(
1056
+ 'mesh.plans.approve',
1057
+ sc.encode(JSON.stringify({ plan_id: planId })),
1058
+ { timeout: 10000 }
1059
+ );
1060
+ const result = JSON.parse(sc.decode(reply.data));
1061
+ console.log(`Plan approved: ${result.plan_id}`);
1062
+ console.log(` Status: ${result.status}`);
1063
+ console.log(` Wave 0 dispatched with ${result.subtasks.filter(s => s.status !== 'pending').length} subtasks`);
1064
+ } finally {
1065
+ await nc.close();
1066
+ }
1067
+ return;
1068
+ }
1069
+
1070
+ case 'abort': {
1071
+ const planId = args[1];
1072
+ let reason = 'Manual abort';
1073
+ for (let i = 2; i < args.length; i++) {
1074
+ if (args[i] === '--reason' && args[i + 1]) { reason = args[++i]; }
1075
+ }
1076
+
1077
+ if (!planId) {
1078
+ console.error('Usage: mesh plan abort <plan-id> [--reason "..."]');
1079
+ process.exit(1);
1080
+ }
1081
+
1082
+ const nc = await connect({ servers: NATS_URL, timeout: 5000 });
1083
+ try {
1084
+ const reply = await nc.request(
1085
+ 'mesh.plans.abort',
1086
+ sc.encode(JSON.stringify({ plan_id: planId, reason })),
1087
+ { timeout: 10000 }
1088
+ );
1089
+ const result = JSON.parse(sc.decode(reply.data));
1090
+ console.log(`Plan aborted: ${result.plan_id}`);
1091
+ } finally {
1092
+ await nc.close();
1093
+ }
1094
+ return;
1095
+ }
1096
+
1097
+ default:
1098
+ console.log([
1099
+ '',
1100
+ 'mesh plan -- Plan management commands',
1101
+ '',
1102
+ ' mesh plan templates List available templates',
1103
+ ' mesh plan create --template <id> Create plan from template',
1104
+ ' --context "<description>" Context for template variables',
1105
+ ' --parent-task <task-id> Link to parent task',
1106
+ ' --set subtask.field=value Override subtask fields post-instantiation',
1107
+ ' e.g., --set implement.delegation.mode=collab_mesh',
1108
+ ' --set test.budget_minutes=30',
1109
+ ' mesh plan list List all plans',
1110
+ ' --status <status> Filter by status',
1111
+ ' mesh plan show <plan-id> Show full plan with subtask tree',
1112
+ ' mesh plan approve <plan-id> Approve and start executing',
1113
+ ' mesh plan abort <plan-id> Abort a plan',
1114
+ ' --reason "..." Reason for abort',
1115
+ '',
1116
+ ].join('\n'));
1117
+ }
1118
+ }
1119
+
709
1120
  function cmdHelp() {
710
1121
  console.log([
711
1122
  '',
@@ -725,15 +1136,20 @@ function cmdHelp() {
725
1136
  ' cat task.yaml | mesh submit Submit from stdin',
726
1137
  ' mesh tasks List all mesh tasks',
727
1138
  ' mesh tasks --status running Filter mesh tasks by status',
1139
+ ' mesh tasks review List tasks pending human review',
1140
+ ' mesh tasks approve <task-id> Approve a pending_review task',
1141
+ ' mesh tasks reject <task-id> Reject and re-queue a task',
1142
+ ' --reason "..." Reason for rejection',
1143
+ ' mesh plan <subcommand> Plan management (templates, create, approve)',
728
1144
  ' mesh health Health check this node',
729
1145
  ' mesh health --all Health check ALL nodes',
730
1146
  ' mesh health --json Health check (JSON output)',
731
1147
  ' mesh repair Self-repair this node',
732
1148
  ' mesh repair --all Self-repair ALL nodes',
733
1149
  ' mesh deploy Deploy to all nodes',
734
- ' mesh deploy --force Force deploy (even if up to date)',
735
- ' mesh deploy --component <name> Deploy specific component',
736
- ' mesh deploy --node <name> Deploy to specific node',
1150
+ ' mesh deploy --force Force deploy (skip cache)',
1151
+ ' mesh deploy --node ubuntu Deploy to specific node',
1152
+ ' mesh deploy --component mesh-daemons Deploy specific component',
737
1153
  '',
738
1154
  'NODE ALIASES:',
739
1155
  ' ubuntu, linux = Ubuntu VM (calos-vmware-virtual-platform)',
@@ -765,6 +1181,7 @@ async function main() {
765
1181
  case 'health': return cmdHealth(args);
766
1182
  case 'repair': return cmdRepair(args);
767
1183
  case 'deploy': return cmdDeploy(args);
1184
+ case 'plan': return cmdPlan(args);
768
1185
  case 'help':
769
1186
  case '--help':
770
1187
  case '-h': return cmdHelp();
@@ -0,0 +1,95 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "permissions": {
4
+ "allow": [
5
+ "Bash(git status*)",
6
+ "Bash(git diff*)",
7
+ "Bash(git log*)",
8
+ "Bash(git branch*)",
9
+ "Bash(git rev-parse*)",
10
+ "Bash(ls *)",
11
+ "Bash(npm test*)",
12
+ "Bash(npm run*)",
13
+ "Bash(node bin/*)",
14
+ "Bash(node test/*)"
15
+ ],
16
+ "deny": [
17
+ "Bash(rm -rf /*)",
18
+ "Bash(git push --force*)",
19
+ "Bash(git push -f *)",
20
+ "Bash(git reset --hard*)",
21
+ "Bash(git clean -f*)",
22
+ "Bash(sudo *)",
23
+ "Bash(*>.env*)",
24
+ "Bash(cat *.env*)",
25
+ "Read(**/.env*)"
26
+ ]
27
+ },
28
+ "hooks": {
29
+ "SessionStart": [
30
+ {
31
+ "matcher": "",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "bash .claude/hooks/session-start.sh",
36
+ "timeout": 10
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "PreToolUse": [
42
+ {
43
+ "matcher": "Bash",
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "bash .claude/hooks/validate-commit.sh",
48
+ "timeout": 15
49
+ },
50
+ {
51
+ "type": "command",
52
+ "command": "bash .claude/hooks/validate-push.sh",
53
+ "timeout": 10
54
+ }
55
+ ]
56
+ }
57
+ ],
58
+ "PreCompact": [
59
+ {
60
+ "matcher": "",
61
+ "hooks": [
62
+ {
63
+ "type": "command",
64
+ "command": "bash .claude/hooks/pre-compact.sh",
65
+ "timeout": 10
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ "Stop": [
71
+ {
72
+ "matcher": "",
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "bash .claude/hooks/session-stop.sh",
77
+ "timeout": 10
78
+ }
79
+ ]
80
+ }
81
+ ],
82
+ "SubagentStart": [
83
+ {
84
+ "matcher": "",
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "bash .claude/hooks/log-agent.sh",
89
+ "timeout": 5
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ }
95
+ }
@@ -9,7 +9,8 @@
9
9
  "idleThresholdMs": 900000,
10
10
  "sessionRecapMs": 600000,
11
11
  "maintenanceMs": 1800000,
12
- "obsidianSyncMs": 1800000
12
+ "obsidianSyncMs": 1800000,
13
+ "hyperagentReflectMs": 1800000
13
14
  },
14
15
  "clawvaultBin": "bin/clawvault-local",
15
16
  "obsidianVault": "projects/arcane-vault"
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # Git pre-commit hook — delegates to Claude Code hook for consistent validation.
3
+ # Installed by openclaw-node installer. LLM-agnostic: works regardless of AI tool.
4
+
5
+ HOOK_DIR="$(git rev-parse --show-toplevel 2>/dev/null)/.claude/hooks"
6
+
7
+ if [ -x "${HOOK_DIR}/validate-commit.sh" ]; then
8
+ # Simulate the input format the hook expects (git commit context)
9
+ echo '{"tool_input":{"command":"git commit"}}' | bash "${HOOK_DIR}/validate-commit.sh"
10
+ exit $?
11
+ fi
12
+
13
+ exit 0
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # Git pre-push hook — delegates to Claude Code hook for consistent validation.
3
+ # Installed by openclaw-node installer. LLM-agnostic: works regardless of AI tool.
4
+
5
+ HOOK_DIR="$(git rev-parse --show-toplevel 2>/dev/null)/.claude/hooks"
6
+
7
+ if [ -x "${HOOK_DIR}/validate-push.sh" ]; then
8
+ echo '{"tool_input":{"command":"git push"}}' | bash "${HOOK_DIR}/validate-push.sh"
9
+ exit $?
10
+ fi
11
+
12
+ exit 0