openclaw-node-harness 2.0.4 → 2.1.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.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/lane-watchdog.js +23 -2
- package/bin/mesh-agent.js +439 -28
- package/bin/mesh-bridge.js +69 -3
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +821 -26
- package/bin/mesh.js +411 -20
- package/config/claude-settings.json +95 -0
- package/config/daemon.json.template +2 -1
- package/config/git-hooks/pre-commit +13 -0
- package/config/git-hooks/pre-push +12 -0
- package/config/harness-rules.json +174 -0
- package/config/plan-templates/team-bugfix.yaml +52 -0
- package/config/plan-templates/team-deploy.yaml +50 -0
- package/config/plan-templates/team-feature.yaml +71 -0
- package/config/roles/qa-engineer.yaml +36 -0
- package/config/roles/solidity-dev.yaml +51 -0
- package/config/roles/tech-architect.yaml +36 -0
- package/config/rules/framework/solidity.md +22 -0
- package/config/rules/framework/typescript.md +21 -0
- package/config/rules/framework/unity.md +21 -0
- package/config/rules/universal/design-docs.md +18 -0
- package/config/rules/universal/git-hygiene.md +18 -0
- package/config/rules/universal/security.md +19 -0
- package/config/rules/universal/test-standards.md +19 -0
- package/identity/DELEGATION.md +6 -6
- package/install.sh +296 -10
- package/lib/agent-activity.js +2 -2
- package/lib/circling-parser.js +119 -0
- package/lib/exec-safety.js +105 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +24 -31
- package/lib/llm-providers.js +16 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +530 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +252 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +483 -165
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +79 -50
- package/lib/mesh-tasks.js +132 -49
- package/lib/nats-resolve.js +4 -4
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +322 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +461 -0
- package/lib/transcript-parser.mjs +292 -0
- package/mission-control/drizzle/soul_schema_update.sql +29 -0
- package/mission-control/drizzle.config.ts +1 -4
- package/mission-control/package-lock.json +1571 -83
- package/mission-control/package.json +6 -2
- package/mission-control/scripts/gen-chronology.js +3 -3
- package/mission-control/scripts/import-pipeline-v2.js +0 -16
- package/mission-control/scripts/import-pipeline.js +0 -15
- package/mission-control/src/app/api/cowork/clusters/[id]/members/route.ts +117 -0
- package/mission-control/src/app/api/cowork/clusters/[id]/route.ts +84 -0
- package/mission-control/src/app/api/cowork/clusters/route.ts +141 -0
- package/mission-control/src/app/api/cowork/dispatch/route.ts +128 -0
- package/mission-control/src/app/api/cowork/events/route.ts +65 -0
- package/mission-control/src/app/api/cowork/intervene/route.ts +259 -0
- package/mission-control/src/app/api/cowork/sessions/[id]/route.ts +37 -0
- package/mission-control/src/app/api/cowork/sessions/route.ts +64 -0
- package/mission-control/src/app/api/diagnostics/route.ts +97 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +990 -0
- package/mission-control/src/app/api/memory/search/route.ts +6 -3
- package/mission-control/src/app/api/mesh/events/route.ts +95 -19
- package/mission-control/src/app/api/mesh/identity/route.ts +11 -0
- package/mission-control/src/app/api/mesh/tasks/[id]/route.ts +92 -0
- package/mission-control/src/app/api/mesh/tasks/route.ts +91 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +21 -5
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +14 -2
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +8 -2
- package/mission-control/src/app/api/tasks/[id]/route.ts +90 -4
- package/mission-control/src/app/api/tasks/route.ts +21 -30
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/app/cowork/page.tsx +261 -0
- package/mission-control/src/app/diagnostics/page.tsx +385 -0
- package/mission-control/src/app/graph/page.tsx +26 -0
- package/mission-control/src/app/memory/page.tsx +1 -1
- package/mission-control/src/app/obsidian/page.tsx +36 -6
- package/mission-control/src/app/roadmap/page.tsx +24 -0
- package/mission-control/src/app/souls/page.tsx +2 -2
- package/mission-control/src/components/board/execution-config.tsx +431 -0
- package/mission-control/src/components/board/kanban-board.tsx +75 -9
- package/mission-control/src/components/board/kanban-column.tsx +135 -19
- package/mission-control/src/components/board/task-card.tsx +55 -2
- package/mission-control/src/components/board/unified-task-dialog.tsx +82 -4
- package/mission-control/src/components/cowork/cluster-card.tsx +176 -0
- package/mission-control/src/components/cowork/create-cluster-dialog.tsx +251 -0
- package/mission-control/src/components/cowork/dispatch-form.tsx +423 -0
- package/mission-control/src/components/cowork/role-picker.tsx +102 -0
- package/mission-control/src/components/cowork/session-card.tsx +284 -0
- package/mission-control/src/components/layout/sidebar.tsx +39 -2
- package/mission-control/src/lib/__tests__/daily-log.test.ts +82 -0
- package/mission-control/src/lib/__tests__/memory-md.test.ts +87 -0
- package/mission-control/src/lib/__tests__/mesh-kv-sync.test.ts +465 -0
- package/mission-control/src/lib/__tests__/mocks/mock-kv.ts +131 -0
- package/mission-control/src/lib/__tests__/status-kanban.test.ts +46 -0
- package/mission-control/src/lib/__tests__/task-markdown.test.ts +188 -0
- package/mission-control/src/lib/__tests__/wikilinks.test.ts +175 -0
- package/mission-control/src/lib/config.ts +67 -0
- package/mission-control/src/lib/db/index.ts +85 -1
- package/mission-control/src/lib/db/schema.ts +61 -3
- package/mission-control/src/lib/hooks.ts +309 -0
- package/mission-control/src/lib/memory/entities.ts +3 -2
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/nats.ts +66 -1
- package/mission-control/src/lib/parsers/task-markdown.ts +52 -2
- package/mission-control/src/lib/parsers/transcript.ts +4 -4
- package/mission-control/src/lib/scheduler.ts +12 -11
- package/mission-control/src/lib/sync/mesh-kv.ts +279 -0
- package/mission-control/src/lib/sync/tasks.ts +23 -1
- package/mission-control/src/lib/task-id.ts +32 -0
- package/mission-control/src/lib/tts/index.ts +33 -9
- package/mission-control/src/middleware.ts +82 -0
- package/mission-control/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +1 -1
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/uninstall.sh +37 -9
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
package/bin/mesh.js
CHANGED
|
@@ -88,27 +88,16 @@ function remoteNode() {
|
|
|
88
88
|
|
|
89
89
|
// ─── Exec safety ─────────────────────────────────────
|
|
90
90
|
|
|
91
|
-
const
|
|
92
|
-
/\brm\s+(-[a-zA-Z]*)?r[a-zA-Z]*f/, // rm -rf, rm -fr, rm --recursive --force
|
|
93
|
-
/\brm\s+(-[a-zA-Z]*)?f[a-zA-Z]*r/, // rm -fr variants
|
|
94
|
-
/\bmkfs\b/, // format filesystem
|
|
95
|
-
/\bdd\s+.*of=/, // raw disk write
|
|
96
|
-
/\b>\s*\/dev\/[sh]d/, // write to raw device
|
|
97
|
-
/\bcurl\b.*\|\s*(ba)?sh/, // curl pipe to shell
|
|
98
|
-
/\bwget\b.*\|\s*(ba)?sh/, // wget pipe to shell
|
|
99
|
-
/\bchmod\s+(-[a-zA-Z]*\s+)?777\s+\//, // chmod 777 on root paths
|
|
100
|
-
/\b:(){ :\|:& };:/, // fork bomb
|
|
101
|
-
];
|
|
91
|
+
const { checkDestructivePatterns } = require('../lib/exec-safety');
|
|
102
92
|
|
|
103
93
|
function checkExecSafety(command) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
94
|
+
const result = checkDestructivePatterns(command);
|
|
95
|
+
if (result.blocked) {
|
|
96
|
+
console.error(`BLOCKED: Command matches destructive pattern.`);
|
|
97
|
+
console.error(` Command: ${command}`);
|
|
98
|
+
console.error(` Pattern: ${result.pattern}`);
|
|
99
|
+
console.error(`\nIf this is intentional, SSH into the node and run it directly.`);
|
|
100
|
+
process.exit(1);
|
|
112
101
|
}
|
|
113
102
|
}
|
|
114
103
|
|
|
@@ -474,6 +463,62 @@ async function cmdSubmit(args) {
|
|
|
474
463
|
* mesh tasks [--status <filter>] — list mesh tasks.
|
|
475
464
|
*/
|
|
476
465
|
async function cmdTasks(args) {
|
|
466
|
+
const subCmd = args[0];
|
|
467
|
+
|
|
468
|
+
// Subcommands: approve, reject
|
|
469
|
+
if (subCmd === 'approve') {
|
|
470
|
+
const taskId = args[1];
|
|
471
|
+
if (!taskId) { console.error('Usage: mesh tasks approve <task-id>'); process.exit(1); }
|
|
472
|
+
const nc = await natsConnect();
|
|
473
|
+
try {
|
|
474
|
+
const result = await natsRequest(nc, 'mesh.tasks.approve', { task_id: taskId });
|
|
475
|
+
console.log(`Task approved: ${result.task_id} → ${result.status}`);
|
|
476
|
+
} finally { await nc.close(); }
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (subCmd === 'reject') {
|
|
481
|
+
const taskId = args[1];
|
|
482
|
+
if (!taskId) { console.error('Usage: mesh tasks reject <task-id> [--reason "..."]'); process.exit(1); }
|
|
483
|
+
let reason = 'Rejected by reviewer';
|
|
484
|
+
for (let i = 2; i < args.length; i++) {
|
|
485
|
+
if (args[i] === '--reason' && args[i + 1]) { reason = args[++i]; }
|
|
486
|
+
}
|
|
487
|
+
const nc = await natsConnect();
|
|
488
|
+
try {
|
|
489
|
+
const result = await natsRequest(nc, 'mesh.tasks.reject', { task_id: taskId, reason });
|
|
490
|
+
console.log(`Task rejected: ${result.task_id} → re-queued`);
|
|
491
|
+
console.log(` Reason: ${reason}`);
|
|
492
|
+
} finally { await nc.close(); }
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (subCmd === 'review') {
|
|
497
|
+
// List only pending_review tasks
|
|
498
|
+
const nc = await natsConnect();
|
|
499
|
+
const result = await natsRequest(nc, 'mesh.tasks.list', { status: 'pending_review' });
|
|
500
|
+
const tasks = result.data || [];
|
|
501
|
+
if (tasks.length === 0) {
|
|
502
|
+
console.log('No tasks pending review.');
|
|
503
|
+
await nc.close();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
console.log(`Tasks pending review (${tasks.length}):\n`);
|
|
507
|
+
for (const t of tasks) {
|
|
508
|
+
console.log(` ${t.task_id} "${t.title}"`);
|
|
509
|
+
if (t.result?.summary) console.log(` Result: ${t.result.summary.slice(0, 200)}`);
|
|
510
|
+
if (t.result?.harness?.warnings?.length) {
|
|
511
|
+
console.log(` Harness warnings: ${t.result.harness.warnings.length}`);
|
|
512
|
+
}
|
|
513
|
+
console.log(` Approve: mesh tasks approve ${t.task_id}`);
|
|
514
|
+
console.log(` Reject: mesh tasks reject ${t.task_id} --reason "..."`);
|
|
515
|
+
console.log('');
|
|
516
|
+
}
|
|
517
|
+
await nc.close();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Default: list all tasks
|
|
477
522
|
const nc = await natsConnect();
|
|
478
523
|
const filter = {};
|
|
479
524
|
const statusIdx = args.indexOf('--status');
|
|
@@ -494,7 +539,8 @@ async function cmdTasks(args) {
|
|
|
494
539
|
: t.started_at
|
|
495
540
|
? ((Date.now() - new Date(t.started_at)) / 1000).toFixed(0) + 's (running)'
|
|
496
541
|
: '-';
|
|
497
|
-
|
|
542
|
+
const reviewTag = t.status === 'pending_review' ? ' ⏳' : '';
|
|
543
|
+
console.log(` ${t.task_id} [${t.status}]${reviewTag} "${t.title}"`);
|
|
498
544
|
console.log(` Owner: ${t.owner || '-'} Elapsed: ${elapsed} Attempts: ${t.attempts.length}`);
|
|
499
545
|
if (t.metric) console.log(` Metric: ${t.metric}`);
|
|
500
546
|
if (t.result?.summary) console.log(` Result: ${t.result.summary.slice(0, 120)}`);
|
|
@@ -721,6 +767,345 @@ async function cmdDeploy(args) {
|
|
|
721
767
|
/**
|
|
722
768
|
* mesh help — show usage.
|
|
723
769
|
*/
|
|
770
|
+
// ── Plan Commands ──────────────────────────────────
|
|
771
|
+
|
|
772
|
+
async function cmdPlan(args) {
|
|
773
|
+
const { loadTemplate, listTemplates, validateTemplate, instantiateTemplate } = require('../lib/plan-templates');
|
|
774
|
+
const TEMPLATES_DIR = process.env.OPENCLAW_TEMPLATES_DIR || path.join(process.env.HOME, '.openclaw', 'plan-templates');
|
|
775
|
+
const FALLBACK_DIR = path.join(__dirname, '..', 'config', 'plan-templates');
|
|
776
|
+
|
|
777
|
+
const sub = args[0];
|
|
778
|
+
|
|
779
|
+
switch (sub) {
|
|
780
|
+
case 'templates': {
|
|
781
|
+
// List available templates
|
|
782
|
+
const templates = [
|
|
783
|
+
...listTemplates(TEMPLATES_DIR),
|
|
784
|
+
...listTemplates(FALLBACK_DIR),
|
|
785
|
+
];
|
|
786
|
+
// Deduplicate by id
|
|
787
|
+
const seen = new Set();
|
|
788
|
+
const unique = templates.filter(t => { if (seen.has(t.id)) return false; seen.add(t.id); return true; });
|
|
789
|
+
|
|
790
|
+
if (unique.length === 0) {
|
|
791
|
+
console.log('No plan templates found.');
|
|
792
|
+
console.log(`Checked: ${TEMPLATES_DIR}`);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
console.log('Available plan templates:\n');
|
|
797
|
+
for (const t of unique) {
|
|
798
|
+
console.log(` ${t.id.padEnd(20)} ${t.description}`);
|
|
799
|
+
}
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
case 'create': {
|
|
804
|
+
// Parse --template, --context, --parent-task, and --set flags
|
|
805
|
+
let templateId = null;
|
|
806
|
+
let context = '';
|
|
807
|
+
let parentTaskId = null;
|
|
808
|
+
const overrides = []; // [{path: 'implement.delegation.mode', value: 'collab_mesh'}, ...]
|
|
809
|
+
|
|
810
|
+
for (let i = 1; i < args.length; i++) {
|
|
811
|
+
if (args[i] === '--template' && args[i + 1]) { templateId = args[++i]; continue; }
|
|
812
|
+
if (args[i] === '--context' && args[i + 1]) { context = args[++i]; continue; }
|
|
813
|
+
if (args[i] === '--parent-task' && args[i + 1]) { parentTaskId = args[++i]; continue; }
|
|
814
|
+
if (args[i] === '--set' && args[i + 1]) {
|
|
815
|
+
// Format: subtask_id.field.path=value
|
|
816
|
+
const raw = args[++i];
|
|
817
|
+
const eqIdx = raw.indexOf('=');
|
|
818
|
+
if (eqIdx === -1) {
|
|
819
|
+
console.error(`Invalid --set format: "${raw}" (expected subtask_id.field=value)`);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
overrides.push({ path: raw.slice(0, eqIdx), value: raw.slice(eqIdx + 1) });
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!templateId) {
|
|
828
|
+
console.error('Usage: mesh plan create --template <id> --context "<description>" [--parent-task <task-id>] [--set subtask.field=value]');
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Find template file
|
|
833
|
+
let templatePath = null;
|
|
834
|
+
for (const dir of [TEMPLATES_DIR, FALLBACK_DIR]) {
|
|
835
|
+
const candidate = path.join(dir, `${templateId}.yaml`);
|
|
836
|
+
if (fs.existsSync(candidate)) { templatePath = candidate; break; }
|
|
837
|
+
const candidateYml = path.join(dir, `${templateId}.yml`);
|
|
838
|
+
if (fs.existsSync(candidateYml)) { templatePath = candidateYml; break; }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (!templatePath) {
|
|
842
|
+
console.error(`Template not found: ${templateId}`);
|
|
843
|
+
console.error(`Run "mesh plan templates" to see available templates.`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const template = loadTemplate(templatePath);
|
|
848
|
+
const validation = validateTemplate(template);
|
|
849
|
+
if (!validation.valid) {
|
|
850
|
+
console.error('Template validation failed:');
|
|
851
|
+
validation.errors.forEach(e => console.error(` - ${e}`));
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const plan = instantiateTemplate(template, context, { parent_task_id: parentTaskId });
|
|
856
|
+
|
|
857
|
+
// Apply --set overrides to instantiated plan subtasks
|
|
858
|
+
// Format: subtask_id.field.nested=value (e.g., implement.delegation.mode=collab_mesh)
|
|
859
|
+
for (const { path: setPath, value } of overrides) {
|
|
860
|
+
const parts = setPath.split('.');
|
|
861
|
+
const subtaskId = parts[0];
|
|
862
|
+
const st = plan.subtasks.find(s => s.subtask_id === subtaskId);
|
|
863
|
+
if (!st) {
|
|
864
|
+
console.error(`--set: unknown subtask "${subtaskId}". Available: ${plan.subtasks.map(s => s.subtask_id).join(', ')}`);
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
// Walk the nested path and set the value
|
|
868
|
+
let target = st;
|
|
869
|
+
for (let j = 1; j < parts.length - 1; j++) {
|
|
870
|
+
if (target[parts[j]] === undefined || target[parts[j]] === null) target[parts[j]] = {};
|
|
871
|
+
target = target[parts[j]];
|
|
872
|
+
}
|
|
873
|
+
const finalKey = parts[parts.length - 1];
|
|
874
|
+
// Auto-coerce numbers and booleans
|
|
875
|
+
let coerced = value;
|
|
876
|
+
if (value === 'true') coerced = true;
|
|
877
|
+
else if (value === 'false') coerced = false;
|
|
878
|
+
else if (/^\d+$/.test(value)) coerced = parseInt(value, 10);
|
|
879
|
+
target[finalKey] = coerced;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Submit to mesh via NATS
|
|
883
|
+
const nc = await connect({ servers: NATS_URL, timeout: 5000 });
|
|
884
|
+
try {
|
|
885
|
+
const reply = await nc.request(
|
|
886
|
+
'mesh.plans.create',
|
|
887
|
+
sc.encode(JSON.stringify(plan)),
|
|
888
|
+
{ timeout: 10000 }
|
|
889
|
+
);
|
|
890
|
+
const result = JSON.parse(sc.decode(reply.data));
|
|
891
|
+
console.log(`Plan created: ${result.plan_id}`);
|
|
892
|
+
console.log(` Subtasks: ${result.subtasks.length}`);
|
|
893
|
+
console.log(` Waves: ${result.estimated_waves}`);
|
|
894
|
+
console.log(` Budget: ${result.total_budget_minutes}min`);
|
|
895
|
+
console.log(` Status: ${result.status}`);
|
|
896
|
+
if (result.requires_approval) {
|
|
897
|
+
console.log(`\n Approve with: mesh plan approve ${result.plan_id}`);
|
|
898
|
+
}
|
|
899
|
+
} finally {
|
|
900
|
+
await nc.close();
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
case 'list': {
|
|
906
|
+
let statusFilter = null;
|
|
907
|
+
for (let i = 1; i < args.length; i++) {
|
|
908
|
+
if (args[i] === '--status' && args[i + 1]) { statusFilter = args[++i]; }
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const nc = await connect({ servers: NATS_URL, timeout: 5000 });
|
|
912
|
+
try {
|
|
913
|
+
const payload = statusFilter ? { status: statusFilter } : {};
|
|
914
|
+
const reply = await nc.request(
|
|
915
|
+
'mesh.plans.list',
|
|
916
|
+
sc.encode(JSON.stringify(payload)),
|
|
917
|
+
{ timeout: 10000 }
|
|
918
|
+
);
|
|
919
|
+
const plans = JSON.parse(sc.decode(reply.data));
|
|
920
|
+
if (plans.length === 0) {
|
|
921
|
+
console.log('No plans found.');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
console.log(`Plans (${plans.length}):\n`);
|
|
926
|
+
for (const p of plans) {
|
|
927
|
+
const status = p.status.padEnd(12);
|
|
928
|
+
const subtasks = `${p.total_subtasks} subtasks`;
|
|
929
|
+
console.log(` ${p.plan_id} ${status} ${subtasks} "${p.title}"`);
|
|
930
|
+
}
|
|
931
|
+
} finally {
|
|
932
|
+
await nc.close();
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
case 'show': {
|
|
938
|
+
const planId = args[1];
|
|
939
|
+
if (!planId) {
|
|
940
|
+
console.error('Usage: mesh plan show <plan-id>');
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const nc = await connect({ servers: NATS_URL, timeout: 5000 });
|
|
945
|
+
try {
|
|
946
|
+
const reply = await nc.request(
|
|
947
|
+
'mesh.plans.get',
|
|
948
|
+
sc.encode(JSON.stringify({ plan_id: planId })),
|
|
949
|
+
{ timeout: 10000 }
|
|
950
|
+
);
|
|
951
|
+
const plan = JSON.parse(sc.decode(reply.data));
|
|
952
|
+
if (!plan || plan.error) {
|
|
953
|
+
console.error(plan?.error || `Plan not found: ${planId}`);
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Header
|
|
958
|
+
console.log(`\nPlan: ${plan.plan_id}`);
|
|
959
|
+
console.log(` Title: ${plan.title}`);
|
|
960
|
+
console.log(` Status: ${plan.status}`);
|
|
961
|
+
console.log(` Policy: ${plan.failure_policy || 'continue_best_effort'}`);
|
|
962
|
+
console.log(` Approval: ${plan.requires_approval ? 'required' : 'auto'}`);
|
|
963
|
+
if (plan.created_at) console.log(` Created: ${plan.created_at}`);
|
|
964
|
+
if (plan.parent_task_id) console.log(` Parent: ${plan.parent_task_id}`);
|
|
965
|
+
|
|
966
|
+
// Compute waves from subtask dependencies
|
|
967
|
+
const subtasks = plan.subtasks || [];
|
|
968
|
+
const waves = new Map();
|
|
969
|
+
for (const st of subtasks) {
|
|
970
|
+
const wave = st.wave ?? 0;
|
|
971
|
+
if (!waves.has(wave)) waves.set(wave, []);
|
|
972
|
+
waves.get(wave).push(st);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// If no wave field, group by dependency depth
|
|
976
|
+
if (waves.size <= 1 && subtasks.length > 1) {
|
|
977
|
+
waves.clear();
|
|
978
|
+
const idToSt = new Map(subtasks.map(s => [s.subtask_id, s]));
|
|
979
|
+
const depths = new Map();
|
|
980
|
+
function getDepth(id) {
|
|
981
|
+
if (depths.has(id)) return depths.get(id);
|
|
982
|
+
const st = idToSt.get(id);
|
|
983
|
+
if (!st || !st.depends_on || st.depends_on.length === 0) { depths.set(id, 0); return 0; }
|
|
984
|
+
const d = 1 + Math.max(...st.depends_on.map(dep => getDepth(dep)));
|
|
985
|
+
depths.set(id, d);
|
|
986
|
+
return d;
|
|
987
|
+
}
|
|
988
|
+
for (const st of subtasks) getDepth(st.subtask_id);
|
|
989
|
+
for (const st of subtasks) {
|
|
990
|
+
const w = depths.get(st.subtask_id) || 0;
|
|
991
|
+
if (!waves.has(w)) waves.set(w, []);
|
|
992
|
+
waves.get(w).push(st);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Render subtask tree
|
|
997
|
+
const sortedWaves = [...waves.keys()].sort((a, b) => a - b);
|
|
998
|
+
for (const w of sortedWaves) {
|
|
999
|
+
console.log(`\n ── Wave ${w} ${'─'.repeat(50)}`);
|
|
1000
|
+
for (const st of waves.get(w)) {
|
|
1001
|
+
const status = (st.status || 'pending').toUpperCase();
|
|
1002
|
+
const critical = st.critical ? ' [CRITICAL]' : '';
|
|
1003
|
+
const mode = st.delegation?.mode || 'auto';
|
|
1004
|
+
const reason = st.delegation?.reason ? ` (${st.delegation.reason})` : '';
|
|
1005
|
+
const budget = st.budget_minutes ? ` ${st.budget_minutes}min` : '';
|
|
1006
|
+
const metric = st.metric ? ` metric:"${st.metric}"` : '';
|
|
1007
|
+
const deps = st.depends_on?.length ? ` deps:[${st.depends_on.join(',')}]` : '';
|
|
1008
|
+
|
|
1009
|
+
console.log(` ${status.padEnd(10)} ${st.subtask_id}${critical}`);
|
|
1010
|
+
console.log(` "${st.title}"`);
|
|
1011
|
+
console.log(` route:${mode}${reason}${budget}${metric}${deps}`);
|
|
1012
|
+
|
|
1013
|
+
if (st.result) {
|
|
1014
|
+
const success = st.result.success ? '✓' : '✗';
|
|
1015
|
+
const summary = st.result.summary || '';
|
|
1016
|
+
console.log(` result: ${success} ${summary}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Summary
|
|
1022
|
+
const completed = subtasks.filter(s => s.status === 'completed').length;
|
|
1023
|
+
const failed = subtasks.filter(s => s.status === 'failed').length;
|
|
1024
|
+
const blocked = subtasks.filter(s => s.status === 'blocked').length;
|
|
1025
|
+
const pending = subtasks.filter(s => s.status === 'pending' || s.status === 'queued').length;
|
|
1026
|
+
const running = subtasks.filter(s => s.status === 'running').length;
|
|
1027
|
+
console.log(`\n Summary: ${subtasks.length} subtasks — ${completed} done, ${running} running, ${pending} pending, ${failed} failed, ${blocked} blocked`);
|
|
1028
|
+
|
|
1029
|
+
} finally {
|
|
1030
|
+
await nc.close();
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
case 'approve': {
|
|
1036
|
+
const planId = args[1];
|
|
1037
|
+
if (!planId) {
|
|
1038
|
+
console.error('Usage: mesh plan approve <plan-id>');
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const nc = await connect({ servers: NATS_URL, timeout: 5000 });
|
|
1043
|
+
try {
|
|
1044
|
+
const reply = await nc.request(
|
|
1045
|
+
'mesh.plans.approve',
|
|
1046
|
+
sc.encode(JSON.stringify({ plan_id: planId })),
|
|
1047
|
+
{ timeout: 10000 }
|
|
1048
|
+
);
|
|
1049
|
+
const result = JSON.parse(sc.decode(reply.data));
|
|
1050
|
+
console.log(`Plan approved: ${result.plan_id}`);
|
|
1051
|
+
console.log(` Status: ${result.status}`);
|
|
1052
|
+
console.log(` Wave 0 dispatched with ${result.subtasks.filter(s => s.status !== 'pending').length} subtasks`);
|
|
1053
|
+
} finally {
|
|
1054
|
+
await nc.close();
|
|
1055
|
+
}
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
case 'abort': {
|
|
1060
|
+
const planId = args[1];
|
|
1061
|
+
let reason = 'Manual abort';
|
|
1062
|
+
for (let i = 2; i < args.length; i++) {
|
|
1063
|
+
if (args[i] === '--reason' && args[i + 1]) { reason = args[++i]; }
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (!planId) {
|
|
1067
|
+
console.error('Usage: mesh plan abort <plan-id> [--reason "..."]');
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const nc = await connect({ servers: NATS_URL, timeout: 5000 });
|
|
1072
|
+
try {
|
|
1073
|
+
const reply = await nc.request(
|
|
1074
|
+
'mesh.plans.abort',
|
|
1075
|
+
sc.encode(JSON.stringify({ plan_id: planId, reason })),
|
|
1076
|
+
{ timeout: 10000 }
|
|
1077
|
+
);
|
|
1078
|
+
const result = JSON.parse(sc.decode(reply.data));
|
|
1079
|
+
console.log(`Plan aborted: ${result.plan_id}`);
|
|
1080
|
+
} finally {
|
|
1081
|
+
await nc.close();
|
|
1082
|
+
}
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
default:
|
|
1087
|
+
console.log([
|
|
1088
|
+
'',
|
|
1089
|
+
'mesh plan -- Plan management commands',
|
|
1090
|
+
'',
|
|
1091
|
+
' mesh plan templates List available templates',
|
|
1092
|
+
' mesh plan create --template <id> Create plan from template',
|
|
1093
|
+
' --context "<description>" Context for template variables',
|
|
1094
|
+
' --parent-task <task-id> Link to parent task',
|
|
1095
|
+
' --set subtask.field=value Override subtask fields post-instantiation',
|
|
1096
|
+
' e.g., --set implement.delegation.mode=collab_mesh',
|
|
1097
|
+
' --set test.budget_minutes=30',
|
|
1098
|
+
' mesh plan list List all plans',
|
|
1099
|
+
' --status <status> Filter by status',
|
|
1100
|
+
' mesh plan show <plan-id> Show full plan with subtask tree',
|
|
1101
|
+
' mesh plan approve <plan-id> Approve and start executing',
|
|
1102
|
+
' mesh plan abort <plan-id> Abort a plan',
|
|
1103
|
+
' --reason "..." Reason for abort',
|
|
1104
|
+
'',
|
|
1105
|
+
].join('\n'));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
724
1109
|
function cmdHelp() {
|
|
725
1110
|
console.log([
|
|
726
1111
|
'',
|
|
@@ -740,6 +1125,11 @@ function cmdHelp() {
|
|
|
740
1125
|
' cat task.yaml | mesh submit Submit from stdin',
|
|
741
1126
|
' mesh tasks List all mesh tasks',
|
|
742
1127
|
' mesh tasks --status running Filter mesh tasks by status',
|
|
1128
|
+
' mesh tasks review List tasks pending human review',
|
|
1129
|
+
' mesh tasks approve <task-id> Approve a pending_review task',
|
|
1130
|
+
' mesh tasks reject <task-id> Reject and re-queue a task',
|
|
1131
|
+
' --reason "..." Reason for rejection',
|
|
1132
|
+
' mesh plan <subcommand> Plan management (templates, create, approve)',
|
|
743
1133
|
' mesh health Health check this node',
|
|
744
1134
|
' mesh health --all Health check ALL nodes',
|
|
745
1135
|
' mesh health --json Health check (JSON output)',
|
|
@@ -780,6 +1170,7 @@ async function main() {
|
|
|
780
1170
|
case 'health': return cmdHealth(args);
|
|
781
1171
|
case 'repair': return cmdRepair(args);
|
|
782
1172
|
case 'deploy': return cmdDeploy(args);
|
|
1173
|
+
case 'plan': return cmdPlan(args);
|
|
783
1174
|
case 'help':
|
|
784
1175
|
case '--help':
|
|
785
1176
|
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
|