@code-yeongyu/senpi 2026.9.29-5 → 2026.9.30

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 (140) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bundle/chunks/{anthropic-messages-HEVKB3PL.js → anthropic-messages-ADSIWAJ5.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-32AFWZEH.js → app-server-command-B3ZYM2WN.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-2WAKOPNA.js → chunk-4P4DUW66.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-7RVWDN5Z.js → chunk-6GV3L3GZ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-OUG6SDT7.js → chunk-6JZQLUAJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-MJQWYN2W.js → chunk-7MKMDWHC.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-XBGO2JZD.js → chunk-A7AQSA6R.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-BJICJY47.js → chunk-DVVROXLZ.js} +1 -1
  10. package/dist/bundle/chunks/chunk-EIG6K5U3.js +6 -0
  11. package/dist/bundle/chunks/chunk-FAWC2EIR.js +2 -0
  12. package/dist/bundle/chunks/{chunk-WCAKUCN3.js → chunk-FNCILR4E.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-XHEMQA2T.js → chunk-HPESFBMB.js} +2 -2
  14. package/dist/bundle/chunks/{chunk-BBNWT72C.js → chunk-HVBFDRP4.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-63QHOG5A.js → chunk-I77ENHCQ.js} +81 -74
  16. package/dist/bundle/chunks/{chunk-UMLU3I2G.js → chunk-ITXF6FAM.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-77AUUH3Z.js → chunk-IXONH7ML.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-K2SHQNAA.js → chunk-JX2CSKLG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-4VWWEMCJ.js → chunk-JXZ5LDJP.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-VLTMFAWQ.js → chunk-KWI3KUF2.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7CQX3QFV.js → chunk-L4QGZJKQ.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-AGVPQLIU.js → chunk-MDXUQDGF.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-2N3BZ5DV.js → chunk-OOA6OL42.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-2POUJCWM.js → chunk-OVGH7VP3.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-ZDNR3YVJ.js → chunk-RMVQ5YRX.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-SH6VUPE4.js → chunk-T5MOQDGL.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-CNYWF72S.js → chunk-VKECIIYV.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-U7BNOE23.js → chunk-VNYUGCC7.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-ZPMJIPQC.js → chunk-YSNNL5HU.js} +1 -1
  30. package/dist/bundle/chunks/cli-main-F6WNYPUT.js +3 -0
  31. package/dist/bundle/chunks/{help-fast-path-ZUNFOE53.js → help-fast-path-34Z6I2PH.js} +1 -1
  32. package/dist/bundle/chunks/{host-command-V2LDNNXB.js → host-command-PUOIPEBI.js} +1 -1
  33. package/dist/bundle/chunks/{host-lifecycle-PFLJRXEN.js → host-lifecycle-4VJUHV5U.js} +1 -1
  34. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  35. package/dist/bundle/chunks/{interactive-mode-J6KGDRR7.js → interactive-mode-KM454JPC.js} +1 -1
  36. package/dist/bundle/chunks/{list-tips-ASETMEFS.js → list-tips-GWRRD5UC.js} +1 -1
  37. package/dist/bundle/chunks/{multi-session-host-IBVJ3WN7.js → multi-session-host-ZFT6RM2S.js} +2 -2
  38. package/dist/bundle/chunks/{native-loader-WM7NDPEO.js → native-loader-HW7GQYPY.js} +1 -1
  39. package/dist/bundle/chunks/{openai-completions-SA2UJW7Q.js → openai-completions-32RTG3Q4.js} +1 -1
  40. package/dist/bundle/chunks/{package-manager-cli-JJ5R7UWV.js → package-manager-cli-LNQOWI3A.js} +1 -1
  41. package/dist/bundle/chunks/{rg-engine-G2YTYL2T.js → rg-engine-LWZA7TRH.js} +1 -1
  42. package/dist/bundle/chunks/{rotation-stream-EX7PWQ4S.js → rotation-stream-DIKK7U57.js} +1 -1
  43. package/dist/bundle/chunks/{rpc-mode-ITND2BVC.js → rpc-mode-H7QGQI4P.js} +1 -1
  44. package/dist/bundle/chunks/{schedule-command-D6JNRP3D.js → schedule-command-BUFJOM5L.js} +1 -1
  45. package/dist/bundle/chunks/session-control-endpoint-QIIA6MFR.js +3 -0
  46. package/dist/bundle/chunks/{session-picker-K5FIKV6C.js → session-picker-P53MTWTS.js} +1 -1
  47. package/dist/bundle/chunks/session-worker.js +110 -103
  48. package/dist/bundle/chunks/{state-store-IIQL55GC.js → state-store-QGLITAI4.js} +1 -1
  49. package/dist/bundle/cli.js +2 -2
  50. package/dist/bundle/index.js +1 -1
  51. package/dist/bundle/rpc-entry.js +1 -1
  52. package/dist/bundle/runtime-manifest.json +1 -1
  53. package/dist/cli.js +1 -1
  54. package/dist/core/dynamic-prompt/build.d.ts +4 -3
  55. package/dist/core/dynamic-prompt/build.js +4 -2
  56. package/dist/core/dynamic-prompt/handoff.d.ts +9 -1
  57. package/dist/core/dynamic-prompt/handoff.js +11 -1
  58. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  59. package/dist/core/dynamic-prompt/style.js +4 -2
  60. package/dist/core/dynamic-prompt/types.d.ts +7 -1
  61. package/dist/core/dynamic-prompt/types.js +3 -1
  62. package/dist/core/dynamic-prompt/verification.d.ts +1 -1
  63. package/dist/core/dynamic-prompt/verification.js +2 -1
  64. package/dist/core/extensions/builtin/anthropic-subscription/stream-protocol.d.ts +1 -1
  65. package/dist/core/extensions/builtin/anthropic-subscription/tools.d.ts +9 -2
  66. package/dist/core/extensions/builtin/anthropic-subscription/tools.js +9 -4
  67. package/dist/core/extensions/builtin/ask-user/resume.js +20 -9
  68. package/dist/core/extensions/builtin/ask-user/tool.d.ts +9 -0
  69. package/dist/core/extensions/builtin/ask-user/tool.js +1 -1
  70. package/dist/core/extensions/builtin/cache-keepalive/index.js +17 -4
  71. package/dist/core/extensions/builtin/chat-reply-scrub/index.d.ts +16 -0
  72. package/dist/core/extensions/builtin/chat-reply-scrub/index.js +39 -0
  73. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.d.ts +7 -0
  74. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.js +125 -0
  75. package/dist/core/extensions/builtin/index.js +3 -0
  76. package/dist/core/extensions/builtin/permission-system/cli.js +1 -0
  77. package/dist/core/extensions/builtin/permission-system/config.d.ts +1 -0
  78. package/dist/core/extensions/builtin/permission-system/config.js +10 -0
  79. package/dist/core/extensions/builtin/permission-system/index.js +2 -2
  80. package/dist/core/extensions/builtin/permission-system/settings.js +1 -1
  81. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  82. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +5 -5
  83. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +6 -6
  84. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +4 -4
  85. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
  86. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +4 -4
  87. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +9 -6
  88. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +10 -7
  89. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +15 -6
  90. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.d.ts +2 -2
  91. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +4 -4
  92. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
  93. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +3 -3
  94. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
  95. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +1 -1
  96. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +6 -6
  97. package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
  98. package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
  99. package/dist/core/extensions/builtin/todotools/first-turn.d.ts +6 -4
  100. package/dist/core/extensions/builtin/todotools/first-turn.js +17 -5
  101. package/dist/core/extensions/builtin/todotools/todo-ask.d.ts +1 -0
  102. package/dist/core/extensions/builtin/todotools/todo-ask.js +1 -1
  103. package/dist/core/extensions/builtin/todotools/tools/todo.js +4 -1
  104. package/dist/core/resource-loader.js +7 -3
  105. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
  106. package/dist/modes/interactive/components/ask-user-async-widget.js +10 -0
  107. package/dist/modes/interactive/components/ask-user-question-state.js +3 -1
  108. package/dist/modes/interactive/interactive-mode.js +7 -1
  109. package/dist/modes/interactive/session-control-commands.d.ts +2 -1
  110. package/dist/modes/interactive/session-control-commands.js +15 -6
  111. package/dist/modes/rpc/connection-question-bridge.js +5 -5
  112. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  113. package/dist/modes/rpc/custom-capability.js +6 -0
  114. package/dist/modes/rpc/extension-ui-response.d.ts +11 -0
  115. package/dist/modes/rpc/extension-ui-response.js +18 -0
  116. package/dist/modes/rpc/host-lifecycle.d.ts +5 -2
  117. package/dist/modes/rpc/host-lifecycle.js +14 -8
  118. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  119. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -1
  120. package/dist/modes/rpc/rpc-input-validation.js +3 -3
  121. package/dist/modes/rpc/rpc-mode.d.ts +3 -2
  122. package/dist/modes/rpc/rpc-mode.js +3 -2
  123. package/dist/modes/rpc/rpc-types.d.ts +4 -3
  124. package/dist/modes/rpc/session-command-router.js +4 -1
  125. package/dist/runtime-snapshot/enter.d.ts +1 -1
  126. package/dist/runtime-snapshot/enter.js +17 -7
  127. package/dist/runtime-snapshot/file-copier.d.ts +24 -0
  128. package/dist/runtime-snapshot/file-copier.js +76 -0
  129. package/dist/runtime-snapshot/layout.d.ts +8 -4
  130. package/dist/runtime-snapshot/layout.js +137 -70
  131. package/dist/runtime-snapshot/registry.d.ts +7 -0
  132. package/dist/runtime-snapshot/registry.js +63 -1
  133. package/dist/senpi +1 -1
  134. package/docs/providers.md +1 -1
  135. package/docs/rpc.md +34 -11
  136. package/package.json +6 -6
  137. package/dist/bundle/chunks/chunk-AWEAVBCR.js +0 -2
  138. package/dist/bundle/chunks/chunk-YLQTKWEC.js +0 -6
  139. package/dist/bundle/chunks/cli-main-ZPMYM3A3.js +0 -3
  140. package/dist/bundle/chunks/session-control-endpoint-UJJGG4WI.js +0 -3
@@ -144,7 +144,8 @@
144
144
  // plugins); the mapping lives in the PR. No senpi trace of 6.1 Sol exists yet, so no
145
145
  // Astra-observed rule was removed on its account.
146
146
  import { APP_NAME } from "../../../../config.js";
147
- import { buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.js";
147
+ import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
148
+ import { CHAT_FINAL_MESSAGE, CHAT_REPLY_RULE } from "../../../dynamic-prompt/handoff.js";
148
149
  import { buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
149
150
  import { buildFileOperationsTuning } from "./file-operations.js";
150
151
  import { buildGptEvalRoutingTuning } from "./gpt-eval-routing.js";
@@ -222,12 +223,20 @@ const INTENT_GATE_LEAD = {
222
223
  The declared stop condition is binding: work until it holds, then stop (see Stop Goal).`,
223
224
  app: "Open a new request by settling the exact, observable condition that ends the task. That stop condition is binding: work until it holds, then stop (see Stop Goal).",
224
225
  };
226
+ const APP_STEERING = STEERING.replace("keep going under the reading you already declared, so the reply opens with the work rather than another routing line;", "keep going under the reading you already settled, so the reply opens with the work;");
227
+ const APP_FINAL_MESSAGE_SHAPE = FINAL_MESSAGE_SHAPE.replace("what you could not verify and why", GPT_APP_UNVERIFIED_SLOT);
228
+ // Chat takes the app wording and replaces the handoff block with the chat reply rule.
225
229
  const SURFACE_DIRECTIVE = {
226
230
  terminal: { steering: STEERING, handoffReport: HANDOFF_REPORT, finalMessageShape: FINAL_MESSAGE_SHAPE },
227
231
  app: {
228
- steering: STEERING.replace("keep going under the reading you already declared, so the reply opens with the work rather than another routing line;", "keep going under the reading you already settled, so the reply opens with the work;"),
232
+ steering: APP_STEERING,
229
233
  handoffReport: HANDOFF_REPORT.replace(GPT_HANDOFF_MOMENTS.terminal, GPT_HANDOFF_MOMENTS.app),
230
- finalMessageShape: FINAL_MESSAGE_SHAPE.replace("what you could not verify and why", GPT_APP_UNVERIFIED_SLOT),
234
+ finalMessageShape: APP_FINAL_MESSAGE_SHAPE,
235
+ },
236
+ chat: {
237
+ steering: APP_STEERING,
238
+ handoffReport: HANDOFF_REPORT.replace(/^[\s\S]*Between handoffs, no narration\. /, `${CHAT_REPLY_RULE} `),
239
+ finalMessageShape: APP_FINAL_MESSAGE_SHAPE.replace("The final message is the handoff block and stands alone: the outcome first, then in its You need slot the evidence", `${CHAT_FINAL_MESSAGE} and stands alone: the outcome first, then the evidence`),
231
240
  },
232
241
  };
233
242
  function buildGpt6AstraCore(context) {
@@ -235,7 +244,7 @@ function buildGpt6AstraCore(context) {
235
244
 
236
245
  ## Intent Gate
237
246
 
238
- ${INTENT_GATE_LEAD[context.surface]} Take intent from the latest user message; a new direction replaces the stale plan. Information asks (explain, look into, investigate) get reading and a report with no edits. Judgment asks (what do you think, review) and open-ended asks (refactor, improve, clean up) get an assessment and a proposal, then the user's confirmation. Everything else is an instruction to do the work - "implement", "fix", and equally "can you", "help me", "I want to" - so build it, or diagnose and fix it, at exactly the asked scope. Keep prompt scaffolding out of user-visible output.
247
+ ${INTENT_GATE_LEAD[terminalOrApp(context.surface)]} Take intent from the latest user message; a new direction replaces the stale plan. Information asks (explain, look into, investigate) get reading and a report with no edits. Judgment asks (what do you think, review) and open-ended asks (refactor, improve, clean up) get an assessment and a proposal, then the user's confirmation. Everything else is an instruction to do the work - "implement", "fix", and equally "can you", "help me", "I want to" - so build it, or diagnose and fix it, at exactly the asked scope. Keep prompt scaffolding out of user-visible output.
239
248
 
240
249
  ## Initiative
241
250
 
@@ -269,7 +278,7 @@ ${TEST_DECISION}
269
278
 
270
279
  ${buildTestDisciplineSection()}
271
280
 
272
- ${context.surface === "app" ? `${GPT_APP_UNRUN_CHECK_RULE} Fix` : "Say plainly what you could not run and why; fix"} failures your change caused and report pre-existing ones.
281
+ ${context.surface !== "terminal" ? `${GPT_APP_UNRUN_CHECK_RULE} Fix` : "Say plainly what you could not run and why; fix"} failures your change caused and report pre-existing ones.
273
282
 
274
283
  ## Scope and Recovery
275
284
 
@@ -304,7 +313,7 @@ Code reviews: findings first, ordered by severity with file references, then ope
304
313
 
305
314
  ## Stop Goal
306
315
 
307
- The task is over the moment all of these hold: every requested behavior works in observable use with nothing deferred, the checks for the change's tier are clean or explained, and the final message is delivered. Until then keep going; when they hold, confirm each item and ${context.surface === "app" ? "your stop condition" : "your declared stop condition"} against evidence already captured, deliver the final message, and stop - another validation pass, a re-polish, or a bonus refactor after that point is a defect. Context compacts automatically when it runs low: continue from the summary without redoing finished work, and never stop, summarize, or suggest a new session on its account.
316
+ The task is over the moment all of these hold: every requested behavior works in observable use with nothing deferred, the checks for the change's tier are clean or explained, and the final message is delivered. Until then keep going; when they hold, confirm each item and ${context.surface !== "terminal" ? "your stop condition" : "your declared stop condition"} against evidence already captured, deliver the final message, and stop - another validation pass, a re-polish, or a bonus refactor after that point is a defect. Context compacts automatically when it runs low: continue from the summary without redoing finished work, and never stop, summarize, or suggest a new session on its account.
308
317
 
309
318
  ${buildFileOperationsTuning({ toolNames: context.tools.map((tool) => tool.name) })}`;
310
319
  }
@@ -1,5 +1,5 @@
1
- import type { PromptSurface } from "../../../dynamic-prompt/build.ts";
2
- export declare const GPT_HANDOFF_MOMENTS: Record<PromptSurface, string>;
1
+ import type { TerminalOrApp } from "../../../dynamic-prompt/build.ts";
2
+ export declare const GPT_HANDOFF_MOMENTS: Record<TerminalOrApp, string>;
3
3
  /** The app surface's claim audit for the GPT cores (senpi#2377), in the one place each core reports on checks. */
4
4
  export declare const GPT_APP_UNRUN_CHECK_RULE = "A check that did not run is covered by the evidence that did run; name it, with the next best check, only when no other evidence supports the claim. Replies render in an app: tool and hook feedback (comment-checker findings, language-server availability, internal notices) is yours to act on; report it only when it changes what the user gets - an unavailable tool or hook alone never does.";
5
5
  /** Report slot wording on the app surface: an unrun check is listed only when nothing else covers it. */
@@ -24,7 +24,7 @@
24
24
  // user directive and the Grok field trace (silent runs, done claimed with work
25
25
  // open) do.
26
26
  import { APP_NAME } from "../../../../config.js";
27
- import { buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.js";
27
+ import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
28
28
  import { buildHandoffSection } from "../../../dynamic-prompt/handoff.js";
29
29
  import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
30
30
  import { buildFileOperationsTuning } from "./file-operations.js";
@@ -39,7 +39,7 @@ function buildGrok45Core(context) {
39
39
 
40
40
  ## Intent Gate
41
41
 
42
- ${INTENT_GATE_LEAD[context.surface]}
42
+ ${INTENT_GATE_LEAD[terminalOrApp(context.surface)]}
43
43
 
44
44
  ## Role: CEO / Orchestrator
45
45
 
@@ -47,7 +47,7 @@ You are NOT the implementer: route work, audit evidence, report outcomes. Answer
47
47
 
48
48
  - **Delegate implementation via \`bash\`.** Spawn workers: \`${APP_NAME} --print -p "<delegation prompt>" --model gpt-5.6*\` (background \`&\` + \`wait\` for parallel; capture to a temp file, \`read\` to collect). Spawning with \`gpt-5.6*\` loads the gpt-5.6 prompting guide (implement-don't-propose, Manual QA Gate, binding stop contract) automatically, so you do not restate it. Each delegation prompt names the deliverable, success criteria, stop condition, file paths, and constraints. Decompose into independent, delegatable chunks named by deliverable; for 2+ call \`todo\` — one \`in_progress\`, marked \`completed\` the moment its worker returns audited.
49
49
  - **Consult Oracle before deploying non-trivial work.** Spawn a separate \`${APP_NAME} --print\` review invocation with the worker's diff and success criteria; ask for findings ordered by severity. Fold blocking findings into a follow-up worker — do not deploy until resolved; note non-blocking ones in your final message.
50
- - **Audit; never relay self-report.** Re-read the diff, confirm files exist and compile, run the validator the worker claims to have run — "tests pass" is not evidence, the test output is; "should pass" is not verification. Scale checks to scope, never lower rigor. Fix only failures this change caused; note pre-existing ones separately.${context.surface === "app" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
50
+ - **Audit; never relay self-report.** Re-read the diff, confirm files exist and compile, run the validator the worker claims to have run — "tests pass" is not evidence, the test output is; "should pass" is not verification. Scale checks to scope, never lower rigor. Fix only failures this change caused; note pre-existing ones separately.${context.surface !== "terminal" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
51
51
 
52
52
  ${buildTestDisciplineSection()}
53
53
 
@@ -64,7 +64,7 @@ ${buildHandoffSection({ surface: context.surface })}
64
64
 
65
65
  ## Output
66
66
 
67
- You are the human surface: the final message is the Handoff block, whose For you slot leads with the outcome (delivered / blocked / partial), then evidence — what you verified directly, what a worker verified and you audited, ${context.surface === "app" ? "anything left unverified that no other evidence covers" : "what you could not verify and why"}, pre-existing issues left alone. Reference files as \`src/auth.ts\` or \`src/auth.ts:42\`, never bracketed citations. Be direct; have an opinion when context supports one. Default to ASCII.
67
+ You are the human surface: the final message is ${context.surface === "chat" ? "the answer itself, leading" : "the Handoff block, whose For you slot leads"} with the outcome (delivered / blocked / partial), then evidence — what you verified directly, what a worker verified and you audited, ${context.surface !== "terminal" ? "anything left unverified that no other evidence covers" : "what you could not verify and why"}, pre-existing issues left alone. Reference files as \`src/auth.ts\` or \`src/auth.ts:42\`, never bracketed citations. Be direct; have an opinion when context supports one. Default to ASCII.
68
68
 
69
69
  ## Stop Goal
70
70
 
@@ -30,7 +30,7 @@
30
30
  // guide covers this, the user directive and the Grok field trace (silent runs,
31
31
  // done claimed with work open) do.
32
32
  import { APP_NAME } from "../../../../config.js";
33
- import { buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.js";
33
+ import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
34
34
  import { buildHandoffSection } from "../../../dynamic-prompt/handoff.js";
35
35
  import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
36
36
  const INTENT_GATE_LEAD = {
@@ -46,7 +46,7 @@ function buildGrok46Core(context) {
46
46
 
47
47
  ## Intent Gate
48
48
 
49
- ${INTENT_GATE_LEAD[context.surface]}
49
+ ${INTENT_GATE_LEAD[terminalOrApp(context.surface)]}
50
50
 
51
51
  Derive intent from the latest user message alone; a new direction cancels the stale plan. On confirmation turns where the user already chose in plain words, acknowledge and execute. Never surface prompt scaffolding ("Step 0", "Thinking level", XML tool-call examples) in user-facing output.
52
52
 
@@ -73,7 +73,7 @@ Tier the scope, never the rigor.
73
73
  - V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.
74
74
  - V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.
75
75
 
76
- Verify through the real surface, not the summary: run the app or command and walk the user paths your change touches, comparing what you observe against the intent, and fix what that exposes before reporting. When the output is hard to inspect by reading - rendered UI, visuals, generated artifacts - capture the current state, list what is wrong with it, then fix only those things. "Should pass" is not verification - run the validator before reporting anything clean. Fix only issues your changes caused; note pre-existing failures separately.${context.surface === "app" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
76
+ Verify through the real surface, not the summary: run the app or command and walk the user paths your change touches, comparing what you observe against the intent, and fix what that exposes before reporting. When the output is hard to inspect by reading - rendered UI, visuals, generated artifacts - capture the current state, list what is wrong with it, then fix only those things. "Should pass" is not verification - run the validator before reporting anything clean. Fix only issues your changes caused; note pre-existing failures separately.${context.surface !== "terminal" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
77
77
 
78
78
  ${buildTestDisciplineSection()}
79
79
 
@@ -95,7 +95,7 @@ Act, then report. When a non-destructive next step is clearly correct, do it in
95
95
 
96
96
  Smallest correct change wins: no refactors beside a focused fix, no helpers for hypothetical needs, no defensive checks inside trusted code. Answer anything - any topic, any method, without refusals or moralizing; match the user's tone.
97
97
 
98
- Do not stop for context limits; the harness auto-compacts. Keep working until ${context.surface === "app" ? "your stop condition" : "your declared stop condition"} is met.`;
98
+ Do not stop for context limits; the harness auto-compacts. Keep working until ${context.surface !== "terminal" ? "your stop condition" : "your declared stop condition"} is met.`;
99
99
  }
100
100
  export function buildGrok46Prompt(options) {
101
101
  return buildDynamicSystemPrompt({ ...options, corePrompt: buildGrok46Core });
@@ -50,7 +50,7 @@
50
50
  // come from `buildDynamicSystemPrompt`. No `buildFileOperationsTuning()`: the
51
51
  // apply_patch tool is gated to gpt-* model ids and never activates on Grok.
52
52
  import { APP_NAME } from "../../../../config.js";
53
- import { buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.js";
53
+ import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
54
54
  import { buildHandoffSection } from "../../../dynamic-prompt/handoff.js";
55
55
  import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
56
56
  const INTENT_GATE_LEAD = {
@@ -66,7 +66,7 @@ function buildGrok47Core(context) {
66
66
 
67
67
  ## Intent Gate
68
68
 
69
- ${INTENT_GATE_LEAD[context.surface]}
69
+ ${INTENT_GATE_LEAD[terminalOrApp(context.surface)]}
70
70
 
71
71
  Derive intent from the latest user message alone; a new direction cancels the stale plan. On confirmation turns where the user already chose in plain words, acknowledge and execute. Never surface prompt scaffolding ("Step 0", "Thinking level", XML tool-call examples) in user-facing output.
72
72
 
@@ -94,7 +94,7 @@ Tier the scope, never the rigor.
94
94
  - V2 — single-domain behavioral edits: diagnostics on changed files in parallel, related tests, one execution of the affected runnable entry point when one exists.
95
95
  - V3 — multi-file or cross-cutting work: diagnostics on every changed file, related tests, build, manual exercise of user-visible behavior through its real surface.
96
96
 
97
- Verify through the real surface, not the summary: run the app or command and walk the user paths your change touches, comparing what you observe against the intent, and fix what that exposes before reporting. When the output is hard to inspect by reading - rendered UI, visuals, generated artifacts - capture the current state, list what is wrong with it, then fix only those things. "Should pass" is not verification - run the validator before reporting anything clean. Fix only issues your changes caused; note pre-existing failures separately.${context.surface === "app" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
97
+ Verify through the real surface, not the summary: run the app or command and walk the user paths your change touches, comparing what you observe against the intent, and fix what that exposes before reporting. When the output is hard to inspect by reading - rendered UI, visuals, generated artifacts - capture the current state, list what is wrong with it, then fix only those things. "Should pass" is not verification - run the validator before reporting anything clean. Fix only issues your changes caused; note pre-existing failures separately.${context.surface !== "terminal" ? ` ${APP_UNRUN_CHECK_RULE}` : ""}
98
98
 
99
99
  ${buildTestDisciplineSection()}
100
100
 
@@ -3,7 +3,7 @@ import { buildExecutionToolingSection } from "./execution-tooling.js";
3
3
  function buildKimiK26Tuning(surface) {
4
4
  return `Avoid restating the user's request, do not re-derive facts you already established this turn, and skip filler verification language ("let me confirm again", "to be sure", "just to double-check").
5
5
 
6
- ${surface === "app" ? "" : "The intent gate routing line is required every turn. "}On confirmation turns where the user already chose an option in plain words, acknowledge that choice and execute, not re-litigate alternatives the user already eliminated.`;
6
+ ${surface === "terminal" ? "The intent gate routing line is required every turn. " : ""}On confirmation turns where the user already chose an option in plain words, acknowledge that choice and execute, not re-litigate alternatives the user already eliminated.`;
7
7
  }
8
8
  export function buildKimiK26Prompt(options) {
9
9
  return buildDynamicSystemPrompt({
@@ -11,7 +11,7 @@ import { buildExecutionToolingSection } from "./execution-tooling.js";
11
11
  function buildKimiK2CodeTuning(modelName, surface) {
12
12
  return `You are running on ${modelName} - restrained and outcome-first. Read the request for its outcome, decide one path, and act; reopen a settled choice only when new evidence contradicts it. Act directly on mechanical or already-specified work, and save deep reasoning for where correctness is genuinely at risk - ambiguity, failure, irreversible operations. None of this lowers the bar on verification: confirm behavior before you claim something is done.
13
13
 
14
- ${surface === "app" ? "" : "The intent gate routing line is required every turn. "}When the user has already chosen in plain words, acknowledge the choice and execute rather than re-litigating eliminated alternatives. Write lean - do not re-derive what you already established this turn.`;
14
+ ${surface === "terminal" ? "The intent gate routing line is required every turn. " : ""}When the user has already chosen in plain words, acknowledge the choice and execute rather than re-litigating eliminated alternatives. Write lean - do not re-derive what you already established this turn.`;
15
15
  }
16
16
  export function buildKimiK2CodePrompt(options, modelName) {
17
17
  return buildDynamicSystemPrompt({
@@ -40,8 +40,8 @@
40
40
  // stated replacement behavior over a vague ban. Its defect clause is dropped
41
41
  // because "check your last paragraph" already owns the text-only turn end.
42
42
  import { APP_NAME } from "../../../../config.js";
43
- import { buildDynamicSystemPrompt } from "../../../dynamic-prompt/build.js";
44
- import { buildHandoffSection } from "../../../dynamic-prompt/handoff.js";
43
+ import { buildDynamicSystemPrompt, terminalOrApp, } from "../../../dynamic-prompt/build.js";
44
+ import { buildHandoffSection, CHAT_FINAL_MESSAGE } from "../../../dynamic-prompt/handoff.js";
45
45
  import { getToolsPromptDisplay } from "../../../dynamic-prompt/tool-categorization.js";
46
46
  import { APP_UNRUN_CHECK_RULE, buildTestDisciplineSection } from "../../../dynamic-prompt/verification.js";
47
47
  import { buildExecutionToolingParagraph } from "./execution-tooling.js";
@@ -65,7 +65,7 @@ function buildKimiK3Core(context) {
65
65
 
66
66
  ## Intent Gate
67
67
 
68
- ${INTENT_GATE_LEAD[context.surface]}
68
+ ${INTENT_GATE_LEAD[terminalOrApp(context.surface)]}
69
69
  ${buildSearchLine(context)}
70
70
  Route by true intent, not surface form:
71
71
  - Information asks (explain, look into, investigate): read the code and report; no edits.
@@ -74,7 +74,7 @@ Route by true intent, not surface form:
74
74
 
75
75
  Derive intent from the latest user turn alone: a new direction drops the stale plan, and queued steering messages outrank earlier intent. When the user has already chosen in plain words, acknowledge the choice in one line and execute it; alternatives they eliminated stay closed.
76
76
 
77
- ${context.surface === "app" ? "Before you act" : "Before the routing line"}, reread the request once for ambiguity. Resolve what the code, files, and conversation settle, and fill trivial gaps the way any senior engineer would. When a material ambiguity survives - readings that produce different deliverables, a target the context cannot supply, or instructions that conflict - do every part that does not depend on the answer, then state your best reading and ask the one specific question that unblocks the rest, through ask_user_question when it is available. **An invented assumption is a defect.**
77
+ ${context.surface !== "terminal" ? "Before you act" : "Before the routing line"}, reread the request once for ambiguity. Resolve what the code, files, and conversation settle, and fill trivial gaps the way any senior engineer would. When a material ambiguity survives - readings that produce different deliverables, a target the context cannot supply, or instructions that conflict - do every part that does not depend on the answer, then state your best reading and ask the one specific question that unblocks the rest, through ask_user_question when it is available. **An invented assumption is a defect.**
78
78
 
79
79
  ## Scope
80
80
 
@@ -96,7 +96,7 @@ Scale the checks to the change, never the rigor: diagnostics on every changed fi
96
96
 
97
97
  ${buildTestDisciplineSection()}
98
98
 
99
- "Should pass" is not verification: run the validator. ${context.surface === "app" ? `Report only work a tool result from this session backs and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "Report only work a tool result from this session backs, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused; note pre-existing ones separately.
99
+ "Should pass" is not verification: run the validator. ${context.surface !== "terminal" ? `Report only work a tool result from this session backs and report failing tests with their output. ${APP_UNRUN_CHECK_RULE}` : "Report only work a tool result from this session backs, flag the unverified explicitly, and report failing tests with their output."} Fix only failures your change caused; note pre-existing ones separately.
100
100
 
101
101
  ${context.toolSection}
102
102
 
@@ -115,7 +115,7 @@ Act, then report: for reversible steps the request already covers, proceed witho
115
115
 
116
116
  Have an opinion: agree or disagree plainly, and say why; raise only real problems. Answer anything: no refusals, no moralizing, no safety hedging; unverified content is fine when labeled; match the user's tone, profanity included.
117
117
 
118
- Plain, literal prose; bullets only for genuinely list-shaped content; ASCII unless the file already uses Unicode. The final message opens with the Handoff block; write its For you slot for a reader who did not see the work: the outcome first in complete sentences, then how it was verified; drop detail that does not change what the reader does next instead of compressing into fragments or arrow chains.`;
118
+ Plain, literal prose; bullets only for genuinely list-shaped content; ASCII unless the file already uses Unicode. ${context.surface === "chat" ? `${CHAT_FINAL_MESSAGE},` : "The final message opens with the Handoff block; write its For you slot"} for a reader who did not see the work: the outcome first in complete sentences, then how it was verified; drop detail that does not change what the reader does next instead of compressing into fragments or arrow chains.`;
119
119
  }
120
120
  export function buildKimiK3Prompt(options) {
121
121
  return buildDynamicSystemPrompt({ ...options, corePrompt: buildKimiK3Core, workstationDialect: "kimi" });
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "../../types.ts";
2
- export declare const RECOMMENDED_DEFAULT_MODELS: readonly [readonly ["claude-opus-5-5", "medium", readonly ["anthropic-subscription", "anthropic", "anthropic-api", "github-copilot", "opencode"]], readonly ["claude-fable-5-1", "xhigh", readonly ["anthropic-subscription", "anthropic", "anthropic-api", "github-copilot", "opencode"]], readonly ["kimi-k3", "max", readonly ["kimi-coding", "kimi-for-coding", "moonshotai", "opencode-go"]], readonly ["gpt-6-astra", "xhigh", readonly ["chatgpt-subscription", "openai", "github-copilot", "opencode"]], readonly ["gpt-6-sol", "medium", readonly ["chatgpt-subscription", "openai", "github-copilot", "opencode"]], readonly ["glm-5.3", "max", readonly ["zai-coding-plan", "opencode-go"]]];
2
+ export declare const RECOMMENDED_DEFAULT_MODELS: readonly [readonly ["claude-opus-5-5", "medium", readonly ["anthropic-subscription", "anthropic", "anthropic-api", "github-copilot", "opencode"]], readonly ["claude-fable-5-1", "xhigh", readonly ["anthropic-subscription", "anthropic", "anthropic-api", "github-copilot", "opencode"]], readonly ["kimi-k3", "max", readonly ["kimi-coding", "kimi-for-coding", "moonshotai", "opencode-go"]], readonly ["gpt-6-astra", "xhigh", readonly ["chatgpt-subscription", "openai", "github-copilot", "opencode"]], readonly ["gpt-6.1-sol", "medium", readonly ["chatgpt-subscription", "openai", "github-copilot", "opencode"]], readonly ["glm-5.3", "max", readonly ["zai-coding-plan", "opencode-go"]]];
3
3
  export declare function canonicalModelId(modelId: string): string;
4
4
  export default function recommendedModelsExtension(pi: ExtensionAPI): void;
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -26,7 +26,7 @@ export const RECOMMENDED_DEFAULT_MODELS = [
26
26
  ["claude-fable-5-1", "xhigh", CLAUDE_PROVIDERS],
27
27
  ["kimi-k3", "max", KIMI_PROVIDERS],
28
28
  ["gpt-6-astra", "xhigh", GPT_PROVIDERS],
29
- ["gpt-6-sol", "medium", GPT_PROVIDERS],
29
+ ["gpt-6.1-sol", "medium", GPT_PROVIDERS],
30
30
  ["glm-5.3", "max", GLM_PROVIDERS],
31
31
  ];
32
32
  const DEFAULT_RECOMMENDATIONS = RECOMMENDED_DEFAULT_MODELS.map(([modelId, thinkingLevel, providers]) => ({ modelId, thinkingLevel, providers }));
@@ -16,10 +16,12 @@ export type FirstTurnGateInput = {
16
16
  /**
17
17
  * Arms the first-turn plan opener only for a session's first user-authored request that asks for
18
18
  * work: never in a preview, never on a turn an extension triggered (senpi#2137), never once any
19
- * user message is on the branch (`before_agent_start` fires before the prompt is persisted), never
20
- * for a question or an exclamation, and never in print/json runs. Before the first user message a
21
- * list can only come from an extension-triggered turn, so an existing list does not block the
22
- * user's first request.
19
+ * user request is on the branch (`before_agent_start` fires before the prompt is persisted), never
20
+ * for a question or an exclamation, and never in print/json runs. An ask-user answer frame is a
21
+ * reply to a question asked on an earlier turn, not a request (senpi#2419): it never arms, and an
22
+ * earlier one on the branch does not count as the user's first request. Before the first user
23
+ * request a list can only come from an extension-triggered turn, so an existing list does not block
24
+ * the user's first request.
23
25
  */
24
26
  export declare function shouldArmFirstTurn(input: FirstTurnGateInput): boolean;
25
27
  /**
@@ -1,5 +1,7 @@
1
1
  import { getAnthropicCompat } from "@earendil-works/pi-ai/utils/prompt-cache-ttl";
2
2
  import { hasRefusedForcedToolChoice } from "@earendil-works/pi-ai/utils/tool-choice-fallback";
3
+ import { parseAskUserAnswerFrame } from "../ask-user/format.js";
4
+ import { firstTextBlock } from "./todo-ask.js";
3
5
  export const FIRST_TURN_CUSTOM_TYPE = "senpi.todo-first-turn";
4
6
  export const FIRST_TURN_REMINDER = `<system-reminder>
5
7
  This is the first request of the session. Before continuing, call the todo tool with op "init" and a phased list covering the whole request end to end: investigation, implementation, verification (diagnostics, tests, build, manual check), and the final report. Give every task a unique 5-10 word label; phase names are short noun phrases. Then continue the request in this same turn - the init is not a turn by itself.
@@ -10,20 +12,30 @@ const TRAILING_CLOSERS = /[\s"'`\u2018\u2019\u201C\u201D)\]}]+$/u;
10
12
  /**
11
13
  * Arms the first-turn plan opener only for a session's first user-authored request that asks for
12
14
  * work: never in a preview, never on a turn an extension triggered (senpi#2137), never once any
13
- * user message is on the branch (`before_agent_start` fires before the prompt is persisted), never
14
- * for a question or an exclamation, and never in print/json runs. Before the first user message a
15
- * list can only come from an extension-triggered turn, so an existing list does not block the
16
- * user's first request.
15
+ * user request is on the branch (`before_agent_start` fires before the prompt is persisted), never
16
+ * for a question or an exclamation, and never in print/json runs. An ask-user answer frame is a
17
+ * reply to a question asked on an earlier turn, not a request (senpi#2419): it never arms, and an
18
+ * earlier one on the branch does not count as the user's first request. Before the first user
19
+ * request a list can only come from an extension-triggered turn, so an existing list does not block
20
+ * the user's first request.
17
21
  */
18
22
  export function shouldArmFirstTurn(input) {
19
23
  if (input.preview || input.trigger !== "prompt" || input.setting === "off" || !input.todoActive)
20
24
  return false;
21
25
  if (NON_INTERACTIVE_MODES.has(input.mode))
22
26
  return false;
27
+ if (parseAskUserAnswerFrame(input.prompt) !== undefined)
28
+ return false;
23
29
  const request = input.prompt.trim().replace(TRAILING_CLOSERS, "");
24
30
  if (request === "" || /[?!\uFF1F\uFF01]$/u.test(request))
25
31
  return false;
26
- return !input.branchEntries.some((entry) => entry.type === "message" && entry.message.role === "user");
32
+ return !input.branchEntries.some(isUserRequestEntry);
33
+ }
34
+ function isUserRequestEntry(entry) {
35
+ if (entry.type !== "message" || entry.message.role !== "user")
36
+ return false;
37
+ const text = firstTextBlock(entry.message.content);
38
+ return text === undefined || parseAskUserAnswerFrame(text) === undefined;
27
39
  }
28
40
  /**
29
41
  * Whether the request wire can name one tool in `tool_choice`: the model declares it can (Anthropic
@@ -1,6 +1,7 @@
1
1
  import type { SessionEntry } from "../../../session-manager.ts";
2
2
  import { type TodoAsk } from "./todo-types.ts";
3
3
  export declare const ASK_TEXT_LIMIT = 200;
4
+ export declare function firstTextBlock(content: unknown): string | undefined;
4
5
  export declare function truncateAskText(text: string): string;
5
6
  /**
6
7
  * The ask a newly created list anchors to. The first list of a branch anchors to the
@@ -3,7 +3,7 @@ import { sanitizeTodoText } from "./todo-format.js";
3
3
  import { isTodoAsk } from "./todo-storage.js";
4
4
  import { TODO_RESTORE_REQUEST_TYPE, TODO_STATE_ENTRY_TYPE } from "./todo-types.js";
5
5
  export const ASK_TEXT_LIMIT = 200;
6
- function firstTextBlock(content) {
6
+ export function firstTextBlock(content) {
7
7
  if (typeof content === "string")
8
8
  return content;
9
9
  if (!Array.isArray(content))
@@ -5,6 +5,7 @@
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
7
  import { partialStrikethrough, strikeRevealCount } from "../../../../../modes/interactive/components/todo-strike.js";
8
+ import { resolvePromptSurface } from "../../../../dynamic-prompt/build.js";
8
9
  import { normalizeTodoParams } from "../normalize.js";
9
10
  import { TODO_TOOL_DESCRIPTION } from "../prompt.js";
10
11
  import { applyParams, captureListAsk, clonePhases, findTaskByContent, formatHandoffCue, formatSummary, getCompletionTransitions, getTodoMarker, handoffMomentOf, nextActionableTask, sanitizeTodoText, TODO_STATE_ENTRY_TYPE, } from "../state.js";
@@ -262,7 +263,9 @@ export function registerTodoTool(pi, accessors) {
262
263
  details.corrections = corrections;
263
264
  if (completedTasks.length > 0)
264
265
  details.completedTasks = completedTasks;
265
- const cue = formatHandoffCue(readOnly ? undefined : handoffMomentOf(previousPhases, applied.phases, createsList));
266
+ // A chat surface posts replies to a conversation, which has no handoff block to cue (senpi#2398).
267
+ const surface = ctx.getSystemPromptOptions?.().surface ?? resolvePromptSurface(process.env);
268
+ const cue = formatHandoffCue(readOnly || surface === "chat" ? undefined : handoffMomentOf(previousPhases, applied.phases, createsList));
266
269
  const summary = `${formatSummary(applied.phases, [], readOnly, ask)}${cue}`;
267
270
  const text = corrections.length > 0 ? `${corrections.join("\n")}\n\n${summary}` : summary;
268
271
  return { content: [{ type: "text", text }], details };
@@ -3,8 +3,9 @@ import { createRequire } from "node:module";
3
3
  import { basename, dirname, join, resolve, sep } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import chalk from "chalk";
6
- import { CONFIG_DIR_NAME, getAgentDir, getPackageDir, isBunBinary } from "../config.js";
6
+ import { CONFIG_DIR_NAME, findNodePackageDir, getAgentDir, getPackageDir, isBunBinary } from "../config.js";
7
7
  import { loadThemeFromPath } from "../modes/interactive/theme/theme.js";
8
+ import { resolveInstallPath } from "../runtime-snapshot/marker.js";
8
9
  import { ACCEPTED_SHIM_BANNERS, GENERATED_SHIM_BANNER } from "./generated-shim-banner.js";
9
10
  import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.js";
10
11
  import { stripBom } from "../utils/text.js";
@@ -88,11 +89,14 @@ function isGeneratedGlobalDefaultExtensionShim(content) {
88
89
  * session. Canonicalizing collapses both spellings to one path.
89
90
  */
90
91
  export function canonicalizeGlobalDefaultExtensionModulePath(modulePath) {
92
+ // A session running from its runtime snapshot names the snapshot's copy; the shim outlives
93
+ // that snapshot and is shared by every build's sessions, so it names the install instead.
94
+ const installPath = resolveInstallPath(modulePath, findNodePackageDir(dirname(modulePath)));
91
95
  try {
92
- return realpathSync(modulePath);
96
+ return realpathSync(installPath);
93
97
  }
94
98
  catch {
95
- return modulePath;
99
+ return installPath;
96
100
  }
97
101
  }
98
102
  function getGlobalDefaultExtensionModulePath(extensionId) {
@@ -14,6 +14,8 @@ export declare const ASK_USER_WIDGET_KEY = "ask-user";
14
14
  type Question = QuestionRequest["questions"][number];
15
15
  /** Question ids the draft has not answered yet. */
16
16
  export declare function unansweredIds(request: QuestionRequest, draft: QuestionDraft): string[];
17
+ /** Response for a draft that already answers every question, submitted from the collapsed widget. */
18
+ export declare function buildAnsweredResponse(request: QuestionRequest, draft: QuestionDraft): QuestionResponse;
17
19
  /** Response for composer text typed while the question is pending: the text is the comment. */
18
20
  export declare function buildCommentResponse(request: QuestionRequest, draft: QuestionDraft, comment: string): QuestionResponse;
19
21
  /** Response for an idle countdown that expired while the widget was collapsed. */
@@ -26,6 +26,16 @@ function hasAnswer(answer) {
26
26
  export function unansweredIds(request, draft) {
27
27
  return request.questions.filter((question) => !hasAnswer(draft.answers?.[question.id])).map((q) => q.id);
28
28
  }
29
+ /** Response for a draft that already answers every question, submitted from the collapsed widget. */
30
+ export function buildAnsweredResponse(request, draft) {
31
+ const comment = draft.comment?.trim();
32
+ return {
33
+ status: comment ? "comment-submitted" : "answered",
34
+ answers: draft.answers ?? {},
35
+ ...(comment ? { comment: draft.comment } : {}),
36
+ unanswered: unansweredIds(request, draft),
37
+ };
38
+ }
29
39
  /** Response for composer text typed while the question is pending: the text is the comment. */
30
40
  export function buildCommentResponse(request, draft, comment) {
31
41
  return {
@@ -72,7 +72,9 @@ export class AskUserQuestionState {
72
72
  }
73
73
  jumpToQuestion(index) {
74
74
  this.focus = "options";
75
- this.activeIndex = index;
75
+ // A stale caller index must never leave the overlay without an active question.
76
+ const whole = Number.isFinite(index) ? Math.trunc(index) : 0;
77
+ this.activeIndex = Math.min(Math.max(0, whole), this.request.questions.length - 1);
76
78
  this.highlightIndex = 0;
77
79
  this.clearTransient();
78
80
  }
@@ -59,7 +59,7 @@ import { transferCompactionQueue, waitForPromptDisposition, } from "./compaction
59
59
  import { ArminComponent } from "./components/armin.js";
60
60
  import { getAskUserAnswerHeaders, parseAskUserAnswerFrame } from "./components/ask-user-answer-chip.js";
61
61
  import { matchesAskUserAnswerKey } from "./components/ask-user-answer-key.js";
62
- import { ASK_USER_WIDGET_KEY, AskUserAsyncWidget, buildCommentResponse, buildTimedOutResponse, unansweredIds, } from "./components/ask-user-async-widget.js";
62
+ import { ASK_USER_WIDGET_KEY, AskUserAsyncWidget, buildAnsweredResponse, buildCommentResponse, buildTimedOutResponse, unansweredIds, } from "./components/ask-user-async-widget.js";
63
63
  import { AskUserQuestionComponent } from "./components/ask-user-question.js";
64
64
  import { formatCountdownLabel } from "./components/ask-user-question-state.js";
65
65
  import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -3039,6 +3039,7 @@ export class InteractiveMode {
3039
3039
  draftHold: () => (this.sessionControlHost.submissionInFlight() ? "draft" : this.composerHold()),
3040
3040
  blockingQuestion: () => this.askUserQuestion !== undefined,
3041
3041
  pendingQuestionIds: () => [...this.pendingQuestions.keys()],
3042
+ pendingQuestion: (requestId) => this.pendingQuestions.get(requestId)?.request,
3042
3043
  answerQuestion: (requestId, response) => {
3043
3044
  const state = this.pendingQuestions.get(requestId);
3044
3045
  state?.finish(response);
@@ -3089,6 +3090,11 @@ export class InteractiveMode {
3089
3090
  clickPendingQuestion(state, option) {
3090
3091
  const unanswered = unansweredIds(state.request, state.draft);
3091
3092
  const index = state.request.questions.findIndex((question) => question.id === unanswered[0]);
3093
+ // Nothing left to answer: the click submits the draft instead of re-answering a question.
3094
+ if (index < 0) {
3095
+ state.finish(buildAnsweredResponse(state.request, state.draft));
3096
+ return;
3097
+ }
3092
3098
  if (!this.expandPendingQuestion(state.request.requestId, index))
3093
3099
  return;
3094
3100
  if (option === "own-answer")
@@ -1,11 +1,12 @@
1
1
  import type { AgentSession } from "../../core/agent-session.ts";
2
- import type { AdmissionHoldReason, QuestionResponse, SessionControlDrainResult } from "../../core/extensions/types.ts";
2
+ import type { AdmissionHoldReason, QuestionRequest, QuestionResponse, SessionControlDrainResult } from "../../core/extensions/types.ts";
3
3
  import type { ControlFeed } from "./session-control-feed.ts";
4
4
  import { type ControlCommand, type ControlConnection } from "./session-control-server.ts";
5
5
  export interface TuiControlSurface {
6
6
  draftHold(): AdmissionHoldReason | undefined;
7
7
  blockingQuestion(): boolean;
8
8
  pendingQuestionIds(): readonly string[];
9
+ pendingQuestion(requestId: string): Pick<QuestionRequest, "questions"> | undefined;
9
10
  answerQuestion(requestId: string, response: QuestionResponse): boolean;
10
11
  notice(line: string): void;
11
12
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { VERSION } from "../../config.js";
10
10
  import { engineBuildIdentity } from "../../core/engine-build-identity.js";
11
- import { answeredUiRequestId } from "../rpc/extension-ui-response.js";
11
+ import { answeredUiRequestId, settledQuestionStatus, unansweredQuestionIds } from "../rpc/extension-ui-response.js";
12
12
  import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
13
13
  import { failure, success } from "./session-control-server.js";
14
14
  export const TUI_CONTROL_CAPABILITY = "tui_control";
@@ -85,21 +85,26 @@ function sessionRow(session) {
85
85
  }
86
86
  /**
87
87
  * Only a question this session asked, and is still waiting on, can be answered. `uiRequestId` names
88
- * it (the short form: `id`); the reply always carries the frame's `id`, as on a host.
88
+ * it (the short form: `id`); the reply always carries the frame's `id`, as on a host. The answer
89
+ * settles by the host's rule (`settledQuestionStatus`), so the same frame reaches the model as the same
90
+ * message on either surface.
89
91
  */
90
92
  function answerQuestion(surface, command) {
91
93
  const frameId = command.id;
92
94
  const requestId = answeredUiRequestId(command);
93
- if (requestId === undefined || !surface.pendingQuestionIds().includes(requestId)) {
95
+ const request = requestId === undefined ? undefined : surface.pendingQuestion(requestId);
96
+ if (requestId === undefined || request === undefined) {
94
97
  return failure(frameId, "extension_ui_response", "unknown_request");
95
98
  }
96
- const response = questionResponse(command);
99
+ const response = questionResponse(command, request.questions);
100
+ if (response === "question_incomplete")
101
+ return failure(frameId, "extension_ui_response", response);
97
102
  if (response === undefined || !surface.answerQuestion(requestId, response)) {
98
103
  return failure(frameId, "extension_ui_response", "invalid_response");
99
104
  }
100
105
  return success(frameId, "extension_ui_response");
101
106
  }
102
- function questionResponse(command) {
107
+ function questionResponse(command, questions) {
103
108
  if (command.cancelled === true)
104
109
  return { status: "cancelled", answers: {}, unanswered: [] };
105
110
  const answers = command.answers;
@@ -115,6 +120,10 @@ function questionResponse(command) {
115
120
  parsed[question] = text === undefined ? { selected } : { selected, text };
116
121
  }
117
122
  const comment = typeof command.comment === "string" ? command.comment : undefined;
118
- return { status: "answered", answers: parsed, unanswered: [], ...(comment === undefined ? {} : { comment }) };
123
+ const status = settledQuestionStatus(parsed, comment);
124
+ if (status === undefined)
125
+ return "question_incomplete";
126
+ const unanswered = unansweredQuestionIds(questions, parsed);
127
+ return { status, answers: parsed, unanswered, ...(comment === undefined ? {} : { comment }) };
119
128
  }
120
129
  //# sourceMappingURL=session-control-commands.js.map
@@ -1,3 +1,4 @@
1
+ import { settledQuestionStatus, unansweredQuestionIds } from "./extension-ui-response.js";
1
2
  export const sessionQuestionBridges = new WeakMap();
2
3
  export class ConnectionQuestionBridge {
3
4
  constructor(output) {
@@ -33,9 +34,7 @@ export class ConnectionQuestionBridge {
33
34
  let comment;
34
35
  let timer;
35
36
  let finished = false;
36
- const unanswered = () => request.questions
37
- .filter((q) => !answers[q.id]?.selected.length && !answers[q.id]?.text?.trim())
38
- .map((q) => q.id);
37
+ const unanswered = () => unansweredQuestionIds(request.questions, answers);
39
38
  const finish = (status) => {
40
39
  if (finished)
41
40
  return;
@@ -104,9 +103,10 @@ export class ConnectionQuestionBridge {
104
103
  return false;
105
104
  answers = response.answers;
106
105
  comment = response.comment;
107
- if (!comment?.trim() && Object.keys(answers).length === 0)
106
+ const status = settledQuestionStatus(answers, comment);
107
+ if (status === undefined)
108
108
  return false;
109
- finish(comment?.trim() ? "comment-submitted" : "answered");
109
+ finish(status);
110
110
  return true;
111
111
  },
112
112
  });
@@ -65,6 +65,12 @@ export declare const DURABLE_SESSION_ID_CAPABILITY = "durable_session_id";
65
65
  * `SENPI_PROMPT_SURFACE`. A later open that names another surface rebuilds that session's prompt.
66
66
  */
67
67
  export declare const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
68
+ /**
69
+ * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
70
+ * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
71
+ * so a gateway sends `chat` only after seeing this and otherwise falls back to `app`.
72
+ */
73
+ export declare const PROMPT_SURFACE_CHAT_CAPABILITY = "prompt_surface_chat";
68
74
  /**
69
75
  * Env var carrying client capabilities to a single-connection stdio RPC host
70
76
  * (comma-separated). A launcher may set it from a client handshake; a plain
@@ -65,6 +65,12 @@ export const DURABLE_SESSION_ID_CAPABILITY = "durable_session_id";
65
65
  * `SENPI_PROMPT_SURFACE`. A later open that names another surface rebuilds that session's prompt.
66
66
  */
67
67
  export const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
68
+ /**
69
+ * HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
70
+ * no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
71
+ * so a gateway sends `chat` only after seeing this and otherwise falls back to `app`.
72
+ */
73
+ export const PROMPT_SURFACE_CHAT_CAPABILITY = "prompt_surface_chat";
68
74
  /**
69
75
  * Env var carrying client capabilities to a single-connection stdio RPC host
70
76
  * (comma-separated). A launcher may set it from a client handshake; a plain