@kontextmind/kxm 0.7.40 → 0.7.44

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 (178) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +45 -7
  3. package/docs/README.md +5 -3
  4. package/docs/agent-skills.md +1 -1
  5. package/docs/architecture.md +1 -1
  6. package/docs/configuration.md +28 -5
  7. package/docs/{vnext → contracts}/README.md +7 -7
  8. package/docs/{vnext → contracts}/architecture.md +2 -2
  9. package/docs/{vnext → contracts}/migration.md +25 -25
  10. package/docs/{vnext → contracts}/routing.md +2 -2
  11. package/docs/{vnext → contracts}/synchronization.md +1 -1
  12. package/docs/{vnext → contracts}/validation.md +7 -7
  13. package/docs/getting-started.md +1 -1
  14. package/docs/kb/qa-authentik-authentication.md +47 -0
  15. package/docs/kb/qa-extension-install-and-hub-bootstrap.md +68 -0
  16. package/docs/kb/qa-hub-on-a-public-host.md +31 -0
  17. package/docs/kb/qa-sqlite-vs-duckdb.md +18 -0
  18. package/docs/kb/qa-what-the-hub-stores.md +47 -0
  19. package/docs/packages.md +91 -0
  20. package/docs/templates/architecture.md +1 -1
  21. package/docs/test-matrix.md +6 -6
  22. package/docs/tui-components.md +131 -0
  23. package/examples/README.md +2 -2
  24. package/examples/{vnext → project}/.kxm/agents/critic-3.yaml +1 -1
  25. package/examples/{vnext → project}/README.md +4 -4
  26. package/package.json +18 -8
  27. package/packages/core/tui/CHANGELOG.md +19 -0
  28. package/packages/core/tui/LICENSE +21 -0
  29. package/packages/core/tui/README.md +45 -0
  30. package/packages/core/tui/dist/index.js +1439 -0
  31. package/packages/core/tui/src/adapters/pi.ts +73 -0
  32. package/packages/core/tui/src/adapters/terminal.ts +106 -0
  33. package/packages/core/tui/src/exports/index.ts +30 -0
  34. package/packages/core/tui/src/services/registry.ts +274 -0
  35. package/packages/core/tui/src/tui/keys.ts +130 -0
  36. package/packages/core/tui/src/tui/layout.ts +117 -0
  37. package/packages/core/tui/src/tui/panel.ts +435 -0
  38. package/packages/core/tui/src/tui/panelComponent.ts +208 -0
  39. package/packages/core/tui/src/tui/render.ts +309 -0
  40. package/packages/core/tui/src/tui/theme.ts +145 -0
  41. package/packages/core/tui/src/types/surface.ts +407 -0
  42. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  43. package/plugins/kxm/dist/cli.js +681 -553
  44. package/plugins/kxm/dist/extension.js +18 -18
  45. package/plugins/kxm/dist/mcp-server.js +1 -1
  46. package/plugins/kxm/dist/{vnext-runtime-supervisor.js → runtime-supervisor.js} +553 -553
  47. package/plugins/kxm/dist/runtime.js +608 -608
  48. package/plugins/kxm/dist/server.js +13 -13
  49. package/plugins/kxm/package.json +2 -2
  50. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +2 -2
  51. package/plugins/kxm/skills/kxm-runs/SKILL.md +3 -3
  52. package/plugins/kxm/skills/kxm-session/SKILL.md +1 -1
  53. package/plugins/kxm/skills/kxm-workflow/SKILL.md +1 -1
  54. package/plugins/kxm/src/autocomplete.ts +4 -4
  55. package/plugins/kxm/src/{vnext-bindings.ts → bindings.ts} +48 -48
  56. package/plugins/kxm/src/cli/hub.ts +3 -3
  57. package/plugins/kxm/src/cli/{vnext.ts → project.ts} +106 -106
  58. package/plugins/kxm/src/cli/roles.ts +9 -9
  59. package/plugins/kxm/src/cli/system.ts +2 -2
  60. package/plugins/kxm/src/cli/tasks.ts +8 -8
  61. package/plugins/kxm/src/cli/workflows.ts +8 -8
  62. package/plugins/kxm/src/cli.ts +47 -47
  63. package/plugins/kxm/src/config.ts +69 -14
  64. package/plugins/kxm/src/database.ts +4 -4
  65. package/plugins/kxm/src/{vnext-engine-artifacts.ts → engine-artifacts.ts} +7 -7
  66. package/plugins/kxm/src/{vnext-engine-command.ts → engine-command.ts} +40 -40
  67. package/plugins/kxm/src/{vnext-engine-compile.ts → engine-compile.ts} +64 -64
  68. package/plugins/kxm/src/{vnext-engine-evidence.ts → engine-evidence.ts} +26 -26
  69. package/plugins/kxm/src/{vnext-engine-fold.ts → engine-fold.ts} +99 -99
  70. package/plugins/kxm/src/{vnext-engine-gate-records.ts → engine-gate-records.ts} +105 -105
  71. package/plugins/kxm/src/{vnext-engine-plan.ts → engine-plan.ts} +69 -69
  72. package/plugins/kxm/src/{vnext-engine.ts → engine.ts} +502 -502
  73. package/plugins/kxm/src/gate-hash.ts +10 -0
  74. package/plugins/kxm/src/{vnext-harness.ts → harness.ts} +1 -1
  75. package/plugins/kxm/src/hub.ts +1 -1
  76. package/plugins/kxm/src/init-guide-setup.ts +3 -3
  77. package/plugins/kxm/src/{vnext-init.ts → init.ts} +93 -93
  78. package/plugins/kxm/src/kxm-install-kind.ts +1 -1
  79. package/plugins/kxm/src/kxm-update-config.ts +2 -2
  80. package/plugins/kxm/src/local-snapshot.ts +23 -23
  81. package/plugins/kxm/src/mcp-server.ts +1 -1
  82. package/plugins/kxm/src/{vnext-migrate.ts → migrate.ts} +123 -123
  83. package/plugins/kxm/src/model-inventory.ts +1 -1
  84. package/plugins/kxm/src/{vnext-oneshot-evidence.ts → oneshot-evidence.ts} +2 -2
  85. package/plugins/kxm/src/{vnext-oneshot-process.ts → oneshot-process.ts} +4 -4
  86. package/plugins/kxm/src/{vnext-oneshot-producer.ts → oneshot-producer.ts} +24 -24
  87. package/plugins/kxm/src/{vnext-permission.ts → permission.ts} +86 -86
  88. package/plugins/kxm/src/{vnext-pi-producer.ts → pi-producer.ts} +13 -13
  89. package/plugins/kxm/src/{vnext-config.ts → project-config.ts} +177 -177
  90. package/plugins/kxm/src/{vnext-repair.ts → repair.ts} +159 -159
  91. package/plugins/kxm/src/restricted-yaml.d.mts +3 -3
  92. package/plugins/kxm/src/restricted-yaml.mjs +4 -4
  93. package/plugins/kxm/src/{vnext-runtime-owner.ts → runtime-owner.ts} +35 -35
  94. package/plugins/kxm/src/{vnext-runtime.ts → runtime-service.ts} +136 -136
  95. package/plugins/kxm/src/{vnext-runtime-store.ts → runtime-store.ts} +145 -145
  96. package/plugins/kxm/src/{vnext-runtime-supervisor.ts → runtime-supervisor.ts} +80 -80
  97. package/plugins/kxm/src/runtime.ts +6 -6
  98. package/plugins/kxm/src/session-work.ts +3 -3
  99. package/plugins/kxm/src/studio-layout.ts +6 -6
  100. package/plugins/kxm/src/{vnext-template.ts → template.ts} +33 -33
  101. package/plugins/kxm/src/tui.ts +10 -24
  102. package/schemas/{vnext/README.md → README.md} +3 -3
  103. package/schemas/{vnext/agent.schema.json → agent.schema.json} +1 -1
  104. package/schemas/{vnext/assignment-result.schema.json → assignment-result.schema.json} +1 -1
  105. package/schemas/{vnext/backup-manifest.schema.json → backup-manifest.schema.json} +1 -1
  106. package/schemas/{vnext/candidate.schema.json → candidate.schema.json} +1 -1
  107. package/schemas/{vnext/common.schema.json → common.schema.json} +2 -2
  108. package/schemas/{vnext/context-candidate.schema.json → context-candidate.schema.json} +1 -1
  109. package/schemas/{vnext/context-packet.schema.json → context-packet.schema.json} +1 -1
  110. package/schemas/{vnext/delivery-manifest.schema.json → delivery-manifest.schema.json} +1 -1
  111. package/schemas/{vnext/drive-receipt.schema.json → drive-receipt.schema.json} +2 -2
  112. package/schemas/{vnext/environment.schema.json → environment.schema.json} +1 -1
  113. package/schemas/{vnext/gate-registry.schema.json → gate-registry.schema.json} +1 -1
  114. package/schemas/{vnext/handoff-manifest.schema.json → handoff-manifest.schema.json} +1 -1
  115. package/schemas/{vnext/init-operation.schema.json → init-operation.schema.json} +1 -1
  116. package/schemas/{vnext/local-repository-bindings.schema.json → local-repository-bindings.schema.json} +1 -1
  117. package/schemas/{vnext/memory-record.schema.json → memory-record.schema.json} +1 -1
  118. package/schemas/{vnext/migration-decision.schema.json → migration-decision.schema.json} +1 -1
  119. package/schemas/{vnext/migration-plan.schema.json → migration-plan.schema.json} +1 -1
  120. package/schemas/{vnext/migration-receipt.schema.json → migration-receipt.schema.json} +1 -1
  121. package/schemas/{vnext/model.schema.json → model.schema.json} +1 -1
  122. package/schemas/{vnext/modes.schema.json → modes.schema.json} +1 -1
  123. package/schemas/{vnext/permission-diff.schema.json → permission-diff.schema.json} +1 -1
  124. package/schemas/policy-draft/README.md +2 -2
  125. package/schemas/{vnext/prices.schema.json → prices.schema.json} +1 -1
  126. package/schemas/{vnext/project.schema.json → project.schema.json} +1 -1
  127. package/schemas/{vnext/repository.schema.json → repository.schema.json} +1 -1
  128. package/schemas/{vnext/role.schema.json → role.schema.json} +1 -1
  129. package/schemas/{vnext/run-event.schema.json → run-event.schema.json} +1 -1
  130. package/schemas/{vnext/session-brief.schema.json → session-brief.schema.json} +2 -2
  131. package/schemas/{vnext/sync-event.schema.json → sync-event.schema.json} +1 -1
  132. package/schemas/{vnext/template-provenance.schema.json → template-provenance.schema.json} +1 -1
  133. package/schemas/{vnext/workflow.schema.json → workflow.schema.json} +1 -1
  134. package/scripts/build-package.mjs +59 -0
  135. package/scripts/build-runtime.mjs +3 -2
  136. package/scripts/check-generated.mjs +2 -1
  137. package/scripts/check-versions.mjs +12 -1
  138. package/scripts/docker-install-smoke.mjs +268 -0
  139. package/scripts/emit-codex-artifacts.mjs +1 -1
  140. package/scripts/kxm-bump-version.mjs +53 -3
  141. package/scripts/kxm-hub.mjs +36 -0
  142. package/scripts/kxm-runtime-supervisor.mjs +3 -3
  143. package/scripts/package-surfaces.mjs +39 -0
  144. package/plugins/kxm/src/vnext-gate-hash.ts +0 -10
  145. /package/docs/{vnext → contracts}/effects-and-recovery.md +0 -0
  146. /package/docs/{vnext → contracts}/lifecycles.md +0 -0
  147. /package/docs/{vnext → contracts}/terminology.md +0 -0
  148. /package/examples/{vnext → project}/.kxm/agents/coordinator.yaml +0 -0
  149. /package/examples/{vnext → project}/.kxm/agents/critic-1.yaml +0 -0
  150. /package/examples/{vnext → project}/.kxm/agents/critic-2.yaml +0 -0
  151. /package/examples/{vnext → project}/.kxm/agents/implementer.yaml +0 -0
  152. /package/examples/{vnext → project}/.kxm/agents/planner.yaml +0 -0
  153. /package/examples/{vnext → project}/.kxm/agents/reproducer.yaml +0 -0
  154. /package/examples/{vnext → project}/.kxm/agents/reviewer.yaml +0 -0
  155. /package/examples/{vnext → project}/.kxm/gates.yaml +0 -0
  156. /package/examples/{vnext → project}/.kxm/models/critic-claude.yaml +0 -0
  157. /package/examples/{vnext → project}/.kxm/models/critic-gemini.yaml +0 -0
  158. /package/examples/{vnext → project}/.kxm/models/critic-grok.yaml +0 -0
  159. /package/examples/{vnext → project}/.kxm/models/implementation.yaml +0 -0
  160. /package/examples/{vnext → project}/.kxm/models/primary.yaml +0 -0
  161. /package/examples/{vnext → project}/.kxm/prices.yaml +0 -0
  162. /package/examples/{vnext → project}/.kxm/project/env.yaml +0 -0
  163. /package/examples/{vnext → project}/.kxm/project.yaml +0 -0
  164. /package/examples/{vnext → project}/.kxm/repo/repo.yaml +0 -0
  165. /package/examples/{vnext → project}/.kxm/workflows/default.yaml +0 -0
  166. /package/examples/{vnext → project}/.kxm/workflows/fix.yaml +0 -0
  167. /package/examples/{vnext → project}/.kxm/workflows/improve.yaml +0 -0
  168. /package/examples/{vnext → project}/records/assignment-result-recorded.json +0 -0
  169. /package/examples/{vnext → project}/records/assignment-result.json +0 -0
  170. /package/examples/{vnext → project}/records/context-candidate.json +0 -0
  171. /package/examples/{vnext → project}/records/delivery-manifest.json +0 -0
  172. /package/examples/{vnext → project}/records/effect-uncertainty-resolved-sync.json +0 -0
  173. /package/examples/{vnext → project}/records/effect-uncertainty-resolved.json +0 -0
  174. /package/examples/{vnext → project}/records/run-created.json +0 -0
  175. /package/examples/{vnext → project}/records/sync-event.json +0 -0
  176. /package/examples/{vnext → project}/repositories/api/.kxm/repo/env.yaml +0 -0
  177. /package/examples/{vnext → project}/repositories/api/.kxm/repo/repo.yaml +0 -0
  178. /package/examples/{vnext → project}/repositories/web/.kxm/repo/repo.yaml +0 -0
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Shared declarative contract for KXM terminal surfaces.
3
+ *
4
+ * A surface is described, not drawn: an owner publishes sections and fields,
5
+ * one renderer draws them all, and the owner still performs every write. That
6
+ * keeps the panel from ever becoming a second source of truth for project,
7
+ * roster, route, gate, role, or workflow configuration.
8
+ *
9
+ * Shapes are derived from the reviewed `doompi` config-panel pattern
10
+ * (`SPC e c`), narrowed for this repository's fail-closed rules: values here
11
+ * are validated before rendering, an unknown kind is refused rather than
12
+ * guessed, and a rejected edit is reported back on the field that caused it.
13
+ *
14
+ * Bounds are protocol limits, not suggestions. Oversized payloads are
15
+ * `invalid` so a surface cannot be used to exfiltrate a huge dump through a
16
+ * terminal pane.
17
+ */
18
+
19
+ export const KXM_TUI_CONTRACT_SCHEMA = "kxm.tui-contract.v1" as const;
20
+
21
+ export type KxmTuiFieldKind = "text" | "enum" | "choice" | "info";
22
+ export type KxmTuiStatus = "ready" | "available" | "blocked";
23
+ export type KxmTuiStepState = "pending" | "satisfied" | "running" | "done" | "failed";
24
+ export type KxmTuiNoticeLevel = "info" | "error";
25
+
26
+ export const KXM_TUI_FIELD_KINDS: readonly KxmTuiFieldKind[] = ["text", "enum", "choice", "info"];
27
+ export const KXM_TUI_STATUSES: readonly KxmTuiStatus[] = ["ready", "available", "blocked"];
28
+ export const KXM_TUI_STEP_STATES: readonly KxmTuiStepState[] = ["pending", "satisfied", "running", "done", "failed"];
29
+ export const KXM_TUI_NOTICE_LEVELS: readonly KxmTuiNoticeLevel[] = ["info", "error"];
30
+
31
+ /** Actions the panel offers on the focused field, sent back verbatim. */
32
+ export const KXM_TUI_SET_ACTION = "set";
33
+ export const KXM_TUI_CLEAR_ACTION = "clear";
34
+ export const KXM_TUI_ABORT_ACTION = "abort";
35
+
36
+ export const KXM_TUI_LIMITS = Object.freeze({
37
+ sourceMax: 128,
38
+ idMax: 128,
39
+ choiceIdMax: 256,
40
+ labelMax: 48,
41
+ choiceLabelMax: 96,
42
+ detailMax: 240,
43
+ shortDetailMax: 96,
44
+ stepDetailMax: 160,
45
+ statusTextMax: 240,
46
+ valueMax: 4096,
47
+ keyPathMax: 128,
48
+ outputLineMax: 512,
49
+ outputMaxLines: 64,
50
+ choicesMax: 128,
51
+ actionsMax: 4,
52
+ stepsMax: 24,
53
+ fieldsMax: 64,
54
+ sectionsMax: 32,
55
+ orderMax: 1000,
56
+ });
57
+
58
+ const ID_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
59
+ const SOURCE_PATTERN = /^[A-Za-z0-9@][A-Za-z0-9@/._:-]*$/;
60
+ const KEY_PATH_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.[\]-]*$/;
61
+ const ACTION_KEY_PATTERN = /^[A-Za-z0-9]$/;
62
+
63
+ /** One reason a published surface was refused. Renderers never see partial data. */
64
+ export interface KxmTuiIssue {
65
+ readonly path: string;
66
+ readonly code: string;
67
+ readonly message: string;
68
+ }
69
+
70
+ export interface KxmTuiChoice {
71
+ /** The value a selection writes. For models this is the full `provider/id`. */
72
+ readonly id: string;
73
+ readonly label: string;
74
+ /** Right-aligned by the renderer: sizes, prices, provider names. */
75
+ readonly detail?: string;
76
+ /** A heading the cursor skips over, e.g. the provider of a model. */
77
+ readonly group?: string;
78
+ readonly status?: KxmTuiStatus;
79
+ readonly statusText?: string;
80
+ /** What Enter does here. A model that must be fetched carries its own action. */
81
+ readonly action?: string;
82
+ }
83
+
84
+ export interface KxmTuiAction {
85
+ readonly key: string;
86
+ readonly label: string;
87
+ readonly action: string;
88
+ }
89
+
90
+ export interface KxmTuiStep {
91
+ readonly label: string;
92
+ readonly state: KxmTuiStepState;
93
+ readonly detail?: string;
94
+ }
95
+
96
+ export interface KxmTuiProgress {
97
+ readonly label: string;
98
+ /** 0..1. Omitted when the work has no measurable total. */
99
+ readonly ratio?: number;
100
+ }
101
+
102
+ export interface KxmTuiField {
103
+ readonly id: string;
104
+ readonly label: string;
105
+ readonly kind: KxmTuiFieldKind;
106
+ /** Absent means unset; the renderer shows `placeholder` instead. */
107
+ readonly value?: string;
108
+ readonly placeholder?: string;
109
+ readonly detail?: string;
110
+ /** The configuration key this writes, shown so the file stays legible. */
111
+ readonly keyPath?: string;
112
+ /** When the underlying value was last read or written, as an ISO timestamp. */
113
+ readonly updatedAt?: string;
114
+ readonly status?: KxmTuiStatus;
115
+ /** Doubles as the error line: a rejected edit returns as old value plus this. */
116
+ readonly statusText?: string;
117
+ readonly choices?: readonly KxmTuiChoice[];
118
+ readonly actions?: readonly KxmTuiAction[];
119
+ /** Work is running; the field dims and offers abort. */
120
+ readonly busy?: boolean;
121
+ readonly progress?: KxmTuiProgress;
122
+ readonly steps?: readonly KxmTuiStep[];
123
+ /** Tail of a running command's output, newest last. */
124
+ readonly output?: readonly string[];
125
+ /** The command is waiting for a typed line. */
126
+ readonly awaitingInput?: boolean;
127
+ }
128
+
129
+ export interface KxmTuiSection {
130
+ readonly id: string;
131
+ readonly title: string;
132
+ readonly detail?: string;
133
+ readonly order: number;
134
+ /** Section-wide state: readiness, or why a write failed. */
135
+ readonly notice?: string;
136
+ readonly noticeLevel?: KxmTuiNoticeLevel;
137
+ readonly fields: readonly KxmTuiField[];
138
+ }
139
+
140
+ /** A section as published by a named owner. */
141
+ export interface KxmTuiSectionView extends KxmTuiSection {
142
+ readonly source: string;
143
+ }
144
+
145
+ export interface KxmTuiInvocation {
146
+ readonly source: string;
147
+ readonly sectionId: string;
148
+ readonly fieldId: string;
149
+ readonly action: string;
150
+ readonly value?: string;
151
+ }
152
+
153
+ function issue(path: string, code: string, message: string): KxmTuiIssue {
154
+ return { path, code, message };
155
+ }
156
+
157
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+
161
+ function boundedText(issues: KxmTuiIssue[], path: string, value: unknown, max: number, required: boolean): void {
162
+ if (value === undefined) {
163
+ if (required) issues.push(issue(path, "required", `${path} is required`));
164
+ return;
165
+ }
166
+ if (typeof value !== "string" || value.length === 0) {
167
+ issues.push(issue(path, "invalid_string", `${path} must be a non-empty string`));
168
+ return;
169
+ }
170
+ if (value.length > max) {
171
+ issues.push(issue(path, "too_long", `${path} exceeds ${max} characters`));
172
+ return;
173
+ }
174
+ // Escape and control bytes must never reach a pane: they would break the
175
+ // per-line SGR reset the renderer relies on.
176
+ if (/[\u0000-\u0008\u000a-\u001f\u007f]/u.test(value)) {
177
+ issues.push(issue(path, "control_character", `${path} must not contain control characters`));
178
+ }
179
+ }
180
+
181
+ function optionalEnum(issues: KxmTuiIssue[], path: string, value: unknown, allowed: readonly string[]): void {
182
+ if (value === undefined) return;
183
+ if (typeof value !== "string" || !allowed.includes(value)) {
184
+ issues.push(issue(path, "invalid_choice", `${path} must be one of ${allowed.join(", ")}`));
185
+ }
186
+ }
187
+
188
+ function ratioField(issues: KxmTuiIssue[], path: string, value: unknown): void {
189
+ if (value === undefined) return;
190
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
191
+ issues.push(issue(path, "invalid_ratio", `${path} must be a finite number between 0 and 1`));
192
+ }
193
+ }
194
+
195
+ function validateChoice(issues: KxmTuiIssue[], path: string, raw: unknown): void {
196
+ if (!isPlainObject(raw)) {
197
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
198
+ return;
199
+ }
200
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.choiceIdMax, true);
201
+ if (typeof raw.id === "string" && /[\s]/u.test(raw.id)) {
202
+ issues.push(issue(`${path}.id`, "invalid_choice_id", `${path}.id must not contain whitespace`));
203
+ }
204
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.choiceLabelMax, true);
205
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
206
+ boundedText(issues, `${path}.group`, raw.group, KXM_TUI_LIMITS.shortDetailMax, false);
207
+ boundedText(issues, `${path}.statusText`, raw.statusText, KXM_TUI_LIMITS.shortDetailMax, false);
208
+ boundedText(issues, `${path}.action`, raw.action, KXM_TUI_LIMITS.idMax, false);
209
+ optionalEnum(issues, `${path}.status`, raw.status, KXM_TUI_STATUSES);
210
+ }
211
+
212
+ function validateAction(issues: KxmTuiIssue[], path: string, raw: unknown): void {
213
+ if (!isPlainObject(raw)) {
214
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
215
+ return;
216
+ }
217
+ if (typeof raw.key !== "string" || !ACTION_KEY_PATTERN.test(raw.key)) {
218
+ issues.push(issue(`${path}.key`, "invalid_action_key", `${path}.key must be a single alphanumeric key`));
219
+ }
220
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
221
+ boundedText(issues, `${path}.action`, raw.action, KXM_TUI_LIMITS.idMax, true);
222
+ }
223
+
224
+ function validateStep(issues: KxmTuiIssue[], path: string, raw: unknown): void {
225
+ if (!isPlainObject(raw)) {
226
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
227
+ return;
228
+ }
229
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.shortDetailMax, true);
230
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.stepDetailMax, false);
231
+ optionalEnum(issues, `${path}.state`, raw.state, KXM_TUI_STEP_STATES);
232
+ if (raw.state === undefined) issues.push(issue(`${path}.state`, "required", `${path}.state is required`));
233
+ }
234
+
235
+ function validateField(issues: KxmTuiIssue[], path: string, raw: unknown): void {
236
+ if (!isPlainObject(raw)) {
237
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
238
+ return;
239
+ }
240
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
241
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
242
+ issues.push(issue(`${path}.id`, "invalid_id", `${path}.id must match ${ID_PATTERN}`));
243
+ }
244
+ boundedText(issues, `${path}.label`, raw.label, KXM_TUI_LIMITS.labelMax, true);
245
+ if (raw.kind === undefined) {
246
+ issues.push(issue(`${path}.kind`, "required", `${path}.kind is required`));
247
+ } else {
248
+ optionalEnum(issues, `${path}.kind`, raw.kind, KXM_TUI_FIELD_KINDS);
249
+ }
250
+ boundedText(issues, `${path}.value`, raw.value, KXM_TUI_LIMITS.valueMax, false);
251
+ boundedText(issues, `${path}.placeholder`, raw.placeholder, KXM_TUI_LIMITS.shortDetailMax, false);
252
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.detailMax, false);
253
+ boundedText(issues, `${path}.statusText`, raw.statusText, KXM_TUI_LIMITS.statusTextMax, false);
254
+ if (raw.updatedAt !== undefined) {
255
+ boundedText(issues, `${path}.updatedAt`, raw.updatedAt, 64, false);
256
+ if (typeof raw.updatedAt === "string" && !Number.isFinite(Date.parse(raw.updatedAt))) {
257
+ issues.push(issue(`${path}.updatedAt`, "invalid_timestamp", `${path}.updatedAt must be an ISO timestamp`));
258
+ }
259
+ }
260
+ optionalEnum(issues, `${path}.status`, raw.status, KXM_TUI_STATUSES);
261
+ if (raw.keyPath !== undefined) {
262
+ boundedText(issues, `${path}.keyPath`, raw.keyPath, KXM_TUI_LIMITS.keyPathMax, false);
263
+ if (typeof raw.keyPath === "string" && !KEY_PATH_PATTERN.test(raw.keyPath)) {
264
+ issues.push(issue(`${path}.keyPath`, "invalid_key_path", `${path}.keyPath must be a dotted config key`));
265
+ }
266
+ }
267
+ for (const listKey of ["busy", "awaitingInput"] as const) {
268
+ if (raw[listKey] !== undefined && typeof raw[listKey] !== "boolean") {
269
+ issues.push(issue(`${path}.${listKey}`, "invalid_boolean", `${path}.${listKey} must be a boolean`));
270
+ }
271
+ }
272
+ if (raw.progress !== undefined) {
273
+ if (!isPlainObject(raw.progress)) {
274
+ issues.push(issue(`${path}.progress`, "invalid_object", `${path}.progress must be an object`));
275
+ } else {
276
+ boundedText(issues, `${path}.progress.label`, raw.progress.label, KXM_TUI_LIMITS.shortDetailMax, true);
277
+ ratioField(issues, `${path}.progress.ratio`, raw.progress.ratio);
278
+ }
279
+ }
280
+ for (const [listKey, max, check] of [
281
+ ["choices", KXM_TUI_LIMITS.choicesMax, validateChoice],
282
+ ["actions", KXM_TUI_LIMITS.actionsMax, validateAction],
283
+ ["steps", KXM_TUI_LIMITS.stepsMax, validateStep],
284
+ ] as const) {
285
+ const list = raw[listKey];
286
+ if (list === undefined) continue;
287
+ if (!Array.isArray(list) || list.length > max) {
288
+ issues.push(issue(`${path}.${listKey}`, "invalid_list", `${path}.${listKey} must be an array of at most ${max}`));
289
+ continue;
290
+ }
291
+ list.forEach((entry, index) => check(issues, `${path}.${listKey}[${index}]`, entry));
292
+ }
293
+ const output = raw.output;
294
+ if (output !== undefined) {
295
+ if (!Array.isArray(output) || output.length > KXM_TUI_LIMITS.outputMaxLines) {
296
+ issues.push(issue(`${path}.output`, "invalid_list", `${path}.output must be an array of at most ${KXM_TUI_LIMITS.outputMaxLines} lines`));
297
+ } else {
298
+ output.forEach((line, index) => {
299
+ if (typeof line !== "string") issues.push(issue(`${path}.output[${index}]`, "invalid_string", "output lines must be strings"));
300
+ else if (line.length > KXM_TUI_LIMITS.outputLineMax) issues.push(issue(`${path}.output[${index}]`, "too_long", "output line is too long"));
301
+ });
302
+ }
303
+ }
304
+ }
305
+
306
+ function validateSection(issues: KxmTuiIssue[], path: string, raw: unknown): void {
307
+ if (!isPlainObject(raw)) {
308
+ issues.push(issue(path, "invalid_object", `${path} must be an object`));
309
+ return;
310
+ }
311
+ boundedText(issues, `${path}.id`, raw.id, KXM_TUI_LIMITS.idMax, true);
312
+ if (typeof raw.id === "string" && !ID_PATTERN.test(raw.id)) {
313
+ issues.push(issue(`${path}.id`, "invalid_id", `${path}.id must match ${ID_PATTERN}`));
314
+ }
315
+ boundedText(issues, `${path}.title`, raw.title, KXM_TUI_LIMITS.labelMax, true);
316
+ boundedText(issues, `${path}.detail`, raw.detail, KXM_TUI_LIMITS.shortDetailMax, false);
317
+ boundedText(issues, `${path}.notice`, raw.notice, KXM_TUI_LIMITS.detailMax, false);
318
+ optionalEnum(issues, `${path}.noticeLevel`, raw.noticeLevel, KXM_TUI_NOTICE_LEVELS);
319
+ if (typeof raw.order !== "number" || !Number.isInteger(raw.order) || raw.order < 0 || raw.order > KXM_TUI_LIMITS.orderMax) {
320
+ issues.push(issue(`${path}.order`, "invalid_order", `${path}.order must be an integer between 0 and ${KXM_TUI_LIMITS.orderMax}`));
321
+ }
322
+ if (!Array.isArray(raw.fields) || raw.fields.length > KXM_TUI_LIMITS.fieldsMax) {
323
+ issues.push(issue(`${path}.fields`, "invalid_list", `${path}.fields must be an array of at most ${KXM_TUI_LIMITS.fieldsMax}`));
324
+ return;
325
+ }
326
+ const seen = new Set<string>();
327
+ raw.fields.forEach((field, index) => {
328
+ const fieldPath = `${path}.fields[${index}]`;
329
+ validateField(issues, fieldPath, field);
330
+ if (isPlainObject(field) && typeof field.id === "string") {
331
+ if (seen.has(field.id)) issues.push(issue(`${fieldPath}.id`, "duplicate_id", `${field.id} is published twice in ${path}`));
332
+ seen.add(field.id);
333
+ }
334
+ });
335
+ }
336
+
337
+ /** Validate a full published surface. Returns issues; an empty array is a pass. */
338
+ export function validateKxmTuiSurface(sections: unknown): KxmTuiIssue[] {
339
+ if (!Array.isArray(sections)) return [issue("sections", "invalid_list", "sections must be an array")];
340
+ if (sections.length > KXM_TUI_LIMITS.sectionsMax) {
341
+ return [issue("sections", "too_many", `sections must number at most ${KXM_TUI_LIMITS.sectionsMax}`)];
342
+ }
343
+ const issues: KxmTuiIssue[] = [];
344
+ const seenSources = new Map<string, Set<string>>();
345
+ sections.forEach((section, index) => {
346
+ const path = `sections[${index}]`;
347
+ validateSection(issues, path, section);
348
+ if (!isPlainObject(section)) return;
349
+ boundedText(issues, `${path}.source`, section.source, KXM_TUI_LIMITS.sourceMax, true);
350
+ if (typeof section.source === "string" && !SOURCE_PATTERN.test(section.source)) {
351
+ issues.push(issue(`${path}.source`, "invalid_source", `${path}.source must be an addressable owner id`));
352
+ }
353
+ if (typeof section.source === "string" && typeof section.id === "string") {
354
+ const ids = seenSources.get(section.source) ?? new Set<string>();
355
+ seenSources.set(section.source, ids);
356
+ if (ids.has(section.id)) issues.push(issue(`${path}.id`, "duplicate_section", `${section.source}/${section.id} is published twice`));
357
+ ids.add(section.id);
358
+ }
359
+ });
360
+ return issues;
361
+ }
362
+
363
+ /** Throwing variant for owners that must not publish a half-valid surface. */
364
+ export function assertKxmTuiSurface(sections: unknown): KxmTuiSectionView[] {
365
+ const issues = validateKxmTuiSurface(sections);
366
+ if (issues.length > 0) {
367
+ const first = issues[0]!;
368
+ throw new Error(`kxm tui surface refused (${issues.length} issue(s)): ${first.path} ${first.code}: ${first.message}`);
369
+ }
370
+ return (sections as unknown[]).slice().sort((a, b) => sectionOrder(a) - sectionOrder(b)) as KxmTuiSectionView[];
371
+ }
372
+
373
+ function sectionOrder(value: unknown): number {
374
+ return isPlainObject(value) && typeof value.order === "number" ? value.order : Number.MAX_SAFE_INTEGER;
375
+ }
376
+
377
+ /** `info` rows are labels, so the cursor never rests on one. */
378
+ export function isSelectableField(field: KxmTuiField | undefined): boolean {
379
+ return field !== undefined && field.kind !== "info";
380
+ }
381
+
382
+ /** Whether a field can be entered for editing or a choice list. */
383
+ export function fieldIsEditable(field: KxmTuiField | undefined): boolean {
384
+ if (!isSelectableField(field)) return false;
385
+ const editable = field as KxmTuiField;
386
+ return editable.kind === "text" || editable.kind === "enum" || (editable.kind === "choice" && (editable.choices?.length ?? 0) > 0);
387
+ }
388
+
389
+ /** Choices sorted into display groups, in first-seen order. */
390
+ export function groupedKxmTuiChoices(field: KxmTuiField): { readonly group: string; readonly choices: readonly KxmTuiChoice[] }[] {
391
+ const choices = field.choices ?? [];
392
+ if (choices.length === 0) return [];
393
+ if (choices.every((choice) => choice.group === undefined)) return [{ group: "", choices }];
394
+ const groups: { group: string; choices: KxmTuiChoice[] }[] = [];
395
+ const index = new Map<string, KxmTuiChoice[]>();
396
+ for (const choice of choices) {
397
+ const group = choice.group ?? "";
398
+ let bucket = index.get(group);
399
+ if (!bucket) {
400
+ bucket = [];
401
+ index.set(group, bucket);
402
+ groups.push({ group, choices: bucket });
403
+ }
404
+ bucket.push(choice);
405
+ }
406
+ return groups;
407
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "kxm",
4
4
  "displayName": "KXM",
5
- "version": "0.7.40",
5
+ "version": "0.7.44",
6
6
  "description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
7
7
  "author": {
8
8
  "name": "KontextMind",