@kontextmind/kxm 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,628 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { StringDecoder } from "node:string_decoder";
4
+ import {
5
+ registerTrustedProducer,
6
+ type VnextProducer,
7
+ type VnextProducerRequest,
8
+ type VnextProducerResult,
9
+ } from "./vnext-engine.ts";
10
+ import {
11
+ probeHarnessAssignment,
12
+ type HarnessInventory,
13
+ } from "./vnext-harness.ts";
14
+ import {
15
+ calculateModelCost,
16
+ loadPriceCatalog,
17
+ type PriceCatalog,
18
+ } from "./prices.ts";
19
+
20
+ export interface PiRpcProcess {
21
+ readonly stdin: { write(chunk: string): boolean };
22
+ readonly stdout: NodeJS.ReadableStream;
23
+ readonly stderr?: NodeJS.ReadableStream | undefined;
24
+ kill(signal?: NodeJS.Signals): boolean;
25
+ on(event: "close", listener: (code: number | null, signal: string | null) => void): this;
26
+ on(event: "error", listener: (err: Error) => void): this;
27
+ }
28
+
29
+ export interface PiUsageStats {
30
+ input?: number | undefined;
31
+ output?: number | undefined;
32
+ cacheRead?: number | undefined;
33
+ cacheWrite?: number | undefined;
34
+ totalTokens?: number | undefined;
35
+ contextUsage?: {
36
+ tokens?: number | undefined;
37
+ contextWindow?: number | undefined;
38
+ percent?: number | undefined;
39
+ } | undefined;
40
+ }
41
+
42
+ export interface PiPromptResult {
43
+ outcome: string;
44
+ text: string;
45
+ usage?: PiUsageStats | undefined;
46
+ }
47
+
48
+ export function formatPiSessionKey(params: {
49
+ runId: string;
50
+ agentId: string;
51
+ instanceNo?: number | undefined;
52
+ scopeEpoch?: number | undefined;
53
+ }): string {
54
+ if (params.agentId === "coordinator") {
55
+ return `coordinator:${params.runId}`;
56
+ }
57
+ return `${params.runId}:${params.agentId}:${params.instanceNo ?? 1}:${params.scopeEpoch ?? 1}`;
58
+ }
59
+
60
+ export function formatPiSessionDisplayName(params: {
61
+ runId: string;
62
+ agentId: string;
63
+ instanceNo?: number | undefined;
64
+ scopeEpoch?: number | undefined;
65
+ }): string {
66
+ const shortRun = params.runId.length > 8 ? params.runId.slice(0, 8) : params.runId;
67
+ if (params.agentId === "coordinator") {
68
+ return `coordinator@${shortRun}`;
69
+ }
70
+ return `${params.agentId}@${shortRun}#${params.instanceNo ?? 1}.${params.scopeEpoch ?? 1}`;
71
+ }
72
+
73
+ function extractText(content: unknown): string {
74
+ if (typeof content === "string") return content;
75
+ if (Array.isArray(content)) {
76
+ return content
77
+ .map((part) => (typeof part === "object" && part && "text" in part && typeof (part as { text: unknown }).text === "string" ? (part as { text: string }).text : ""))
78
+ .join("\n");
79
+ }
80
+ return "";
81
+ }
82
+
83
+ function determineOutcome(text: string, allowedOutcomes: readonly string[]): string {
84
+ const normalized = text.trim();
85
+ const jsonMatch = /"outcome"\s*:\s*"([^"]+)"/.exec(normalized);
86
+ if (jsonMatch && allowedOutcomes.includes(jsonMatch[1]!)) {
87
+ return jsonMatch[1]!;
88
+ }
89
+ for (const outcome of allowedOutcomes) {
90
+ const regex = new RegExp(`\\b${outcome}\\b`, "i");
91
+ if (regex.test(normalized)) {
92
+ return outcome;
93
+ }
94
+ }
95
+ if (allowedOutcomes.includes("passed")) return "passed";
96
+ return allowedOutcomes[0] ?? "completed";
97
+ }
98
+
99
+ export class PiSession {
100
+ readonly key: string;
101
+ readonly displayName: string;
102
+ readonly runId: string;
103
+ readonly agentId: string;
104
+ readonly instanceNo: number;
105
+ readonly scopeEpoch: number;
106
+ readonly sessionDir?: string | undefined;
107
+ readonly model?: string | undefined;
108
+
109
+ private process: PiRpcProcess;
110
+ private status: "idle" | "busy" | "closed" = "idle";
111
+ private commandCounter = 0;
112
+ private pendingCommands = new Map<string, { resolve: (res: Record<string, unknown>) => void; reject: (err: Error) => void }>();
113
+ private activePrompt?: {
114
+ resolve: (res: PiPromptResult) => void;
115
+ reject: (err: Error) => void;
116
+ allowedOutcomes: readonly string[];
117
+ text: string;
118
+ usage: PiUsageStats;
119
+ signal?: AbortSignal | undefined;
120
+ signalCleanup?: (() => void) | undefined;
121
+ aborted?: boolean | undefined;
122
+ } | undefined;
123
+ private lineDecoder = new StringDecoder("utf8");
124
+ private lineBuffer = "";
125
+
126
+ constructor(params: {
127
+ key: string;
128
+ displayName: string;
129
+ runId: string;
130
+ agentId: string;
131
+ instanceNo: number;
132
+ scopeEpoch: number;
133
+ sessionDir?: string | undefined;
134
+ model?: string | undefined;
135
+ process: PiRpcProcess;
136
+ }) {
137
+ this.key = params.key;
138
+ this.displayName = params.displayName;
139
+ this.runId = params.runId;
140
+ this.agentId = params.agentId;
141
+ this.instanceNo = params.instanceNo;
142
+ this.scopeEpoch = params.scopeEpoch;
143
+ this.sessionDir = params.sessionDir;
144
+ this.model = params.model;
145
+ this.process = params.process;
146
+
147
+ this.attachStdout();
148
+ this.attachProcessEvents();
149
+ }
150
+
151
+ isClosed(): boolean {
152
+ return this.status === "closed";
153
+ }
154
+
155
+ isBusy(): boolean {
156
+ return this.status === "busy";
157
+ }
158
+
159
+ private attachStdout(): void {
160
+ this.process.stdout.on("data", (chunk: Buffer | string) => {
161
+ this.lineBuffer += this.lineDecoder.write(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
162
+ let newlineIdx: number;
163
+ while ((newlineIdx = this.lineBuffer.indexOf("\n")) !== -1) {
164
+ let line = this.lineBuffer.slice(0, newlineIdx);
165
+ this.lineBuffer = this.lineBuffer.slice(newlineIdx + 1);
166
+ if (line.endsWith("\r")) line = line.slice(0, -1);
167
+ const trimmed = line.trim();
168
+ if (!trimmed) continue;
169
+ try {
170
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
171
+ this.handleRpcMessage(parsed);
172
+ } catch {
173
+ // ignore non-JSON output from subprocess
174
+ }
175
+ }
176
+ });
177
+ }
178
+
179
+ private attachProcessEvents(): void {
180
+ this.process.on("close", (code, signal) => {
181
+ this.status = "closed";
182
+ const err = new Error(`pi_process_closed: code=${code ?? "null"} signal=${signal ?? "null"}`);
183
+ for (const pending of this.pendingCommands.values()) {
184
+ pending.reject(err);
185
+ }
186
+ this.pendingCommands.clear();
187
+ if (this.activePrompt) {
188
+ this.activePrompt.signalCleanup?.();
189
+ this.activePrompt.reject(err);
190
+ this.activePrompt = undefined;
191
+ }
192
+ });
193
+
194
+ this.process.on("error", (err) => {
195
+ this.status = "closed";
196
+ for (const pending of this.pendingCommands.values()) {
197
+ pending.reject(err);
198
+ }
199
+ this.pendingCommands.clear();
200
+ if (this.activePrompt) {
201
+ this.activePrompt.signalCleanup?.();
202
+ this.activePrompt.reject(err);
203
+ this.activePrompt = undefined;
204
+ }
205
+ });
206
+ }
207
+
208
+ private handleRpcMessage(msg: Record<string, unknown>): void {
209
+ if (msg.type === "response" && typeof msg.id === "string") {
210
+ const pending = this.pendingCommands.get(msg.id);
211
+ if (pending) {
212
+ this.pendingCommands.delete(msg.id);
213
+ if (msg.success === false) {
214
+ pending.reject(new Error(typeof msg.error === "string" ? msg.error : "rpc_command_failed"));
215
+ } else {
216
+ pending.resolve(msg);
217
+ }
218
+ }
219
+ return;
220
+ }
221
+
222
+ if (!this.activePrompt) return;
223
+
224
+ if (msg.type === "message_update") {
225
+ if (msg.usage && typeof msg.usage === "object") {
226
+ this.updateUsage(msg.usage as Record<string, unknown>);
227
+ }
228
+ } else if (msg.type === "turn_end") {
229
+ const message = msg.message as Record<string, unknown> | undefined;
230
+ if (message && message.content !== undefined) {
231
+ this.activePrompt.text += `${extractText(message.content)}\n`;
232
+ }
233
+ if (message && message.usage && typeof message.usage === "object") {
234
+ this.updateUsage(message.usage as Record<string, unknown>);
235
+ }
236
+ } else if (msg.type === "agent_end") {
237
+ const messages = msg.messages;
238
+ if (Array.isArray(messages)) {
239
+ for (const m of messages) {
240
+ if (m && typeof m === "object" && (m as Record<string, unknown>).role === "assistant") {
241
+ const content = (m as Record<string, unknown>).content;
242
+ if (content) this.activePrompt.text += `${extractText(content)}\n`;
243
+ const usage = (m as Record<string, unknown>).usage;
244
+ if (usage && typeof usage === "object") this.updateUsage(usage as Record<string, unknown>);
245
+ }
246
+ }
247
+ }
248
+ } else if (msg.type === "agent_settled") {
249
+ const active = this.activePrompt;
250
+ this.activePrompt = undefined;
251
+ this.status = "idle";
252
+ active.signalCleanup?.();
253
+ const isAborted = active.aborted || active.signal?.aborted;
254
+ let outcome: string;
255
+ if (isAborted) {
256
+ outcome = active.allowedOutcomes.includes("cancelled")
257
+ ? "cancelled"
258
+ : (active.allowedOutcomes.includes("failed") ? "failed" : active.allowedOutcomes[0]!);
259
+ } else {
260
+ outcome = determineOutcome(active.text, active.allowedOutcomes);
261
+ }
262
+ active.resolve({
263
+ outcome,
264
+ text: active.text.trim() || (isAborted ? "aborted" : ""),
265
+ usage: active.usage,
266
+ });
267
+ }
268
+ }
269
+
270
+ private updateUsage(raw: Record<string, unknown>): void {
271
+ if (!this.activePrompt) return;
272
+ const u = this.activePrompt.usage;
273
+ if (typeof raw.input === "number") u.input = raw.input;
274
+ if (typeof raw.output === "number") u.output = raw.output;
275
+ if (typeof raw.cacheRead === "number") u.cacheRead = raw.cacheRead;
276
+ if (typeof raw.cacheWrite === "number") u.cacheWrite = raw.cacheWrite;
277
+ if (typeof raw.totalTokens === "number") u.totalTokens = raw.totalTokens;
278
+ if (raw.contextUsage && typeof raw.contextUsage === "object") {
279
+ const cu = raw.contextUsage as Record<string, unknown>;
280
+ u.contextUsage = {
281
+ tokens: typeof cu.tokens === "number" ? cu.tokens : undefined,
282
+ contextWindow: typeof cu.contextWindow === "number" ? cu.contextWindow : undefined,
283
+ percent: typeof cu.percent === "number" ? cu.percent : undefined,
284
+ };
285
+ }
286
+ }
287
+
288
+ async sendCommand(command: Record<string, unknown>): Promise<Record<string, unknown>> {
289
+ if (this.status === "closed") {
290
+ throw new Error("pi_session_closed");
291
+ }
292
+ const id = `cmd_${++this.commandCounter}`;
293
+ const payload = { ...command, id };
294
+ return new Promise((resolve, reject) => {
295
+ this.pendingCommands.set(id, { resolve, reject });
296
+ try {
297
+ this.process.stdin.write(JSON.stringify(payload) + "\n");
298
+ } catch (err) {
299
+ this.pendingCommands.delete(id);
300
+ reject(err instanceof Error ? err : new Error(String(err)));
301
+ }
302
+ });
303
+ }
304
+
305
+ async prompt(
306
+ message: string,
307
+ allowedOutcomes: readonly string[],
308
+ signal?: AbortSignal,
309
+ ): Promise<PiPromptResult> {
310
+ if (this.status === "closed") {
311
+ throw new Error("pi_session_closed");
312
+ }
313
+ if (this.status === "busy") {
314
+ throw new Error("pi_session_busy");
315
+ }
316
+ if (signal?.aborted) {
317
+ const outcome = allowedOutcomes.includes("cancelled") ? "cancelled" : (allowedOutcomes.includes("failed") ? "failed" : allowedOutcomes[0]!);
318
+ return { outcome, text: "aborted", usage: {} };
319
+ }
320
+
321
+ this.status = "busy";
322
+
323
+ return new Promise((resolve, reject) => {
324
+ let signalCleanup: (() => void) | undefined;
325
+ if (signal) {
326
+ const onAbort = () => {
327
+ if (this.activePrompt) {
328
+ this.activePrompt.aborted = true;
329
+ }
330
+ void this.abort().catch(() => {});
331
+ };
332
+ signal.addEventListener("abort", onAbort, { once: true });
333
+ signalCleanup = () => signal.removeEventListener("abort", onAbort);
334
+ }
335
+
336
+ this.activePrompt = {
337
+ resolve,
338
+ reject,
339
+ allowedOutcomes,
340
+ text: "",
341
+ usage: {},
342
+ signal,
343
+ signalCleanup,
344
+ aborted: false,
345
+ };
346
+
347
+ this.sendCommand({ type: "prompt", message }).catch((err) => {
348
+ if (this.activePrompt) {
349
+ this.activePrompt.signalCleanup?.();
350
+ this.activePrompt = undefined;
351
+ this.status = "idle";
352
+ }
353
+ reject(err);
354
+ });
355
+ });
356
+ }
357
+
358
+ async abort(): Promise<void> {
359
+ if (this.status === "closed") return;
360
+ if (this.activePrompt) {
361
+ this.activePrompt.aborted = true;
362
+ }
363
+ try {
364
+ await this.sendCommand({ type: "abort" });
365
+ } catch {
366
+ // ignore abort write errors
367
+ }
368
+ }
369
+
370
+ async close(): Promise<void> {
371
+ if (this.status === "closed") return;
372
+ this.status = "closed";
373
+ try {
374
+ this.process.kill();
375
+ } catch {
376
+ // ignore kill errors
377
+ }
378
+ }
379
+ }
380
+
381
+ export interface VnextPiProducerOptions {
382
+ projectRoot?: string | undefined;
383
+ sessionDir?: string | undefined;
384
+ piCommand?: string | undefined;
385
+ inventory?: HarnessInventory | undefined;
386
+ priceCatalog?: PriceCatalog | undefined;
387
+ env?: NodeJS.ProcessEnv | undefined;
388
+ spawnProcess?: ((command: string, args: readonly string[], options: Record<string, unknown>) => PiRpcProcess) | undefined;
389
+ probeHarness?: typeof probeHarnessAssignment | undefined;
390
+ resolveModel?: ((agentId: string, runId: string) => { provider?: string | undefined; model?: string | undefined; thinking?: string | undefined } | undefined) | undefined;
391
+ defaultModel?: string | undefined;
392
+ defaultProvider?: string | undefined;
393
+ }
394
+
395
+ export interface VnextPiProducer extends VnextProducer {
396
+ readonly id: "pi";
397
+ readonly sessions: ReadonlyMap<string, PiSession>;
398
+ produce(request: VnextProducerRequest): Promise<VnextProducerResult>;
399
+ close(): Promise<void>;
400
+ closeRun(runId: string): Promise<void>;
401
+ }
402
+
403
+ export function createVnextPiProducer(options: VnextPiProducerOptions = {}): VnextPiProducer {
404
+ const sessions = new Map<string, PiSession>();
405
+ const defaultModel = options.defaultModel ?? "xai/grok-4.6";
406
+ const defaultProvider = options.defaultProvider ?? "xai";
407
+
408
+ function parseModelString(spec: string): { provider: string; model: string } {
409
+ const trimmed = spec.trim();
410
+ if (trimmed.includes("/")) {
411
+ const idx = trimmed.indexOf("/");
412
+ return { provider: trimmed.slice(0, idx).toLowerCase(), model: trimmed.slice(idx + 1) };
413
+ }
414
+ return { provider: defaultProvider, model: trimmed };
415
+ }
416
+
417
+ function resolveModelForRequest(request: VnextProducerRequest): { provider: string; model: string; thinking?: string | undefined } {
418
+ if (request.model) {
419
+ const parsed = parseModelString(request.model);
420
+ return {
421
+ provider: request.provider?.toLowerCase() ?? parsed.provider,
422
+ model: parsed.model,
423
+ thinking: request.thinking,
424
+ };
425
+ }
426
+ if (options.resolveModel) {
427
+ const resolved = options.resolveModel(request.agentId, request.runId);
428
+ if (resolved && resolved.model) {
429
+ const parsed = parseModelString(resolved.model);
430
+ return {
431
+ provider: resolved.provider?.toLowerCase() ?? parsed.provider,
432
+ model: parsed.model,
433
+ thinking: resolved.thinking,
434
+ };
435
+ }
436
+ }
437
+ const parsed = parseModelString(defaultModel);
438
+ return { provider: parsed.provider, model: parsed.model };
439
+ }
440
+
441
+ function checkPiAuth(provider: string, model: string): void {
442
+ if (options.inventory) {
443
+ const piEntry = options.inventory.harnesses.find((h) => h.id === "pi");
444
+ if (!piEntry || !piEntry.detected || piEntry.authenticated === false) {
445
+ throw new Error("pi_not_authenticated: pi harness not authenticated in inventory");
446
+ }
447
+ }
448
+
449
+ const probeFn = options.probeHarness ?? probeHarnessAssignment;
450
+ const probe = probeFn({
451
+ harness: "pi",
452
+ provider,
453
+ model,
454
+ env: options.env,
455
+ });
456
+
457
+ if (!probe.detected) {
458
+ throw new Error(`pi_not_authenticated: pi harness not detected (${probe.issues.join(", ")})`);
459
+ }
460
+ if (probe.authenticated !== true) {
461
+ throw new Error(`pi_not_authenticated: pi not authenticated for provider ${provider} (${probe.issues.join(", ")})`);
462
+ }
463
+ }
464
+
465
+ async function getOrCreateSession(request: VnextProducerRequest, model: string): Promise<PiSession> {
466
+ const key = formatPiSessionKey({
467
+ runId: request.runId,
468
+ agentId: request.agentId,
469
+ instanceNo: request.instanceNo,
470
+ scopeEpoch: request.scopeEpoch,
471
+ });
472
+
473
+ const existing = sessions.get(key);
474
+ if (existing && !existing.isClosed()) {
475
+ return existing;
476
+ }
477
+
478
+ const displayName = formatPiSessionDisplayName({
479
+ runId: request.runId,
480
+ agentId: request.agentId,
481
+ instanceNo: request.instanceNo,
482
+ scopeEpoch: request.scopeEpoch,
483
+ });
484
+
485
+ const command = options.piCommand ?? "pi";
486
+ const args = ["--mode", "rpc", "--name", displayName];
487
+ if (options.sessionDir) {
488
+ const safeDirName = key.replace(/[^a-zA-Z0-9_-]/g, "_");
489
+ args.push("--session-dir", join(options.sessionDir, request.runId, safeDirName));
490
+ }
491
+ if (model) {
492
+ args.push("--model", model);
493
+ }
494
+
495
+ let proc: PiRpcProcess;
496
+ if (options.spawnProcess) {
497
+ proc = options.spawnProcess(command, args, { env: options.env ?? process.env });
498
+ } else {
499
+ const child = spawn(command, args, {
500
+ env: options.env ?? process.env,
501
+ stdio: ["pipe", "pipe", "pipe"],
502
+ });
503
+ proc = {
504
+ stdin: child.stdin!,
505
+ stdout: child.stdout!,
506
+ stderr: child.stderr ?? undefined,
507
+ kill: (sig) => child.kill(sig),
508
+ on: (ev: any, listener: any) => {
509
+ child.on(ev, listener);
510
+ return proc;
511
+ },
512
+ };
513
+ }
514
+
515
+ const session = new PiSession({
516
+ key,
517
+ displayName,
518
+ runId: request.runId,
519
+ agentId: request.agentId,
520
+ instanceNo: request.instanceNo ?? 1,
521
+ scopeEpoch: request.scopeEpoch ?? 1,
522
+ sessionDir: options.sessionDir,
523
+ model,
524
+ process: proc,
525
+ });
526
+
527
+ sessions.set(key, session);
528
+ return session;
529
+ }
530
+
531
+ const producer: VnextPiProducer = {
532
+ id: "pi",
533
+ get sessions(): ReadonlyMap<string, PiSession> {
534
+ return sessions;
535
+ },
536
+
537
+ async produce(request: VnextProducerRequest): Promise<VnextProducerResult> {
538
+ const startTime = Date.now();
539
+ const resolved = resolveModelForRequest(request);
540
+
541
+ // Preflight auth check - fails closed
542
+ checkPiAuth(resolved.provider, resolved.model);
543
+
544
+ // Get or create session
545
+ const session = await getOrCreateSession(request, resolved.model);
546
+
547
+ const promptMessage = request.prompt
548
+ ?? `Execute step ${request.stepId} (attempt ${request.stepAttempt}) for agent ${request.agentId}. Allowed outcomes: ${request.allowedOutcomes.join(", ")}.`;
549
+
550
+ let promptResult: PiPromptResult;
551
+ try {
552
+ promptResult = await session.prompt(promptMessage, request.allowedOutcomes, request.signal);
553
+ } catch (err) {
554
+ return Promise.reject(err);
555
+ }
556
+
557
+ const latencyMs = Math.max(0, Date.now() - startTime);
558
+ const usage = promptResult.usage;
559
+ const tokensIn = typeof usage?.input === "number" ? usage.input : null;
560
+ const tokensOut = typeof usage?.output === "number" ? usage.output : null;
561
+ const cacheReadTokens = typeof usage?.cacheRead === "number" ? usage.cacheRead : null;
562
+ const cacheWriteTokens = typeof usage?.cacheWrite === "number" ? usage.cacheWrite : null;
563
+ const contextTokens = typeof usage?.contextUsage?.tokens === "number" ? usage.contextUsage.tokens : tokensIn;
564
+
565
+ // Price calculation
566
+ const catalog = options.priceCatalog ?? loadPriceCatalog(options.projectRoot ?? process.cwd());
567
+ let costBasis: "metered" | "unknown" = "unknown";
568
+ let costUsd: number | null = null;
569
+ let priceRef: string | undefined;
570
+
571
+ if (catalog) {
572
+ const calculated = calculateModelCost(catalog, {
573
+ model: resolved.model,
574
+ provider: resolved.provider,
575
+ tokensIn,
576
+ tokensOut,
577
+ cacheReadTokens,
578
+ cacheWriteTokens,
579
+ contextTokens,
580
+ });
581
+ if (calculated) {
582
+ costBasis = "metered";
583
+ costUsd = calculated.costUsd;
584
+ priceRef = calculated.priceRef;
585
+ }
586
+ }
587
+
588
+ return {
589
+ outcome: promptResult.outcome,
590
+ costBasis,
591
+ costUsd,
592
+ tokensIn,
593
+ tokensOut,
594
+ cacheReadTokens,
595
+ cacheWriteTokens,
596
+ contextTokens,
597
+ latencyMs,
598
+ harness: "pi",
599
+ provider: resolved.provider,
600
+ requestedModel: resolved.model,
601
+ effectiveModel: resolved.model,
602
+ ...(resolved.thinking !== undefined ? { thinking: resolved.thinking } : {}),
603
+ agentRole: request.agentRole ?? request.agentId,
604
+ ...(priceRef !== undefined ? { priceRef } : {}),
605
+ };
606
+ },
607
+
608
+ async closeRun(runId: string): Promise<void> {
609
+ const toClose: PiSession[] = [];
610
+ for (const [key, session] of sessions.entries()) {
611
+ if (session.runId === runId) {
612
+ toClose.push(session);
613
+ sessions.delete(key);
614
+ }
615
+ }
616
+ await Promise.all(toClose.map((s) => s.close()));
617
+ },
618
+
619
+ async close(): Promise<void> {
620
+ const toClose = [...sessions.values()];
621
+ sessions.clear();
622
+ await Promise.all(toClose.map((s) => s.close()));
623
+ },
624
+ };
625
+
626
+ registerTrustedProducer(producer);
627
+ return producer;
628
+ }