openclaw-node-harness 2.0.4 → 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.
- package/README.md +646 -3
- package/bin/hyperagent.mjs +419 -0
- package/bin/mesh-agent.js +401 -12
- package/bin/mesh-bridge.js +66 -1
- package/bin/mesh-task-daemon.js +816 -26
- package/bin/mesh.js +403 -1
- 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 +293 -8
- package/lib/circling-parser.js +119 -0
- package/lib/hyperagent-store.mjs +652 -0
- package/lib/kanban-io.js +9 -0
- package/lib/mcp-knowledge/bench.mjs +118 -0
- package/lib/mcp-knowledge/core.mjs +528 -0
- package/lib/mcp-knowledge/package.json +25 -0
- package/lib/mcp-knowledge/server.mjs +245 -0
- package/lib/mcp-knowledge/test.mjs +802 -0
- package/lib/memory-budget.mjs +261 -0
- package/lib/mesh-collab.js +301 -1
- package/lib/mesh-harness.js +427 -0
- package/lib/mesh-plans.js +13 -5
- package/lib/mesh-tasks.js +67 -0
- package/lib/plan-templates.js +226 -0
- package/lib/pre-compression-flush.mjs +320 -0
- package/lib/role-loader.js +292 -0
- package/lib/rule-loader.js +358 -0
- package/lib/session-store.mjs +458 -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/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/tasks/[id]/handoff/route.ts +1 -1
- 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/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 +58 -0
- package/mission-control/src/lib/db/index.ts +69 -0
- 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/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/tsconfig.json +2 -1
- package/mission-control/vitest.config.ts +14 -0
- package/package.json +15 -2
- package/services/service-manifest.json +1 -1
- package/skills/cc-godmode/references/agents.md +8 -8
- package/workspace-bin/memory-daemon.mjs +199 -5
- package/workspace-bin/session-search.mjs +204 -0
- package/workspace-bin/web-fetch.mjs +65 -0
|
@@ -35,6 +35,11 @@ export interface Task {
|
|
|
35
35
|
metric: string | null;
|
|
36
36
|
budgetMinutes: number | null;
|
|
37
37
|
scope: string | null;
|
|
38
|
+
// Collab routing fields
|
|
39
|
+
collaboration: string | null;
|
|
40
|
+
preferredNodes: string | null;
|
|
41
|
+
excludeNodes: string | null;
|
|
42
|
+
clusterId: string | null;
|
|
38
43
|
showInCalendar: number | null;
|
|
39
44
|
acknowledgedAt: string | null;
|
|
40
45
|
updatedAt: string;
|
|
@@ -451,6 +456,213 @@ export function useBurndown(projectId: string | null) {
|
|
|
451
456
|
return { burndown: data ?? null, error, isLoading };
|
|
452
457
|
}
|
|
453
458
|
|
|
459
|
+
// --- Cowork: Clusters & Collab Sessions ---
|
|
460
|
+
|
|
461
|
+
export interface ClusterMemberView {
|
|
462
|
+
id: number;
|
|
463
|
+
nodeId: string;
|
|
464
|
+
role: string;
|
|
465
|
+
nodeStatus: string;
|
|
466
|
+
createdAt: string;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface ClusterView {
|
|
470
|
+
id: string;
|
|
471
|
+
name: string;
|
|
472
|
+
description: string | null;
|
|
473
|
+
color: string | null;
|
|
474
|
+
defaultMode: string;
|
|
475
|
+
defaultConvergence: string;
|
|
476
|
+
convergenceThreshold: number;
|
|
477
|
+
maxRounds: number;
|
|
478
|
+
status: string;
|
|
479
|
+
members: ClusterMemberView[];
|
|
480
|
+
updatedAt: string;
|
|
481
|
+
createdAt: string;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export interface CollabNode {
|
|
485
|
+
node_id: string;
|
|
486
|
+
role: string;
|
|
487
|
+
scope: string[];
|
|
488
|
+
joined_at: string;
|
|
489
|
+
status: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export interface CollabReflection {
|
|
493
|
+
node_id: string;
|
|
494
|
+
summary: string;
|
|
495
|
+
learnings: string;
|
|
496
|
+
artifacts: string[];
|
|
497
|
+
confidence: number;
|
|
498
|
+
vote: string;
|
|
499
|
+
parse_failed?: boolean;
|
|
500
|
+
synthetic?: boolean;
|
|
501
|
+
submitted_at: string;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export interface CollabRound {
|
|
505
|
+
round_number: number;
|
|
506
|
+
started_at: string;
|
|
507
|
+
completed_at: string | null;
|
|
508
|
+
shared_intel: string;
|
|
509
|
+
reflections: CollabReflection[];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export interface CollabSession {
|
|
513
|
+
session_id: string;
|
|
514
|
+
task_id: string;
|
|
515
|
+
mode: string;
|
|
516
|
+
status: string;
|
|
517
|
+
min_nodes: number;
|
|
518
|
+
max_nodes: number | null;
|
|
519
|
+
nodes: CollabNode[];
|
|
520
|
+
current_round: number;
|
|
521
|
+
max_rounds: number;
|
|
522
|
+
rounds: CollabRound[];
|
|
523
|
+
convergence: {
|
|
524
|
+
type: string;
|
|
525
|
+
threshold: number;
|
|
526
|
+
metric: string | null;
|
|
527
|
+
min_quorum: number;
|
|
528
|
+
};
|
|
529
|
+
scope_strategy: string;
|
|
530
|
+
result: Record<string, unknown> | null;
|
|
531
|
+
audit_log: Array<{ ts: string; event: string; [k: string]: unknown }>;
|
|
532
|
+
created_at: string;
|
|
533
|
+
completed_at: string | null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function useClusters() {
|
|
537
|
+
const { data, error, isLoading } = useSWR<{ clusters: ClusterView[] }>(
|
|
538
|
+
"/api/cowork/clusters",
|
|
539
|
+
fetcher,
|
|
540
|
+
{ refreshInterval: 10000 }
|
|
541
|
+
);
|
|
542
|
+
return { clusters: data?.clusters ?? [], error, isLoading };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function useCollabSessions(status?: string, refreshInterval = 5000) {
|
|
546
|
+
const params = status ? `?status=${status}` : "";
|
|
547
|
+
const { data, error, isLoading } = useSWR<{
|
|
548
|
+
sessions: CollabSession[];
|
|
549
|
+
natsAvailable: boolean;
|
|
550
|
+
}>(`/api/cowork/sessions${params}`, fetcher, { refreshInterval });
|
|
551
|
+
return {
|
|
552
|
+
sessions: data?.sessions ?? [],
|
|
553
|
+
natsAvailable: data?.natsAvailable ?? true,
|
|
554
|
+
error,
|
|
555
|
+
isLoading,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function createCluster(params: {
|
|
560
|
+
name: string;
|
|
561
|
+
description?: string;
|
|
562
|
+
color?: string;
|
|
563
|
+
defaultMode?: string;
|
|
564
|
+
defaultConvergence?: string;
|
|
565
|
+
convergenceThreshold?: number;
|
|
566
|
+
maxRounds?: number;
|
|
567
|
+
members?: Array<{ nodeId: string; role: string }>;
|
|
568
|
+
}) {
|
|
569
|
+
const res = await fetch("/api/cowork/clusters", {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { "Content-Type": "application/json" },
|
|
572
|
+
body: JSON.stringify(params),
|
|
573
|
+
});
|
|
574
|
+
const data = await res.json();
|
|
575
|
+
await mutate("/api/cowork/clusters");
|
|
576
|
+
return data;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export async function updateCluster(id: string, updates: Record<string, unknown>) {
|
|
580
|
+
const res = await fetch(`/api/cowork/clusters/${id}`, {
|
|
581
|
+
method: "PATCH",
|
|
582
|
+
headers: { "Content-Type": "application/json" },
|
|
583
|
+
body: JSON.stringify(updates),
|
|
584
|
+
});
|
|
585
|
+
const data = await res.json();
|
|
586
|
+
await mutate("/api/cowork/clusters");
|
|
587
|
+
return data;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function deleteCluster(id: string) {
|
|
591
|
+
await fetch(`/api/cowork/clusters/${id}`, { method: "DELETE" });
|
|
592
|
+
await mutate("/api/cowork/clusters");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function addClusterMember(clusterId: string, nodeId: string, role: string) {
|
|
596
|
+
const res = await fetch(`/api/cowork/clusters/${clusterId}/members`, {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers: { "Content-Type": "application/json" },
|
|
599
|
+
body: JSON.stringify({ nodeId, role }),
|
|
600
|
+
});
|
|
601
|
+
const data = await res.json();
|
|
602
|
+
await mutate("/api/cowork/clusters");
|
|
603
|
+
return data;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export async function updateClusterMember(clusterId: string, nodeId: string, role: string) {
|
|
607
|
+
const res = await fetch(`/api/cowork/clusters/${clusterId}/members`, {
|
|
608
|
+
method: "PATCH",
|
|
609
|
+
headers: { "Content-Type": "application/json" },
|
|
610
|
+
body: JSON.stringify({ nodeId, role }),
|
|
611
|
+
});
|
|
612
|
+
const data = await res.json();
|
|
613
|
+
await mutate("/api/cowork/clusters");
|
|
614
|
+
return data;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export async function removeClusterMember(clusterId: string, nodeId: string) {
|
|
618
|
+
await fetch(
|
|
619
|
+
`/api/cowork/clusters/${clusterId}/members?nodeId=${encodeURIComponent(nodeId)}`,
|
|
620
|
+
{ method: "DELETE" }
|
|
621
|
+
);
|
|
622
|
+
await mutate("/api/cowork/clusters");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function dispatchCollabTask(params: {
|
|
626
|
+
title: string;
|
|
627
|
+
description?: string;
|
|
628
|
+
clusterId?: string;
|
|
629
|
+
nodes?: Array<{ nodeId: string; role: string }>;
|
|
630
|
+
mode?: string;
|
|
631
|
+
convergence?: { type: string; threshold?: number; metric?: string };
|
|
632
|
+
scopeStrategy?: string;
|
|
633
|
+
budgetMinutes?: number;
|
|
634
|
+
maxRounds?: number;
|
|
635
|
+
metric?: string;
|
|
636
|
+
scope?: string[];
|
|
637
|
+
}) {
|
|
638
|
+
const res = await fetch("/api/cowork/dispatch", {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: { "Content-Type": "application/json" },
|
|
641
|
+
body: JSON.stringify(params),
|
|
642
|
+
});
|
|
643
|
+
const data = await res.json();
|
|
644
|
+
await mutate("/api/cowork/sessions");
|
|
645
|
+
await mutate("/api/cowork/clusters");
|
|
646
|
+
await mutate("/api/tasks");
|
|
647
|
+
return data;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function interveneSession(params: {
|
|
651
|
+
action: "abort" | "force_converge" | "remove_node";
|
|
652
|
+
sessionId: string;
|
|
653
|
+
nodeId?: string;
|
|
654
|
+
}) {
|
|
655
|
+
const res = await fetch("/api/cowork/intervene", {
|
|
656
|
+
method: "POST",
|
|
657
|
+
headers: { "Content-Type": "application/json" },
|
|
658
|
+
body: JSON.stringify(params),
|
|
659
|
+
});
|
|
660
|
+
const data = await res.json();
|
|
661
|
+
await mutate("/api/cowork/sessions");
|
|
662
|
+
await mutate("/api/tasks");
|
|
663
|
+
return data;
|
|
664
|
+
}
|
|
665
|
+
|
|
454
666
|
// --- Mesh SSE integration ---
|
|
455
667
|
|
|
456
668
|
/**
|
|
@@ -470,6 +682,14 @@ export function useMeshSSE() {
|
|
|
470
682
|
mutate("/api/activity");
|
|
471
683
|
};
|
|
472
684
|
|
|
685
|
+
const invalidateCollab = () => {
|
|
686
|
+
mutate("/api/cowork/sessions");
|
|
687
|
+
mutate("/api/cowork/clusters");
|
|
688
|
+
mutate("/api/tasks");
|
|
689
|
+
mutate("/api/activity");
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// Task events
|
|
473
693
|
es.addEventListener("completed", invalidate);
|
|
474
694
|
es.addEventListener("claimed", invalidate);
|
|
475
695
|
es.addEventListener("started", invalidate);
|
|
@@ -478,6 +698,22 @@ export function useMeshSSE() {
|
|
|
478
698
|
es.addEventListener("released", invalidate);
|
|
479
699
|
es.addEventListener("cancelled", invalidate);
|
|
480
700
|
|
|
701
|
+
// Collab events (mesh.events.collab.* → event type is "collab.{action}")
|
|
702
|
+
es.addEventListener("collab.created", invalidateCollab);
|
|
703
|
+
es.addEventListener("collab.joined", invalidateCollab);
|
|
704
|
+
es.addEventListener("collab.round_started", invalidateCollab);
|
|
705
|
+
es.addEventListener("collab.reflection_received", invalidateCollab);
|
|
706
|
+
es.addEventListener("collab.converged", invalidateCollab);
|
|
707
|
+
es.addEventListener("collab.completed", invalidateCollab);
|
|
708
|
+
es.addEventListener("collab.aborted", invalidateCollab);
|
|
709
|
+
es.addEventListener("collab.node_removed", invalidateCollab);
|
|
710
|
+
|
|
711
|
+
// KV task state changes (from dual-iterator watcher)
|
|
712
|
+
es.addEventListener("kv.task.updated", () => {
|
|
713
|
+
mutate("/api/mesh/tasks");
|
|
714
|
+
mutate("/api/tasks");
|
|
715
|
+
});
|
|
716
|
+
|
|
481
717
|
es.onerror = () => {
|
|
482
718
|
// EventSource auto-reconnects
|
|
483
719
|
};
|
|
@@ -521,6 +757,79 @@ export function useTokenUsage(period: "today" | "week" | "month" = "today") {
|
|
|
521
757
|
return { tokenData: data ?? null, error, isLoading };
|
|
522
758
|
}
|
|
523
759
|
|
|
760
|
+
// --- Mesh Tasks (Distributed MC) ---
|
|
761
|
+
|
|
762
|
+
export interface MeshTask {
|
|
763
|
+
task_id: string;
|
|
764
|
+
title: string;
|
|
765
|
+
description: string;
|
|
766
|
+
status: string;
|
|
767
|
+
origin: string;
|
|
768
|
+
owner: string | null;
|
|
769
|
+
priority: number;
|
|
770
|
+
budget_minutes: number;
|
|
771
|
+
metric: string | null;
|
|
772
|
+
created_at: string;
|
|
773
|
+
[key: string]: unknown;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function useMeshTasks() {
|
|
777
|
+
const { data, error, isLoading } = useSWR<{
|
|
778
|
+
tasks: MeshTask[];
|
|
779
|
+
natsAvailable: boolean;
|
|
780
|
+
}>("/api/mesh/tasks", fetcher, {
|
|
781
|
+
refreshInterval: 5000,
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
meshTasks: data?.tasks ?? [],
|
|
785
|
+
natsAvailable: data?.natsAvailable ?? false,
|
|
786
|
+
error,
|
|
787
|
+
isLoading,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function useNodeIdentity() {
|
|
792
|
+
const { data, error, isLoading } = useSWR<{
|
|
793
|
+
nodeId: string;
|
|
794
|
+
role: "lead" | "worker";
|
|
795
|
+
platform: string;
|
|
796
|
+
}>("/api/mesh/identity", fetcher);
|
|
797
|
+
return { identity: data ?? null, error, isLoading };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export async function createMeshTask(task: {
|
|
801
|
+
title: string;
|
|
802
|
+
description?: string;
|
|
803
|
+
priority?: number;
|
|
804
|
+
budget_minutes?: number;
|
|
805
|
+
metric?: string;
|
|
806
|
+
[key: string]: unknown;
|
|
807
|
+
}) {
|
|
808
|
+
const res = await fetch("/api/mesh/tasks", {
|
|
809
|
+
method: "POST",
|
|
810
|
+
headers: { "Content-Type": "application/json" },
|
|
811
|
+
body: JSON.stringify(task),
|
|
812
|
+
});
|
|
813
|
+
const data = await res.json();
|
|
814
|
+
await mutate("/api/mesh/tasks");
|
|
815
|
+
return data;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export async function updateMeshTask(
|
|
819
|
+
id: string,
|
|
820
|
+
updates: Record<string, unknown>,
|
|
821
|
+
revision: number
|
|
822
|
+
) {
|
|
823
|
+
const res = await fetch(`/api/mesh/tasks/${id}`, {
|
|
824
|
+
method: "PATCH",
|
|
825
|
+
headers: { "Content-Type": "application/json" },
|
|
826
|
+
body: JSON.stringify({ ...updates, revision }),
|
|
827
|
+
});
|
|
828
|
+
const data = await res.json();
|
|
829
|
+
await mutate("/api/mesh/tasks");
|
|
830
|
+
return data;
|
|
831
|
+
}
|
|
832
|
+
|
|
524
833
|
// --- Scheduler ---
|
|
525
834
|
|
|
526
835
|
export function useSchedulerTick(intervalMs = 30_000) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getRawDb } from "../db";
|
|
15
|
+
import { AGENT_NAME, HUMAN_NAME } from "../config";
|
|
15
16
|
|
|
16
17
|
// ── Types ──
|
|
17
18
|
|
|
@@ -32,8 +33,8 @@ export interface RelationMatch {
|
|
|
32
33
|
// Bootstrap the graph with known entities. The extraction loop will discover new ones.
|
|
33
34
|
|
|
34
35
|
const KNOWN_ENTITIES: EntityMatch[] = [
|
|
35
|
-
{ name:
|
|
36
|
-
{ name:
|
|
36
|
+
{ name: HUMAN_NAME, type: "person" },
|
|
37
|
+
{ name: AGENT_NAME, type: "person" },
|
|
37
38
|
{ name: "Arcane", type: "project", aliases: ["Arcane Rapture", "arcane-rapture"] },
|
|
38
39
|
{ name: "Mission Control", type: "project", aliases: ["MC", "mission-control"] },
|
|
39
40
|
{ name: "OpenClaw", type: "project", aliases: ["openclaw"] },
|
|
@@ -11,6 +11,8 @@ interface NatsGlobals {
|
|
|
11
11
|
__nats_nc?: NatsConnection | null;
|
|
12
12
|
__nats_connectingSince?: number | null;
|
|
13
13
|
__nats_healthKv?: KV | null;
|
|
14
|
+
__nats_collabKv?: KV | null;
|
|
15
|
+
__nats_tasksKv?: KV | null;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const g = globalThis as unknown as NatsGlobals;
|
|
@@ -45,6 +47,7 @@ const NATS_URL = resolveNatsUrl();
|
|
|
45
47
|
|
|
46
48
|
// KV bucket name — nodes write here, MC reads
|
|
47
49
|
const HEALTH_BUCKET = "MESH_NODE_HEALTH";
|
|
50
|
+
const TASKS_BUCKET = "MESH_TASKS";
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Get or create a singleton NATS connection.
|
|
@@ -73,16 +76,22 @@ export async function getNats(): Promise<NatsConnection | null> {
|
|
|
73
76
|
});
|
|
74
77
|
console.log("[nats] connected to", NATS_URL);
|
|
75
78
|
|
|
76
|
-
// Reset KV
|
|
79
|
+
// Reset KV handles on reconnect so they're re-fetched fresh
|
|
77
80
|
g.__nats_healthKv = null;
|
|
81
|
+
g.__nats_collabKv = null;
|
|
82
|
+
g.__nats_tasksKv = null;
|
|
78
83
|
|
|
79
84
|
g.__nats_nc.closed().then(() => {
|
|
80
85
|
console.log("[nats] connection closed — will reconnect on next request");
|
|
81
86
|
g.__nats_nc = null;
|
|
82
87
|
g.__nats_healthKv = null;
|
|
88
|
+
g.__nats_collabKv = null;
|
|
89
|
+
g.__nats_tasksKv = null;
|
|
83
90
|
}).catch(() => {
|
|
84
91
|
g.__nats_nc = null;
|
|
85
92
|
g.__nats_healthKv = null;
|
|
93
|
+
g.__nats_collabKv = null;
|
|
94
|
+
g.__nats_tasksKv = null;
|
|
86
95
|
});
|
|
87
96
|
|
|
88
97
|
return g.__nats_nc;
|
|
@@ -123,4 +132,60 @@ export async function getHealthKv(): Promise<KV | null> {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
// KV bucket for collab sessions — daemon writes, MC reads
|
|
136
|
+
const COLLAB_BUCKET = "MESH_COLLAB";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the MESH_COLLAB KV bucket (JetStream).
|
|
140
|
+
*
|
|
141
|
+
* Collab sessions are written by mesh-collab-daemon, MC reads only.
|
|
142
|
+
* No TTL — sessions are managed by daemon lifecycle, not expiry.
|
|
143
|
+
* Returns null if NATS is unavailable.
|
|
144
|
+
*/
|
|
145
|
+
export async function getCollabKv(): Promise<KV | null> {
|
|
146
|
+
if (g.__nats_collabKv) return g.__nats_collabKv;
|
|
147
|
+
|
|
148
|
+
const conn = await getNats();
|
|
149
|
+
if (!conn) return null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const js = conn.jetstream();
|
|
153
|
+
g.__nats_collabKv = await js.views.kv(COLLAB_BUCKET, {
|
|
154
|
+
history: 1,
|
|
155
|
+
});
|
|
156
|
+
return g.__nats_collabKv;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error("[nats] Collab KV bucket error:", (err as Error).message);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the MESH_TASKS KV bucket (JetStream).
|
|
165
|
+
*
|
|
166
|
+
* Tasks are coordinated through this bucket — the lead writes,
|
|
167
|
+
* workers read via watch. On worker nodes, this enables the
|
|
168
|
+
* Kanban to display mesh tasks in real-time.
|
|
169
|
+
*
|
|
170
|
+
* Creates the bucket on first call if it doesn't exist (idempotent).
|
|
171
|
+
* Returns null if NATS is unavailable.
|
|
172
|
+
*/
|
|
173
|
+
export async function getTasksKv(): Promise<KV | null> {
|
|
174
|
+
if (g.__nats_tasksKv) return g.__nats_tasksKv;
|
|
175
|
+
|
|
176
|
+
const conn = await getNats();
|
|
177
|
+
if (!conn) return null;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const js = conn.jetstream();
|
|
181
|
+
g.__nats_tasksKv = await js.views.kv(TASKS_BUCKET, {
|
|
182
|
+
history: 1,
|
|
183
|
+
});
|
|
184
|
+
return g.__nats_tasksKv;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error("[nats] Tasks KV bucket error:", (err as Error).message);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
126
191
|
export { sc, StringCodec };
|
|
@@ -36,6 +36,11 @@ export interface ParsedTask {
|
|
|
36
36
|
metric: string | null;
|
|
37
37
|
budgetMinutes: number;
|
|
38
38
|
scope: string[];
|
|
39
|
+
// Collab routing fields
|
|
40
|
+
collaboration: Record<string, unknown> | null;
|
|
41
|
+
preferredNodes: string[];
|
|
42
|
+
excludeNodes: string[];
|
|
43
|
+
clusterId: string | null;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/* ------------------------------------------------------------------ */
|
|
@@ -51,6 +56,7 @@ const STATUS_TO_KANBAN: Record<string, string> = {
|
|
|
51
56
|
"waiting-user": "review",
|
|
52
57
|
done: "done",
|
|
53
58
|
cancelled: "done",
|
|
59
|
+
archived: "done", // hidden from view but still in done column if shown
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
const KANBAN_TO_STATUS: Record<string, string> = {
|
|
@@ -90,7 +96,7 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
|
|
|
90
96
|
const lines = liveSection.split("\n");
|
|
91
97
|
|
|
92
98
|
let current: Partial<ParsedTask> | null = null;
|
|
93
|
-
let currentArrayKey: "successCriteria" | "artifacts" | "scope" | null = null;
|
|
99
|
+
let currentArrayKey: "successCriteria" | "artifacts" | "scope" | "preferredNodes" | "excludeNodes" | null = null;
|
|
94
100
|
|
|
95
101
|
function flush() {
|
|
96
102
|
if (current && current.id) {
|
|
@@ -125,6 +131,10 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
|
|
|
125
131
|
metric: current.metric ?? null,
|
|
126
132
|
budgetMinutes: current.budgetMinutes ?? 30,
|
|
127
133
|
scope: current.scope ?? [],
|
|
134
|
+
collaboration: current.collaboration ?? null,
|
|
135
|
+
preferredNodes: current.preferredNodes ?? [],
|
|
136
|
+
excludeNodes: current.excludeNodes ?? [],
|
|
137
|
+
clusterId: current.clusterId ?? null,
|
|
128
138
|
});
|
|
129
139
|
}
|
|
130
140
|
}
|
|
@@ -134,7 +144,7 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
|
|
|
134
144
|
const taskIdMatch = line.match(/^- task_id:\s*(.+)$/);
|
|
135
145
|
if (taskIdMatch) {
|
|
136
146
|
flush();
|
|
137
|
-
current = { id: taskIdMatch[1].trim(), successCriteria: [], artifacts: [], scope: [] };
|
|
147
|
+
current = { id: taskIdMatch[1].trim(), successCriteria: [], artifacts: [], scope: [], preferredNodes: [], excludeNodes: [] };
|
|
138
148
|
currentArrayKey = null;
|
|
139
149
|
continue;
|
|
140
150
|
}
|
|
@@ -285,6 +295,27 @@ export function parseTasksMarkdown(content: string): ParsedTask[] {
|
|
|
285
295
|
current.scope = [];
|
|
286
296
|
currentArrayKey = "scope";
|
|
287
297
|
break;
|
|
298
|
+
// Collab routing fields
|
|
299
|
+
case "collaboration":
|
|
300
|
+
try {
|
|
301
|
+
current.collaboration = value ? JSON.parse(value) : null;
|
|
302
|
+
} catch {
|
|
303
|
+
current.collaboration = null;
|
|
304
|
+
}
|
|
305
|
+
currentArrayKey = null;
|
|
306
|
+
break;
|
|
307
|
+
case "preferred_nodes":
|
|
308
|
+
current.preferredNodes = [];
|
|
309
|
+
currentArrayKey = "preferredNodes";
|
|
310
|
+
break;
|
|
311
|
+
case "exclude_nodes":
|
|
312
|
+
current.excludeNodes = [];
|
|
313
|
+
currentArrayKey = "excludeNodes";
|
|
314
|
+
break;
|
|
315
|
+
case "cluster_id":
|
|
316
|
+
current.clusterId = value || null;
|
|
317
|
+
currentArrayKey = null;
|
|
318
|
+
break;
|
|
288
319
|
case "updated_at":
|
|
289
320
|
current.updatedAt = value;
|
|
290
321
|
currentArrayKey = null;
|
|
@@ -450,6 +481,25 @@ export function serializeTasksMarkdown(tasks: ParsedTask[]): string {
|
|
|
450
481
|
lines.push(` - ${s}`);
|
|
451
482
|
}
|
|
452
483
|
}
|
|
484
|
+
// Collab routing fields
|
|
485
|
+
if (t.collaboration) {
|
|
486
|
+
lines.push(` collaboration: ${JSON.stringify(t.collaboration)}`);
|
|
487
|
+
}
|
|
488
|
+
if (t.preferredNodes && t.preferredNodes.length > 0) {
|
|
489
|
+
lines.push(" preferred_nodes:");
|
|
490
|
+
for (const n of t.preferredNodes) {
|
|
491
|
+
lines.push(` - ${n}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (t.excludeNodes && t.excludeNodes.length > 0) {
|
|
495
|
+
lines.push(" exclude_nodes:");
|
|
496
|
+
for (const n of t.excludeNodes) {
|
|
497
|
+
lines.push(` - ${n}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (t.clusterId) {
|
|
501
|
+
lines.push(` cluster_id: ${t.clusterId}`);
|
|
502
|
+
}
|
|
453
503
|
lines.push(` updated_at: ${t.updatedAt}`);
|
|
454
504
|
|
|
455
505
|
return lines.join("\n");
|
|
@@ -15,10 +15,10 @@ export interface TranscriptEvent {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Encode a workspace path to
|
|
18
|
+
* Encode a workspace path to the agent CLI's project directory format.
|
|
19
19
|
* Same algorithm as agent-activity.js: strip leading /, replace / and . with -
|
|
20
20
|
*/
|
|
21
|
-
function
|
|
21
|
+
function toAgentProjectPath(workspacePath: string): string {
|
|
22
22
|
return workspacePath.replace(/\\/g, "/").replace(/:/g, "").replace(/[/.]/g, "-");
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -29,8 +29,8 @@ const WORKSPACE = process.env.WORKSPACE_ROOT || path.join(HOME, ".openclaw/works
|
|
|
29
29
|
* Transcript source directories — scans ALL frontend transcript stores.
|
|
30
30
|
*/
|
|
31
31
|
const TRANSCRIPT_DIRS = [
|
|
32
|
-
//
|
|
33
|
-
path.join(HOME, ".claude/projects",
|
|
32
|
+
// Agent CLI workspace sessions (cross-platform path encoding)
|
|
33
|
+
path.join(HOME, ".claude/projects", toAgentProjectPath(WORKSPACE)),
|
|
34
34
|
// OpenClaw Gateway sessions (Discord, Telegram, etc.)
|
|
35
35
|
path.join(HOME, ".openclaw/agents/main/sessions"),
|
|
36
36
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq, and, ne, or, lte, like, desc } from "drizzle-orm";
|
|
1
|
+
import { eq, and, ne, or, lte, like, desc, isNull } from "drizzle-orm";
|
|
2
2
|
import { CronExpressionParser } from "cron-parser";
|
|
3
3
|
import { writeFileSync, mkdirSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
@@ -7,7 +7,7 @@ import { tasks, dependencies } from "./db/schema";
|
|
|
7
7
|
import { statusToKanban } from "./parsers/task-markdown";
|
|
8
8
|
import { syncTasksToMarkdown } from "./sync/tasks";
|
|
9
9
|
import { logActivity } from "./activity";
|
|
10
|
-
import { WORKSPACE_ROOT } from "./config";
|
|
10
|
+
import { WORKSPACE_ROOT, AGENT_NAME, DISPATCH_SIGNAL_FILE } from "./config";
|
|
11
11
|
import { gatewayNotify } from "./gateway-notify";
|
|
12
12
|
|
|
13
13
|
|
|
@@ -238,23 +238,23 @@ export function schedulerTick(): TickResult {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// --- Phase 2: Single-task dispatch ---
|
|
241
|
-
// Rule: ONE auto-start task at a time.
|
|
241
|
+
// Rule: ONE auto-start task at a time. The agent owns it and works autonomously.
|
|
242
242
|
// Next task only dispatched when current is done, blocked, or waiting-user.
|
|
243
243
|
|
|
244
|
-
// Check if
|
|
245
|
-
const
|
|
244
|
+
// Check if the agent already has an active auto-dispatched task
|
|
245
|
+
const agentRunning = db
|
|
246
246
|
.select()
|
|
247
247
|
.from(tasks)
|
|
248
248
|
.where(
|
|
249
249
|
and(
|
|
250
250
|
eq(tasks.status, "running"),
|
|
251
|
-
eq(tasks.owner,
|
|
251
|
+
eq(tasks.owner, AGENT_NAME),
|
|
252
252
|
ne(tasks.id, "__LIVE_SESSION__")
|
|
253
253
|
)
|
|
254
254
|
)
|
|
255
255
|
.all();
|
|
256
256
|
|
|
257
|
-
const hasActiveTask =
|
|
257
|
+
const hasActiveTask = agentRunning.length > 0;
|
|
258
258
|
|
|
259
259
|
// Dispatchable: needs_approval=0 AND (status="ready" OR (status="queued" AND trigger_kind="none"))
|
|
260
260
|
const dispatchable = db
|
|
@@ -263,6 +263,7 @@ export function schedulerTick(): TickResult {
|
|
|
263
263
|
.where(
|
|
264
264
|
and(
|
|
265
265
|
eq(tasks.needsApproval, 0),
|
|
266
|
+
or(isNull(tasks.execution), ne(tasks.execution, "mesh")),
|
|
266
267
|
or(
|
|
267
268
|
eq(tasks.status, "ready"),
|
|
268
269
|
and(eq(tasks.status, "queued"), eq(tasks.triggerKind, "none"))
|
|
@@ -308,7 +309,7 @@ export function schedulerTick(): TickResult {
|
|
|
308
309
|
.set({
|
|
309
310
|
status: "running",
|
|
310
311
|
kanbanColumn: statusToKanban("running"),
|
|
311
|
-
owner:
|
|
312
|
+
owner: AGENT_NAME,
|
|
312
313
|
updatedAt: now.toISOString(),
|
|
313
314
|
})
|
|
314
315
|
.where(eq(tasks.id, next.id))
|
|
@@ -322,7 +323,7 @@ export function schedulerTick(): TickResult {
|
|
|
322
323
|
);
|
|
323
324
|
|
|
324
325
|
// Notify via gateway message (TUI chat)
|
|
325
|
-
const notifyText = `MC-Kanban: NEW AUTOTASK FOR
|
|
326
|
+
const notifyText = `MC-Kanban: NEW AUTOTASK FOR ${AGENT_NAME.toUpperCase()} READY: "${next.title}"`;
|
|
326
327
|
gatewayNotify(notifyText).catch((err) => {
|
|
327
328
|
console.error("MC gateway notify failed:", err);
|
|
328
329
|
});
|
|
@@ -332,7 +333,7 @@ export function schedulerTick(): TickResult {
|
|
|
332
333
|
const signalDir = path.join(WORKSPACE_ROOT, ".tmp");
|
|
333
334
|
mkdirSync(signalDir, { recursive: true });
|
|
334
335
|
writeFileSync(
|
|
335
|
-
path.join(signalDir,
|
|
336
|
+
path.join(signalDir, DISPATCH_SIGNAL_FILE),
|
|
336
337
|
JSON.stringify({
|
|
337
338
|
taskId: next.id,
|
|
338
339
|
title: next.title,
|
|
@@ -350,7 +351,7 @@ export function schedulerTick(): TickResult {
|
|
|
350
351
|
result.skipped.push(t.id);
|
|
351
352
|
}
|
|
352
353
|
} else {
|
|
353
|
-
// All eligible tasks stay in backlog (
|
|
354
|
+
// All eligible tasks stay in backlog (agent busy or nothing to dispatch)
|
|
354
355
|
for (const t of eligible) {
|
|
355
356
|
result.skipped.push(t.id);
|
|
356
357
|
}
|