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
@@ -0,0 +1,292 @@
1
+ /**
2
+ * transcript-parser.mjs — Format-agnostic JSONL transcript parser
3
+ *
4
+ * Abstracts away the differences between JSONL formats produced by
5
+ * different OpenClaw frontends:
6
+ *
7
+ * - claude-code: {type: "user"|"assistant", message: {role, content}, timestamp, usage}
8
+ * - openclaw-gateway: {type: "message", message: {role, content}, timestamp}
9
+ * (plus metadata types: session, model_change, etc.)
10
+ * - mesh-agent: Raw text stdout/stderr (non-JSONL, handled separately)
11
+ *
12
+ * Provides a unified stream of { role, content, timestamp } messages
13
+ * regardless of source format.
14
+ *
15
+ * Usage:
16
+ * import { parseJsonl, parseJsonlFile, detectFormat } from './transcript-parser.mjs';
17
+ *
18
+ * // Parse a JSONL file with auto-detected format
19
+ * const messages = await parseJsonlFile('/path/to/session.jsonl');
20
+ *
21
+ * // Parse with explicit format
22
+ * const messages = await parseJsonlFile(path, { format: 'openclaw-gateway' });
23
+ *
24
+ * // Parse a single JSONL line
25
+ * const msg = parseLine(jsonString, { format: 'claude-code' });
26
+ */
27
+
28
+ import fs from 'fs';
29
+ import { createReadStream } from 'fs';
30
+ import { createInterface } from 'readline';
31
+
32
+ // ── Format Registry ────────────────────────────────────
33
+
34
+ /**
35
+ * Adapters indexed by format name.
36
+ * Each adapter has:
37
+ * - isMessage(entry): boolean — does this JSONL entry represent a conversation message?
38
+ * - extractMessage(entry): { role, content, timestamp, metadata? } | null
39
+ */
40
+ const ADAPTERS = {};
41
+
42
+ /**
43
+ * Register a new format adapter.
44
+ * @param {string} name - Format identifier (e.g. 'claude-code', 'openclaw-gateway')
45
+ * @param {Object} adapter - { isMessage(entry), extractMessage(entry) }
46
+ */
47
+ export function registerFormat(name, adapter) {
48
+ ADAPTERS[name] = adapter;
49
+ }
50
+
51
+ // ── Built-in Adapters ────────────────────────────────────
52
+
53
+ // Claude Code format: {type: "user"|"assistant", message: {role, content}, timestamp}
54
+ registerFormat('claude-code', {
55
+ isMessage(entry) {
56
+ return (entry.type === 'user' || entry.type === 'assistant') && entry.message;
57
+ },
58
+
59
+ extractMessage(entry) {
60
+ const content = extractContent(entry.message);
61
+ if (!content) return null;
62
+
63
+ return {
64
+ role: entry.type, // 'user' or 'assistant'
65
+ content,
66
+ timestamp: entry.timestamp || null,
67
+ metadata: {
68
+ usage: entry.usage || null,
69
+ model: entry.model || null,
70
+ costUSD: entry.costUSD || null,
71
+ },
72
+ };
73
+ },
74
+ });
75
+
76
+ // OpenClaw Gateway format: {type: "message", message: {role, content}, timestamp}
77
+ // Metadata types to skip: session, model_change, thinking_level_change, queue-operation, tool_result, custom
78
+ const GATEWAY_SKIP_TYPES = new Set([
79
+ 'session', 'model_change', 'thinking_level_change',
80
+ 'custom', 'queue-operation', 'tool_result',
81
+ ]);
82
+
83
+ registerFormat('openclaw-gateway', {
84
+ isMessage(entry) {
85
+ if (GATEWAY_SKIP_TYPES.has(entry.type)) return false;
86
+ return entry.type === 'message' && entry.message && entry.message.role;
87
+ },
88
+
89
+ extractMessage(entry) {
90
+ let content = extractContent(entry.message);
91
+ if (!content) return null;
92
+
93
+ // Strip gateway header noise: "[Mon 2026-03-22 14:30 GMT-5] actual message"
94
+ content = content.replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+GMT[+-]\d+\]\s*/i, '');
95
+
96
+ // Strip metadata noise: "Conversation info (untrusted metadata): ```json {...} ```"
97
+ content = content.replace(/^Conversation info \(untrusted metadata\):[\s\S]*?```\s*/i, '');
98
+ content = content.trim();
99
+
100
+ if (!content) return null;
101
+
102
+ return {
103
+ role: entry.message.role, // 'user' or 'assistant'
104
+ content,
105
+ timestamp: entry.timestamp || null,
106
+ metadata: {},
107
+ };
108
+ },
109
+ });
110
+
111
+ // Generic fallback: tries both formats
112
+ registerFormat('auto', {
113
+ isMessage(entry) {
114
+ // Role-as-type format (type='user'|'assistant')
115
+ if ((entry.type === 'user' || entry.type === 'assistant') && entry.message) return true;
116
+ // Unified-message format (type='message' with role field)
117
+ if (entry.type === 'message' && entry.message && entry.message.role) return true;
118
+ return false;
119
+ },
120
+
121
+ extractMessage(entry) {
122
+ // Try role-as-type format first
123
+ if ((entry.type === 'user' || entry.type === 'assistant') && entry.message) {
124
+ return ADAPTERS['claude-code'].extractMessage(entry);
125
+ }
126
+ // Try unified-message format
127
+ if (entry.type === 'message' && entry.message && entry.message.role) {
128
+ return ADAPTERS['openclaw-gateway'].extractMessage(entry);
129
+ }
130
+ return null;
131
+ },
132
+ });
133
+
134
+ // ── Content Extraction ────────────────────────────────────
135
+
136
+ /**
137
+ * Extract text content from a message object.
138
+ * Handles both string content and array-of-blocks content.
139
+ *
140
+ * @param {Object} message - { content: string | Array<{type, text}> }
141
+ * @returns {string}
142
+ */
143
+ export function extractContent(message) {
144
+ if (!message) return '';
145
+
146
+ const { content } = message;
147
+ if (typeof content === 'string') return content;
148
+
149
+ if (Array.isArray(content)) {
150
+ return content
151
+ .filter(block => block.type === 'text')
152
+ .map(block => block.text || '')
153
+ .join('\n');
154
+ }
155
+
156
+ return '';
157
+ }
158
+
159
+ // ── Format Detection ────────────────────────────────────
160
+
161
+ /**
162
+ * Auto-detect JSONL format by sampling the first few entries.
163
+ *
164
+ * @param {string} jsonlPath - Path to the JSONL file
165
+ * @param {number} sampleLines - Number of lines to sample (default: 10)
166
+ * @returns {Promise<string>} Format name: 'claude-code' | 'openclaw-gateway' | 'auto'
167
+ */
168
+ export async function detectFormat(jsonlPath, sampleLines = 10) {
169
+ if (!fs.existsSync(jsonlPath)) return 'auto';
170
+
171
+ const rl = createInterface({ input: createReadStream(jsonlPath), crlfDelay: Infinity });
172
+ let roleTypeHits = 0; // role-as-type format (type='user'|'assistant')
173
+ let unifiedMsgHits = 0; // unified-message format (type='message' + role field)
174
+ let lineCount = 0;
175
+
176
+ for await (const line of rl) {
177
+ if (lineCount >= sampleLines) break;
178
+ if (!line.trim()) continue;
179
+
180
+ try {
181
+ const entry = JSON.parse(line);
182
+
183
+ if (entry.type === 'user' || entry.type === 'assistant') roleTypeHits++;
184
+ if (entry.type === 'message' && entry.message?.role) unifiedMsgHits++;
185
+ if (GATEWAY_SKIP_TYPES.has(entry.type)) unifiedMsgHits++; // metadata entries = gateway signal
186
+ if (entry.usage || entry.costUSD) roleTypeHits++; // token tracking = role-type format signal
187
+ } catch { /* skip */ }
188
+
189
+ lineCount++;
190
+ }
191
+
192
+ if (roleTypeHits > unifiedMsgHits) return 'claude-code';
193
+ if (unifiedMsgHits > roleTypeHits) return 'openclaw-gateway';
194
+ return 'auto';
195
+ }
196
+
197
+ // ── Parsing ────────────────────────────────────
198
+
199
+ /**
200
+ * Parse a single JSONL line into a message.
201
+ *
202
+ * @param {string} line - Raw JSONL string
203
+ * @param {Object} opts
204
+ * @param {string} opts.format - Format name (default: 'auto')
205
+ * @returns {{ role: string, content: string, timestamp: string|null, metadata?: object } | null}
206
+ */
207
+ export function parseLine(line, opts = {}) {
208
+ const { format = 'auto' } = opts;
209
+ const adapter = ADAPTERS[format] || ADAPTERS['auto'];
210
+
211
+ try {
212
+ const entry = JSON.parse(line);
213
+ if (!adapter.isMessage(entry)) return null;
214
+ return adapter.extractMessage(entry);
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Parse an entire JSONL file into an array of messages.
222
+ * Optionally returns only the tail (last N messages).
223
+ *
224
+ * @param {string} jsonlPath - Path to the JSONL file
225
+ * @param {Object} opts
226
+ * @param {string} opts.format - Format name (default: auto-detected)
227
+ * @param {number} opts.tail - If set, return only the last N messages
228
+ * @returns {Promise<Array<{ role, content, timestamp, metadata? }>>}
229
+ */
230
+ export async function parseJsonlFile(jsonlPath, opts = {}) {
231
+ let { format, tail } = opts;
232
+
233
+ if (!fs.existsSync(jsonlPath)) return [];
234
+
235
+ // Auto-detect format if not specified
236
+ if (!format) {
237
+ format = await detectFormat(jsonlPath);
238
+ }
239
+
240
+ const adapter = ADAPTERS[format] || ADAPTERS['auto'];
241
+ const messages = [];
242
+
243
+ const rl = createInterface({ input: createReadStream(jsonlPath), crlfDelay: Infinity });
244
+
245
+ for await (const line of rl) {
246
+ if (!line.trim()) continue;
247
+ try {
248
+ const entry = JSON.parse(line);
249
+ if (!adapter.isMessage(entry)) continue;
250
+ const msg = adapter.extractMessage(entry);
251
+ if (msg) messages.push(msg);
252
+ } catch { /* skip malformed */ }
253
+ }
254
+
255
+ if (tail && tail > 0) {
256
+ return messages.slice(-tail);
257
+ }
258
+
259
+ return messages;
260
+ }
261
+
262
+ /**
263
+ * Count messages and estimate tokens from a JSONL file.
264
+ *
265
+ * @param {string} jsonlPath
266
+ * @param {Object} opts
267
+ * @param {string} opts.format - Format name (default: auto-detected)
268
+ * @param {number} opts.charsPerToken - Chars per token estimate (default: 4)
269
+ * @returns {Promise<{ messageCount, totalChars, estimatedTokens }>}
270
+ */
271
+ export async function estimateFileTokens(jsonlPath, opts = {}) {
272
+ const { charsPerToken = 4 } = opts;
273
+ const messages = await parseJsonlFile(jsonlPath, opts);
274
+
275
+ let totalChars = 0;
276
+ for (const msg of messages) {
277
+ totalChars += msg.content.length;
278
+ }
279
+
280
+ return {
281
+ messageCount: messages.length,
282
+ totalChars,
283
+ estimatedTokens: Math.ceil(totalChars / charsPerToken),
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Get list of registered format names.
289
+ */
290
+ export function listFormats() {
291
+ return Object.keys(ADAPTERS);
292
+ }
@@ -0,0 +1,29 @@
1
+ -- Add soul tracking columns to tasks table
2
+ ALTER TABLE tasks ADD COLUMN soul_id TEXT;
3
+ ALTER TABLE tasks ADD COLUMN handoff_source TEXT;
4
+ ALTER TABLE tasks ADD COLUMN handoff_reason TEXT;
5
+
6
+ -- Create soul_handoffs table
7
+ CREATE TABLE IF NOT EXISTS soul_handoffs (
8
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
9
+ task_id TEXT NOT NULL,
10
+ from_soul TEXT NOT NULL,
11
+ to_soul TEXT NOT NULL,
12
+ reason TEXT,
13
+ context_path TEXT,
14
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
15
+ );
16
+
17
+ -- Create soul_evolution_log table
18
+ CREATE TABLE IF NOT EXISTS soul_evolution_log (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
20
+ soul_id TEXT NOT NULL,
21
+ event_id TEXT NOT NULL UNIQUE,
22
+ event_type TEXT NOT NULL,
23
+ description TEXT NOT NULL,
24
+ review_status TEXT NOT NULL DEFAULT 'pending',
25
+ commit_hash TEXT,
26
+ reviewed_by TEXT,
27
+ reviewed_at TEXT,
28
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
@@ -1,13 +1,10 @@
1
1
  import { defineConfig } from "drizzle-kit";
2
- import path from "path";
3
-
4
- const dbPath = process.env.DB_PATH || path.resolve(__dirname, "data", "mission-control.db");
5
2
 
6
3
  export default defineConfig({
7
4
  schema: "./src/lib/db/schema.ts",
8
5
  out: "./drizzle",
9
6
  dialect: "sqlite",
10
7
  dbCredentials: {
11
- url: dbPath,
8
+ url: "./data/mission-control.db",
12
9
  },
13
10
  });