@kontextmind/kxm 0.6.0 → 0.7.4

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 (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +4 -0
  14. package/docs/agent-skills.md +121 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/assignment-runner.md +21 -8
  17. package/docs/configuration.md +11 -2
  18. package/docs/getting-started.md +21 -0
  19. package/docs/kxm-handbook.md +3 -3
  20. package/docs/operations.md +24 -0
  21. package/docs/operator-pi-packages.md +63 -0
  22. package/docs/skills/repo-work-delivery.md +107 -0
  23. package/docs/skills.md +2 -0
  24. package/docs/test-matrix.md +4 -3
  25. package/docs/troubleshooting.md +41 -1
  26. package/docs/vnext/validation.md +9 -0
  27. package/docs/webhook-workflows.md +2 -2
  28. package/examples/README.md +1 -1
  29. package/package.json +16 -17
  30. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  31. package/plugins/kxm/README.md +1 -1
  32. package/plugins/kxm/dist/cli.js +8955 -3934
  33. package/plugins/kxm/dist/core.js +271 -34
  34. package/plugins/kxm/dist/extension.js +7759 -86
  35. package/plugins/kxm/dist/mcp-server.js +75 -21
  36. package/plugins/kxm/dist/runtime.js +5637 -1125
  37. package/plugins/kxm/dist/server.js +3125 -2260
  38. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5808 -565
  39. package/plugins/kxm/package.json +1 -1
  40. package/plugins/kxm/skills/SUITE.md +5 -0
  41. package/plugins/kxm/skills/hints.json +73 -0
  42. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  43. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  44. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  45. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  46. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  47. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  48. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  49. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  50. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  51. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  52. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  53. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  54. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  55. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  56. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  57. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  58. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  59. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  60. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  61. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  62. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  63. package/plugins/kxm/src/autocomplete.ts +9 -3
  64. package/plugins/kxm/src/cli.ts +1637 -73
  65. package/plugins/kxm/src/commands.ts +150 -8
  66. package/plugins/kxm/src/completion-install.ts +223 -0
  67. package/plugins/kxm/src/config.ts +7 -4
  68. package/plugins/kxm/src/context-packet.ts +172 -0
  69. package/plugins/kxm/src/database.ts +1 -1
  70. package/plugins/kxm/src/extension.ts +36 -1
  71. package/plugins/kxm/src/external-effects.ts +357 -8
  72. package/plugins/kxm/src/hub-env.ts +193 -0
  73. package/plugins/kxm/src/hub.ts +2 -4
  74. package/plugins/kxm/src/improve.ts +72 -0
  75. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  76. package/plugins/kxm/src/local-snapshot.ts +1 -1
  77. package/plugins/kxm/src/mcp-server.ts +1 -1
  78. package/plugins/kxm/src/model-inventory.ts +127 -0
  79. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  80. package/plugins/kxm/src/policy-draft.mjs +565 -0
  81. package/plugins/kxm/src/price-calc.ts +17 -18
  82. package/plugins/kxm/src/prices.ts +32 -16
  83. package/plugins/kxm/src/producers.ts +71 -0
  84. package/plugins/kxm/src/protocol.ts +111 -0
  85. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  86. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  87. package/plugins/kxm/src/role.ts +710 -0
  88. package/plugins/kxm/src/routing.ts +99 -1
  89. package/plugins/kxm/src/safety-integrity.ts +76 -0
  90. package/plugins/kxm/src/session-work.ts +9 -2
  91. package/plugins/kxm/src/sqlite.ts +76 -0
  92. package/plugins/kxm/src/store.ts +1 -1
  93. package/plugins/kxm/src/studio-layout.ts +660 -17
  94. package/plugins/kxm/src/suggest.ts +7 -13
  95. package/plugins/kxm/src/telemetry.ts +82 -0
  96. package/plugins/kxm/src/tui.ts +140 -0
  97. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  98. package/plugins/kxm/src/vnext-config.ts +53 -111
  99. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  100. package/plugins/kxm/src/vnext-engine.ts +214 -62
  101. package/plugins/kxm/src/vnext-harness.ts +267 -83
  102. package/plugins/kxm/src/vnext-oneshot-evidence.ts +85 -0
  103. package/plugins/kxm/src/vnext-oneshot-process.ts +166 -0
  104. package/plugins/kxm/src/vnext-oneshot-producer.ts +179 -224
  105. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  106. package/plugins/kxm/src/vnext-runtime-supervisor.ts +120 -5
  107. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  108. package/plugins/kxm/src/workflow-manager.ts +392 -0
  109. package/plugins/kxm/src/workflow-tui.ts +255 -0
  110. package/plugins/kxm/src/workflow.ts +144 -0
  111. package/schemas/policy-draft/README.md +17 -0
  112. package/schemas/policy-draft/model.v2.schema.json +140 -0
  113. package/schemas/policy-draft/role.v2.schema.json +91 -0
  114. package/schemas/vnext/role.schema.json +76 -0
  115. package/schemas/vnext/run-event.schema.json +1 -0
  116. package/scripts/assignment-run.d.mts +1 -1
  117. package/scripts/assignment-run.mjs +44 -35
  118. package/scripts/check-generated.mjs +33 -9
  119. package/scripts/emit-codex-artifacts.mjs +255 -11
  120. package/scripts/harness-run.d.mts +12 -4
  121. package/scripts/harness-run.mjs +65 -17
  122. package/scripts/kxm-bump-version.mjs +146 -0
  123. package/scripts/kxm-hub.mjs +150 -2
  124. package/scripts/kxm-publish-npm.mjs +3 -1
  125. package/scripts/kxm-release-github.mjs +3 -1
  126. package/scripts/kxm.mjs +0 -0
  127. package/scripts/native-critic.d.mts +5 -0
  128. package/scripts/native-critic.mjs +60 -0
  129. package/.kxm/config/README.md +0 -5
  130. package/.kxm/config/agents.json +0 -43
  131. package/.kxm/config/env.example +0 -56
  132. package/.kxm/config/update.example.yaml +0 -9
  133. package/.kxm/config/workflows/fix.json +0 -160
  134. package/.kxm/config/workflows/jira-development.json +0 -116
  135. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  136. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -4,6 +4,8 @@
4
4
  * and Temporal swimlane activity timelines from compiled plans and run states.
5
5
  */
6
6
 
7
+ import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http";
8
+ import { randomUUID } from "node:crypto";
7
9
  import type { VnextCompiledPlan, VnextCompiledStep } from "./vnext-engine-compile.ts";
8
10
  import type { VnextRunState } from "./vnext-engine-fold.ts";
9
11
 
@@ -90,9 +92,13 @@ export function generateStudioLayout(
90
92
 
91
93
  for (const [sourceId, step] of Object.entries(plan.steps)) {
92
94
  for (const transition of Object.values(step.transitions)) {
93
- if (transition.to && plan.steps[transition.to]) {
94
- adj[sourceId]!.push(transition.to);
95
- inDegree[transition.to] = (inDegree[transition.to] ?? 0) + 1;
95
+ if (transition.to === "step") {
96
+ if (transition.edge === "back") continue; // Back-edges are cycles (retries), ignore for DAG forward rank
97
+ const targetStepId = transition.target;
98
+ if (plan.steps[targetStepId]) {
99
+ adj[sourceId]!.push(targetStepId);
100
+ inDegree[targetStepId] = (inDegree[targetStepId] ?? 0) + 1;
101
+ }
96
102
  }
97
103
  }
98
104
  }
@@ -113,13 +119,17 @@ export function generateStudioLayout(
113
119
  }
114
120
  }
115
121
 
116
- while (queue.length > 0) {
122
+ let iterations = 0;
123
+ const maxIterations = stepIds.length * 4;
124
+ while (queue.length > 0 && iterations++ < maxIterations) {
117
125
  const current = queue.shift()!;
118
126
  const curLevel = levels[current] ?? 0;
119
127
  for (const neighbor of adj[current] ?? []) {
120
128
  const nextLevel = Math.max(levels[neighbor] ?? 0, curLevel + 1);
121
- levels[neighbor] = nextLevel;
122
- queue.push(neighbor);
129
+ if (levels[neighbor] === undefined || nextLevel > levels[neighbor]!) {
130
+ levels[neighbor] = nextLevel;
131
+ queue.push(neighbor);
132
+ }
123
133
  }
124
134
  }
125
135
 
@@ -196,17 +206,20 @@ export function generateStudioLayout(
196
206
 
197
207
  // Build edges
198
208
  for (const [outcome, transition] of Object.entries(step.transitions)) {
199
- if (transition.to && plan.steps[transition.to]) {
200
- edges.push({
201
- id: `e_${stepId}_to_${transition.to}_${outcome}`,
202
- source: stepId,
203
- target: transition.to,
204
- label: outcome !== "passed" && outcome !== "completed" ? outcome : undefined,
205
- animated: status === "running",
206
- style: {
207
- stroke: outcome === "failed" ? "#ef4444" : outcome === "warning" ? "#f59e0b" : "#64748b",
208
- },
209
- });
209
+ if (transition.to === "step") {
210
+ const targetStepId = transition.target;
211
+ if (plan.steps[targetStepId]) {
212
+ edges.push({
213
+ id: `e_${stepId}_to_${targetStepId}_${outcome}`,
214
+ source: stepId,
215
+ target: targetStepId,
216
+ label: outcome !== "passed" && outcome !== "completed" ? outcome : undefined,
217
+ animated: status === "running",
218
+ style: {
219
+ stroke: outcome === "failed" ? "#ef4444" : outcome === "warning" ? "#f59e0b" : "#64748b",
220
+ },
221
+ });
222
+ }
210
223
  }
211
224
  }
212
225
  });
@@ -266,3 +279,633 @@ export function generateStudioLayout(
266
279
  temporalSwimlanes,
267
280
  };
268
281
  }
282
+
283
+ export const DEFAULT_STUDIO_PORT = 4242; // Decision Q8 & D14
284
+
285
+ export interface StudioServerOptions {
286
+ port?: number | undefined;
287
+ host?: string | undefined;
288
+ projectRoot?: string | undefined;
289
+ sessionToken?: string | undefined;
290
+ planProvider?: (() => VnextCompiledPlan | undefined) | undefined;
291
+ stateProvider?: (() => VnextRunState | undefined) | undefined;
292
+ onMutation?: ((command: string, args: Record<string, unknown>) => Promise<{ ok: boolean; result?: unknown; error?: string }> | { ok: boolean; result?: unknown; error?: string }) | undefined;
293
+ }
294
+
295
+ export interface StudioServerHandle {
296
+ server: Server;
297
+ port: number;
298
+ listen: () => Promise<number>;
299
+ close: () => Promise<void>;
300
+ }
301
+
302
+ /**
303
+ * Embedded Web Studio Local Server (Decision Q8 & D14).
304
+ * Serves the visual DAG, form stepper, and Temporal swimlane dashboards on http://localhost:4242.
305
+ * Enforces strict audit parity: web mutations authenticate via SessionToken and map 1:1 to CLI commands.
306
+ */
307
+ export function createStudioServer(options: StudioServerOptions = {}): StudioServerHandle {
308
+ const targetPort = options.port ?? DEFAULT_STUDIO_PORT;
309
+ const host = options.host ?? "127.0.0.1";
310
+
311
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
312
+ const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
313
+
314
+ // CORS headers
315
+ res.setHeader("Access-Control-Allow-Origin", "*");
316
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
317
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
318
+
319
+ if (req.method === "OPTIONS") {
320
+ res.writeHead(204);
321
+ res.end();
322
+ return;
323
+ }
324
+
325
+ if (url.pathname === "/health" || url.pathname === "/api/health") {
326
+ res.writeHead(200, { "Content-Type": "application/json" });
327
+ res.end(JSON.stringify({ ok: true, studio: true, port: targetPort }));
328
+ return;
329
+ }
330
+
331
+ if (url.pathname === "/api/layout" && req.method === "GET") {
332
+ const plan = options.planProvider?.();
333
+ if (!plan) {
334
+ res.writeHead(404, { "Content-Type": "application/json" });
335
+ res.end(JSON.stringify({ ok: false, error: "no_active_workflow_plan" }));
336
+ return;
337
+ }
338
+ const state = options.stateProvider?.();
339
+ const layout = generateStudioLayout(plan, state);
340
+ res.writeHead(200, { "Content-Type": "application/json" });
341
+ res.end(JSON.stringify({ ok: true, layout }));
342
+ return;
343
+ }
344
+
345
+ if (url.pathname === "/api/mutate" && req.method === "POST") {
346
+ // 1. Strict Authentication via SessionToken
347
+ if (options.sessionToken) {
348
+ const auth = req.headers.authorization;
349
+ const expected = `Bearer ${options.sessionToken}`;
350
+ if (!auth || auth !== expected) {
351
+ res.writeHead(401, { "Content-Type": "application/json" });
352
+ res.end(JSON.stringify({
353
+ ok: false,
354
+ error: "unauthorized",
355
+ message: "Valid SessionToken required for Web Studio mutations",
356
+ }));
357
+ return;
358
+ }
359
+ }
360
+
361
+ // 2. Read request body
362
+ const chunks: Buffer[] = [];
363
+ for await (const chunk of req) {
364
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
365
+ }
366
+ const bodyStr = Buffer.concat(chunks).toString("utf8");
367
+ let body: { command?: string; args?: Record<string, unknown> };
368
+ try {
369
+ body = JSON.parse(bodyStr);
370
+ } catch {
371
+ res.writeHead(400, { "Content-Type": "application/json" });
372
+ res.end(JSON.stringify({ ok: false, error: "malformed_json" }));
373
+ return;
374
+ }
375
+
376
+ const command = body.command;
377
+ const args = body.args ?? {};
378
+ if (!command || typeof command !== "string") {
379
+ res.writeHead(400, { "Content-Type": "application/json" });
380
+ res.end(JSON.stringify({ ok: false, error: "missing_command" }));
381
+ return;
382
+ }
383
+
384
+ // 3. Strict Audit Parity: verify command maps 1:1 to underlying CLI commands
385
+ const ALLOWED_CLI_MUTATIONS = [
386
+ "workflow.signal",
387
+ "workflow.checkpoint",
388
+ "workflow.wait",
389
+ "workflow.start",
390
+ "run.cancel",
391
+ "config.set",
392
+ "task.update",
393
+ "task.sync",
394
+ "peer.send",
395
+ "peer.reply",
396
+ "gate.signal",
397
+ "gate.degrade",
398
+ ];
399
+
400
+ if (!ALLOWED_CLI_MUTATIONS.includes(command)) {
401
+ res.writeHead(400, { "Content-Type": "application/json" });
402
+ res.end(JSON.stringify({
403
+ ok: false,
404
+ error: "unsupported_web_mutation",
405
+ message: `Command '${command}' does not have a 1:1 underlying CLI audit mapping`,
406
+ }));
407
+ return;
408
+ }
409
+
410
+ const mutationId = `mut_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
411
+ if (options.onMutation) {
412
+ try {
413
+ const outcome = await options.onMutation(command, args);
414
+ res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "application/json" });
415
+ res.end(JSON.stringify({ mutationId, ...outcome }));
416
+ } catch (err: unknown) {
417
+ res.writeHead(500, { "Content-Type": "application/json" });
418
+ res.end(JSON.stringify({ mutationId, ok: false, error: (err as Error).message }));
419
+ }
420
+ return;
421
+ }
422
+
423
+ res.writeHead(200, { "Content-Type": "application/json" });
424
+ res.end(JSON.stringify({
425
+ ok: true,
426
+ mutationId,
427
+ command,
428
+ mappedToCli: true,
429
+ executedAt: new Date().toISOString(),
430
+ }));
431
+ return;
432
+ }
433
+
434
+ if (url.pathname === "/" || url.pathname === "/index.html") {
435
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
436
+ res.end(`<!DOCTYPE html>
437
+ <html lang="en">
438
+ <head>
439
+ <meta charset="UTF-8">
440
+ <title>KXM Web Studio</title>
441
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
442
+ <style>
443
+ :root {
444
+ --bg: #0b0f19;
445
+ --panel-bg: #111827;
446
+ --card-bg: #1f2937;
447
+ --border: #374151;
448
+ --text: #f9fafb;
449
+ --text-muted: #9ca3af;
450
+ --primary: #38bdf8;
451
+ --success: #10b981;
452
+ --warning: #f59e0b;
453
+ --danger: #ef4444;
454
+ }
455
+ * { box-sizing: border-box; }
456
+ body {
457
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
458
+ background: var(--bg);
459
+ color: var(--text);
460
+ margin: 0;
461
+ padding: 0;
462
+ display: flex;
463
+ flex-direction: column;
464
+ height: 100vh;
465
+ overflow: hidden;
466
+ }
467
+ header {
468
+ background: var(--panel-bg);
469
+ border-bottom: 1px solid var(--border);
470
+ padding: 12px 24px;
471
+ display: flex;
472
+ align-items: center;
473
+ justify-content: space-between;
474
+ }
475
+ .brand { display: flex; align-items: center; gap: 12px; }
476
+ h1 { font-size: 18px; font-weight: 700; color: var(--primary); margin: 0; }
477
+ .badge {
478
+ display: inline-flex;
479
+ align-items: center;
480
+ gap: 6px;
481
+ padding: 3px 8px;
482
+ border-radius: 9999px;
483
+ background: #1e293b;
484
+ color: var(--text-muted);
485
+ font-size: 12px;
486
+ font-weight: 500;
487
+ }
488
+ .dot-live { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
489
+ .telemetry-bar {
490
+ display: flex;
491
+ gap: 16px;
492
+ font-size: 13px;
493
+ color: var(--text-muted);
494
+ font-family: ui-monospace, monospace;
495
+ }
496
+ .telemetry-val { color: var(--text); font-weight: 600; }
497
+ main {
498
+ flex: 1;
499
+ display: flex;
500
+ flex-direction: column;
501
+ overflow: hidden;
502
+ padding: 16px 24px;
503
+ gap: 16px;
504
+ }
505
+ .panel {
506
+ background: var(--panel-bg);
507
+ border: 1px solid var(--border);
508
+ border-radius: 8px;
509
+ padding: 16px;
510
+ }
511
+ .panel-title {
512
+ font-size: 13px;
513
+ text-transform: uppercase;
514
+ letter-spacing: 0.05em;
515
+ color: var(--text-muted);
516
+ margin: 0 0 12px 0;
517
+ font-weight: 600;
518
+ }
519
+ /* Stepper */
520
+ .stepper-container {
521
+ display: flex;
522
+ align-items: center;
523
+ gap: 8px;
524
+ overflow-x: auto;
525
+ padding: 4px 0;
526
+ }
527
+ .step-badge {
528
+ display: flex;
529
+ align-items: center;
530
+ gap: 8px;
531
+ padding: 8px 14px;
532
+ border-radius: 6px;
533
+ background: var(--card-bg);
534
+ border: 1px solid var(--border);
535
+ font-size: 13px;
536
+ font-weight: 500;
537
+ white-space: nowrap;
538
+ }
539
+ .step-badge.active {
540
+ border-color: var(--primary);
541
+ box-shadow: 0 0 0 1px var(--primary);
542
+ color: var(--primary);
543
+ }
544
+ .step-badge.completed { border-color: var(--success); color: var(--success); }
545
+ .step-badge.failed { border-color: var(--danger); color: var(--danger); }
546
+ .stepper-arrow { color: var(--text-muted); font-size: 14px; }
547
+ /* Middle row */
548
+ .middle-row {
549
+ flex: 1;
550
+ display: flex;
551
+ gap: 16px;
552
+ overflow: hidden;
553
+ }
554
+ /* DAG Canvas */
555
+ .dag-panel {
556
+ flex: 3;
557
+ position: relative;
558
+ overflow: auto;
559
+ background: #0d1321;
560
+ border-radius: 8px;
561
+ border: 1px solid var(--border);
562
+ }
563
+ #dagCanvas {
564
+ position: relative;
565
+ min-width: 100%;
566
+ min-height: 100%;
567
+ padding: 32px;
568
+ }
569
+ .dag-node {
570
+ position: absolute;
571
+ width: 220px;
572
+ background: var(--card-bg);
573
+ border: 1px solid var(--border);
574
+ border-radius: 8px;
575
+ padding: 12px;
576
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.3);
577
+ cursor: pointer;
578
+ transition: border-color 0.2s, transform 0.2s;
579
+ }
580
+ .dag-node:hover {
581
+ border-color: var(--primary);
582
+ transform: translateY(-2px);
583
+ }
584
+ .dag-node.active { border-color: var(--primary); }
585
+ .dag-node.passed { border-color: var(--success); }
586
+ .dag-node.failed { border-color: var(--danger); }
587
+ .dag-node-header {
588
+ display: flex;
589
+ justify-content: space-between;
590
+ align-items: center;
591
+ margin-bottom: 6px;
592
+ }
593
+ .dag-node-title { font-size: 14px; font-weight: 600; color: var(--text); }
594
+ .dag-node-kind {
595
+ font-size: 10px;
596
+ text-transform: uppercase;
597
+ padding: 2px 6px;
598
+ background: #374151;
599
+ border-radius: 4px;
600
+ color: #94a3b8;
601
+ }
602
+ .dag-node-body { font-size: 12px; color: var(--text-muted); display: flex; flex-direction: column; gap: 4px; }
603
+ /* Side Panel */
604
+ .side-panel {
605
+ flex: 2;
606
+ display: flex;
607
+ flex-direction: column;
608
+ gap: 16px;
609
+ overflow-y: auto;
610
+ }
611
+ /* Swimlanes */
612
+ .timeline-track {
613
+ margin-bottom: 8px;
614
+ }
615
+ .track-label { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }
616
+ .track-bar-container {
617
+ background: var(--card-bg);
618
+ height: 20px;
619
+ border-radius: 4px;
620
+ position: relative;
621
+ overflow: hidden;
622
+ }
623
+ .track-bar {
624
+ position: absolute;
625
+ top: 2px;
626
+ bottom: 2px;
627
+ background: var(--primary);
628
+ border-radius: 3px;
629
+ opacity: 0.85;
630
+ }
631
+ /* Mutation Actions */
632
+ .mutation-form {
633
+ display: flex;
634
+ flex-direction: column;
635
+ gap: 10px;
636
+ }
637
+ .form-group { display: flex; flex-direction: column; gap: 4px; }
638
+ label { font-size: 12px; color: var(--text-muted); }
639
+ input, select, textarea {
640
+ background: var(--card-bg);
641
+ border: 1px solid var(--border);
642
+ border-radius: 4px;
643
+ color: var(--text);
644
+ padding: 8px;
645
+ font-size: 12px;
646
+ font-family: inherit;
647
+ }
648
+ button.btn {
649
+ background: #0284c7;
650
+ color: white;
651
+ border: none;
652
+ border-radius: 4px;
653
+ padding: 8px 12px;
654
+ font-size: 13px;
655
+ font-weight: 600;
656
+ cursor: pointer;
657
+ transition: background 0.2s;
658
+ }
659
+ button.btn:hover { background: #0369a1; }
660
+ .status-msg {
661
+ font-size: 12px;
662
+ padding: 8px;
663
+ border-radius: 4px;
664
+ font-family: monospace;
665
+ }
666
+ .status-ok { background: #064e3b; color: #a7f3d0; }
667
+ .status-err { background: #7f1d1d; color: #fecaca; }
668
+ </style>
669
+ </head>
670
+ <body>
671
+ <header>
672
+ <div class="brand">
673
+ <h1>KXM Web Studio</h1>
674
+ <span class="badge"><span class="dot-live"></span> Port ${targetPort} · Embedded Hub Host</span>
675
+ </div>
676
+ <div class="telemetry-bar">
677
+ <div>Run: <span id="runIdVal" class="telemetry-val">-</span></div>
678
+ <div>Workflow: <span id="wfIdVal" class="telemetry-val">default</span></div>
679
+ <div>Status: <span id="statusVal" class="telemetry-val">READY</span></div>
680
+ </div>
681
+ </header>
682
+
683
+ <main>
684
+ <!-- Stepper Section -->
685
+ <div class="panel">
686
+ <div class="panel-title">Workflow Stage Progress Stepper (Decision D14)</div>
687
+ <div class="stepper-container" id="stepperContainer">
688
+ <div class="step-badge">[▶ implement]</div>
689
+ <div class="stepper-arrow">──></div>
690
+ <div class="step-badge">[⧗ review-arch]</div>
691
+ <div class="stepper-arrow">──></div>
692
+ <div class="step-badge">[○ review-cli]</div>
693
+ <div class="stepper-arrow">──></div>
694
+ <div class="step-badge">[○ verify]</div>
695
+ </div>
696
+ </div>
697
+
698
+ <!-- Middle: DAG Canvas + Side Swimlanes/Mutations -->
699
+ <div class="middle-row">
700
+ <!-- DAG Panel -->
701
+ <div class="dag-panel">
702
+ <svg id="svgEdges" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;"></svg>
703
+ <div id="dagCanvas">
704
+ <!-- Dynamically injected nodes -->
705
+ </div>
706
+ </div>
707
+
708
+ <!-- Side Panel: Swimlanes & Mutation Actions -->
709
+ <div class="side-panel">
710
+ <div class="panel">
711
+ <div class="panel-title">Temporal Activity Swimlanes</div>
712
+ <div id="swimlanesContainer">
713
+ <div class="timeline-track">
714
+ <div class="track-label">implement (Grok)</div>
715
+ <div class="track-bar-container"><div class="track-bar" style="left:5%; width:40%;"></div></div>
716
+ </div>
717
+ <div class="timeline-track">
718
+ <div class="track-label">review-arch (Claude Fable)</div>
719
+ <div class="track-bar-container"><div class="track-bar" style="left:48%; width:30%; background: #a855f7;"></div></div>
720
+ </div>
721
+ <div class="timeline-track">
722
+ <div class="track-label">verify (verify-gate)</div>
723
+ <div class="track-bar-container"><div class="track-bar" style="left:80%; width:15%; background: #10b981;"></div></div>
724
+ </div>
725
+ </div>
726
+ </div>
727
+
728
+ <div class="panel">
729
+ <div class="panel-title">Audited CLI Mutations (1:1 Audit Parity)</div>
730
+ <form class="mutation-form" id="mutationForm" onsubmit="handleMutate(event)">
731
+ <div class="form-group">
732
+ <label>Command (1:1 CLI audit mapping)</label>
733
+ <select id="mutateCmd">
734
+ <option value="workflow.signal">workflow.signal</option>
735
+ <option value="workflow.checkpoint">workflow.checkpoint</option>
736
+ <option value="workflow.wait">workflow.wait</option>
737
+ <option value="gate.signal">gate.signal</option>
738
+ <option value="gate.degrade">gate.degrade</option>
739
+ </select>
740
+ </div>
741
+ <div class="form-group">
742
+ <label>Arguments (JSON)</label>
743
+ <textarea id="mutateArgs" rows="3" style="font-family: monospace;">{"signalKey": "approval", "status": "passed"}</textarea>
744
+ </div>
745
+ <div class="form-group">
746
+ <label>Session Token (Authorization)</label>
747
+ <input type="password" id="sessionTokenInput" placeholder="Optional if not authenticated">
748
+ </div>
749
+ <button type="submit" class="btn">Execute Audited Mutation</button>
750
+ <div id="mutateResult" style="display:none;" class="status-msg"></div>
751
+ </form>
752
+ </div>
753
+ </div>
754
+ </div>
755
+ </main>
756
+
757
+ <script>
758
+ async function loadLayout() {
759
+ try {
760
+ const res = await fetch('/api/layout');
761
+ if (!res.ok) return;
762
+ const data = await res.json();
763
+ if (!data.ok || !data.layout) return;
764
+ const layout = data.layout;
765
+
766
+ document.getElementById('wfIdVal').textContent = layout.workflowId || 'default';
767
+ if (layout.runId) document.getElementById('runIdVal').textContent = layout.runId;
768
+
769
+ // Render Stepper
770
+ if (layout.stepper && layout.stepper.length > 0) {
771
+ const container = document.getElementById('stepperContainer');
772
+ container.innerHTML = '';
773
+ layout.stepper.forEach((step, idx) => {
774
+ const el = document.createElement('div');
775
+ el.className = 'step-badge ' + (step.status || 'pending');
776
+ let icon = '○';
777
+ if (step.status === 'completed') icon = '✔';
778
+ else if (step.status === 'active') icon = '▶';
779
+ else if (step.status === 'failed') icon = '✖';
780
+ el.textContent = icon + ' ' + (step.label || step.id);
781
+ container.appendChild(el);
782
+
783
+ if (idx < layout.stepper.length - 1) {
784
+ const arrow = document.createElement('div');
785
+ arrow.className = 'stepper-arrow';
786
+ arrow.textContent = '──>';
787
+ container.appendChild(arrow);
788
+ }
789
+ });
790
+ }
791
+
792
+ // Render DAG Nodes
793
+ if (layout.dag && layout.dag.nodes) {
794
+ const canvas = document.getElementById('dagCanvas');
795
+ canvas.innerHTML = '';
796
+ layout.dag.nodes.forEach(node => {
797
+ const div = document.createElement('div');
798
+ div.className = 'dag-node ' + (node.data.status || 'pending');
799
+ div.style.left = (node.position.x + 32) + 'px';
800
+ div.style.top = (node.position.y + 32) + 'px';
801
+ div.innerHTML = '<div class="dag-node-header">' +
802
+ '<span class="dag-node-title">' + (node.data.label || node.id) + '</span>' +
803
+ '<span class="dag-node-kind">' + node.data.kind + '</span>' +
804
+ '</div>' +
805
+ '<div class="dag-node-body">' +
806
+ (node.data.role ? '<div>Role: <b>' + node.data.role + '</b></div>' : '') +
807
+ (node.data.gate ? '<div>Gate: <b>' + node.data.gate + '</b></div>' : '') +
808
+ '<div>Status: ' + node.data.status + '</div>' +
809
+ '</div>';
810
+ canvas.appendChild(div);
811
+ });
812
+ }
813
+
814
+ // Render Swimlanes
815
+ if (layout.temporalSwimlanes && layout.temporalSwimlanes.length > 0) {
816
+ const container = document.getElementById('swimlanesContainer');
817
+ container.innerHTML = '';
818
+ layout.temporalSwimlanes.forEach(lane => {
819
+ const div = document.createElement('div');
820
+ div.className = 'timeline-track';
821
+ div.innerHTML = '<div class="track-label">' + lane.laneId + (lane.role ? ' (' + lane.role + ')' : '') + '</div>' +
822
+ '<div class="track-bar-container"><div class="track-bar" style="left:10%; width:70%;"></div></div>';
823
+ container.appendChild(div);
824
+ });
825
+ }
826
+ } catch (err) {
827
+ console.error("Layout polling error:", err);
828
+ }
829
+ }
830
+
831
+ async function handleMutate(e) {
832
+ e.preventDefault();
833
+ const cmd = document.getElementById('mutateCmd').value;
834
+ const argsRaw = document.getElementById('mutateArgs').value;
835
+ const token = document.getElementById('sessionTokenInput').value;
836
+ const resDiv = document.getElementById('mutateResult');
837
+
838
+ let args = {};
839
+ try {
840
+ args = JSON.parse(argsRaw);
841
+ } catch {
842
+ resDiv.style.display = 'block';
843
+ resDiv.className = 'status-msg status-err';
844
+ resDiv.textContent = 'Malformed JSON arguments';
845
+ return;
846
+ }
847
+
848
+ try {
849
+ const headers = { 'Content-Type': 'application/json' };
850
+ if (token) headers['Authorization'] = 'Bearer ' + token;
851
+ const res = await fetch('/api/mutate', {
852
+ method: 'POST',
853
+ headers,
854
+ body: JSON.stringify({ command: cmd, args })
855
+ });
856
+ const out = await res.json();
857
+ resDiv.style.display = 'block';
858
+ if (res.ok && out.ok) {
859
+ resDiv.className = 'status-msg status-ok';
860
+ resDiv.textContent = 'Mutation applied (Audit ID: ' + out.mutationId + ')';
861
+ loadLayout();
862
+ } else {
863
+ resDiv.className = 'status-msg status-err';
864
+ resDiv.textContent = 'Mutation failed: ' + (out.message || out.error || res.status);
865
+ }
866
+ } catch (err) {
867
+ resDiv.style.display = 'block';
868
+ resDiv.className = 'status-msg status-err';
869
+ resDiv.textContent = 'Network error: ' + err.message;
870
+ }
871
+ }
872
+
873
+ loadLayout();
874
+ setInterval(loadLayout, 3000);
875
+ </script>
876
+ </body>
877
+ </html>`);
878
+ return;
879
+ }
880
+
881
+ res.writeHead(404, { "Content-Type": "application/json" });
882
+ res.end(JSON.stringify({ ok: false, error: "not_found" }));
883
+ });
884
+
885
+ return {
886
+ server,
887
+ port: targetPort,
888
+ listen: () =>
889
+ new Promise((resolveListen, rejectListen) => {
890
+ server.once("error", rejectListen);
891
+ server.listen(targetPort, host, () => {
892
+ server.removeListener("error", rejectListen);
893
+ const addr = server.address();
894
+ const actualPort = typeof addr === "object" && addr ? addr.port : targetPort;
895
+ resolveListen(actualPort);
896
+ });
897
+ }),
898
+ close: () =>
899
+ new Promise((resolveClose, rejectClose) => {
900
+ try {
901
+ server.closeAllConnections?.();
902
+ } catch {
903
+ // ignore if not supported
904
+ }
905
+ server.close((err) => {
906
+ if (err) rejectClose(err);
907
+ else resolveClose();
908
+ });
909
+ }),
910
+ };
911
+ }