@gajae-code/coding-agent 0.11.4 → 0.11.6

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 (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +31 -1
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +98 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/session/yield-queue.d.ts +8 -0
  50. package/dist/types/task/executor.d.ts +9 -0
  51. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  52. package/dist/types/tools/ask.d.ts +48 -0
  53. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  54. package/dist/types/tools/index.d.ts +4 -0
  55. package/package.json +9 -9
  56. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  57. package/src/cli/notify-cli.ts +40 -18
  58. package/src/commands/sdk.ts +7 -2
  59. package/src/config/settings-schema.ts +31 -1
  60. package/src/daemon/runtime.ts +2 -2
  61. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  63. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  64. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  65. package/src/eval/py/index.ts +2 -2
  66. package/src/export/html/template.generated.ts +1 -1
  67. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  68. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  69. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  70. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  71. package/src/gjc-runtime/launch-tmux.ts +61 -6
  72. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  73. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  74. package/src/gjc-runtime/state-runtime.ts +38 -12
  75. package/src/gjc-runtime/team-runtime.ts +1 -1
  76. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  77. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  78. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  79. package/src/harness-control-plane/owner.ts +59 -3
  80. package/src/hindsight/client.ts +65 -2
  81. package/src/internal-urls/agent-protocol.ts +76 -5
  82. package/src/internal-urls/docs-index.generated.ts +8 -7
  83. package/src/internal-urls/local-protocol.ts +416 -18
  84. package/src/lsp/index.ts +4 -88
  85. package/src/main.ts +36 -12
  86. package/src/modes/acp/acp-agent.ts +8 -0
  87. package/src/modes/components/ansi-display-validator.ts +54 -0
  88. package/src/modes/components/notifications-settings-editor.ts +26 -2
  89. package/src/modes/components/session-observer-overlay.ts +40 -24
  90. package/src/modes/components/settings-selector.ts +3 -1
  91. package/src/modes/components/tool-transcript-format.ts +321 -0
  92. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  93. package/src/modes/controllers/command-controller.ts +15 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +14 -2
  95. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  96. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  97. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  98. package/src/modes/controllers/selector-controller.ts +70 -8
  99. package/src/modes/interactive-mode.ts +1 -0
  100. package/src/modes/runtime-init.ts +16 -2
  101. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  102. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  103. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  104. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  105. package/src/plan-mode/approved-plan.ts +3 -1
  106. package/src/prompts/agents/architect.md +1 -0
  107. package/src/prompts/agents/critic.md +1 -0
  108. package/src/prompts/system/custom-system-prompt.md +1 -1
  109. package/src/prompts/system/system-prompt.md +2 -1
  110. package/src/prompts/tools/lsp.md +3 -3
  111. package/src/runtime-mcp/manager.ts +16 -1
  112. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  113. package/src/sdk/broker/session-index.ts +122 -64
  114. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  115. package/src/sdk/bus/chat-daemon-control.ts +538 -130
  116. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  117. package/src/sdk/bus/config-commands.ts +22 -0
  118. package/src/sdk/bus/config.ts +37 -0
  119. package/src/sdk/bus/index.ts +388 -94
  120. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  121. package/src/sdk/bus/notification-service.ts +284 -62
  122. package/src/sdk/bus/recent-activity.ts +14 -5
  123. package/src/sdk/bus/session-control.ts +150 -44
  124. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  125. package/src/sdk/bus/telegram-daemon-contract.ts +11 -3
  126. package/src/sdk/bus/telegram-daemon-control.ts +312 -76
  127. package/src/sdk/bus/telegram-daemon.ts +1916 -285
  128. package/src/sdk/host/control/dispatch.ts +14 -5
  129. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  130. package/src/sdk/session.ts +155 -27
  131. package/src/secrets/index.ts +53 -10
  132. package/src/secrets/obfuscator.ts +21 -18
  133. package/src/secrets/regex.ts +2 -0
  134. package/src/session/agent-session.ts +1447 -852
  135. package/src/session/artifacts.ts +188 -23
  136. package/src/session/context-estimation.ts +7 -2
  137. package/src/session/internal/managed-session-scope.ts +200 -38
  138. package/src/session/internal/managed-session-storage.ts +1152 -46
  139. package/src/session/session-manager.ts +1121 -148
  140. package/src/session/session-storage.ts +109 -11
  141. package/src/session/streaming-output.ts +6 -0
  142. package/src/session/yield-queue.ts +20 -0
  143. package/src/slash-commands/builtin-registry.ts +36 -0
  144. package/src/task/executor.ts +71 -26
  145. package/src/task/index.ts +63 -20
  146. package/src/tool-discovery/tool-index.ts +18 -0
  147. package/src/tools/ask-answer-registry.ts +15 -1
  148. package/src/tools/ask.ts +129 -32
  149. package/src/tools/bash-skill-urls.ts +14 -3
  150. package/src/tools/bash.ts +2 -2
  151. package/src/tools/index.ts +4 -0
  152. package/src/tools/plan-mode-guard.ts +1 -0
  153. package/src/tools/sqlite-reader.ts +3 -2
  154. package/src/tools/subagent.ts +2 -0
@@ -301,15 +301,29 @@ export async function initializeExtensions(session: AgentSession, options: Initi
301
301
  try {
302
302
  return {
303
303
  handoff: await session.handoff(
304
- typeof input.instructions === "string" ? input.instructions : undefined,
304
+ typeof input.target === "string"
305
+ ? input.target
306
+ : typeof input.instructions === "string"
307
+ ? input.instructions
308
+ : undefined,
305
309
  ),
306
310
  };
307
311
  } catch (error) {
312
+ const typed = error as { code?: unknown; handoffDocument?: unknown };
313
+ const handoffDocument =
314
+ typeof typed?.handoffDocument === "string"
315
+ ? { handoffDocument: typed.handoffDocument }
316
+ : undefined;
317
+ // Preserve a safe typed code (e.g. transient `busy`) so clients keep
318
+ // correct retry/backoff semantics; only synthesize invalid_request for
319
+ // otherwise-untyped failures.
320
+ const code = typed?.code === "busy" ? "busy" : "invalid_request";
308
321
  throw Object.assign(
309
322
  new Error(
310
323
  error instanceof Error ? error.message : "Handoff is unavailable for the current state.",
311
324
  ),
312
- { code: "invalid_request" },
325
+ { code },
326
+ handoffDocument,
313
327
  );
314
328
  }
315
329
  case "session.export_html":
@@ -10,6 +10,11 @@
10
10
  * This is the pure mapping primitive used by SDK-native workflow gate emitters.
11
11
  */
12
12
 
13
+ import { isDeepInterviewAskQuestion } from "../../../deep-interview/render-middleware";
14
+ import {
15
+ assertDeepInterviewInputWithinLimit,
16
+ MAX_USER_RESPONSE_LENGTH,
17
+ } from "../../../gjc-runtime/deep-interview-state";
13
18
  import type { OpenGateInput } from "./workflow-gate-broker";
14
19
  import {
15
20
  type AskGateDeepInterviewState,
@@ -134,7 +139,17 @@ export function buildAskGateStageState(question: AskGateQuestion, labels: string
134
139
  /** Build the `workflow_gate` open-input for one ask question. */
135
140
  export function questionToGate(question: AskGateQuestion): OpenGateInput {
136
141
  const labels = question.options.map(o => o.label);
137
- const schema = buildAskGateAnswerSchema(question, labels);
142
+ const schema = buildAskGateAnswerSchema(
143
+ {
144
+ multi: question.multi,
145
+ allowEmpty: question.allowEmpty,
146
+ customMaxLength:
147
+ question.deepInterview || isDeepInterviewAskQuestion(question.question)
148
+ ? MAX_USER_RESPONSE_LENGTH
149
+ : undefined,
150
+ },
151
+ labels,
152
+ );
138
153
  return {
139
154
  stage: question.workflowGate?.stage ?? "deep-interview",
140
155
  kind: question.workflowGate?.kind ?? "question",
@@ -229,6 +244,12 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
229
244
  if (other && (answer.custom === undefined || answer.custom.trim() === "")) {
230
245
  throw new DeepInterviewGateError("missing_custom", "custom text is required when `other` is true");
231
246
  }
247
+ if (
248
+ other &&
249
+ (question.deepInterview || isDeepInterviewAskQuestion(question.question)) &&
250
+ answer.custom !== undefined
251
+ )
252
+ assertDeepInterviewInputWithinLimit(answer.custom, MAX_USER_RESPONSE_LENGTH, "user_response");
232
253
  return {
233
254
  id: question.id,
234
255
  question: question.question,
@@ -261,6 +282,15 @@ export function classifyAskGateDisposition(
261
282
  multi: (state as Record<string, unknown>).multi === true,
262
283
  allowEmpty: (state as Record<string, unknown>).allow_empty === true,
263
284
  };
285
+ if (
286
+ (state as Record<string, unknown>).deep_interview_metadata === true &&
287
+ typeof (answer as Record<string, unknown>)?.custom === "string"
288
+ )
289
+ assertDeepInterviewInputWithinLimit(
290
+ (answer as Record<string, unknown>).custom as string,
291
+ MAX_USER_RESPONSE_LENGTH,
292
+ "user_response",
293
+ );
264
294
  if (typeof answer === "object" && answer !== null && (answer as Record<string, unknown>).action === "clarify") {
265
295
  gateAnswerToResult(question, answer);
266
296
  return "resolve_without_commit";
@@ -76,6 +76,13 @@ export interface NotificationGateResolutionOptions {
76
76
  closeClaimInvalid(reason: string): void;
77
77
  }
78
78
 
79
+ export class NotificationGatePolicyChangedError extends Error {
80
+ constructor() {
81
+ super("Notification policy changed while resolving the workflow gate");
82
+ this.name = "NotificationGatePolicyChangedError";
83
+ }
84
+ }
85
+
79
86
  export interface AskSelectedAckRecoveryParticipant {
80
87
  requestRecoveredAskSelectedAck(input: {
81
88
  sessionId: string;
@@ -361,7 +368,8 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
361
368
  daemonDeadlineAt: Date.now() + 8_000,
362
369
  hostTimeoutMs: 10_000,
363
370
  });
364
- } catch {
371
+ } catch (error) {
372
+ if (error instanceof NotificationGatePolicyChangedError) throw error;
365
373
  outcome = { status: "unknown", reason: "host_timeout" };
366
374
  }
367
375
  this.#broker.updateAckPolicy(response.gate_id, {
@@ -378,6 +386,14 @@ export class BrokerWorkflowGateEmitter implements WorkflowGateEmitter {
378
386
  else closeClaimInvalid(resolution.error?.code ?? "invalid_answer");
379
387
  return resolution;
380
388
  } catch (error) {
389
+ if (error instanceof NotificationGatePolicyChangedError) {
390
+ try {
391
+ closeClaimInvalid(error.message);
392
+ } finally {
393
+ this.quarantineGate(response.gate_id);
394
+ }
395
+ throw error;
396
+ }
381
397
  closeClaimInvalid(error instanceof Error ? error.message : "invalid_answer");
382
398
  this.#scheduleRecovery();
383
399
  throw error;
@@ -12,6 +12,7 @@
12
12
  * swapped in later without changing callers.
13
13
  */
14
14
  import { createHash } from "node:crypto";
15
+ import { deepInterviewCharacterCount } from "../../../gjc-runtime/deep-interview-state";
15
16
  import type { JsonSchema, WorkflowGateValidationError } from "./workflow-gate-types";
16
17
 
17
18
  /** Keywords this wrapper understands. Any other keyword is rejected. */
@@ -238,7 +239,8 @@ function validateValue(schema: JsonSchema, value: unknown, path: string, errors:
238
239
  if (!ok) errors.push({ path, keyword: "enum", message: "value not in enum", expected: schema.enum });
239
240
  }
240
241
  if (typeof value === "string") {
241
- if (schema.minLength !== undefined && value.length < schema.minLength) {
242
+ const characterCount = deepInterviewCharacterCount(value);
243
+ if (schema.minLength !== undefined && characterCount < schema.minLength) {
242
244
  errors.push({
243
245
  path,
244
246
  keyword: "minLength",
@@ -246,7 +248,7 @@ function validateValue(schema: JsonSchema, value: unknown, path: string, errors:
246
248
  expected: schema.minLength,
247
249
  });
248
250
  }
249
- if (schema.maxLength !== undefined && value.length > schema.maxLength) {
251
+ if (schema.maxLength !== undefined && characterCount > schema.maxLength) {
250
252
  errors.push({
251
253
  path,
252
254
  keyword: "maxLength",
@@ -1,3 +1,5 @@
1
+ import { deepInterviewCharacterCount } from "../../../gjc-runtime/deep-interview-state";
2
+
1
3
  /** Transport-neutral workflow gate contract used by SDK workflow coordination. */
2
4
 
3
5
  /** Lifecycle stages that emit machine-addressable SDK workflow gates. */
@@ -14,12 +16,28 @@ export type WorkflowGateKind = "question" | "approval" | "execution";
14
16
  /** "Other (type your own)" sentinel, mirroring the interactive ask tool. */
15
17
  export const GATE_OTHER_OPTION = "Other (type your own)";
16
18
 
19
+ /**
20
+ * Structured adapter context queued at interview start. Non-behavioral:
21
+ * `confused_terms` and `references` MUST NOT alter the first question, are never
22
+ * inferred from vocabulary density, and referenced `url`/`excerpt` are inert
23
+ * strings that are never auto-fetched.
24
+ */
25
+ export interface AskGateReference {
26
+ reference_id: string;
27
+ label: string;
28
+ origin: string;
29
+ url?: string;
30
+ excerpt?: string;
31
+ }
32
+
17
33
  export interface AskGateDeepInterviewState {
18
34
  round_id?: string;
19
35
  round: number;
20
36
  component: string;
21
37
  dimension: string;
22
38
  ambiguity: number;
39
+ confused_terms?: string[];
40
+ references?: AskGateReference[];
23
41
  }
24
42
 
25
43
  export interface AskGateStageStateInput {
@@ -34,10 +52,90 @@ export interface AskGateStageStateInput {
34
52
  export interface AskGateSchemaInput {
35
53
  multi?: boolean;
36
54
  allowEmpty?: boolean;
55
+ customMaxLength?: number;
37
56
  }
38
57
 
39
58
  function isBoundedAskGateString(value: unknown): value is string {
40
- return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).byteLength <= 256;
59
+ return (
60
+ typeof value === "string" &&
61
+ value.length > 0 &&
62
+ deepInterviewCharacterCount(value) <= MAX_ASK_GATE_ADAPTER_STRING_LENGTH
63
+ );
64
+ }
65
+
66
+ /** Max characters for adapter metadata identifiers and confused terms. */
67
+ const MAX_ASK_GATE_ADAPTER_STRING_LENGTH = 256;
68
+ const MAX_ASK_GATE_DEEP_INTERVIEW_STRING_LENGTH = 128;
69
+ /** Max characters for a longer inert reference string (url/excerpt); never fetched. */
70
+ const MAX_ASK_GATE_LONG_STRING_LENGTH = 2048;
71
+ const MAX_ASK_GATE_CONFUSED_TERMS = 32;
72
+ const MAX_ASK_GATE_REFERENCES = 32;
73
+
74
+ function isBoundedAskGateAdapterString(value: unknown): value is string {
75
+ return (
76
+ typeof value === "string" &&
77
+ value.length > 0 &&
78
+ deepInterviewCharacterCount(value) <= MAX_ASK_GATE_ADAPTER_STRING_LENGTH
79
+ );
80
+ }
81
+
82
+ function isBoundedAskGateDeepInterviewString(value: unknown): value is string {
83
+ return (
84
+ typeof value === "string" &&
85
+ value.length > 0 &&
86
+ deepInterviewCharacterCount(value) <= MAX_ASK_GATE_DEEP_INTERVIEW_STRING_LENGTH
87
+ );
88
+ }
89
+
90
+ function isBoundedAskGateLongString(value: unknown): value is string {
91
+ return (
92
+ typeof value === "string" &&
93
+ value.length > 0 &&
94
+ deepInterviewCharacterCount(value) <= MAX_ASK_GATE_LONG_STRING_LENGTH
95
+ );
96
+ }
97
+
98
+ function isDenseArray(value: unknown[]): boolean {
99
+ for (let index = 0; index < value.length; index++) if (!Object.hasOwn(value, index)) return false;
100
+ return true;
101
+ }
102
+
103
+ function isValidAskGateConfusedTerms(value: unknown): value is string[] {
104
+ return (
105
+ Array.isArray(value) &&
106
+ isDenseArray(value) &&
107
+ value.length <= MAX_ASK_GATE_CONFUSED_TERMS &&
108
+ value.every(isBoundedAskGateAdapterString)
109
+ );
110
+ }
111
+
112
+ function isValidAskGateReference(value: unknown): boolean {
113
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
114
+ const prototype = Object.getPrototypeOf(value);
115
+ if (prototype !== Object.prototype && prototype !== null) return false;
116
+ const ref = value as Record<string, unknown>;
117
+ const allowed = new Set(["reference_id", "label", "origin", "url", "excerpt"]);
118
+ if (Object.keys(ref).some(key => !allowed.has(key))) return false;
119
+ if (!Object.hasOwn(ref, "reference_id") || !Object.hasOwn(ref, "label") || !Object.hasOwn(ref, "origin"))
120
+ return false;
121
+ if (
122
+ !isBoundedAskGateAdapterString(ref.reference_id) ||
123
+ !isBoundedAskGateAdapterString(ref.label) ||
124
+ !isBoundedAskGateAdapterString(ref.origin)
125
+ )
126
+ return false;
127
+ if (ref.url !== undefined && !isBoundedAskGateLongString(ref.url)) return false;
128
+ if (ref.excerpt !== undefined && !isBoundedAskGateLongString(ref.excerpt)) return false;
129
+ return true;
130
+ }
131
+
132
+ function isValidAskGateReferences(value: unknown): boolean {
133
+ return (
134
+ Array.isArray(value) &&
135
+ isDenseArray(value) &&
136
+ value.length <= MAX_ASK_GATE_REFERENCES &&
137
+ value.every(isValidAskGateReference)
138
+ );
41
139
  }
42
140
 
43
141
  /** Rejects stage state that is not exactly compatible with the generic ask-gate producer. */
@@ -62,6 +160,8 @@ export function validateAskGateStageState(value: unknown): asserts value is Reco
62
160
  "challenge_mode",
63
161
  "ambiguity",
64
162
  "topology_gate",
163
+ "confused_terms",
164
+ "references",
65
165
  ]);
66
166
  if (Object.keys(state).some(key => !allowed.has(key))) throw new Error("ask gate stage_state has an unknown field");
67
167
  if (
@@ -76,7 +176,10 @@ export function validateAskGateStageState(value: unknown): asserts value is Reco
76
176
  throw new Error("ask gate stage_state has invalid required fields");
77
177
  if (state.navigation_label !== undefined && state.navigation_label !== "Next" && state.navigation_label !== "Done")
78
178
  throw new Error("ask gate stage_state has an invalid navigation label");
79
- for (const key of ["round_id", "component", "dimension", "mode", "challenge_mode"])
179
+ for (const key of ["round_id", "component", "dimension"])
180
+ if (state[key] !== undefined && !isBoundedAskGateDeepInterviewString(state[key]))
181
+ throw new Error(`ask gate stage_state has an invalid ${key}`);
182
+ for (const key of ["mode", "challenge_mode"])
80
183
  if (state[key] !== undefined && !isBoundedAskGateString(state[key]))
81
184
  throw new Error(`ask gate stage_state has an invalid ${key}`);
82
185
  if (state.deep_interview_metadata !== undefined && typeof state.deep_interview_metadata !== "boolean")
@@ -93,6 +196,10 @@ export function validateAskGateStageState(value: unknown): asserts value is Reco
93
196
  throw new Error("ask gate stage_state has an invalid ambiguity");
94
197
  if (state.topology_gate !== undefined && typeof state.topology_gate !== "boolean")
95
198
  throw new Error("ask gate stage_state has an invalid topology flag");
199
+ if (state.confused_terms !== undefined && !isValidAskGateConfusedTerms(state.confused_terms))
200
+ throw new Error("ask gate stage_state has invalid confused_terms");
201
+ if (state.references !== undefined && !isValidAskGateReferences(state.references))
202
+ throw new Error("ask gate stage_state has invalid references");
96
203
  }
97
204
 
98
205
  /** Builds the canonical generic ask answer schema. */
@@ -114,6 +221,7 @@ export function buildAskGateAnswerSchema(question: AskGateSchemaInput, labels: s
114
221
  custom: {
115
222
  type: "string",
116
223
  minLength: 1,
224
+ ...(question.customMaxLength === undefined ? {} : { maxLength: question.customMaxLength }),
117
225
  pattern: "\\S",
118
226
  description: "free-text answer; required when `other` is true",
119
227
  },
@@ -142,7 +250,12 @@ export function buildAskGateAnswerSchema(question: AskGateSchemaInput, labels: s
142
250
  properties: {
143
251
  selected: selectedWithOther,
144
252
  other: { const: true },
145
- custom: { type: "string", minLength: 1, pattern: "\\S" },
253
+ custom: {
254
+ type: "string",
255
+ minLength: 1,
256
+ ...(question.customMaxLength === undefined ? {} : { maxLength: question.customMaxLength }),
257
+ pattern: "\\S",
258
+ },
146
259
  action: { const: "answer" },
147
260
  },
148
261
  required: ["selected", "other", "custom"],
@@ -176,6 +289,10 @@ export function buildAskGateStageState(input: AskGateStageStateInput, labels: st
176
289
  dimension: input.deepInterview.dimension,
177
290
  ambiguity: input.deepInterview.ambiguity,
178
291
  ...(input.deepInterview.round_id === undefined ? {} : { round_id: input.deepInterview.round_id }),
292
+ ...(input.deepInterview.confused_terms === undefined
293
+ ? {}
294
+ : { confused_terms: input.deepInterview.confused_terms }),
295
+ ...(input.deepInterview.references === undefined ? {} : { references: input.deepInterview.references }),
179
296
  }
180
297
  : (input.fallbackState ?? {})),
181
298
  };
@@ -114,6 +114,7 @@ interface RenameApprovedPlanFileOptions {
114
114
  planFilePath: string;
115
115
  finalPlanFilePath: string;
116
116
  getArtifactsDir: () => string | null;
117
+ isManagedDestination?: () => boolean;
117
118
  getSessionId: () => string | null;
118
119
  }
119
120
 
@@ -124,12 +125,13 @@ function assertLocalUrl(path: string, label: "source" | "destination"): void {
124
125
  }
125
126
 
126
127
  export async function renameApprovedPlanFile(options: RenameApprovedPlanFileOptions): Promise<void> {
127
- const { planFilePath, finalPlanFilePath, getArtifactsDir, getSessionId } = options;
128
+ const { planFilePath, finalPlanFilePath, getArtifactsDir, getSessionId, isManagedDestination } = options;
128
129
  assertLocalUrl(planFilePath, "source");
129
130
  assertLocalUrl(finalPlanFilePath, "destination");
130
131
 
131
132
  const resolveOptions = {
132
133
  getArtifactsDir: () => getArtifactsDir(),
134
+ isManagedDestination,
133
135
  getSessionId: () => getSessionId(),
134
136
  };
135
137
  const resolvedSource = resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), resolveOptions);
@@ -33,6 +33,7 @@ You may receive a forked parent-conversation snapshot as background. Your read-o
33
33
  - Never approve CRITICAL or HIGH severity issues.
34
34
  - Do not skip spec compliance to jump to style nitpicks.
35
35
  - Be constructive: explain why an issue matters and how to fix it or strengthen the design.
36
+ - Practical sufficiency beats endless logical completeness: treat preemptive logic for edge cases with no observed or evidenced trigger as over-engineering, but never downgrade genuine CRITICAL/HIGH architecture, extensibility, or robustness risks that have observability-based evidence.
36
37
  </constraints>
37
38
 
38
39
  <review_stages>
@@ -23,6 +23,7 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
23
23
  - Do not invent problems; report no issues found when the plan passes.
24
24
  - Escalate routing needs upward: planner for plan revision, the deep-interview skill for requirements gathering, architect for code analysis.
25
25
  - For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
26
+ - Prefer practical sufficiency over endless logical completeness: do not demand preemptive logic for edge cases that are not actually observed or evidenced, yet still require extensibility and robustness when observability-based evidence shows the investment is worth the token and time.
26
27
  </constraints>
27
28
 
28
29
  <execution_loop>
@@ -37,6 +37,6 @@ Rules are local constraints. You MUST read `rule://<name>` when working in that
37
37
  {{/if}}
38
38
  {{#if secretsEnabled}}
39
39
  <redacted-content>
40
- Some values in tool output are redacted for security. They appear as `#XXXX#` tokens (4 uppercase-alphanumeric characters wrapped in `#`). These are **not errors** — they are intentional placeholders for sensitive values (API keys, passwords, tokens). Treat them as opaque strings. Do not attempt to decode, fix, or report them as problems.
40
+ Some values in tool output are redacted for security. They appear as versioned `#GJC1_…#` tokens. These are **not errors** — they are intentional placeholders for sensitive values (API keys, passwords, tokens). Treat them as opaque strings. Do not attempt to decode, fix, or report them as problems.
41
41
  </redacted-content>
42
42
  {{/if}}
@@ -94,7 +94,7 @@ Discoverable capabilities include browser automation, scheduling, debugging, and
94
94
 
95
95
  {{#if secretsEnabled}}
96
96
  <redacted-content>
97
- Some tool output values are intentionally redacted as `#XXXX#` tokens. Treat them as opaque sensitive strings.
97
+ Some tool output values are intentionally redacted as versioned `#GJC1_…#` tokens. Treat them as opaque sensitive strings.
98
98
  </redacted-content>
99
99
  {{/if}}
100
100
 
@@ -206,6 +206,7 @@ Guardrails? None. Restrictions? None. Freedom? Unlimited.
206
206
  - **Do your homework first** - Read the files, search, and understand the context before asking
207
207
  - **Don’t Stop at Analysis** - Once you’ve assessed the situation, take the next step (session, comment, merge, fix, cleanup) on your own
208
208
  - **No Inflating Operational/Meta Issues** - Just because the backlog is empty doesn’t mean you should force operational follow-ups, verifiability checks, or meta issues. Only raise issues for actual repository problems; otherwise, stop.
209
+ - **Practical sufficiency over completeness** - Don’t add logic that preemptively handles edge cases you haven’t actually observed; endless logical completeness loses to practical sufficiency. Extensibility and robustness matter, but they must be observability-based and evidence-based—any engineering without underlying evidence that it’s worth the token and time is over-engineering.
209
210
  - **Always maintain execution** - Do not end your turn with just reporting, summarizing, or analyzing. If an action is possible, execute it immediately during the same turn.
210
211
  - **Always be in action mode** - The default stance is intervention, not observation. If you see something that needs cleaning up, clean it up; if you see a session that needs opening, open it; if you see a PR that needs blocking, block it; if you see a PR that needs merging, review it and merge it.
211
212
  - **No begging for permission** - For obvious follow-up actions (cleaning up sessions, marking items as stale, fixing routing, updating documentation to reflect Doctrine), don’t ask—just do it.
@@ -1,7 +1,7 @@
1
1
  Interacts with Language Server Protocol servers for code intelligence.
2
2
 
3
3
  <operations>
4
- - `diagnostics`: Get errors/warnings for a file, a glob of files, or the entire workspace (`file: "*"`)
4
+ - `diagnostics`: Get errors/warnings for a concrete file or a glob of files
5
5
  - `definition`: Go to symbol definition → file path + position + 3-line source context
6
6
  - `type_definition`: Go to symbol type definition → file path + position + 3-line source context
7
7
  - `implementation`: Find concrete implementations → file path + position + 3-line source context
@@ -18,7 +18,7 @@ Interacts with Language Server Protocol servers for code intelligence.
18
18
  </operations>
19
19
 
20
20
  <parameters>
21
- - `file`: File path, glob pattern (e.g. `src/**/*.ts`), or `"*"` for workspace scope. Globs are expanded locally before dispatch. `"*"` routes `diagnostics`/`symbols`/`reload` to their workspace-wide form.
21
+ - `file`: File path, glob pattern (e.g. `src/**/*.ts`), or `"*"` for workspace scope where supported. Globs are expanded locally before dispatch. `"*"` routes `symbols`/`reload` to their workspace-wide form; workspace build diagnostics are unavailable through `lsp`.
22
22
  - `line`: 1-indexed line number for position-based actions
23
23
  - `symbol`: Substring on the target line used to resolve column automatically. Append `#N` to pick the Nth occurrence on that line (1-indexed; default 1) — e.g. `foo#2` selects the second `foo`.
24
24
  - `query`: Symbol search query, code-action kind filter / selector (list/apply mode), or LSP method name when `action: request`
@@ -31,7 +31,7 @@ Interacts with Language Server Protocol servers for code intelligence.
31
31
  <caution>
32
32
  - Requires running LSP server for target language
33
33
  - Some operations require file to be saved to disk
34
- - Glob expansion samples up to 20 files per request; use `file: "*"` for broader coverage
34
+ - Glob expansion samples up to 20 files per request; narrow broad patterns when they exceed that limit
35
35
  - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `#N` occurrence selectors return an explicit error instead of silently falling back
36
36
  </caution>
37
37
 
@@ -235,6 +235,7 @@ export class MCPManager {
235
235
  #epoch = 0;
236
236
  readonly #toolsOnly: boolean;
237
237
  #toolsOnlyConfigLoaded = false;
238
+ #connectionSetSealed = false;
238
239
 
239
240
  #serverError(message: string): string {
240
241
  return this.#toolsOnly ? "MCP server unavailable" : message;
@@ -242,6 +243,17 @@ export class MCPManager {
242
243
  #assertRawMCPAccessAllowed(): void {
243
244
  if (this.#toolsOnly) throw new Error("Tools-only MCP manager does not allow raw MCP access");
244
245
  }
246
+ #assertConnectionSetMutable(): void {
247
+ if (this.#connectionSetSealed) throw new Error("MCP manager connection set is sealed");
248
+ }
249
+
250
+ sealConnectionSet(): void {
251
+ this.#connectionSetSealed = true;
252
+ }
253
+
254
+ isConnectionSetSealed(): boolean {
255
+ return this.#connectionSetSealed;
256
+ }
245
257
 
246
258
  #isCurrentConnection(
247
259
  name: string,
@@ -379,6 +391,7 @@ export class MCPManager {
379
391
  * Returns tools and any connection errors.
380
392
  */
381
393
  async discoverAndConnect(options?: MCPDiscoverOptions): Promise<MCPLoadResult> {
394
+ this.#assertConnectionSetMutable();
382
395
  const hasConfigPath = options?.configPath !== undefined;
383
396
  if (this.#toolsOnly !== hasConfigPath) {
384
397
  throw new Error(
@@ -414,6 +427,7 @@ export class MCPManager {
414
427
  onConnecting?: (serverNames: string[]) => void,
415
428
  ): Promise<MCPLoadResult> {
416
429
  this.#assertRawMCPAccessAllowed();
430
+ this.#assertConnectionSetMutable();
417
431
  return this.#connectServers(configs, sources, onConnecting);
418
432
  }
419
433
 
@@ -923,6 +937,7 @@ export class MCPManager {
923
937
  */
924
938
  async disconnectServer(name: string): Promise<void> {
925
939
  this.#assertRawMCPAccessAllowed();
940
+ this.#assertConnectionSetMutable();
926
941
  await this.#disconnectServer(name);
927
942
  }
928
943
 
@@ -1050,7 +1065,7 @@ export class MCPManager {
1050
1065
  * Returns the new connection, or null if reconnection failed.
1051
1066
  */
1052
1067
  async reconnectServer(name: string): Promise<MCPServerConnection | null> {
1053
- if (this.#toolsOnly) return null;
1068
+ if (this.#toolsOnly || this.#connectionSetSealed) return null;
1054
1069
  const pending = this.#pendingReconnections.get(name);
1055
1070
  if (pending) return pending;
1056
1071