ultimate-pi 0.20.0 → 0.22.1

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 (149) hide show
  1. package/.agents/skills/harness-context/SKILL.md +3 -3
  2. package/.agents/skills/harness-debate-plan/SKILL.md +2 -2
  3. package/.agents/skills/harness-decisions/SKILL.md +68 -2
  4. package/.agents/skills/harness-eval/SKILL.md +1 -1
  5. package/.agents/skills/harness-git-commit/SKILL.md +72 -0
  6. package/.agents/skills/harness-governor/SKILL.md +6 -6
  7. package/.agents/skills/harness-ls-lint-setup/SKILL.md +59 -0
  8. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  9. package/.agents/skills/harness-plan/SKILL.md +14 -12
  10. package/.agents/skills/harness-review/SKILL.md +3 -3
  11. package/.agents/skills/harness-sentrux-repair/SKILL.md +48 -0
  12. package/.agents/skills/harness-sentrux-setup/SKILL.md +2 -2
  13. package/.agents/skills/harness-spec/SKILL.md +1 -1
  14. package/.agents/skills/harness-steer/SKILL.md +2 -2
  15. package/.agents/skills/posthog-analyst/SKILL.md +1 -1
  16. package/.agents/skills/sentrux/SKILL.md +6 -4
  17. package/.agents/skills/web-retrieval/SKILL.md +1 -1
  18. package/.agents/skills/wiki-save/SKILL.md +1 -1
  19. package/.pi/PACKAGING.md +6 -0
  20. package/.pi/SYSTEM.md +21 -3
  21. package/.pi/agents/harness/ls-lint-steward.md +49 -0
  22. package/.pi/agents/harness/planning/decompose.md +5 -5
  23. package/.pi/agents/harness/planning/execution-plan-author.md +1 -1
  24. package/.pi/agents/harness/planning/hypothesis-validator.md +1 -1
  25. package/.pi/agents/harness/planning/hypothesis.md +1 -1
  26. package/.pi/agents/harness/planning/plan-adversary.md +1 -1
  27. package/.pi/agents/harness/planning/plan-evaluator.md +2 -2
  28. package/.pi/agents/harness/planning/plan-synthesizer.md +2 -2
  29. package/.pi/agents/harness/planning/review-integrator.md +1 -1
  30. package/.pi/agents/harness/planning/sprint-contract-auditor.md +5 -5
  31. package/.pi/agents/harness/reviewing/evaluator.md +1 -1
  32. package/.pi/agents/harness/running/executor.md +2 -2
  33. package/.pi/agents/harness/sentrux-repair-advisor.md +50 -0
  34. package/.pi/agents/harness/sentrux-steward.md +2 -2
  35. package/.pi/agents/pi-pi/prompt-expert.md +17 -2
  36. package/.pi/auto-commit.json +9 -2
  37. package/.pi/extensions/debate-orchestrator.ts +3 -0
  38. package/.pi/extensions/harness-anchored-edit.ts +7 -9
  39. package/.pi/extensions/harness-ask-user.ts +13 -34
  40. package/.pi/extensions/harness-debate-tools.ts +43 -4
  41. package/.pi/extensions/harness-live-widget.ts +28 -19
  42. package/.pi/extensions/harness-run-context.ts +278 -115
  43. package/.pi/extensions/harness-web-tools.ts +598 -471
  44. package/.pi/extensions/ls-lint-rules-sync.ts +103 -0
  45. package/.pi/extensions/observation-bus.ts +4 -0
  46. package/.pi/extensions/policy-gate.ts +270 -229
  47. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  48. package/.pi/extensions/soundboard.ts +48 -48
  49. package/.pi/harness/README.md +4 -0
  50. package/.pi/harness/agents.manifest.json +24 -16
  51. package/.pi/harness/agents.policy.yaml +49 -82
  52. package/.pi/harness/docs/adrs/0052-ls-lint-naming-lifecycle.md +45 -0
  53. package/.pi/harness/docs/adrs/0052-sentrux-structured-repair.md +38 -0
  54. package/.pi/harness/docs/adrs/0053-plan-task-clarification-gate.md +39 -0
  55. package/.pi/harness/docs/adrs/0054-harness-native-ask-user.md +40 -0
  56. package/.pi/harness/docs/adrs/0055-auto-commit-coauthor-lifecycle.md +40 -0
  57. package/.pi/harness/docs/adrs/README.md +5 -0
  58. package/.pi/harness/docs/practice-map.md +10 -5
  59. package/.pi/harness/evals/smoke/ls-lint-stub.json +10 -0
  60. package/.pi/harness/evolution/self-healing-rules.json +16 -0
  61. package/.pi/harness/ls-lint/naming.manifest.json +128 -0
  62. package/.pi/harness/sentrux/architecture.manifest.json +1 -1
  63. package/.pi/harness/specs/auto-commit.schema.json +63 -0
  64. package/.pi/harness/specs/ls-lint-manifest-proposal.schema.json +80 -0
  65. package/.pi/harness/specs/ls-lint-signal.schema.json +47 -0
  66. package/.pi/harness/specs/naming-manifest.schema.json +54 -0
  67. package/.pi/harness/specs/plan-task-clarification.schema.json +88 -0
  68. package/.pi/harness/specs/sentrux-diagnostics.schema.json +173 -0
  69. package/.pi/harness/specs/sentrux-repair-plan.schema.json +133 -0
  70. package/.pi/harness/specs/sentrux-report.schema.json +119 -0
  71. package/.pi/harness/specs/sentrux-signal.schema.json +34 -1
  72. package/.pi/lib/agents-policy.d.mts +26 -51
  73. package/.pi/lib/agents-policy.mjs +41 -28
  74. package/.pi/lib/agt/build-evaluation-context.ts +136 -64
  75. package/.pi/lib/ask-user/constants.mjs +3 -0
  76. package/.pi/lib/ask-user/constants.ts +4 -0
  77. package/.pi/lib/ask-user/contracts/glimpse-parse.ts +56 -0
  78. package/.pi/lib/ask-user/contracts/glimpse-payload-build.ts +58 -0
  79. package/.pi/lib/ask-user/contracts/glimpse-payload.ts +38 -0
  80. package/.pi/lib/ask-user/core/questionnaire.ts +74 -0
  81. package/.pi/lib/ask-user/dialog.ts +2 -314
  82. package/.pi/lib/ask-user/fallback.ts +2 -78
  83. package/.pi/lib/ask-user/format.ts +85 -0
  84. package/.pi/lib/ask-user/glimpseui.d.ts +10 -0
  85. package/.pi/lib/ask-user/index.ts +114 -0
  86. package/.pi/lib/ask-user/merge-task-clarification.ts +98 -0
  87. package/.pi/lib/ask-user/policy.mjs +43 -0
  88. package/.pi/lib/ask-user/policy.ts +104 -0
  89. package/.pi/lib/ask-user/presenters/glimpse.ts +130 -0
  90. package/.pi/lib/ask-user/presenters/headless.ts +131 -0
  91. package/.pi/lib/ask-user/presenters/select.ts +60 -0
  92. package/.pi/lib/ask-user/presenters/tui.ts +373 -0
  93. package/.pi/lib/ask-user/presenters/types.ts +13 -0
  94. package/.pi/lib/ask-user/render.ts +40 -9
  95. package/.pi/lib/ask-user/schema.ts +66 -13
  96. package/.pi/lib/ask-user/types.ts +60 -3
  97. package/.pi/lib/ask-user/validate-core.mjs +193 -7
  98. package/.pi/lib/ask-user/validate.ts +53 -34
  99. package/.pi/lib/harness-anchored-edit/package.json +3 -0
  100. package/.pi/lib/harness-artifact-gate.ts +75 -21
  101. package/.pi/lib/harness-auto-commit-config.mjs +321 -0
  102. package/.pi/lib/harness-lens/clients/lsp/client.ts +62 -39
  103. package/.pi/lib/harness-lens/clients/tool-policy.ts +73 -181
  104. package/.pi/lib/harness-lens/index.ts +241 -108
  105. package/.pi/lib/harness-lens/tools/lsp-navigation.ts +10 -8
  106. package/.pi/lib/harness-repair-brief.ts +84 -25
  107. package/.pi/lib/harness-run-context.ts +42 -52
  108. package/.pi/lib/harness-sentrux-parse.mjs +272 -0
  109. package/.pi/lib/harness-sentrux-root.mjs +78 -0
  110. package/.pi/lib/harness-slash-completions.ts +116 -0
  111. package/.pi/lib/harness-spawn-topology.ts +121 -87
  112. package/.pi/lib/harness-subagent-submit-registry.ts +10 -0
  113. package/.pi/lib/harness-subagents-bridge.ts +4 -1
  114. package/.pi/lib/harness-ui-state.ts +95 -48
  115. package/.pi/lib/plan-approval/dialog.ts +5 -0
  116. package/.pi/lib/plan-approval/validate.ts +1 -1
  117. package/.pi/lib/plan-approval-readiness.ts +32 -0
  118. package/.pi/lib/plan-debate-gate.ts +154 -114
  119. package/.pi/lib/plan-task-clarification.ts +158 -0
  120. package/.pi/prompts/harness-auto.md +2 -2
  121. package/.pi/prompts/harness-ls-lint-steward.md +43 -0
  122. package/.pi/prompts/harness-plan.md +63 -13
  123. package/.pi/prompts/harness-review.md +44 -10
  124. package/.pi/prompts/harness-run.md +35 -13
  125. package/.pi/prompts/harness-sentrux-steward.md +2 -2
  126. package/.pi/prompts/harness-setup.md +74 -5
  127. package/.pi/prompts/harness-steer.md +6 -5
  128. package/.pi/prompts/wiki-save.md +5 -4
  129. package/.pi/scripts/README.md +8 -0
  130. package/.pi/scripts/generate-agents-policy-yaml.mjs +14 -2
  131. package/.pi/scripts/harness-auto-commit-bootstrap.mjs +96 -0
  132. package/.pi/scripts/harness-cli-verify.sh +47 -0
  133. package/.pi/scripts/harness-git-churn.mjs +77 -0
  134. package/.pi/scripts/harness-git-commit.mjs +173 -0
  135. package/.pi/scripts/harness-ls-lint-bootstrap.mjs +142 -0
  136. package/.pi/scripts/harness-ls-lint-cli.mjs +184 -0
  137. package/.pi/scripts/harness-seed-project-contracts.mjs +47 -0
  138. package/.pi/scripts/harness-sentrux-diagnostics.mjs +230 -0
  139. package/.pi/scripts/harness-sentrux-report.mjs +256 -0
  140. package/.pi/scripts/harness-verify.mjs +361 -125
  141. package/.pi/scripts/ls-lint-rules-sync.mjs +265 -0
  142. package/.pi/scripts/run-tests.mjs +1 -0
  143. package/.pi/settings.example.json +1 -0
  144. package/.sentrux/rules.toml +1 -1
  145. package/AGENTS.md +2 -0
  146. package/CHANGELOG.md +32 -0
  147. package/README.md +13 -4
  148. package/package.json +13 -6
  149. package/vendor/pi-vcc/src/hooks/before-compact.ts +86 -60
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Load and merge .pi/auto-commit.json (project overrides package).
3
+ * Format commit subjects and append Co-authored-by trailers.
4
+ */
5
+
6
+ import { readFile, access } from "node:fs/promises";
7
+ import { constants } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ const TEMPLATE_PLACEHOLDERS = new Set(["type", "scope", "subject", "login", "email"]);
11
+
12
+ const DEFAULT_CO_AUTHOR = {
13
+ login: "pi-mono",
14
+ email: "261679550+pi-mono@users.noreply.github.com",
15
+ required: true,
16
+ };
17
+
18
+ const DEFAULT_MESSAGE = {
19
+ template: "{type}({scope}): {subject}",
20
+ templateNoScope: "{type}: {subject}",
21
+ typeDefault: "chore",
22
+ scopeDefault: "harness",
23
+ bodySeparator: "\n\n",
24
+ coAuthorTrailer: "Co-authored-by: {login} <{email}>",
25
+ maxSubjectLength: 72,
26
+ };
27
+
28
+ /** @param {unknown} value */
29
+ function isPlainObject(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ /**
34
+ * Deep-merge objects; arrays and scalars from override replace base.
35
+ * @param {Record<string, unknown>} base
36
+ * @param {Record<string, unknown>} override
37
+ */
38
+ export function deepMerge(base, override) {
39
+ const out = { ...base };
40
+ for (const [key, val] of Object.entries(override)) {
41
+ if (
42
+ isPlainObject(val) &&
43
+ isPlainObject(out[key]) &&
44
+ !Array.isArray(val)
45
+ ) {
46
+ out[key] = deepMerge(
47
+ /** @type {Record<string, unknown>} */ (out[key]),
48
+ /** @type {Record<string, unknown>} */ (val),
49
+ );
50
+ } else {
51
+ out[key] = val;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ async function readJsonIfExists(path) {
58
+ try {
59
+ await access(path, constants.R_OK);
60
+ } catch {
61
+ return null;
62
+ }
63
+ const raw = await readFile(path, "utf-8");
64
+ return JSON.parse(raw);
65
+ }
66
+
67
+ /**
68
+ * @param {string} template
69
+ */
70
+ export function assertValidTemplate(template) {
71
+ const re = /\{([a-zA-Z_]+)\}/g;
72
+ let m;
73
+ while ((m = re.exec(template)) !== null) {
74
+ if (!TEMPLATE_PLACEHOLDERS.has(m[1])) {
75
+ throw new Error(
76
+ `auto-commit: unknown placeholder {${m[1]}} in template (allowed: ${[...TEMPLATE_PLACEHOLDERS].join(", ")})`,
77
+ );
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param {Record<string, unknown>} config
84
+ */
85
+ export function validateAutoCommitConfig(config) {
86
+ const coAuthor = /** @type {Record<string, unknown>} */ (
87
+ config.coAuthor ?? {}
88
+ );
89
+ const login = coAuthor.login;
90
+ const email = coAuthor.email;
91
+ if (typeof login !== "string" || !login.trim()) {
92
+ throw new Error("auto-commit: coAuthor.login is required");
93
+ }
94
+ if (typeof email !== "string" || !email.trim() || !email.includes("@")) {
95
+ throw new Error("auto-commit: coAuthor.email must be a valid email");
96
+ }
97
+
98
+ const message = /** @type {Record<string, unknown>} */ (
99
+ config.message ?? {}
100
+ );
101
+ const template = message.template;
102
+ if (typeof template !== "string" || !template.trim()) {
103
+ throw new Error("auto-commit: message.template is required");
104
+ }
105
+ assertValidTemplate(template);
106
+ const templateNoScope = message.templateNoScope;
107
+ if (templateNoScope != null) {
108
+ if (typeof templateNoScope !== "string" || !templateNoScope.trim()) {
109
+ throw new Error("auto-commit: message.templateNoScope must be non-empty");
110
+ }
111
+ assertValidTemplate(templateNoScope);
112
+ }
113
+ const trailer = message.coAuthorTrailer;
114
+ if (typeof trailer !== "string" || !trailer.trim()) {
115
+ throw new Error("auto-commit: message.coAuthorTrailer is required");
116
+ }
117
+ assertValidTemplate(trailer);
118
+ }
119
+
120
+ /**
121
+ * @param {string} projectRoot
122
+ * @param {string} upPkg
123
+ */
124
+ export async function resolveAutoCommitConfig(projectRoot, upPkg) {
125
+ const pkgPath = join(upPkg, ".pi", "auto-commit.json");
126
+ const projectPath = join(projectRoot, ".pi", "auto-commit.json");
127
+
128
+ const pkgRaw = (await readJsonIfExists(pkgPath)) ?? {};
129
+ const projectRaw = (await readJsonIfExists(projectPath)) ?? {};
130
+
131
+ const base = {
132
+ dryRun: false,
133
+ coAuthor: { ...DEFAULT_CO_AUTHOR },
134
+ message: { ...DEFAULT_MESSAGE },
135
+ ...(isPlainObject(pkgRaw) ? pkgRaw : {}),
136
+ };
137
+ const merged = deepMerge(
138
+ /** @type {Record<string, unknown>} */ (base),
139
+ /** @type {Record<string, unknown>} */ (
140
+ isPlainObject(projectRaw) ? projectRaw : {}
141
+ ),
142
+ );
143
+
144
+ if (!isPlainObject(merged.message)) {
145
+ merged.message = { ...DEFAULT_MESSAGE };
146
+ } else {
147
+ merged.message = { ...DEFAULT_MESSAGE, ...merged.message };
148
+ }
149
+ if (!isPlainObject(merged.coAuthor)) {
150
+ merged.coAuthor = { ...DEFAULT_CO_AUTHOR };
151
+ } else {
152
+ merged.coAuthor = { ...DEFAULT_CO_AUTHOR, ...merged.coAuthor };
153
+ }
154
+
155
+ validateAutoCommitConfig(merged);
156
+ return merged;
157
+ }
158
+
159
+ /**
160
+ * @param {string} template
161
+ * @param {Record<string, string>} vars
162
+ */
163
+ function applyTemplate(template, vars) {
164
+ return template.replace(/\{([a-zA-Z_]+)\}/g, (_, key) => vars[key] ?? "");
165
+ }
166
+
167
+ /**
168
+ * @param {Record<string, unknown>} config
169
+ * @param {{ type?: string, scope?: string, subject: string, body?: string }} input
170
+ */
171
+ export function formatCommitMessage(config, input) {
172
+ const message = /** @type {Record<string, unknown>} */ (config.message);
173
+ const type =
174
+ (input.type ?? message.typeDefault ?? "chore").toString().trim() ||
175
+ "chore";
176
+ let scope = (input.scope ?? message.scopeDefault ?? "").toString().trim();
177
+ const subject = input.subject.trim();
178
+ if (!subject) {
179
+ throw new Error("auto-commit: subject is required");
180
+ }
181
+
182
+ const maxLen =
183
+ typeof message.maxSubjectLength === "number"
184
+ ? message.maxSubjectLength
185
+ : 72;
186
+ let subjectLine = subject.split(/\r?\n/)[0] ?? subject;
187
+ if (subjectLine.length > maxLen) {
188
+ subjectLine = `${subjectLine.slice(0, maxLen - 3)}...`;
189
+ }
190
+
191
+ const template =
192
+ scope.length > 0
193
+ ? String(message.template)
194
+ : String(message.templateNoScope ?? message.template);
195
+ const subjectFormatted = applyTemplate(template, {
196
+ type,
197
+ scope,
198
+ subject: subjectLine,
199
+ });
200
+
201
+ const body = (input.body ?? "").trim();
202
+ const bodySep = String(message.bodySeparator ?? "\n\n");
203
+ if (!body) {
204
+ return subjectFormatted;
205
+ }
206
+ return `${subjectFormatted}${bodySep}${body}`;
207
+ }
208
+
209
+ /**
210
+ * Strip trailing co-authored-by lines from commit message body.
211
+ * @param {string} message
212
+ */
213
+ export function stripCoAuthorTrailers(message) {
214
+ const lines = message.replace(/\r\n/g, "\n").split("\n");
215
+ while (lines.length > 0) {
216
+ const last = lines[lines.length - 1]?.trim() ?? "";
217
+ if (!last) {
218
+ lines.pop();
219
+ continue;
220
+ }
221
+ if (/^co-authored-by:/i.test(last)) {
222
+ lines.pop();
223
+ continue;
224
+ }
225
+ break;
226
+ }
227
+ return lines.join("\n").trimEnd();
228
+ }
229
+
230
+ /**
231
+ * @param {Record<string, unknown>} coAuthor
232
+ * @param {string} trailerTemplate
233
+ */
234
+ export function renderCoAuthorTrailer(coAuthor, trailerTemplate) {
235
+ return applyTemplate(trailerTemplate, {
236
+ login: String(coAuthor.login).trim(),
237
+ email: String(coAuthor.email).trim(),
238
+ type: "",
239
+ scope: "",
240
+ subject: "",
241
+ });
242
+ }
243
+
244
+ /**
245
+ * @param {string} message
246
+ * @param {Record<string, unknown>} coAuthor
247
+ * @param {string} trailerTemplate
248
+ */
249
+ export function messageHasCoAuthorTrailer(message, coAuthor, trailerTemplate) {
250
+ const expected = renderCoAuthorTrailer(coAuthor, trailerTemplate)
251
+ .trim()
252
+ .toLowerCase();
253
+ const normalized = message.replace(/\r\n/g, "\n").toLowerCase();
254
+ return normalized.includes(expected);
255
+ }
256
+
257
+ /**
258
+ * @param {string} message
259
+ * @param {Record<string, unknown>} config
260
+ */
261
+ export function appendCoAuthorTrailer(message, config) {
262
+ const coAuthor = /** @type {Record<string, unknown>} */ (config.coAuthor);
263
+ const messageCfg = /** @type {Record<string, unknown>} */ (config.message);
264
+ const trailerTemplate = String(
265
+ messageCfg.coAuthorTrailer ?? DEFAULT_MESSAGE.coAuthorTrailer,
266
+ );
267
+
268
+ if (coAuthor.required === false) {
269
+ return message;
270
+ }
271
+
272
+ const stripped = stripCoAuthorTrailers(message);
273
+ if (messageHasCoAuthorTrailer(stripped, coAuthor, trailerTemplate)) {
274
+ return stripped;
275
+ }
276
+
277
+ const trailer = renderCoAuthorTrailer(coAuthor, trailerTemplate);
278
+ if (!stripped) {
279
+ return trailer;
280
+ }
281
+ return `${stripped}\n\n${trailer}`;
282
+ }
283
+
284
+ /**
285
+ * Build final commit message (subject/body + trailer).
286
+ * @param {Record<string, unknown>} config
287
+ * @param {{ type?: string, scope?: string, subject?: string, body?: string, message?: string }} input
288
+ */
289
+ export function buildFullCommitMessage(config, input) {
290
+ let core;
291
+ if (input.message != null && String(input.message).trim()) {
292
+ core = String(input.message).trim();
293
+ } else if (input.subject != null && String(input.subject).trim()) {
294
+ core = formatCommitMessage(config, {
295
+ type: input.type,
296
+ scope: input.scope,
297
+ subject: String(input.subject),
298
+ body: input.body,
299
+ });
300
+ } else {
301
+ throw new Error(
302
+ "auto-commit: provide --message or --subject for commit text",
303
+ );
304
+ }
305
+ return appendCoAuthorTrailer(core, config);
306
+ }
307
+
308
+ /** @param {string} message */
309
+ export function splitSubjectAndBody(message) {
310
+ const normalized = message.replace(/\r\n/g, "\n");
311
+ const idx = normalized.indexOf("\n\n");
312
+ if (idx === -1) {
313
+ return { subject: normalized.trim(), body: "" };
314
+ }
315
+ return {
316
+ subject: normalized.slice(0, idx).trim(),
317
+ body: normalized.slice(idx + 2).trim(),
318
+ };
319
+ }
320
+
321
+ export { DEFAULT_CO_AUTHOR, DEFAULT_MESSAGE, TEMPLATE_PLACEHOLDERS };
@@ -833,6 +833,60 @@ async function navRequest<T>(
833
833
  }) as Promise<T | undefined>;
834
834
  }
835
835
 
836
+ async function initializeLspOrThrow(input: {
837
+ connection: ReturnType<typeof createMessageConnection>;
838
+ root: string;
839
+ initialization?: Record<string, unknown>;
840
+ initializeTimeoutMs: number;
841
+ lspProcess: LSPProcess;
842
+ onStderr: (chunk: Buffer | string) => void;
843
+ }): Promise<Awaited<ReturnType<typeof safeSendRequest>>> {
844
+ const {
845
+ connection,
846
+ root,
847
+ initialization,
848
+ initializeTimeoutMs,
849
+ lspProcess,
850
+ onStderr,
851
+ } = input;
852
+ try {
853
+ return await withTimeout(
854
+ safeSendRequest(connection, "initialize", {
855
+ processId: process.pid,
856
+ rootUri: pathToFileURL(root).href,
857
+ workspaceFolders: [
858
+ { name: "workspace", uri: pathToFileURL(root).href },
859
+ ],
860
+ capabilities: {
861
+ window: { workDoneProgress: true },
862
+ workspace: {
863
+ workspaceFolders: true,
864
+ configuration: true,
865
+ didChangeWatchedFiles: { dynamicRegistration: true },
866
+ },
867
+ textDocument: {
868
+ synchronization: { didOpen: true, didChange: true },
869
+ publishDiagnostics: { versionSupport: true },
870
+ },
871
+ },
872
+ initializationOptions: initialization,
873
+ }),
874
+ initializeTimeoutMs,
875
+ );
876
+ } catch (err) {
877
+ const pid = lspProcess.pid;
878
+ void killProcessTree(lspProcess.process, pid);
879
+ setTimeout(() => {
880
+ if (!lspProcess.process.killed && process.platform !== "win32") {
881
+ lspProcess.process.kill("SIGKILL");
882
+ }
883
+ }, 2000);
884
+ throw err;
885
+ } finally {
886
+ (lspProcess.stderr as NodeJS.ReadableStream).off("data", onStderr);
887
+ }
888
+ }
889
+
836
890
  // --- Client Factory ---
837
891
 
838
892
  export async function createLSPClient(options: {
@@ -987,45 +1041,14 @@ export async function createLSPClient(options: {
987
1041
  connection.listen();
988
1042
  setupConnectionLifecycle(state);
989
1043
 
990
- let initResult: Awaited<ReturnType<typeof safeSendRequest>>;
991
- try {
992
- initResult = await withTimeout(
993
- safeSendRequest(connection, "initialize", {
994
- processId: process.pid,
995
- rootUri: pathToFileURL(root).href,
996
- workspaceFolders: [
997
- { name: "workspace", uri: pathToFileURL(root).href },
998
- ],
999
- capabilities: {
1000
- window: { workDoneProgress: true },
1001
- workspace: {
1002
- workspaceFolders: true,
1003
- configuration: true,
1004
- didChangeWatchedFiles: { dynamicRegistration: true },
1005
- },
1006
- textDocument: {
1007
- synchronization: { didOpen: true, didChange: true },
1008
- publishDiagnostics: { versionSupport: true },
1009
- },
1010
- },
1011
- initializationOptions: initialization,
1012
- }),
1013
- initializeTimeoutMs,
1014
- );
1015
- } catch (err) {
1016
- // Hard-kill the hung process so it doesn't become a zombie.
1017
- // SIGTERM alone is unreliable on Windows for cmd.exe/PowerShell trees.
1018
- const pid = lspProcess.pid;
1019
- void killProcessTree(lspProcess.process, pid);
1020
- setTimeout(() => {
1021
- if (!lspProcess.process.killed && process.platform !== "win32") {
1022
- lspProcess.process.kill("SIGKILL");
1023
- }
1024
- }, 2000);
1025
- throw err;
1026
- } finally {
1027
- (lspProcess.stderr as NodeJS.ReadableStream).off("data", onStderr);
1028
- }
1044
+ const initResult = await initializeLspOrThrow({
1045
+ connection,
1046
+ root,
1047
+ initialization,
1048
+ initializeTimeoutMs,
1049
+ lspProcess,
1050
+ onStderr,
1051
+ });
1029
1052
 
1030
1053
  if (initResult === undefined) {
1031
1054
  const compactStderr = startupState.stderr