@kontextmind/kxm 0.6.0 → 0.7.10

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 (175) 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 +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -0,0 +1,255 @@
1
+ /**
2
+ * KXM Workflow TUI module for Pi Extension.
3
+ * Renders active workflow run, stage progress stepper, assigned roles, and model/spend metrics.
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { DatabaseSync } from "./sqlite.ts";
9
+ import { repoConfigDirectory } from "./config.ts";
10
+ import { readRoutingRecords, telemetryPath } from "./telemetry.ts";
11
+
12
+ export interface WorkflowProgressMetrics {
13
+ totalSpendUsd?: number | undefined;
14
+ inputTokens?: number | undefined;
15
+ outputTokens?: number | undefined;
16
+ cacheReadTokens?: number | undefined;
17
+ latencyMs?: number | undefined;
18
+ harness?: string | undefined;
19
+ model?: string | undefined;
20
+ effort?: string | undefined;
21
+ memoryRevision?: string | undefined;
22
+ transitions?: number | undefined;
23
+ maxTransitions?: number | undefined;
24
+ }
25
+
26
+ export interface WorkflowProgressState {
27
+ runId?: string | undefined;
28
+ workflowId: string;
29
+ status: "idle" | "running" | "waiting" | "passed" | "failed" | "completed" | "cancelled";
30
+ currentStage?: string | undefined;
31
+ currentRole?: string | undefined;
32
+ attempt?: number | undefined;
33
+ maxAttempts?: number | undefined;
34
+ stages: Array<{
35
+ id: string;
36
+ label?: string | undefined;
37
+ kind?: "agent" | "gate" | "wait" | "approval" | undefined;
38
+ status: "pending" | "running" | "passed" | "failed" | "waiting" | "cancelled";
39
+ role?: string | undefined;
40
+ attempts?: number | undefined;
41
+ }>;
42
+ metrics?: WorkflowProgressMetrics | undefined;
43
+ updatedAt?: string | undefined;
44
+ }
45
+
46
+ /**
47
+ * Load current active or latest workflow progress from SQLite state and telemetry.
48
+ */
49
+ export function loadActiveWorkflowProgress(
50
+ repoRoot = process.cwd(),
51
+ targetRunId?: string,
52
+ ): WorkflowProgressState | undefined {
53
+ const dbPath = join(repoConfigDirectory(repoRoot), "state", "kxm.db");
54
+ if (!existsSync(dbPath)) return undefined;
55
+
56
+ let db: DatabaseSync | undefined;
57
+ try {
58
+ db = new DatabaseSync(dbPath, { readOnly: true });
59
+ let row: { record: string } | undefined;
60
+ if (targetRunId) {
61
+ row = db.prepare("SELECT record FROM workflow_runs WHERE id = ?").get(targetRunId) as { record: string } | undefined;
62
+ } else {
63
+ row = db.prepare(
64
+ "SELECT record FROM workflow_runs ORDER BY CASE json_extract(record, '$.status') WHEN 'running' THEN 1 WHEN 'waiting' THEN 2 ELSE 3 END, json_extract(record, '$.updatedAt') DESC LIMIT 1",
65
+ ).get() as { record: string } | undefined;
66
+ }
67
+
68
+ if (!row) return undefined;
69
+
70
+ const raw = JSON.parse(row.record) as Record<string, unknown>;
71
+ const runId = (raw.id as string) || targetRunId;
72
+ const workflowId = (raw.definitionId as string) || (raw.workflowId as string) || "workflow";
73
+ const status = (raw.status as WorkflowProgressState["status"]) || "running";
74
+ const currentStage = (raw.currentStage as string) || undefined;
75
+ const updatedAt = (raw.updatedAt as string) || undefined;
76
+
77
+ const stages: WorkflowProgressState["stages"] = [];
78
+ const rawStages = Array.isArray(raw.stages) ? raw.stages : [];
79
+ for (const s of rawStages) {
80
+ if (s && typeof s === "object") {
81
+ stages.push({
82
+ id: String((s as Record<string, unknown>).id || ""),
83
+ label: (s as Record<string, unknown>).label as string | undefined,
84
+ kind: (s as Record<string, unknown>).kind as any,
85
+ status: ((s as Record<string, unknown>).status as any) || "pending",
86
+ role: (s as Record<string, unknown>).role as string | undefined,
87
+ attempts: typeof (s as Record<string, unknown>).attempts === "number" ? ((s as Record<string, unknown>).attempts as number) : undefined,
88
+ });
89
+ }
90
+ }
91
+
92
+ let metrics: WorkflowProgressMetrics | undefined;
93
+ try {
94
+ const logsDir = join(repoConfigDirectory(repoRoot), "logs");
95
+ const telemFile = telemetryPath(logsDir);
96
+ if (existsSync(telemFile)) {
97
+ const records = readRoutingRecords(telemFile);
98
+ const routingRunId = (routing: (typeof records)[number]["routing"]): string | undefined =>
99
+ "runId" in routing ? routing.runId : routing.workflowRunId;
100
+ let matching = records.filter((r) => routingRunId(r.routing) === runId);
101
+ if (matching.length === 0) {
102
+ const rawLines = readFileSync(telemFile, "utf8").split(/\r?\n/);
103
+ for (const line of rawLines) {
104
+ if (!line.trim()) continue;
105
+ try {
106
+ const p = JSON.parse(line) as Record<string, unknown>;
107
+ const rt = (p.routing || p) as any;
108
+ if (rt && (routingRunId(rt) === runId || p.runId === runId)) {
109
+ matching.push({ recordedAt: (p.timestamp as string) || new Date().toISOString(), routing: rt });
110
+ }
111
+ } catch {
112
+ // ignore
113
+ }
114
+ }
115
+ }
116
+ if (matching.length > 0) {
117
+ const latest = matching[matching.length - 1]!.routing;
118
+ let totalSpend = 0;
119
+ let totalIn = 0;
120
+ let totalOut = 0;
121
+ let totalCache = 0;
122
+ let totalLatency = 0;
123
+ for (const m of matching) {
124
+ const rt = m.routing;
125
+ if (typeof rt.costUsd === "number") totalSpend += rt.costUsd;
126
+ if ("tokensIn" in rt && typeof rt.tokensIn === "number") totalIn += rt.tokensIn;
127
+ else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { input?: unknown }).input === "number") totalIn += (rt.tokens as { input: number }).input;
128
+ if ("tokensOut" in rt && typeof rt.tokensOut === "number") totalOut += rt.tokensOut;
129
+ else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { output?: unknown }).output === "number") totalOut += (rt.tokens as { output: number }).output;
130
+ if ("cacheReadTokens" in rt && typeof rt.cacheReadTokens === "number") totalCache += rt.cacheReadTokens;
131
+ else if ("tokens" in rt && rt.tokens && typeof (rt.tokens as { cacheRead?: unknown }).cacheRead === "number") totalCache += (rt.tokens as { cacheRead: number }).cacheRead;
132
+ if ("latencyMs" in rt && typeof rt.latencyMs === "number") totalLatency += rt.latencyMs;
133
+ }
134
+ const latestRecord = latest as unknown as Record<string, unknown>;
135
+ const latestMetadata = latestRecord.providerMetadata && typeof latestRecord.providerMetadata === "object"
136
+ ? latestRecord.providerMetadata as Record<string, unknown>
137
+ : undefined;
138
+ metrics = {
139
+ totalSpendUsd: totalSpend,
140
+ inputTokens: totalIn,
141
+ outputTokens: totalOut,
142
+ cacheReadTokens: totalCache,
143
+ latencyMs: totalLatency,
144
+ harness: typeof latestRecord.harness === "string" ? latestRecord.harness : (typeof latestMetadata?.harness === "string" ? latestMetadata.harness : undefined),
145
+ model: typeof latestRecord.effectiveModel === "string" ? latestRecord.effectiveModel : (typeof latestRecord.model === "string" ? latestRecord.model : (typeof latestRecord.requestedModel === "string" ? latestRecord.requestedModel : undefined)),
146
+ effort: typeof latestRecord.thinking === "string" ? latestRecord.thinking : (typeof latestRecord.reasoningEffort === "string" ? latestRecord.reasoningEffort : undefined),
147
+ };
148
+ }
149
+ }
150
+ } catch {
151
+ // Telemetry optional
152
+ }
153
+
154
+ const activeStageObj = stages.find((s) => s.id === currentStage);
155
+ const currentRole = activeStageObj?.role || (raw.targetAgentName as string) || undefined;
156
+ const attempt = activeStageObj?.attempts || 1;
157
+
158
+ return {
159
+ runId,
160
+ workflowId,
161
+ status,
162
+ currentStage,
163
+ currentRole,
164
+ attempt,
165
+ stages,
166
+ metrics,
167
+ updatedAt,
168
+ };
169
+ } catch {
170
+ return undefined;
171
+ } finally {
172
+ try {
173
+ db?.close();
174
+ } catch {
175
+ // ignore
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Format stage progress indicator into a compact horizontal visual stepper.
182
+ * e.g.: [✔ implement] ──> [▶ review-arch] ──> [⧗ review-cli] ──> [○ verify]
183
+ */
184
+ export function formatStageStepper(stages: WorkflowProgressState["stages"], currentStage?: string): string {
185
+ if (stages.length === 0) {
186
+ return currentStage ? `[▶ ${currentStage}]` : "[idle]";
187
+ }
188
+
189
+ const parts = stages.map((s) => {
190
+ let icon = "○"; // pending
191
+ if (s.status === "passed") icon = "✔";
192
+ else if (s.id === currentStage || s.status === "running") icon = "▶";
193
+ else if (s.status === "waiting") icon = "⧗";
194
+ else if (s.status === "failed") icon = "✖";
195
+ else if (s.status === "cancelled") icon = "⊘";
196
+ return `[${icon} ${s.id}]`;
197
+ });
198
+
199
+ return parts.join(" ──> ");
200
+ }
201
+
202
+ function formatTokenK(tokens?: number): string {
203
+ if (tokens === undefined || tokens === null) return "0";
204
+ if (tokens >= 1000) {
205
+ return `${(tokens / 1000).toFixed(1)}k`;
206
+ }
207
+ return String(tokens);
208
+ }
209
+
210
+ /**
211
+ * Render Pi Extension live widget lines (for `ctx.ui.setWidget("kxm-workflow", lines)`).
212
+ */
213
+ export function renderWorkflowWidgetLines(state?: WorkflowProgressState): string[] {
214
+ if (!state || !state.runId) {
215
+ return [
216
+ "┌─ KXM Workflow ──────────────────────────────────────────────────────────────┐",
217
+ "│ No active workflow running. Use 'kxm workflow start <id>' │",
218
+ "└─────────────────────────────────────────────────────────────────────────────┘",
219
+ ];
220
+ }
221
+
222
+ const shortRun = state.runId ? state.runId.slice(0, 16) : "-";
223
+ const header = `┌─ KXM Workflow: ${state.workflowId} (${shortRun}) ─`;
224
+ const topBorder = header + "─".repeat(Math.max(2, 79 - header.length)) + "┐";
225
+
226
+ const stepper = formatStageStepper(state.stages, state.currentStage);
227
+ const statusStr = state.status.toUpperCase();
228
+ const attemptStr = state.attempt ? ` (Attempt ${state.attempt})` : "";
229
+ const roleStr = state.currentRole ? ` · Role: ${state.currentRole}` : "";
230
+
231
+ const lines: string[] = [topBorder];
232
+ lines.push(`│ Stage: ${stepper}`);
233
+ lines.push(`│ Status: ${statusStr}${attemptStr}${roleStr}`);
234
+
235
+ if (state.metrics) {
236
+ const costStr = state.metrics.totalSpendUsd !== undefined ? `$${state.metrics.totalSpendUsd.toFixed(2)}` : "unmetered";
237
+ const routeStr = state.metrics.model ? `${state.metrics.harness ?? "pi"}:${state.metrics.model}` : "harness";
238
+ const effortStr = state.metrics.effort ? ` (${state.metrics.effort})` : "";
239
+ const inTokens = formatTokenK(state.metrics.inputTokens);
240
+ const outTokens = formatTokenK(state.metrics.outputTokens);
241
+ const cacheTokens = formatTokenK(state.metrics.cacheReadTokens);
242
+ lines.push(`│ Model: ${routeStr}${effortStr} · Spend: ${costStr}`);
243
+ lines.push(`│ Tokens: In: ${inTokens} | Out: ${outTokens} | Cache: ${cacheTokens}`);
244
+ }
245
+
246
+ lines.push("└" + "─".repeat(78) + "┘");
247
+ return lines;
248
+ }
249
+
250
+ /**
251
+ * Render detailed full-width TUI banner / notification string.
252
+ */
253
+ export function renderWorkflowTuiText(state?: WorkflowProgressState): string {
254
+ return renderWorkflowWidgetLines(state).join("\n");
255
+ }
@@ -3,11 +3,17 @@ import {
3
3
  MAX_MESSAGE_TTL_MS,
4
4
  MIN_MESSAGE_TTL_MS,
5
5
  ProtocolError,
6
+ TERMINAL_RECEIPT_SCHEMA,
6
7
  newId,
7
8
  requireString,
9
+ validateTerminalReceipt,
8
10
  type ImprovementArea,
9
11
  type JournalCategory,
10
12
  type MessageRecord,
13
+ type TerminalReceipt,
14
+ type TerminalReceiptEvidence,
15
+ type TerminalReceiptMetrics,
16
+ type TerminalReceiptStatus,
11
17
  type WorkflowCheckpointStatus,
12
18
  type WorkflowEvidenceInput,
13
19
  type WorkflowEvidenceReference,
@@ -18,11 +24,16 @@ import {
18
24
  export type {
19
25
  ImprovementArea,
20
26
  JournalCategory,
27
+ TerminalReceipt,
28
+ TerminalReceiptEvidence,
29
+ TerminalReceiptMetrics,
30
+ TerminalReceiptStatus,
21
31
  WorkflowCheckpointStatus,
22
32
  WorkflowEvidenceInput,
23
33
  WorkflowEvidenceReference,
24
34
  WorkflowEvidenceReferenceInput,
25
35
  } from "./protocol.ts";
36
+ export { TERMINAL_RECEIPT_SCHEMA, validateTerminalReceipt } from "./protocol.ts";
26
37
 
27
38
  export type WorkflowRunStatus = "running" | "waiting" | "completed" | "failed";
28
39
  export type WorkflowStageStatus = "pending" | "in_progress" | "waiting" | WorkflowCheckpointStatus;
@@ -153,6 +164,8 @@ export interface WorkflowStageDefinition {
153
164
  instructions: string;
154
165
  requiredEvidence: string[];
155
166
  maxAttempts: number;
167
+ /** Bounded in-place retry limit before audit escalation (default: 2). */
168
+ autoResumeLimit?: number;
156
169
  area?: ImprovementArea;
157
170
  evidencePolicies?: WorkflowEvidencePolicies;
158
171
  /** Typed outcome map (v0.5). Keys are outcome identities ("passed",
@@ -250,6 +263,13 @@ export interface WorkflowStageState extends WorkflowStageDefinition {
250
263
  startedAt?: string;
251
264
  completedAt?: string;
252
265
  updatedAt?: string;
266
+ receipt?: TerminalReceipt;
267
+ auditEscalation?: {
268
+ reason: string;
269
+ timestamp: string;
270
+ ruling?: string;
271
+ receipt?: TerminalReceipt;
272
+ };
253
273
  }
254
274
 
255
275
  export interface CapturedOracle {
@@ -1003,12 +1023,18 @@ export function parseWorkflowDefinitions(
1003
1023
  && (!Number.isInteger(stageMaxTransitions) || (stageMaxTransitions as number) < 1 || (stageMaxTransitions as number) > 100)) {
1004
1024
  throw new Error(`stage ${stageId} maxTransitions must be an integer between 1 and 100`);
1005
1025
  }
1026
+ const autoResumeLimit = stage.autoResumeLimit;
1027
+ if (autoResumeLimit !== undefined
1028
+ && (!Number.isInteger(autoResumeLimit) || (autoResumeLimit as number) < 1 || (autoResumeLimit as number) > 20)) {
1029
+ throw new Error(`stage ${stageId} autoResumeLimit must be an integer between 1 and 20`);
1030
+ }
1006
1031
  return {
1007
1032
  id: stageId,
1008
1033
  label: requireString(stage.label ?? stageId, "stage.label", { max: 128 }),
1009
1034
  instructions: requireString(stage.instructions, "stage.instructions", { max: 4_000 }),
1010
1035
  requiredEvidence,
1011
1036
  maxAttempts: maxAttempts as number,
1037
+ ...(autoResumeLimit !== undefined ? { autoResumeLimit: autoResumeLimit as number } : {}),
1012
1038
  ...(area ? { area } : {}),
1013
1039
  ...(evidencePolicies ? { evidencePolicies } : {}),
1014
1040
  ...(on ? { on } : {}),
@@ -1377,6 +1403,31 @@ export function checkpointRun(
1377
1403
  delete run.currentStage;
1378
1404
  return { retry: false, completed: false, run };
1379
1405
  }
1406
+ if (stage.autoResumeLimit !== undefined && stage.attempts >= stage.autoResumeLimit) {
1407
+ stage.status = "in_progress";
1408
+ const reason = summary || `autoResumeLimit of ${stage.autoResumeLimit} reached on stage ${stage.id}`;
1409
+ const receipt = validateTerminalReceipt({
1410
+ schema: TERMINAL_RECEIPT_SCHEMA,
1411
+ status: "audit_escalation",
1412
+ seat: stage.id,
1413
+ runId: run.id,
1414
+ stageId: stage.id,
1415
+ timestamp,
1416
+ host: "pi",
1417
+ model: "default",
1418
+ escalationReason: reason,
1419
+ });
1420
+ const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
1421
+ waitForWorkflowSignal(run, stage.id, "audit_escalation", reason, timestamp, expiresAt);
1422
+ stage.receipt = receipt;
1423
+ stage.auditEscalation = {
1424
+ reason,
1425
+ timestamp,
1426
+ receipt,
1427
+ };
1428
+ return { retry: false, completed: false, run };
1429
+ }
1430
+ stage.status = status;
1380
1431
  // Declared failure outcomes create typed transitions (bounded); the
1381
1432
  // outcome identity defaults to the checkpoint status.
1382
1433
  const outcomeKey = outcome ?? status;
@@ -1546,3 +1597,96 @@ export function approveWorkflowDegradation(
1546
1597
  run.updatedAt = timestamp;
1547
1598
  return { run, approval, created: true };
1548
1599
  }
1600
+
1601
+ export function escalateWorkflowStage(
1602
+ run: WorkflowRun,
1603
+ stageId: string,
1604
+ reason: string,
1605
+ timestamp: string,
1606
+ options: {
1607
+ seat?: string;
1608
+ host?: string;
1609
+ model?: string;
1610
+ evidence?: TerminalReceiptEvidence;
1611
+ metrics?: TerminalReceiptMetrics;
1612
+ } = {},
1613
+ ): { run: WorkflowRun; receipt: TerminalReceipt } {
1614
+ if (run.status !== "running") throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_terminal");
1615
+ const stage = run.stages.find((s) => s.id === stageId);
1616
+ if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
1617
+ if (stage.id !== run.currentStage || stage.status !== "in_progress") {
1618
+ throw new ProtocolError(409, `stage ${stageId} is not currently active`, "workflow_stage_out_of_order");
1619
+ }
1620
+
1621
+ const receipt: TerminalReceipt = validateTerminalReceipt({
1622
+ schema: TERMINAL_RECEIPT_SCHEMA,
1623
+ status: "audit_escalation",
1624
+ seat: options.seat ?? stage.id,
1625
+ runId: run.id,
1626
+ stageId,
1627
+ timestamp,
1628
+ host: options.host ?? "pi",
1629
+ model: options.model ?? "default",
1630
+ escalationReason: reason,
1631
+ ...(options.evidence ? { evidence: options.evidence } : {}),
1632
+ ...(options.metrics ? { metrics: options.metrics } : {}),
1633
+ });
1634
+
1635
+ const expiresAt = new Date(Date.parse(timestamp) + 24 * 60 * 60 * 1000).toISOString();
1636
+ waitForWorkflowSignal(run, stageId, "audit_escalation", reason, timestamp, expiresAt);
1637
+
1638
+ stage.receipt = receipt;
1639
+ stage.auditEscalation = {
1640
+ reason,
1641
+ timestamp,
1642
+ receipt,
1643
+ };
1644
+
1645
+ return { run, receipt };
1646
+ }
1647
+
1648
+ export function resumeWorkflowFromRuling(
1649
+ run: WorkflowRun,
1650
+ ruling: string,
1651
+ timestamp: string,
1652
+ options: {
1653
+ status?: WorkflowCheckpointStatus;
1654
+ evidence?: WorkflowEvidenceInput;
1655
+ } = {},
1656
+ ): { retry: boolean; completed: boolean; run: WorkflowRun; stageId: string } {
1657
+ if (run.status !== "waiting" || !run.waiting) {
1658
+ throw new ProtocolError(409, `workflow is ${run.status}`, "workflow_not_waiting");
1659
+ }
1660
+ if (run.waiting.signalKey !== "audit_escalation") {
1661
+ throw new ProtocolError(
1662
+ 409,
1663
+ `workflow is waiting for signal '${run.waiting.signalKey}', not 'audit_escalation'`,
1664
+ "workflow_signal_mismatch",
1665
+ );
1666
+ }
1667
+
1668
+ const stageId = run.waiting.stageId;
1669
+ const stage = run.stages.find((s) => s.id === stageId);
1670
+ if (!stage) throw new ProtocolError(404, `workflow stage not found: ${stageId}`, "workflow_stage_not_found");
1671
+
1672
+ const status = options.status ?? "passed";
1673
+ const evidence = options.evidence ?? {};
1674
+
1675
+ if (stage.auditEscalation) {
1676
+ stage.auditEscalation.ruling = ruling;
1677
+ if (stage.auditEscalation.receipt) {
1678
+ stage.auditEscalation.receipt.ruling = ruling;
1679
+ }
1680
+ }
1681
+ stage.summary = `Resumed by operator ruling: ${ruling}`;
1682
+
1683
+ return resumeWorkflowFromSignal(
1684
+ run,
1685
+ "audit_escalation",
1686
+ status,
1687
+ `Resumed: ${ruling}`,
1688
+ evidence,
1689
+ timestamp,
1690
+ );
1691
+ }
1692
+
@@ -0,0 +1,17 @@
1
+ # Policy-draft schemas (non-authoritative)
2
+
3
+ `kxm.model.v2` and `kxm.role.v2` here are **passive scaffolding**. They are not
4
+ operator settings, live registry identities, or admission.
5
+
6
+ - Live project models remain `kxm.model.v1` under `schemas/vnext/`.
7
+ - Live CLI roles remain `kxm.role.v1`.
8
+ - Runner admission remains `.kxm/roster.json` via the trusted control Git loader.
9
+ - This directory is not discovered as a vNext project resource.
10
+
11
+ Pure validation lives in `plugins/kxm/src/policy-draft.mjs`
12
+ (`validatePolicyDraft`). Callers must supply draft documents, evidence bytes,
13
+ and code-owned ceilings explicitly. The function does not read files, Git,
14
+ credentials, or cwd, and it does not dispatch or admit a route.
15
+
16
+ Roster `route` lists are configured preference order, not a measured ranking.
17
+ `status: candidate` is stored as candidate data and is never treated as admitted.
@@ -0,0 +1,140 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.kxm.dev/policy-draft/model.v2.schema.json",
4
+ "title": "KXM policy-draft model route",
5
+ "description": "Passive kxm.model.v2 scaffolding. Not live registry selection, admission, or operator settings.",
6
+ "type": "object",
7
+ "required": ["schema", "harness", "model", "vendor", "status", "permissions"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "schema": { "const": "kxm.model.v2" },
11
+ "id": {
12
+ "type": "string",
13
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
14
+ "minLength": 1,
15
+ "maxLength": 64
16
+ },
17
+ "harness": {
18
+ "type": "string",
19
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
20
+ "minLength": 1,
21
+ "maxLength": 64
22
+ },
23
+ "model": { "type": "string", "minLength": 1, "maxLength": 200 },
24
+ "vendor": { "type": "string", "minLength": 1, "maxLength": 64 },
25
+ "status": { "enum": ["admitted", "candidate", "retired"] },
26
+ "permissions": {
27
+ "type": "array",
28
+ "items": { "enum": ["edit", "read-only"] },
29
+ "minItems": 1,
30
+ "maxItems": 2,
31
+ "uniqueItems": true
32
+ },
33
+ "origin": {
34
+ "type": "object",
35
+ "required": ["source", "sha256"],
36
+ "additionalProperties": false,
37
+ "properties": {
38
+ "source": { "type": "string", "minLength": 1, "maxLength": 512 },
39
+ "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
40
+ }
41
+ },
42
+ "thinking": { "type": "string", "minLength": 1, "maxLength": 64 },
43
+ "tags": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "string",
47
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
48
+ "minLength": 1,
49
+ "maxLength": 64
50
+ },
51
+ "uniqueItems": true,
52
+ "maxItems": 32
53
+ },
54
+ "capabilities": {
55
+ "type": "array",
56
+ "items": {
57
+ "type": "string",
58
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
59
+ "minLength": 1,
60
+ "maxLength": 64
61
+ },
62
+ "uniqueItems": true,
63
+ "maxItems": 32
64
+ },
65
+ "priority": { "type": "integer", "minimum": -10000, "maximum": 10000 },
66
+ "fallbacks": {
67
+ "type": "array",
68
+ "maxItems": 8,
69
+ "items": {
70
+ "oneOf": [
71
+ {
72
+ "type": "object",
73
+ "required": ["profile"],
74
+ "additionalProperties": false,
75
+ "properties": {
76
+ "profile": {
77
+ "type": "string",
78
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
79
+ "minLength": 1,
80
+ "maxLength": 64
81
+ }
82
+ }
83
+ },
84
+ {
85
+ "type": "object",
86
+ "required": ["tag"],
87
+ "additionalProperties": false,
88
+ "properties": {
89
+ "tag": {
90
+ "type": "string",
91
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
92
+ "minLength": 1,
93
+ "maxLength": 64
94
+ },
95
+ "capabilities": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "string",
99
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
100
+ "minLength": 1,
101
+ "maxLength": 64
102
+ },
103
+ "uniqueItems": true,
104
+ "maxItems": 32
105
+ }
106
+ }
107
+ },
108
+ {
109
+ "type": "object",
110
+ "required": ["provider", "model"],
111
+ "additionalProperties": false,
112
+ "properties": {
113
+ "provider": {
114
+ "type": "string",
115
+ "pattern": "^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$",
116
+ "minLength": 1,
117
+ "maxLength": 64
118
+ },
119
+ "model": { "type": "string", "minLength": 1, "maxLength": 200 }
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ },
125
+ "limits": {
126
+ "type": "object",
127
+ "additionalProperties": false,
128
+ "properties": {
129
+ "contextTokens": { "type": "integer", "minimum": 1 },
130
+ "outputTokens": { "type": "integer", "minimum": 1 },
131
+ "timeoutMs": { "type": "integer", "minimum": 0, "maximum": 31536000000 }
132
+ }
133
+ }
134
+ },
135
+ "if": {
136
+ "properties": { "harness": { "const": "pi" } },
137
+ "required": ["harness"]
138
+ },
139
+ "then": { "required": ["origin"] }
140
+ }