@useorgx/orgx-opencode-plugin 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -3
  3. package/dist/OpenCodeDriver.d.ts +5 -2
  4. package/dist/OpenCodeDriver.d.ts.map +1 -1
  5. package/dist/OpenCodeDriver.js +12 -1
  6. package/dist/OpenCodeDriver.js.map +1 -1
  7. package/dist/attentionBridge.d.ts +38 -0
  8. package/dist/attentionBridge.d.ts.map +1 -0
  9. package/dist/attentionBridge.js +230 -0
  10. package/dist/attentionBridge.js.map +1 -0
  11. package/dist/childProcessEnv.d.ts +5 -0
  12. package/dist/childProcessEnv.d.ts.map +1 -0
  13. package/dist/childProcessEnv.js +17 -0
  14. package/dist/childProcessEnv.js.map +1 -0
  15. package/dist/cli.js +6 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/contextPackHydration.d.ts +14 -0
  18. package/dist/contextPackHydration.d.ts.map +1 -0
  19. package/dist/contextPackHydration.js +57 -0
  20. package/dist/contextPackHydration.js.map +1 -0
  21. package/dist/continuityHealth.d.ts +48 -0
  22. package/dist/continuityHealth.d.ts.map +1 -0
  23. package/dist/continuityHealth.js +118 -0
  24. package/dist/continuityHealth.js.map +1 -0
  25. package/dist/index.d.ts +6 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +8 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/peer.d.ts +14 -0
  30. package/dist/peer.d.ts.map +1 -1
  31. package/dist/peer.js +142 -13
  32. package/dist/peer.js.map +1 -1
  33. package/dist/plugin.d.ts +17 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +124 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/sentry.d.ts +4 -0
  38. package/dist/sentry.d.ts.map +1 -0
  39. package/dist/sentry.js +97 -0
  40. package/dist/sentry.js.map +1 -0
  41. package/dist/sessionSummaryBridge.d.ts +20 -0
  42. package/dist/sessionSummaryBridge.d.ts.map +1 -0
  43. package/dist/sessionSummaryBridge.js +125 -0
  44. package/dist/sessionSummaryBridge.js.map +1 -0
  45. package/dist/v2Canary.d.ts +13 -0
  46. package/dist/v2Canary.d.ts.map +1 -0
  47. package/dist/v2Canary.js +27 -0
  48. package/dist/v2Canary.js.map +1 -0
  49. package/dist/workGraphOutbox.js +2 -2
  50. package/dist/workGraphOutbox.js.map +1 -1
  51. package/dist/workGraphReplay.d.ts +9 -0
  52. package/dist/workGraphReplay.d.ts.map +1 -0
  53. package/dist/workGraphReplay.js +41 -0
  54. package/dist/workGraphReplay.js.map +1 -0
  55. package/package.json +30 -9
  56. package/plugin.manifest.json +8 -2
  57. package/scripts/orgx-work-graph-reconcile.mjs +994 -0
  58. package/scripts/orgx-work-graph-reconcile.node-test.mjs +131 -0
@@ -0,0 +1,994 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ createReadStream,
5
+ existsSync,
6
+ mkdirSync,
7
+ realpathSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { createInterface } from "node:readline";
11
+ import { createHash } from "node:crypto";
12
+ import { basename, dirname, join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
+
16
+ const WORK_GRAPH_SCHEMA_VERSION = "2.0.0";
17
+ const WORK_GRAPH_FINGERPRINT_VERSION = "wgf_v1";
18
+ const DEFAULT_OUTBOX = join(
19
+ homedir(),
20
+ ".config",
21
+ "useorgx",
22
+ "wizard",
23
+ "hooks",
24
+ "events.jsonl"
25
+ );
26
+ const SOURCE_CLIENTS = new Set([
27
+ "codex",
28
+ "claude",
29
+ "claude-code",
30
+ "cursor",
31
+ "opencode",
32
+ "goose",
33
+ "openclaw",
34
+ "slack",
35
+ "mcp",
36
+ "orgx_runtime_hook",
37
+ "github",
38
+ "linear",
39
+ "gmail",
40
+ "calendar",
41
+ "notion",
42
+ "docs",
43
+ "manual",
44
+ "wizard",
45
+ "api",
46
+ "unknown",
47
+ ]);
48
+
49
+ export function parseArgs(argv) {
50
+ const args = {};
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const arg = argv[index];
53
+ if (!arg.startsWith("--")) continue;
54
+ const [rawKey, ...rest] = arg.slice(2).split("=");
55
+ const key = rawKey.trim();
56
+ if (!key) continue;
57
+ if (rest.length > 0) {
58
+ args[key] = rest.join("=");
59
+ } else if (argv[index + 1] && !argv[index + 1].startsWith("--")) {
60
+ args[key] = argv[index + 1];
61
+ index += 1;
62
+ } else {
63
+ args[key] = "true";
64
+ }
65
+ }
66
+ return args;
67
+ }
68
+
69
+ export function pickString(...values) {
70
+ for (const value of values) {
71
+ if (typeof value !== "string") continue;
72
+ const trimmed = value.trim();
73
+ if (trimmed) return trimmed;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ function stableJson(value) {
79
+ if (Array.isArray(value)) {
80
+ return `[${value.map((item) => stableJson(item)).join(",")}]`;
81
+ }
82
+ if (value && typeof value === "object") {
83
+ return `{${Object.keys(value)
84
+ .sort()
85
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
86
+ .join(",")}}`;
87
+ }
88
+ return JSON.stringify(value);
89
+ }
90
+
91
+ export function hashString(value, length = 24) {
92
+ return createHash("sha256").update(value).digest("hex").slice(0, length);
93
+ }
94
+
95
+ function stableHash(value, length = 24) {
96
+ return hashString(stableJson(value), length);
97
+ }
98
+
99
+ function slug(value, fallback = "unknown") {
100
+ return (
101
+ pickString(value)
102
+ ?.toLowerCase()
103
+ .replace(/[^a-z0-9._:-]+/g, "-")
104
+ .replace(/^-+|-+$/g, "")
105
+ .slice(0, 120) || fallback
106
+ );
107
+ }
108
+
109
+ function toIsoTimestamp(value, fallback) {
110
+ const parsed = Date.parse(String(value || ""));
111
+ if (Number.isFinite(parsed)) return new Date(parsed).toISOString();
112
+ return fallback;
113
+ }
114
+
115
+ export function normalizeSourceClient(value) {
116
+ const raw = pickString(value)?.toLowerCase() || "unknown";
117
+ if (raw === "claude_code") return "claude-code";
118
+ if (raw === "claudecode") return "claude-code";
119
+ if (raw === "open-claw") return "openclaw";
120
+ return SOURCE_CLIENTS.has(raw) ? raw : "unknown";
121
+ }
122
+
123
+ function sourceLabel(sourceClient) {
124
+ return {
125
+ codex: "Codex",
126
+ "claude-code": "Claude Code",
127
+ claude: "Claude",
128
+ openclaw: "OpenClaw",
129
+ orgx_runtime_hook: "OrgX runtime hook",
130
+ mcp: "MCP",
131
+ wizard: "OrgX wizard",
132
+ }[sourceClient] || sourceClient;
133
+ }
134
+
135
+ function eventPhase(event) {
136
+ const normalized = String(event || "").toLowerCase();
137
+ if (normalized.includes("stop") || normalized.includes("complete")) {
138
+ return "completed";
139
+ }
140
+ if (normalized.includes("block") || normalized.includes("error")) {
141
+ return "blocked";
142
+ }
143
+ if (normalized.includes("permission")) return "blocked";
144
+ return "in_progress";
145
+ }
146
+
147
+ function recordSortKey(record) {
148
+ return [
149
+ record.timestamp || "",
150
+ record.source_client || "",
151
+ record.run_id || "",
152
+ record.session_id || "",
153
+ record.event || "",
154
+ record.turn_id || "",
155
+ ].join("\u0000");
156
+ }
157
+
158
+ export function normalizeHookRecord(record, index = 0) {
159
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
160
+ return null;
161
+ }
162
+ const sourceClient = normalizeSourceClient(record.source_client);
163
+ const sessionId = pickString(record.session_id, record.sessionId);
164
+ const runId = pickString(record.run_id, record.runId);
165
+ const cwd = pickString(record.cwd, record.workspace, process.cwd());
166
+ const timestamp = toIsoTimestamp(record.timestamp, "1970-01-01T00:00:00.000Z");
167
+ const summary =
168
+ record.summary && typeof record.summary === "object" && !Array.isArray(record.summary)
169
+ ? record.summary
170
+ : {};
171
+ return {
172
+ schema_version: pickString(record.schema_version, "2026-05-07"),
173
+ source: pickString(record.source, "orgx_runtime_hook"),
174
+ source_client: sourceClient,
175
+ event: pickString(record.event, "unknown"),
176
+ run_id: runId,
177
+ session_id: sessionId || `unknown-session-${index + 1}`,
178
+ turn_id: pickString(record.turn_id, record.turnId),
179
+ cwd,
180
+ transcript_path: pickString(record.transcript_path, record.transcriptPath),
181
+ timestamp,
182
+ summary: {
183
+ tool_name: pickString(summary.tool_name, summary.toolName),
184
+ prompt_chars:
185
+ typeof summary.prompt_chars === "number" && Number.isFinite(summary.prompt_chars)
186
+ ? summary.prompt_chars
187
+ : undefined,
188
+ payload_keys: Array.isArray(summary.payload_keys)
189
+ ? summary.payload_keys
190
+ .filter((item) => typeof item === "string")
191
+ .slice(0, 40)
192
+ : [],
193
+ },
194
+ };
195
+ }
196
+
197
+ export async function loadHookOutboxRecords(outboxPath, { maxRecords = 5000 } = {}) {
198
+ if (!existsSync(outboxPath)) {
199
+ return { records: [], skipped: 0, missing: true };
200
+ }
201
+
202
+ const records = [];
203
+ let skipped = 0;
204
+ const rl = createInterface({
205
+ input: createReadStream(outboxPath, { encoding: "utf8" }),
206
+ crlfDelay: Infinity,
207
+ });
208
+
209
+ for await (const line of rl) {
210
+ const trimmed = line.trim();
211
+ if (!trimmed) continue;
212
+ try {
213
+ const parsed = JSON.parse(trimmed);
214
+ const normalized = normalizeHookRecord(parsed, records.length + skipped);
215
+ if (normalized) records.push(normalized);
216
+ else skipped += 1;
217
+ } catch {
218
+ skipped += 1;
219
+ }
220
+ if (records.length >= maxRecords) break;
221
+ }
222
+
223
+ return { records, skipped, missing: false };
224
+ }
225
+
226
+ function toolNames(records) {
227
+ return [
228
+ ...new Set(
229
+ records
230
+ .map((record) => pickString(record.summary?.tool_name))
231
+ .filter(Boolean)
232
+ .sort()
233
+ ),
234
+ ];
235
+ }
236
+
237
+ function hasOrgxSignal(records) {
238
+ return records.some((record) => {
239
+ const haystack = [
240
+ record.source,
241
+ record.source_client,
242
+ record.event,
243
+ record.summary?.tool_name,
244
+ ...(record.summary?.payload_keys || []),
245
+ ]
246
+ .filter(Boolean)
247
+ .join(" ")
248
+ .toLowerCase();
249
+ return haystack.includes("orgx") || haystack.includes("useorgx");
250
+ });
251
+ }
252
+
253
+ function hasMcpSignal(records) {
254
+ return records.some((record) => {
255
+ const haystack = [
256
+ record.event,
257
+ record.summary?.tool_name,
258
+ ...(record.summary?.payload_keys || []),
259
+ ]
260
+ .filter(Boolean)
261
+ .join(" ")
262
+ .toLowerCase();
263
+ return haystack.includes("mcp");
264
+ });
265
+ }
266
+
267
+ function hasOrgxMcpSignal(records) {
268
+ return records.some((record) => {
269
+ const haystack = [
270
+ record.event,
271
+ record.summary?.tool_name,
272
+ ...(record.summary?.payload_keys || []),
273
+ ]
274
+ .filter(Boolean)
275
+ .join(" ")
276
+ .toLowerCase();
277
+ return (
278
+ haystack.includes("mcp") &&
279
+ (haystack.includes("orgx") || haystack.includes("useorgx"))
280
+ );
281
+ });
282
+ }
283
+
284
+ function buildEvidenceRef(record, ordinal) {
285
+ const sourceId = `${record.source_client}:${record.session_id}`;
286
+ const id = `${sourceId}:${ordinal}`;
287
+ const tool = pickString(record.summary?.tool_name);
288
+ const summaryBits = [
289
+ `${sourceLabel(record.source_client)} ${record.event} hook observed.`,
290
+ tool ? `Tool signal: ${tool}.` : undefined,
291
+ typeof record.summary?.prompt_chars === "number"
292
+ ? `Prompt length: ${record.summary.prompt_chars} characters.`
293
+ : undefined,
294
+ ].filter(Boolean);
295
+ return {
296
+ id,
297
+ source_client: record.source_client,
298
+ source_id: sourceId,
299
+ label: `${sourceLabel(record.source_client)} ${record.event}`,
300
+ summary: summaryBits.join(" "),
301
+ occurred_at: record.timestamp,
302
+ confidence: 0.72,
303
+ redaction_state: "public_summary",
304
+ metadata: {
305
+ event: record.event,
306
+ cwd_hash: hashString(record.cwd, 16),
307
+ has_transcript_path: Boolean(record.transcript_path),
308
+ },
309
+ };
310
+ }
311
+
312
+ function buildAttributionNode({
313
+ id,
314
+ kind,
315
+ label,
316
+ summary,
317
+ sourceClient,
318
+ evidenceRefs,
319
+ linkedNodeIds = [],
320
+ confidence = 0.72,
321
+ weight = 50,
322
+ metadata = {},
323
+ }) {
324
+ return {
325
+ id,
326
+ kind,
327
+ label,
328
+ summary,
329
+ source_client: sourceClient,
330
+ evidence_refs: evidenceRefs,
331
+ linked_node_ids: linkedNodeIds,
332
+ confidence,
333
+ weight,
334
+ review_state: "unreviewed",
335
+ dedupe_key: id,
336
+ metadata,
337
+ };
338
+ }
339
+
340
+ function finalStateFor(records) {
341
+ if (records.some((record) => eventPhase(record.event) === "blocked")) {
342
+ return "blocked";
343
+ }
344
+ if (records.some((record) => eventPhase(record.event) === "completed")) {
345
+ return "completed";
346
+ }
347
+ if (records.length > 0) return "in_progress";
348
+ return "unknown";
349
+ }
350
+
351
+ function findingTypeCounts(findings) {
352
+ const counts = {};
353
+ for (const finding of findings) {
354
+ counts[finding.type] = (counts[finding.type] || 0) + 1;
355
+ }
356
+ return counts;
357
+ }
358
+
359
+ function emptyAttributionSpine(evidenceRefs = []) {
360
+ return {
361
+ source_events: [],
362
+ actions: [],
363
+ decisions: [],
364
+ artifacts: [],
365
+ people: [],
366
+ agents: [],
367
+ tools: [],
368
+ businesses: [],
369
+ product_surfaces: [],
370
+ goals: [],
371
+ sources: [],
372
+ initiative_candidates: [],
373
+ evidence_refs: evidenceRefs,
374
+ confidence: evidenceRefs.length ? 0.7 : 0,
375
+ dedupe_keys: [],
376
+ privacy: {
377
+ redaction_state: "public_summary",
378
+ raw_transcripts_included: false,
379
+ public_summary_only: true,
380
+ },
381
+ review: {
382
+ pending_count: 0,
383
+ correction_affordances: [
384
+ "confirm",
385
+ "merge",
386
+ "hide",
387
+ "mark_important",
388
+ "launch_or_dismiss",
389
+ ],
390
+ },
391
+ };
392
+ }
393
+
394
+ export function buildWorkGraphReport(recordsInput, options = {}) {
395
+ const generatedAt = toIsoTimestamp(
396
+ options.generatedAt,
397
+ new Date().toISOString()
398
+ );
399
+ const records = recordsInput
400
+ .map((record, index) => normalizeHookRecord(record, index))
401
+ .filter(Boolean)
402
+ .sort((a, b) => recordSortKey(a).localeCompare(recordSortKey(b)));
403
+ const firstRecord = records[0];
404
+ const workspaceCwd = pickString(options.workspaceCwd, firstRecord?.cwd, process.cwd());
405
+ const workspaceName = pickString(
406
+ options.workspaceName,
407
+ basename(workspaceCwd) || workspaceCwd,
408
+ "Local workspace"
409
+ );
410
+ const workspaceHash = `sha256:${hashString(workspaceCwd, 32)}`;
411
+ const workspace = {
412
+ id: pickString(options.workspaceId, `local:${hashString(workspaceCwd, 16)}`),
413
+ name: workspaceName,
414
+ };
415
+ const sourceClients = [
416
+ ...new Set(records.map((record) => record.source_client).filter(Boolean)),
417
+ ].sort();
418
+ const sessions = [
419
+ ...new Set(
420
+ records
421
+ .map((record) => `${record.source_client}:${record.session_id}`)
422
+ .filter(Boolean)
423
+ ),
424
+ ].sort();
425
+ const observedToolNames = toolNames(records);
426
+ const orgxMcpCalled = hasOrgxMcpSignal(records);
427
+ const evidenceRefs = records.slice(0, 200).map(buildEvidenceRef);
428
+ const primaryEvidenceRef = evidenceRefs[0]?.id || "work-graph:hook-outbox:empty";
429
+ const sourceEvents = records.slice(0, 200).map((record, index) => ({
430
+ source_client: record.source_client,
431
+ source_id: `${record.source_client}:${record.session_id}`,
432
+ source_label: `${sourceLabel(record.source_client)} session`,
433
+ event_type: "runtime_hook",
434
+ occurred_at: record.timestamp,
435
+ evidence_ref: evidenceRefs[index].id,
436
+ confidence: 0.72,
437
+ metadata: {
438
+ event: record.event,
439
+ tool_name: record.summary?.tool_name,
440
+ prompt_chars: record.summary?.prompt_chars,
441
+ },
442
+ }));
443
+ const reportEvents = sourceEvents.map((event) => ({
444
+ source_client: event.source_client,
445
+ source_id: event.source_id,
446
+ source_label: event.source_label,
447
+ event_type: event.metadata?.tool_name ? "tool_signal" : "client_extraction",
448
+ evidence_ref: event.evidence_ref,
449
+ metadata: {
450
+ ...event.metadata,
451
+ occurred_at: event.occurred_at,
452
+ attribution_event_type: event.event_type,
453
+ },
454
+ }));
455
+
456
+ const findings = [];
457
+ if (records.length > 0) {
458
+ findings.push({
459
+ type: "action",
460
+ title: "AI client lifecycle activity observed",
461
+ summary:
462
+ "OrgX captured summary-only hook events from local AI client sessions.",
463
+ source_client: records[0].source_client,
464
+ source_id: `${records[0].source_client}:${records[0].session_id}`,
465
+ evidence_ref: primaryEvidenceRef,
466
+ confidence: 0.72,
467
+ metadata: {
468
+ event_count: records.length,
469
+ session_count: sessions.length,
470
+ source_clients: sourceClients,
471
+ },
472
+ });
473
+ }
474
+ if (observedToolNames.length > 0) {
475
+ findings.push({
476
+ type: "artifact",
477
+ title: "Tool-use trail is available for work graph hydration",
478
+ summary:
479
+ "Hook metadata includes compact tool names that can seed OrgX sources, tools, and evidence refs without raw transcript upload.",
480
+ source_client: records[0]?.source_client || "unknown",
481
+ source_id: records[0]
482
+ ? `${records[0].source_client}:${records[0].session_id}`
483
+ : "unknown:session",
484
+ evidence_ref: primaryEvidenceRef,
485
+ confidence: 0.7,
486
+ metadata: {
487
+ tool_names: observedToolNames.slice(0, 20),
488
+ },
489
+ });
490
+ }
491
+
492
+ const missedOrchestration = [];
493
+ if (records.length > 0 && !orgxMcpCalled) {
494
+ const missed = {
495
+ type: "missed_orchestration_opportunity",
496
+ title: "Session activity was captured without durable OrgX writeback",
497
+ summary:
498
+ "The hook outbox observed client lifecycle events, but no OrgX MCP tool signal was detected in the compact hook metadata.",
499
+ source_client: "wizard",
500
+ source_id: "work-graph:hook-outbox",
501
+ evidence_ref: primaryEvidenceRef,
502
+ confidence: 0.76,
503
+ metadata: {
504
+ event_count: records.length,
505
+ session_count: sessions.length,
506
+ },
507
+ };
508
+ findings.push(missed);
509
+ missedOrchestration.push(missed);
510
+ }
511
+
512
+ const sourceNodes = sourceClients.map((sourceClient) =>
513
+ buildAttributionNode({
514
+ id: `source:${sourceClient}`,
515
+ kind: "source",
516
+ label: sourceLabel(sourceClient),
517
+ summary: `${sourceLabel(sourceClient)} hook events were present in the outbox.`,
518
+ sourceClient,
519
+ evidenceRefs: evidenceRefs
520
+ .filter((ref) => ref.source_client === sourceClient)
521
+ .slice(0, 10)
522
+ .map((ref) => ref.id),
523
+ linkedNodeIds: ["action:hook-lifecycle-captured"],
524
+ confidence: 0.72,
525
+ weight: 70,
526
+ metadata: { connected: true },
527
+ })
528
+ );
529
+ const toolNodes = observedToolNames.slice(0, 50).map((name) =>
530
+ buildAttributionNode({
531
+ id: `tool:${slug(name)}`,
532
+ kind: "tool",
533
+ label: name.slice(0, 120),
534
+ summary: `Tool signal observed from hook metadata: ${name}.`,
535
+ sourceClient: name.toLowerCase().includes("orgx") ? "mcp" : records[0]?.source_client || "unknown",
536
+ evidenceRefs: evidenceRefs
537
+ .filter((ref) => records[evidenceRefs.indexOf(ref)]?.summary?.tool_name === name)
538
+ .slice(0, 10)
539
+ .map((ref) => ref.id),
540
+ linkedNodeIds: ["action:hook-lifecycle-captured"],
541
+ confidence: 0.68,
542
+ weight: name.toLowerCase().includes("orgx") ? 84 : 60,
543
+ metadata: {},
544
+ })
545
+ );
546
+ const actionNode = records.length
547
+ ? buildAttributionNode({
548
+ id: "action:hook-lifecycle-captured",
549
+ kind: "action",
550
+ label: "Capture AI client lifecycle",
551
+ summary:
552
+ "Convert local hook outbox events into OrgX Work Graph source events, evidence refs, and reviewable initiative candidates.",
553
+ sourceClient: "wizard",
554
+ evidenceRefs: evidenceRefs.slice(0, 20).map((ref) => ref.id),
555
+ linkedNodeIds: [...sourceNodes.map((node) => node.id), ...toolNodes.map((node) => node.id)],
556
+ confidence: 0.74,
557
+ weight: 82,
558
+ metadata: { raw_transcripts_sent: false },
559
+ })
560
+ : null;
561
+ const initiativeNode = records.length
562
+ ? buildAttributionNode({
563
+ id: "initiative:continuous-orgx-writeback",
564
+ kind: "goal",
565
+ label: "Install continuous OrgX writeback",
566
+ summary:
567
+ "Review hook-derived session evidence and promote real decisions, blockers, artifacts, and goals into OrgX.",
568
+ sourceClient: "wizard",
569
+ evidenceRefs: evidenceRefs.slice(0, 20).map((ref) => ref.id),
570
+ linkedNodeIds: actionNode ? [actionNode.id] : [],
571
+ confidence: orgxMcpCalled ? 0.64 : 0.78,
572
+ weight: orgxMcpCalled ? 62 : 88,
573
+ metadata: { priority: orgxMcpCalled ? "p2" : "p0" },
574
+ })
575
+ : null;
576
+
577
+ const attributionSpine = emptyAttributionSpine(evidenceRefs);
578
+ attributionSpine.source_events = sourceEvents;
579
+ attributionSpine.actions = actionNode ? [actionNode] : [];
580
+ attributionSpine.sources = sourceNodes;
581
+ attributionSpine.tools = toolNodes;
582
+ attributionSpine.initiative_candidates = initiativeNode ? [initiativeNode] : [];
583
+ attributionSpine.confidence = records.length ? 0.72 : 0;
584
+ attributionSpine.dedupe_keys = [
585
+ ...attributionSpine.actions,
586
+ ...attributionSpine.sources,
587
+ ...attributionSpine.tools,
588
+ ...attributionSpine.initiative_candidates,
589
+ ].map((node) => node.dedupe_key);
590
+ attributionSpine.review.pending_count =
591
+ attributionSpine.actions.length +
592
+ attributionSpine.tools.length +
593
+ attributionSpine.initiative_candidates.length;
594
+
595
+ const counts = findingTypeCounts(findings);
596
+ const fingerprintBasis = {
597
+ schema_version: WORK_GRAPH_SCHEMA_VERSION,
598
+ fingerprint_version: WORK_GRAPH_FINGERPRINT_VERSION,
599
+ workspace_hash: workspaceHash,
600
+ source_clients: sourceClients.length ? sourceClients : ["unknown"],
601
+ connected_source_hashes: sessions.map((session) => `sha256:${hashString(session, 32)}`),
602
+ missing_source_hashes: orgxMcpCalled ? [] : [`sha256:${hashString("orgx-mcp", 32)}`],
603
+ finding_type_counts: counts,
604
+ pattern_hashes: [
605
+ ...records.map((record) =>
606
+ `sha256:${stableHash({
607
+ source_client: record.source_client,
608
+ event: record.event,
609
+ tool_name: record.summary?.tool_name,
610
+ }, 32)}`
611
+ ),
612
+ ].sort(),
613
+ trail_shape_hashes: [],
614
+ recurring_pattern_hashes: [],
615
+ kickoff_hashes: records.length
616
+ ? [`sha256:${hashString("continuous-orgx-writeback", 32)}`]
617
+ : [],
618
+ raw_transcripts_included: false,
619
+ };
620
+ const workGraphFingerprint = `wgf_${stableHash(fingerprintBasis, 24)}`;
621
+ const hydrationKey = `orgx:work-graph:${workGraphFingerprint}`;
622
+ const reportId = `report_${stableHash({ workGraphFingerprint, generatedAt }, 24)}`;
623
+
624
+ return {
625
+ schema_version: WORK_GRAPH_SCHEMA_VERSION,
626
+ report_id: reportId,
627
+ idempotency_key: `work-graph:hook-outbox:${workGraphFingerprint}`,
628
+ work_graph_fingerprint: workGraphFingerprint,
629
+ fingerprint_version: WORK_GRAPH_FINGERPRINT_VERSION,
630
+ fingerprint_basis: fingerprintBasis,
631
+ signup_hydration: {
632
+ strategy: "work_graph_fingerprint_claim",
633
+ hydration_key: hydrationKey,
634
+ eligible: records.length > 0,
635
+ notes: records.length
636
+ ? ["claim after signup", "summary-only hook outbox hydration"]
637
+ : ["no hook events found"],
638
+ },
639
+ generated_at: generatedAt,
640
+ source_client: "wizard",
641
+ session_id: sessions[0] || "hook-outbox-empty",
642
+ workspace,
643
+ audit_method: {
644
+ mode: "ai_client_session_search",
645
+ searched_session_files: records.length > 0 ? 1 : 0,
646
+ skipped_session_files: 0,
647
+ searched_message_count: records.length,
648
+ retained_evidence_lines: evidenceRefs.length,
649
+ searched_source_groups: sourceClients.length,
650
+ extraction_lenses: ["runtime_hooks", "summary_only_work_graph"],
651
+ client_native_packs: sourceClients.map((sourceClient) => ({
652
+ source_client: sourceClient,
653
+ source_label: sourceLabel(sourceClient),
654
+ searched_session_count: records.filter(
655
+ (record) => record.source_client === sourceClient
656
+ ).length,
657
+ finding_count: findings.filter(
658
+ (finding) => finding.source_client === sourceClient
659
+ ).length,
660
+ confidence: 0.72,
661
+ })),
662
+ privacy_contract: [
663
+ "raw transcripts are excluded",
664
+ "hook payload values are summarized before persistence",
665
+ "workspace paths are hashed in public evidence metadata",
666
+ ],
667
+ notes: [],
668
+ },
669
+ domain_coverage: [
670
+ {
671
+ id: "runtime-hooks",
672
+ label: "Runtime hooks",
673
+ summary: `${records.length} summary-only lifecycle events captured.`,
674
+ finding_count: findings.length,
675
+ source_clients: sourceClients.length ? sourceClients : ["unknown"],
676
+ evidence_refs: evidenceRefs.slice(0, 20).map((ref) => ref.id),
677
+ confidence: records.length ? 0.72 : 0,
678
+ },
679
+ ],
680
+ skill_tool_signals: observedToolNames.slice(0, 50).map((name) => ({
681
+ id: `tool:${slug(name)}`,
682
+ label: name.slice(0, 120),
683
+ kind: name.toLowerCase().includes("orgx") ? "mcp_tool" : "client_tool",
684
+ mention_count: records.filter((record) => record.summary?.tool_name === name).length,
685
+ source_clients: sourceClients.length ? sourceClients : ["unknown"],
686
+ evidence_refs: evidenceRefs
687
+ .filter((_, index) => records[index]?.summary?.tool_name === name)
688
+ .slice(0, 20)
689
+ .map((ref) => ref.id),
690
+ confidence: 0.68,
691
+ })),
692
+ source_coverage: {
693
+ connected: sourceClients.map(sourceLabel),
694
+ missing: orgxMcpCalled ? [] : ["OrgX MCP writeback"],
695
+ mcpObserved: hasMcpSignal(records),
696
+ orgxObserved: hasOrgxSignal(records),
697
+ orgxMcpCalled,
698
+ skillOnlySignal: records.length > 0 && !orgxMcpCalled,
699
+ coverage_score: orgxMcpCalled ? 80 : records.length ? 45 : 0,
700
+ manifests: sourceClients.map((sourceClient) => ({
701
+ source_client: sourceClient,
702
+ source_label: sourceLabel(sourceClient),
703
+ status: "connected",
704
+ searched_sources: ["hook_outbox"],
705
+ searched_session_count: records.filter(
706
+ (record) => record.source_client === sourceClient
707
+ ).length,
708
+ skipped_session_count: 0,
709
+ query_count: 0,
710
+ finding_count: findings.filter(
711
+ (finding) => finding.source_client === sourceClient
712
+ ).length,
713
+ confidence: 0.72,
714
+ notes: [],
715
+ })),
716
+ notes: orgxMcpCalled
717
+ ? ["OrgX MCP signal detected in hook metadata."]
718
+ : ["No OrgX MCP writeback signal detected in compact hook metadata."],
719
+ },
720
+ final_state: finalStateFor(records),
721
+ events: reportEvents,
722
+ findings,
723
+ missed_orchestration_opportunities: missedOrchestration,
724
+ trails: [],
725
+ recurring_patterns: [],
726
+ recommendations: records.length && !orgxMcpCalled
727
+ ? [
728
+ {
729
+ id: "recommendation:promote-hook-outbox",
730
+ title: "Promote hook outbox evidence",
731
+ summary:
732
+ "Review summary-only hook records and promote real decisions, blockers, artifacts, and goals into OrgX.",
733
+ action_type: "connect_source",
734
+ trail_ids: [],
735
+ evidence_refs: [primaryEvidenceRef],
736
+ priority: "p0",
737
+ expected_lift: "+work graph durability",
738
+ confidence: 0.76,
739
+ },
740
+ ]
741
+ : [],
742
+ mirror: {
743
+ headline: records.length
744
+ ? "AI work is ready for OrgX review"
745
+ : "No AI hook activity found",
746
+ body: records.length
747
+ ? "OrgX found summary-only lifecycle evidence that can hydrate a work graph without sending raw transcripts."
748
+ : "The hook outbox did not contain records to hydrate.",
749
+ lens: "all",
750
+ generated_at: generatedAt,
751
+ claims: records.length
752
+ ? [
753
+ {
754
+ id: "mirror:hook-outbox",
755
+ text: `${records.length} lifecycle events were captured across ${sessions.length} session(s).`,
756
+ evidence_refs: [primaryEvidenceRef],
757
+ confidence: 0.72,
758
+ },
759
+ ]
760
+ : [],
761
+ },
762
+ tension_metrics: records.length && !orgxMcpCalled
763
+ ? [
764
+ {
765
+ id: "tension:work-without-writeback",
766
+ label: "work without writeback",
767
+ value: String(records.length),
768
+ tone: "warning",
769
+ trail_ids: [],
770
+ evidence_refs: [primaryEvidenceRef],
771
+ explanation:
772
+ "Hook events exist, but compact metadata did not prove durable OrgX MCP writeback.",
773
+ },
774
+ ]
775
+ : [],
776
+ opportunity_score: {
777
+ overall: records.length ? (orgxMcpCalled ? 58 : 76) : 0,
778
+ value_potential: records.length ? 78 : 0,
779
+ evidence_quality: records.length ? 62 : 0,
780
+ urgency: records.length && !orgxMcpCalled ? 80 : 35,
781
+ owner_clarity: records.length ? 55 : 0,
782
+ automation_potential: records.length ? 86 : 0,
783
+ orgx_fit: records.length ? 90 : 0,
784
+ },
785
+ execution_quality: {
786
+ overall: records.length ? 64 : 0,
787
+ evidence_coverage: records.length ? 58 : 0,
788
+ source_attribution: records.length ? 70 : 0,
789
+ trail_depth: 0,
790
+ insight_depth: records.length ? 45 : 0,
791
+ actionability: records.length ? 72 : 0,
792
+ impact_confidence: records.length ? 52 : 0,
793
+ notes: [
794
+ "This producer intentionally emits summary-only evidence.",
795
+ "Durable entities should be confirmed before promotion.",
796
+ ],
797
+ },
798
+ impact_projection: {
799
+ time_saved_hours_per_week: records.length ? 2 : 0,
800
+ acceleration_percent: records.length ? 8 : 0,
801
+ estimated_monthly_value_usd: 0,
802
+ confidence: records.length ? 0.35 : 0,
803
+ basis: ["hook outbox event count", "source coverage"],
804
+ assumptions: ["manual review promotes only confirmed entities"],
805
+ },
806
+ investigation: {
807
+ schema_version: WORK_GRAPH_SCHEMA_VERSION,
808
+ audit_id: `hook-outbox:${stableHash({ workspaceHash, generatedAt }, 16)}`,
809
+ fingerprint: workGraphFingerprint,
810
+ generated_at: generatedAt,
811
+ raw_events_summary: {
812
+ event_count: records.length,
813
+ source_clients: sourceClients,
814
+ sessions: sessions.length,
815
+ },
816
+ corpus_manifest: {
817
+ outbox_path: options.outboxPath ? "<local-hook-outbox>" : undefined,
818
+ raw_transcripts_excluded: true,
819
+ },
820
+ work_loops: [],
821
+ loop_families: [],
822
+ usage_catalogue: {
823
+ tools: observedToolNames,
824
+ },
825
+ source_confidence: {
826
+ hook_outbox: records.length ? 0.72 : 0,
827
+ },
828
+ why_not_100: orgxMcpCalled
829
+ ? [
830
+ {
831
+ code: "compact_hook_records",
832
+ summary: "Hook records are compact and need entity confirmation.",
833
+ },
834
+ ]
835
+ : [
836
+ {
837
+ code: "missing_orgx_mcp_writeback",
838
+ summary: "No OrgX MCP writeback signal was detected.",
839
+ },
840
+ ],
841
+ counterfactuals: [],
842
+ verification_log: [
843
+ {
844
+ step: "load_hook_outbox",
845
+ status: "passed",
846
+ records_read: records.length,
847
+ records_skipped: options.recordsSkipped || 0,
848
+ },
849
+ ],
850
+ critic_log: [],
851
+ verification_log_summary: {
852
+ total: records.length,
853
+ passed: records.length,
854
+ dropped: 0,
855
+ },
856
+ critic_log_summary: {
857
+ total: 0,
858
+ survived: 0,
859
+ dropped_or_demoted: 0,
860
+ },
861
+ mirror_paragraph: {},
862
+ repair_plan: [],
863
+ impact_projection: {},
864
+ redaction_log: [
865
+ {
866
+ rule: "exclude_raw_transcripts",
867
+ status: "passed",
868
+ },
869
+ ],
870
+ raw_transcripts_excluded: true,
871
+ claimable: records.length > 0,
872
+ },
873
+ initiative_kickoffs: records.length
874
+ ? [
875
+ {
876
+ title: "Install continuous OrgX writeback",
877
+ summary:
878
+ "Turn hook-derived lifecycle evidence into reviewed OrgX entities and runtime progress.",
879
+ reason: orgxMcpCalled
880
+ ? "Hook evidence exists and can be attached to the active graph."
881
+ : "Work happened in AI clients without a proven OrgX MCP writeback signal.",
882
+ finding_refs: [primaryEvidenceRef],
883
+ priority: orgxMcpCalled ? "p2" : "p0",
884
+ },
885
+ ]
886
+ : [],
887
+ attribution_spine: attributionSpine,
888
+ redaction_level: "summary_only",
889
+ raw_transcripts_sent: false,
890
+ };
891
+ }
892
+
893
+ export async function postWorkGraphReport({
894
+ report,
895
+ baseUrl,
896
+ apiKey,
897
+ fetchImpl = fetch,
898
+ }) {
899
+ const normalizedBaseUrl = pickString(baseUrl, "https://www.useorgx.com").replace(/\/+$/, "");
900
+ const token = pickString(apiKey);
901
+ if (!token) {
902
+ throw new Error("ORGX_API_KEY is required when posting a Work Graph report");
903
+ }
904
+ const response = await fetchImpl(`${normalizedBaseUrl}/api/client/work-graph/reports`, {
905
+ method: "POST",
906
+ headers: {
907
+ "Content-Type": "application/json",
908
+ Authorization: `Bearer ${token}`,
909
+ },
910
+ body: JSON.stringify({
911
+ report,
912
+ public_share: false,
913
+ attach_artifact: false,
914
+ }),
915
+ });
916
+ const body = await response.json().catch(async () => ({
917
+ text: await response.text().catch(() => ""),
918
+ }));
919
+ if (!response.ok) {
920
+ throw new Error(`Work Graph report post failed with HTTP ${response.status}`);
921
+ }
922
+ return body;
923
+ }
924
+
925
+ export async function main({
926
+ argv = process.argv.slice(2),
927
+ env = process.env,
928
+ now = () => new Date(),
929
+ fetchImpl = fetch,
930
+ } = {}) {
931
+ const args = parseArgs(argv);
932
+ const outboxPath = pickString(args.outbox, env.ORGX_WIZARD_HOOK_OUTBOX, DEFAULT_OUTBOX);
933
+ const maxRecords = Number.parseInt(pickString(args.max_records, args["max-records"], "5000"), 10);
934
+ const loaded = await loadHookOutboxRecords(outboxPath, {
935
+ maxRecords: Number.isFinite(maxRecords) && maxRecords > 0 ? maxRecords : 5000,
936
+ });
937
+ const report = buildWorkGraphReport(loaded.records, {
938
+ outboxPath,
939
+ generatedAt: now().toISOString(),
940
+ workspaceCwd: pickString(args.cwd, env.ORGX_WORKSPACE_CWD),
941
+ workspaceName: pickString(args.workspace_name, args["workspace-name"]),
942
+ workspaceId: pickString(args.workspace_id, args["workspace-id"]),
943
+ recordsSkipped: loaded.skipped,
944
+ });
945
+
946
+ const result = {
947
+ ok: true,
948
+ outbox_path: outboxPath,
949
+ records_read: loaded.records.length,
950
+ records_skipped: loaded.skipped,
951
+ outbox_missing: loaded.missing,
952
+ work_graph_fingerprint: report.work_graph_fingerprint,
953
+ hydration_key: report.signup_hydration.hydration_key,
954
+ report,
955
+ };
956
+
957
+ if (args.post === "true") {
958
+ result.posted = await postWorkGraphReport({
959
+ report,
960
+ baseUrl: pickString(args.base_url, args["base-url"], env.ORGX_BASE_URL),
961
+ apiKey: pickString(args.api_key, args["api-key"], env.ORGX_API_KEY),
962
+ fetchImpl,
963
+ });
964
+ }
965
+
966
+ const outputPath = pickString(args.output);
967
+ if (outputPath) {
968
+ mkdirSync(dirname(outputPath), { recursive: true, mode: 0o700 });
969
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`, {
970
+ encoding: "utf8",
971
+ mode: 0o600,
972
+ });
973
+ } else {
974
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
975
+ }
976
+
977
+ return result;
978
+ }
979
+
980
+ export function isDirectRun({ invokedPath = process.argv[1], moduleUrl = import.meta.url } = {}) {
981
+ if (!invokedPath) return false;
982
+ try {
983
+ return realpathSync(invokedPath) === realpathSync(fileURLToPath(moduleUrl));
984
+ } catch {
985
+ return moduleUrl === pathToFileURL(invokedPath).href;
986
+ }
987
+ }
988
+
989
+ if (isDirectRun()) {
990
+ main().catch((error) => {
991
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
992
+ process.exit(1);
993
+ });
994
+ }