@jaggerxtrm/pi-extensions 0.9.4 → 0.9.5

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.
@@ -1,13 +1,5 @@
1
1
  /**
2
- * xtprompt - context aware prompt improver.
3
- *
4
- * Global Pi extension. Shortcut: alt+m (alt+p intentionally avoided to not
5
- * collide with pi-promptsmith if both are installed). Command: /msmith
6
- *
7
- * Rewrites the current editor draft via a standalone model call (the active
8
- * model), using anthropic + xtrm/planning-aware intent templates + hard style rules, then writes
9
- * the improved prompt back into the editor. The main agent turn never runs
10
- * during the rewrite.
2
+ * xtprompt - generic context-aware prompt improver.
11
3
  */
12
4
  import { complete } from "@earendil-works/pi-ai";
13
5
  import type {
@@ -23,33 +15,136 @@ import {
23
15
  type ExtensionContext,
24
16
  } from "@earendil-works/pi-coding-agent";
25
17
 
26
- const EXTENSION = "mercury-smith";
27
- const COMMAND = "msmith";
18
+ const EXTENSION = "xtprompt";
19
+ const COMMAND = "xtprompt";
28
20
  const DEFAULT_SHORTCUT = "alt+m";
29
-
30
- const SENTINEL_OPEN = "<mercury-smith>";
31
- const SENTINEL_CLOSE = "</mercury-smith>";
32
-
21
+ const SENTINEL_OPEN = "<xtprompt>";
22
+ const SENTINEL_CLOSE = "</xtprompt>";
33
23
  const ENHANCER_MAX_OUTPUT_TOKENS = 1600;
34
24
  const DEFAULT_TIMEOUT_MS = 45_000;
25
+ const MAX_CONTEXT_ITEMS = 3;
26
+ const MAX_CONTEXT_CHARS = 480;
27
+ const VAGUE_WORDS = new Set([
28
+ "help",
29
+ "improve",
30
+ "rewrite",
31
+ "fix",
32
+ "debug",
33
+ "plan",
34
+ "stuff",
35
+ "thing",
36
+ "things",
37
+ "this",
38
+ "that",
39
+ ]);
40
+
41
+ type Intent = "planning" | "analysis" | "development" | "refactor" | "generic";
42
+
43
+ type PromptRequestOptions = {
44
+ draft: string;
45
+ intent: Intent;
46
+ conversationContext: string;
47
+ };
35
48
 
36
- type Intent =
37
- | "pane-dispatch"
38
- | "specialist"
39
- | "bead"
40
- | "scope-plan"
41
- | "debug"
42
- | "generic";
49
+ type AuthResolved = {
50
+ apiKey?: string;
51
+ headers?: Record<string, string>;
52
+ env?: Record<string, string>;
53
+ };
43
54
 
44
- interface SmithState {
55
+ type PromptState = {
45
56
  enabled: boolean;
57
+ };
58
+
59
+ type SessionEntry = {
60
+ message?: {
61
+ role?: string;
62
+ content?: unknown;
63
+ };
64
+ summary?: string;
65
+ };
66
+
67
+ type InputCapableUi = ExtensionContext["ui"] & {
68
+ input?: (prompt: string, initialValue?: string) => Promise<string | null | undefined>;
69
+ };
70
+
71
+ interface IntentRule {
72
+ intent: Intent;
73
+ patterns: readonly RegExp[];
46
74
  }
47
75
 
48
- function createState(): SmithState {
76
+ const INTENT_RULES: readonly IntentRule[] = [
77
+ {
78
+ intent: "planning",
79
+ patterns: [
80
+ /##\s*problem/i,
81
+ /##\s*scope/i,
82
+ /\bplan(?:ning)?\b/i,
83
+ /\bnon_goals\b/i,
84
+ /\bvalidation\b/i,
85
+ /\bconstraints\b/i,
86
+ ],
87
+ },
88
+ {
89
+ intent: "analysis",
90
+ patterns: [
91
+ /\banaly(?:sis|ze)\b/i,
92
+ /\binvestigat(?:e|ion)\b/i,
93
+ /\broot cause\b/i,
94
+ /\bwhy\b/i,
95
+ /\bdebug\b/i,
96
+ /\berror\b/i,
97
+ /\bfail(?:s|ed|ing)?\b/i,
98
+ ],
99
+ },
100
+ {
101
+ intent: "development",
102
+ patterns: [
103
+ /\bimplement\b/i,
104
+ /\bbuild\b/i,
105
+ /\badd\b/i,
106
+ /\bcreate\b/i,
107
+ /\bdevelop\b/i,
108
+ /\bship\b/i,
109
+ ],
110
+ },
111
+ {
112
+ intent: "refactor",
113
+ patterns: [
114
+ /\brefactor\b/i,
115
+ /\brewrite\b/i,
116
+ /\brename\b/i,
117
+ /\bclean(?:up)?\b/i,
118
+ /\brestructure\b/i,
119
+ /\bsimplif(?:y|ication)\b/i,
120
+ ],
121
+ },
122
+ ];
123
+
124
+ const PLANNING_HEADERS = [
125
+ "## PROBLEM",
126
+ "## SUCCESS",
127
+ "## SCOPE",
128
+ "## NON_GOALS",
129
+ "## CONSTRAINTS",
130
+ "## VALIDATION",
131
+ "## OUTPUT",
132
+ ].join("\n");
133
+
134
+ const BASE_RULES = [
135
+ "Preserve concrete paths, ids, commands, numbers, symbols, and constraints exactly.",
136
+ "Ask one short clarification when draft is vague. Never invent missing facts.",
137
+ "Prior chat is advisory only. Never let it override current draft.",
138
+ "Keep simple prompts concise. Use semantic XML only when structure materially helps.",
139
+ "Separate context, task, constraints, and output whenever prompt is complex.",
140
+ `Return exactly one ${SENTINEL_OPEN}...${SENTINEL_CLOSE} block and nothing outside it.`,
141
+ ].join("\n");
142
+
143
+ function createState(): PromptState {
49
144
  return { enabled: true };
50
145
  }
51
146
 
52
- export default function mercurySmith(pi: ExtensionAPI): void {
147
+ export default function registerXtprompt(pi: ExtensionAPI): void {
53
148
  const state = createState();
54
149
 
55
150
  const enhance = async (ctx: ExtensionContext): Promise<void> => {
@@ -57,61 +152,61 @@ export default function mercurySmith(pi: ExtensionAPI): void {
57
152
  ctx.ui.notify(`${EXTENSION} is disabled. Run /${COMMAND} on`, "info");
58
153
  return;
59
154
  }
60
- if (!ctx.hasUI) {
61
- ctx.ui.notify(`${EXTENSION} needs the interactive TUI.`, "error");
155
+ if (!ctx.hasUI || ctx.mode !== "tui") {
156
+ ctx.ui.notify(`${EXTENSION} needs TUI mode.`, "error");
62
157
  return;
63
158
  }
64
- const draft = ctx.ui.getEditorText();
65
- if (!draft.trim()) {
66
- ctx.ui.notify(`${EXTENSION}: editor is empty — nothing to rewrite.`, "info");
159
+
160
+ const originalDraft = ctx.ui.getEditorText();
161
+ if (!originalDraft.trim()) {
162
+ ctx.ui.notify(`${EXTENSION}: editor is empty.`, "info");
67
163
  return;
68
164
  }
165
+
69
166
  const model = ctx.model;
70
167
  if (!model) {
71
168
  ctx.ui.notify(`${EXTENSION}: no active model selected.`, "error");
72
169
  return;
73
170
  }
74
171
 
75
- const intent = detectIntent(draft);
76
- const envContext = await gatherContext(pi).catch(() => "") ?? "";
77
- const systemPrompt = buildSystemPrompt(intent, envContext);
172
+ const clarifiedDraft = await clarifyDraftIfNeeded(ctx, originalDraft);
173
+ if (clarifiedDraft === null) return;
174
+
175
+ const intent = detectIntent(clarifiedDraft);
176
+ const conversationContext = extractConversationContext(
177
+ getSessionEntries(ctx),
178
+ MAX_CONTEXT_ITEMS,
179
+ );
180
+ const request = buildPromptRequest({
181
+ draft: clarifiedDraft,
182
+ intent,
183
+ conversationContext,
184
+ });
78
185
 
79
186
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
80
187
  if (!auth.ok) {
81
- ctx.ui.notify(`${EXTENSION}: cannot resolve API key — ${auth.error}`, "error");
188
+ ctx.ui.notify(`${EXTENSION}: cannot resolve auth — ${auth.error}`, "error");
82
189
  return;
83
190
  }
84
191
 
85
- const userMessage: UserMessage = {
86
- role: "user",
87
- content: draft,
88
- timestamp: Date.now(),
89
- };
90
- const request: Context = {
91
- systemPrompt,
92
- messages: [userMessage],
93
- };
94
-
95
192
  const outcome = await runWithLoader(
96
193
  ctx,
97
194
  `${EXTENSION} rewriting (${intent})…`,
98
195
  async (signal) => {
99
196
  const primary = await callModel(model, request, auth, signal);
100
197
  if (primary === null) return null;
101
- const text = extractText(primary);
102
- const parsed = parseSentinel(text);
198
+ const parsed = parseSentinel(extractText(primary));
103
199
  if (parsed !== undefined) return parsed;
104
- // retry once with a harder format reminder
105
200
  const retried = await callModel(model, retryRequest(request), auth, signal);
106
201
  if (retried === null) return null;
107
- const parsed2 = parseSentinel(extractText(retried));
108
- if (parsed2 !== undefined) return parsed2;
202
+ const retriedParsed = parseSentinel(extractText(retried));
203
+ if (retriedParsed !== undefined) return retriedParsed;
109
204
  throw new Error(
110
- `${EXTENSION}: model did not return the ${SENTINEL_OPEN} block after a retry. Leaving editor unchanged.`,
205
+ `${EXTENSION}: model did not return ${SENTINEL_OPEN} block. Leaving editor unchanged.`,
111
206
  );
112
207
  },
113
- ).catch((err: unknown) => {
114
- ctx.ui.notify(err instanceof Error ? err.message : String(err), "error");
208
+ ).catch((error: unknown) => {
209
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
115
210
  return null;
116
211
  });
117
212
 
@@ -121,12 +216,12 @@ export default function mercurySmith(pi: ExtensionAPI): void {
121
216
  };
122
217
 
123
218
  pi.registerShortcut(DEFAULT_SHORTCUT, {
124
- description: `Rewrite the current editor draft (${EXTENSION})`,
219
+ description: `Rewrite current editor draft (${EXTENSION})`,
125
220
  handler: enhance,
126
221
  });
127
222
 
128
223
  pi.registerCommand(COMMAND, {
129
- description: `${EXTENSION}: rewrite the current editor prompt`,
224
+ description: `${EXTENSION}: rewrite current editor prompt`,
130
225
  handler: async (args, ctx) => {
131
226
  const arg = (args ?? "").trim().toLowerCase();
132
227
  if (arg === "on") {
@@ -151,240 +246,330 @@ export default function mercurySmith(pi: ExtensionAPI): void {
151
246
  });
152
247
  }
153
248
 
154
- /* ------------------------------------------------------------------ intent */
249
+ async function clarifyDraftIfNeeded(
250
+ ctx: ExtensionContext,
251
+ draft: string,
252
+ ): Promise<string | null> {
253
+ if (!isVagueDraft(draft)) return draft;
254
+ const clarification = await requestClarification(ctx, draft);
255
+ if (clarification === null) return null;
256
+ return buildClarifiedDraft(draft, clarification);
257
+ }
155
258
 
156
- interface IntentRule {
157
- intent: Intent;
158
- patterns: RegExp[];
259
+ async function requestClarification(
260
+ ctx: ExtensionContext,
261
+ draft: string,
262
+ ): Promise<string | null> {
263
+ const ui = ctx.ui as InputCapableUi;
264
+ const prompt = `${EXTENSION}: one clarification needed. What exact outcome should rewrite target?`;
265
+ if (typeof ui.input === "function") {
266
+ const result = await ui.input(prompt, draft);
267
+ return normalizeClarification(result);
268
+ }
269
+ ctx.ui.notify(`${EXTENSION}: no input UI available for clarification.`, "error");
270
+ return null;
159
271
  }
160
272
 
161
- const INTENT_RULES: IntentRule[] = [
162
- {
163
- intent: "pane-dispatch",
164
- patterns: [
165
- /\bmultiplex(?:ing)?\b/,
166
- /\bpane\b/,
167
- /\bdispatch(?:ed)?\b/,
168
- /\borchestrat(?:e|or|ion)\b/,
169
- /\bsubordinate\b/,
170
- /\bjudge\b/,
171
- /\bdeploy monitor(?:ing)?\b/,
172
- /\btmux\b/,
173
- /\bsend .*to (?:pane|session|agent)\b/i,
174
- ],
175
- },
176
- {
177
- intent: "specialist",
178
- patterns: [
179
- /\bspecialist\b/,
180
- /\bexecutor\b/,
181
- /\breviewer\b/,
182
- /\bexplorer\b/,
183
- /\bdebugger\b/,
184
- /\bsp (?:run|script|serve|node)\b/,
185
- /\busing-specialists\b/,
186
- /\bquant-methodologist\b/,
187
- /\bquant-researcher\b/,
188
- /\bspecialist chain\b/i,
189
- ],
190
- },
191
- {
192
- intent: "bead",
193
- patterns: [
194
- /\bbead\b/i,
195
- /\bbd (?:create|update|close|show|dep)\b/,
196
- /\b--parent\b/,
197
- /\b--claim\b/,
198
- /\bepic\b/i,
199
- /\bacceptance criteria\b/i,
200
- /\b7-section\b/i,
201
- /\bPROBLEM\b/,
202
- /\bSUCCESS\b/,
203
- /\bNON_GOALS\b/,
204
- /\bVALIDATION\b/,
205
- ],
206
- },
207
- {
208
- intent: "scope-plan",
209
- patterns: [
210
- /\bplanning\b/,
211
- /\bscope(?: out| this)?\b/i,
212
- /\barchitect(?:ure)?\b/,
213
- /\bbreak (?:this )?down\b/i,
214
- /\bdecompos(?:e|ition)\b/,
215
- /\broadmap\b/,
216
- /\bphase structure\b/i,
217
- ],
218
- },
219
- {
220
- intent: "debug",
221
- patterns: [
222
- /\bdebug\b/,
223
- /\bfix\b/,
224
- /\bbug\b/i,
225
- /\bbroken\b/,
226
- /\bfail(?:s|ed|ing)?\b/,
227
- /\bcrash(?:es|ing)?\b/,
228
- /\broot cause\b/i,
229
- /\btraceback\b/,
230
- /\bstack trace\b/i,
231
- /\bregression\b/i,
232
- ],
233
- },
234
- ];
273
+ function normalizeClarification(value: string | null | undefined): string | null {
274
+ if (typeof value !== "string") return null;
275
+ const trimmed = value.trim();
276
+ return trimmed ? trimmed : null;
277
+ }
278
+
279
+ export function buildClarifiedDraft(draft: string, clarification: string): string {
280
+ return [`Original draft: ${draft.trim()}`, `Clarification: ${clarification.trim()}`].join("\n");
281
+ }
282
+
283
+ export function isVagueDraft(draft: string): boolean {
284
+ const trimmed = draft.trim();
285
+ if (!trimmed) return true;
286
+ if (trimmed.length < 16) return true;
287
+ if (!/[\n/:#]/.test(trimmed) && !/\b\d+\b/.test(trimmed) && !/\b[a-z0-9_-]+\.[a-z]{2,}\b/i.test(trimmed)) {
288
+ const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
289
+ if (words.length <= 7) return true;
290
+ if (words.every((word) => VAGUE_WORDS.has(word))) return true;
291
+ }
292
+ return false;
293
+ }
235
294
 
236
- function detectIntent(draft: string): Intent {
237
- const text = draft.toLowerCase();
295
+ export function detectIntent(draft: string): Intent {
238
296
  for (const rule of INTENT_RULES) {
239
- if (rule.patterns.some((p) => p.test(text))) return rule.intent;
297
+ if (rule.patterns.some((pattern) => pattern.test(draft))) return rule.intent;
240
298
  }
241
299
  return "generic";
242
300
  }
243
301
 
244
- /* ----------------------------------------------------------- system prompt */
245
-
246
- const MERCURY_RULES = [
247
- "Target environment: Mercury market-data (futures analytics service stack).",
248
- "1. mmd-api and mmd-mcp-server NEVER compute — they read pre-computed snapshots only; all analytics are produced by the feed services (mmd-snapshot-feed).",
249
- "2. Always parameterize SQL (params={'symbol': symbol}); never f-string user input into queries.",
250
- "3. Modern type hints (dict[str, Any], list[Foo]); imports use the new layout (from analytics... import, from api... import).",
251
- "4. Every analytic family must be wrapped in compute(analytic_family=...); adding one without the wrap is contract drift.",
252
- "5. Default to no comments; add one only when the WHY is non-obvious.",
253
- "6. analytics/ is pure calc — no I/O, no datetime.now() at module scope, no DB access.",
254
- "7. Use bd (beads) for ALL task tracking; create follow-ups with `bd create --parent <id>` so they never get lost.",
255
- "8. Keep work tightly scoped to what is asked; flag unknowns explicitly rather than inventing requirements.",
256
- ].join("\n");
302
+ export function buildPromptRequest(options: PromptRequestOptions): Context {
303
+ const userMessage: UserMessage = {
304
+ role: "user",
305
+ content: options.draft,
306
+ timestamp: Date.now(),
307
+ };
308
+ return {
309
+ systemPrompt: buildSystemPrompt(options),
310
+ messages: [userMessage],
311
+ };
312
+ }
257
313
 
258
- const INTENT_TEMPLATES: Record<Intent, string> = {
259
- "pane-dispatch":
260
- "Rewrite this as a crisp, self-contained instruction for a subordinate agent running in one tmux pane of a multiplexed session. The instruction MUST include: (a) the pane's role and explicitly what it is NOT (helper vs orchestrator), (b) the exact first action, (c) observable success criteria, (d) the escalation/notify rule — when to surface back to the orchestrator and how, (e) the communication protocol in priority order: temp files / structured notes > beads updates > direct notify. Make it dependency-aware: state what this pane must wait for and what it produces for other panes.",
261
- specialist:
262
- "Rewrite this as a specialist dispatch contract suitable for `sp run` / using-specialists-v3. MUST include: the specialist role needed (executor / reviewer / explorer / debugger / quant-methodologist / quant-researcher), a precise task scope (files and symbols), explicit success criteria, the verification the specialist must run, and the OUTPUT the specialist hands back. Scope it to a single chain step.",
263
- bead:
264
- "Rewrite this as a well-formed beads issue description using the 7-section contract, with these exact section headers: ## PROBLEM, ## SUCCESS, ## SCOPE, ## NON_GOALS, ## CONSTRAINTS, ## VALIDATION, ## OUTPUT. Make every section concrete and observable. Include a logging/telemetry note in CONSTRAINTS or VALIDATION where relevant, and at least one smoke/integration check in VALIDATION. No placeholder text. Preserve every concrete detail (bead ids, file paths, symbol names) from the original.",
265
- "scope-plan":
266
- "Rewrite this as a structured plan: distinct phases (P0 scaffold → P1 core → P2 integration), the dependencies between them, what can run in parallel, the top risks, and a short blast-radius summary. Keep it scoped; flag unknowns explicitly.",
267
- debug:
268
- "Rewrite this as a focused debugging prompt: state the symptom precisely, the reproduction/observation steps already taken, the most likely suspect code paths, and what evidence to gather before changing code. Bias toward read-only investigation first (logs, Tempo traces, profiling) before any fix.",
269
- generic:
270
- "Rewrite this prompt to be clearer, more direct, and better structured for a coding agent: explicit goal, constraints, and success criteria up front. Preserve every concrete detail (paths, ids, numbers). Remove vagueness and filler. Do not add requirements not implied by the original.",
271
- };
314
+ function buildSystemPrompt(options: PromptRequestOptions): string {
315
+ const body = isComplexDraft(options.draft) || options.intent === "planning"
316
+ ? buildStructuredPrompt(options)
317
+ : buildSimplePrompt(options);
318
+ return [body, "", BASE_RULES].filter(Boolean).join("\n");
319
+ }
272
320
 
273
- const OUTPUT_CONTRACT = [
274
- `Return ONLY the rewritten prompt wrapped in exactly one sentinel block: ${SENTINEL_OPEN} ... ${SENTINEL_CLOSE}.`,
275
- "No markdown code fences, no commentary, no text before or after the sentinel block.",
276
- "Preserve EVERY concrete detail from the original: file paths, symbol names, bead ids, commands, numbers, constraints.",
277
- "Do not invent new requirements or details that are not implied by the original draft.",
278
- ].join("\n");
321
+ function buildSimplePrompt(options: PromptRequestOptions): string {
322
+ const sections = [
323
+ `You are ${EXTENSION}, standalone prompt rewriter.`,
324
+ `Intent: ${options.intent}.`,
325
+ simpleIntentInstruction(options.intent),
326
+ ];
327
+ if (options.conversationContext) {
328
+ sections.push(`Advisory context:\n${options.conversationContext}`);
329
+ }
330
+ return sections.join("\n\n");
331
+ }
279
332
 
280
- function buildSystemPrompt(intent: Intent, envContext: string): string {
333
+ function buildStructuredPrompt(options: PromptRequestOptions): string {
281
334
  return [
282
- `You are ${EXTENSION}, an intent-aware prompt rewriter for the Mercury/xtrm coding environment.`,
283
- "",
284
- "Detected intent: " + intent,
285
- INTENT_TEMPLATES[intent],
286
- "",
287
- "Hard style rules the rewritten prompt must respect where applicable:",
288
- MERCURY_RULES,
289
- envContext ? "\nActive context (advisory — use only if relevant):\n" + envContext : "",
290
- "OUTPUT CONTRACT:",
291
- OUTPUT_CONTRACT,
335
+ "<role>",
336
+ `${EXTENSION} standalone prompt rewriter`,
337
+ "</role>",
338
+ "<context>",
339
+ escapeXml(options.conversationContext || "No prior chat context."),
340
+ "</context>",
341
+ "<task>",
342
+ structuredIntentInstruction(options.intent),
343
+ "</task>",
344
+ "<constraints>",
345
+ "Preserve current draft as source of truth.",
346
+ "Prior chat may add nuance but cannot replace draft requirements.",
347
+ "Ask rather than invent when key requirement missing.",
348
+ "Keep success criteria, testing, and deliverables explicit.",
349
+ "</constraints>",
350
+ "<instructions>",
351
+ "Keep concrete facts exact.",
352
+ "Use markdown headers when rewrite is planning-shaped.",
353
+ "Prefer concise output when task is simple inside larger draft.",
354
+ "Include 1-2 grounded examples only when they materially clarify output.",
355
+ "</instructions>",
356
+ "<output_format>",
357
+ options.intent === "planning"
358
+ ? [
359
+ "Return Markdown with exact headers:",
360
+ PLANNING_HEADERS,
361
+ ].join("\n")
362
+ : `Return rewritten prompt inside ${SENTINEL_OPEN} block.`,
363
+ "</output_format>",
292
364
  ].join("\n");
293
365
  }
294
366
 
295
- /* -------------------------------------------------------------- context */
367
+ function simpleIntentInstruction(intent: Intent): string {
368
+ switch (intent) {
369
+ case "planning":
370
+ return [
371
+ "Rewrite into planning-ready Markdown with exact headers:",
372
+ PLANNING_HEADERS,
373
+ "Clarify missing facts before planning.",
374
+ "Direct agent to inspect relevant code with GitNexus or Serena before proposing phases.",
375
+ "Structure phases, dependencies, safe parallelism, risks, and blast radius.",
376
+ ].join("\n");
377
+ case "analysis":
378
+ return "Rewrite into analysis prompt that separates evidence, open questions, validation steps, and bounded context before proposing conclusions.";
379
+ case "development":
380
+ return "Rewrite into implementation prompt with explicit success criteria, testing expectations, exact output contract, and 1-2 grounded examples only when useful.";
381
+ case "refactor":
382
+ return "Rewrite into refactor prompt that states current state, preserved behavior, constraints, touched tests, and required validation.";
383
+ case "generic":
384
+ return "Rewrite for clarity, concrete constraints, observable success criteria, and clean separation of context, task, constraints, and output when complexity warrants it.";
385
+ }
386
+ }
296
387
 
297
- async function gatherContext(pi: ExtensionAPI): Promise<string> {
298
- const lines: string[] = [];
299
- const branch = await readExec(pi, "git", ["rev-parse", "--abbrev-ref", "HEAD"]);
300
- if (branch) lines.push("- git branch: " + branch);
301
- const bead = await readExec(pi, "bd", ["list", "--status=in_progress"]);
302
- if (bead) {
303
- const first = bead.split("\n").find((l) => l.trim());
304
- if (first) lines.push("- active bead: " + first.trim().slice(0, 120));
388
+ function structuredIntentInstruction(intent: Intent): string {
389
+ switch (intent) {
390
+ case "planning":
391
+ return [
392
+ "Rewrite into planning-compatible task contract.",
393
+ "Clarify missing facts instead of guessing.",
394
+ "Direct code inspection with GitNexus or Serena for relevant files and execution flow.",
395
+ "Structure phases, dependencies, parallel work, risks, and blast radius.",
396
+ "Produce 7-section bd contract using exact planning headers.",
397
+ "Include telemetry, smoke coverage, E2E checks, and invoke test-planning or handoff when task shape calls for them.",
398
+ ].join("\n");
399
+ case "analysis":
400
+ return [
401
+ "Rewrite into analysis contract biased toward evidence first.",
402
+ "Semantically separate context, task, constraints, and output for complex prompts.",
403
+ "Name evidence, open questions, hypotheses, and validation steps before recommendations.",
404
+ ].join("\n");
405
+ case "development":
406
+ return [
407
+ "Rewrite into development contract for implementation work.",
408
+ "Semantically separate context, task, constraints, and output for complex prompts.",
409
+ "Make success criteria and testing explicit.",
410
+ "Include 1-2 grounded examples only when they materially improve execution.",
411
+ ].join("\n");
412
+ case "refactor":
413
+ return [
414
+ "Rewrite into refactor contract preserving behavior while improving structure.",
415
+ "State current state, preserved behavior, constraints, touched tests, and validation.",
416
+ ].join("\n");
417
+ case "generic":
418
+ return "Rewrite into generic coding-agent prompt with explicit goal, constraints, output, and semantic separation when prompt is complex.";
305
419
  }
306
- return lines.join("\n");
307
420
  }
308
421
 
309
- async function readExec(
310
- pi: ExtensionAPI,
311
- cmd: string,
312
- args: string[],
313
- ): Promise<string | undefined> {
314
- try {
315
- const r = await Promise.race([
316
- pi.exec(cmd, args, { timeout: 4000 }),
317
- new Promise<never>((_, rej) =>
318
- setTimeout(() => rej(new Error("timeout")), 4500),
319
- ),
320
- ]);
321
- const out = ((r.stdout ?? "") + (r.stderr ?? "")).trim();
322
- return out || undefined;
323
- } catch {
324
- return undefined;
422
+ function isComplexDraft(draft: string): boolean {
423
+ return draft.length > 220 || /\n/.test(draft) || /\b(?:constraints|validation|output|scope|problem|success)\b/i.test(draft);
424
+ }
425
+
426
+ function getSessionEntries(ctx: ExtensionContext): readonly SessionEntry[] {
427
+ const manager = ctx.sessionManager as { buildContextEntries?: () => readonly SessionEntry[] } | undefined;
428
+ const entries = manager?.buildContextEntries?.();
429
+ return Array.isArray(entries) ? entries : [];
430
+ }
431
+
432
+ export function extractConversationContext(
433
+ entries: readonly SessionEntry[],
434
+ maxItems: number = MAX_CONTEXT_ITEMS,
435
+ maxChars: number = MAX_CONTEXT_CHARS,
436
+ ): string {
437
+ const lines = entries
438
+ .map(formatConversationEntry)
439
+ .filter((value): value is string => value !== undefined)
440
+ .slice(-maxItems);
441
+ const bounded = boundContextLines(lines, maxChars);
442
+ if (bounded) return bounded;
443
+
444
+ const summary = [...entries]
445
+ .reverse()
446
+ .map((entry) => entry.summary?.trim())
447
+ .find((value) => typeof value === "string" && value.length > 0);
448
+ return summary ? boundContextLines([summary], maxChars) : "";
449
+ }
450
+
451
+ function formatConversationEntry(entry: SessionEntry): string | undefined {
452
+ const role = entry.message?.role;
453
+ if (role !== "user" && role !== "assistant") return undefined;
454
+ const text = extractEntryText(entry.message?.content);
455
+ if (!text) return undefined;
456
+ return `${role}: ${text}`;
457
+ }
458
+
459
+ function boundContextLines(lines: readonly string[], maxChars: number): string {
460
+ const kept: string[] = [];
461
+ let used = 0;
462
+ for (const line of [...lines].reverse()) {
463
+ const nextSize = kept.length === 0 ? line.length : line.length + 1;
464
+ if (kept.length > 0 && used + nextSize > maxChars) break;
465
+ if (kept.length === 0 && line.length > maxChars) {
466
+ return line.slice(0, maxChars).trim();
467
+ }
468
+ kept.unshift(line);
469
+ used += nextSize;
325
470
  }
471
+ return kept.join("\n");
326
472
  }
327
473
 
328
- /* ----------------------------------------------------------- model calls */
474
+ function extractEntryText(content: unknown): string | undefined {
475
+ if (typeof content === "string") return collapseWhitespace(content);
476
+ if (!Array.isArray(content)) return undefined;
477
+ const text = content
478
+ .map((part) => {
479
+ if (!part || typeof part !== "object") return undefined;
480
+ const record = part as { type?: string; text?: string };
481
+ if (record.type !== "text" || typeof record.text !== "string") return undefined;
482
+ return collapseWhitespace(record.text);
483
+ })
484
+ .filter((value): value is string => Boolean(value))
485
+ .join(" ");
486
+ return text || undefined;
487
+ }
329
488
 
330
- interface AuthOk {
331
- apiKey?: string;
332
- headers?: Record<string, string>;
489
+ function collapseWhitespace(text: string): string {
490
+ return text.replace(/\s+/g, " ").trim();
491
+ }
492
+
493
+ function escapeXml(text: string): string {
494
+ let escaped = "";
495
+ for (const character of text) {
496
+ switch (character) {
497
+ case "&":
498
+ escaped += "&amp;";
499
+ break;
500
+ case "<":
501
+ escaped += "&lt;";
502
+ break;
503
+ case ">":
504
+ escaped += "&gt;";
505
+ break;
506
+ case '"':
507
+ escaped += "&quot;";
508
+ break;
509
+ case "'":
510
+ escaped += "&apos;";
511
+ break;
512
+ default:
513
+ escaped += character;
514
+ break;
515
+ }
516
+ }
517
+ return escaped;
333
518
  }
334
519
 
335
520
  async function callModel(
336
521
  model: Model<Api>,
337
522
  request: Context,
338
- auth: AuthOk,
523
+ auth: AuthResolved,
339
524
  signal: AbortSignal,
340
525
  ): Promise<AssistantMessage | null> {
341
- const timeoutCtl = new AbortController();
342
- const t = setTimeout(() => timeoutCtl.abort(), DEFAULT_TIMEOUT_MS);
343
- const sig = AbortSignal.any([signal, timeoutCtl.signal]);
526
+ const timeoutController = new AbortController();
527
+ const timeout = setTimeout(() => timeoutController.abort(), DEFAULT_TIMEOUT_MS);
528
+ const mergedSignal = AbortSignal.any([signal, timeoutController.signal]);
344
529
  try {
345
- const res = await Promise.race<AssistantMessage | null>([
530
+ return await Promise.race<AssistantMessage | null>([
346
531
  complete(model, request, {
347
- ...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
348
- ...(auth.headers ? { headers: auth.headers } : {}),
349
- signal: sig,
532
+ ...auth,
533
+ signal: mergedSignal,
350
534
  maxTokens: Math.min(model.maxTokens, ENHANCER_MAX_OUTPUT_TOKENS),
351
535
  }),
352
536
  abortGuard(signal, null),
353
537
  ]);
354
- return res;
355
538
  } finally {
356
- clearTimeout(t);
539
+ clearTimeout(timeout);
357
540
  }
358
541
  }
359
542
 
360
543
  function retryRequest(request: Context): Context {
361
544
  const last = request.messages.at(-1);
362
- const baseText = last && typeof last.content === "string" ? last.content : "";
363
- const reminder = `\n\nIMPORTANT: reply with exactly one ${SENTINEL_OPEN} block and nothing else (no fences, no commentary).`;
545
+ const currentText = last && typeof last.content === "string" ? last.content : "";
364
546
  const reminded: UserMessage = {
365
547
  role: "user",
366
- content: baseText + reminder,
548
+ content: `${currentText}\n\nIMPORTANT: reply with exactly one ${SENTINEL_OPEN} block and nothing else.`,
367
549
  timestamp: Date.now(),
368
550
  };
369
- return { ...request, messages: [...request.messages.slice(0, -1), reminded] };
551
+ return {
552
+ ...request,
553
+ messages: [...request.messages.slice(0, -1), reminded],
554
+ };
370
555
  }
371
556
 
372
557
  function abortGuard<T>(signal: AbortSignal, value: T): Promise<T> {
373
558
  if (signal.aborted) return Promise.resolve(value);
374
- return new Promise<T>((resolve) =>
375
- signal.addEventListener("abort", () => resolve(value), { once: true }),
376
- );
559
+ return new Promise<T>((resolve) => {
560
+ signal.addEventListener("abort", () => resolve(value), { once: true });
561
+ });
377
562
  }
378
563
 
379
- function extractText(msg: AssistantMessage): string {
380
- return msg.content
381
- .filter((p): p is { type: "text"; text: string } => p.type === "text")
382
- .map((p) => p.text)
564
+ function extractText(message: AssistantMessage): string {
565
+ return message.content
566
+ .filter((part): part is { type: "text"; text: string } => part.type === "text")
567
+ .map((part) => part.text)
383
568
  .join("\n")
384
569
  .trim();
385
570
  }
386
571
 
387
- function parseSentinel(text: string): string | undefined {
572
+ export function parseSentinel(text: string): string | undefined {
388
573
  const start = text.indexOf(SENTINEL_OPEN);
389
574
  const end = text.lastIndexOf(SENTINEL_CLOSE);
390
575
  if (start === -1 || end === -1 || end <= start) return undefined;
@@ -392,27 +577,25 @@ function parseSentinel(text: string): string | undefined {
392
577
  return inner || undefined;
393
578
  }
394
579
 
395
- /* --------------------------------------------------------------- loader */
396
-
397
580
  async function runWithLoader(
398
581
  ctx: ExtensionContext,
399
582
  message: string,
400
583
  task: (signal: AbortSignal) => Promise<string | null>,
401
584
  ): Promise<string | null> {
402
585
  let taskError: Error | undefined;
403
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
586
+ const result = await ctx.ui.custom<string | null>((tui, theme, _keybindings, done) => {
404
587
  const loader = new BorderedLoader(tui, theme, message, { cancellable: true });
405
588
  loader.onAbort = () => done(null);
406
589
  void task(loader.signal)
407
- .then((r) => {
408
- if (!loader.signal.aborted) done(r);
590
+ .then((value) => {
591
+ if (!loader.signal.aborted) done(value);
409
592
  })
410
- .catch((err: unknown) => {
593
+ .catch((error: unknown) => {
411
594
  if (loader.signal.aborted) {
412
595
  done(null);
413
596
  return;
414
597
  }
415
- taskError = err instanceof Error ? err : new Error(`${EXTENSION} failed.`);
598
+ taskError = error instanceof Error ? error : new Error(`${EXTENSION} failed.`);
416
599
  done(null);
417
600
  });
418
601
  return loader;