@sayknow-cli/coding-agent 0.5.1 → 0.5.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 (122) hide show
  1. package/CHANGELOG.md +185 -14
  2. package/dist/types/cli/update-cli.d.ts +8 -1
  3. package/dist/types/commands/session.d.ts +7 -0
  4. package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-registry.d.ts +1 -1
  7. package/dist/types/config/models-config-schema.d.ts +4 -0
  8. package/dist/types/config/settings-schema.d.ts +9 -0
  9. package/dist/types/config/telegram-autostart.d.ts +9 -1
  10. package/dist/types/dap/client.d.ts +1 -0
  11. package/dist/types/lsp/client.d.ts +5 -0
  12. package/dist/types/modes/components/pet-capability.d.ts +8 -7
  13. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  14. package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
  15. package/dist/types/modes/components/welcome.d.ts +1 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +13 -1
  18. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
  19. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
  20. package/dist/types/modes/types.d.ts +7 -1
  21. package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
  22. package/dist/types/session/agent-session.d.ts +9 -0
  23. package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
  24. package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
  25. package/dist/types/session/session-manager.d.ts +9 -0
  26. package/dist/types/session/session-storage.d.ts +17 -0
  27. package/dist/types/session-import/claude.d.ts +26 -0
  28. package/dist/types/session-import/codex.d.ts +22 -0
  29. package/dist/types/session-import/command.d.ts +20 -0
  30. package/dist/types/session-import/detect.d.ts +22 -0
  31. package/dist/types/session-import/index.d.ts +7 -0
  32. package/dist/types/session-import/redact.d.ts +23 -0
  33. package/dist/types/session-import/service.d.ts +28 -0
  34. package/dist/types/session-import/types.d.ts +125 -0
  35. package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
  36. package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
  37. package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
  38. package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
  39. package/dist/types/skc-runtime/session-restore.d.ts +99 -0
  40. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
  41. package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
  42. package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
  43. package/dist/types/slash-commands/types.d.ts +5 -0
  44. package/dist/types/tools/ask.d.ts +169 -4
  45. package/package.json +16 -7
  46. package/scripts/generate-sdk-operation-inventory.ts +5 -6
  47. package/src/cli/config-cli.ts +23 -8
  48. package/src/cli/update-cli.ts +265 -21
  49. package/src/commands/session.ts +88 -2
  50. package/src/config/atomic-yaml-patch.ts +167 -30
  51. package/src/config/keybindings.ts +10 -0
  52. package/src/config/model-profiles.ts +69 -1
  53. package/src/config/model-registry.ts +72 -38
  54. package/src/config/models-config-schema.ts +1 -1
  55. package/src/config/settings-schema.ts +9 -0
  56. package/src/config/settings.ts +13 -2
  57. package/src/config/telegram-autostart.ts +11 -4
  58. package/src/dap/client.ts +78 -30
  59. package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
  60. package/src/internal-urls/docs-index.generated.ts +5 -4
  61. package/src/lsp/client.ts +17 -29
  62. package/src/main.ts +1 -1
  63. package/src/modes/action-registry.ts +2 -0
  64. package/src/modes/components/pet-capability.ts +22 -13
  65. package/src/modes/components/pet-selector.ts +1 -1
  66. package/src/modes/components/sayknow-pet-widget.ts +41 -7
  67. package/src/modes/components/welcome.ts +5 -0
  68. package/src/modes/controllers/event-controller.ts +1 -1
  69. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  70. package/src/modes/controllers/input-controller.ts +71 -6
  71. package/src/modes/controllers/plan-mode-controller.ts +8 -1
  72. package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
  73. package/src/modes/controllers/selector-controller.ts +47 -19
  74. package/src/modes/interactive-mode.ts +54 -4
  75. package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
  76. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
  77. package/src/modes/types.ts +5 -1
  78. package/src/notifications/lifecycle-control-runtime.ts +258 -179
  79. package/src/prompts/system/eager-todo.md +2 -0
  80. package/src/prompts/system/plan-mode-approved.md +1 -1
  81. package/src/prompts/system/system-prompt.md +4 -2
  82. package/src/runtime-mcp/transports/stdio.ts +96 -33
  83. package/src/sdk/broker/lifecycle.ts +6 -5
  84. package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
  85. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  86. package/src/sdk/session.ts +25 -18
  87. package/src/session/agent-session.ts +120 -25
  88. package/src/session/internal/managed-session-scope.ts +41 -6
  89. package/src/session/internal/managed-session-storage.ts +14 -0
  90. package/src/session/session-manager.ts +113 -11
  91. package/src/session/session-storage.ts +70 -0
  92. package/src/session-import/claude.ts +382 -0
  93. package/src/session-import/codex.ts +458 -0
  94. package/src/session-import/command.ts +61 -0
  95. package/src/session-import/detect.ts +133 -0
  96. package/src/session-import/index.ts +27 -0
  97. package/src/session-import/redact.ts +125 -0
  98. package/src/session-import/service.ts +749 -0
  99. package/src/session-import/types.ts +163 -0
  100. package/src/skc-runtime/boot-generation.ts +172 -0
  101. package/src/skc-runtime/launch-tmux.ts +219 -41
  102. package/src/skc-runtime/launch-worktree.ts +277 -4
  103. package/src/skc-runtime/session-restore-runtime.ts +120 -0
  104. package/src/skc-runtime/session-restore.ts +296 -0
  105. package/src/skc-runtime/session-state-sidecar.ts +41 -0
  106. package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
  107. package/src/skc-runtime/tmux-sessions.ts +284 -108
  108. package/src/slash-commands/acp-builtins.ts +5 -1
  109. package/src/slash-commands/builtin-registry.ts +109 -5
  110. package/src/slash-commands/types.ts +5 -0
  111. package/src/tools/ask.ts +188 -10
  112. package/src/tools/eval.ts +2 -2
  113. package/src/export/html/template.css +0 -1060
  114. package/src/export/html/template.html +0 -47
  115. package/src/export/html/template.js +0 -2348
  116. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  117. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  118. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  119. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  120. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  121. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  122. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -39,6 +39,7 @@ import { AgentStorage } from "../session/agent-storage";
39
39
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
40
40
  import {
41
41
  type AtomicYamlPatch,
42
+ AtomicYamlRetargetError,
42
43
  applyAtomicYamlPatches,
43
44
  applyAtomicYamlPatchesWithCurrent,
44
45
  atomicYamlPathHash,
@@ -830,22 +831,28 @@ export class Settings implements NotificationSettingsReader {
830
831
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) this.#queueSave();
831
832
  this.#releasePendingSaveSlot();
832
833
  const observedSave = this.#savePromise;
834
+ let saveError: unknown;
833
835
  try {
834
836
  await observedSave;
835
- } catch {
837
+ } catch (error) {
836
838
  // Historical flush() behavior logs background failures but does not reject.
839
+ saveError = error;
837
840
  }
841
+ if (saveError instanceof AtomicYamlRetargetError) return;
838
842
  // A failed predecessor may settle just before a new mutation observes its
839
843
  // still-reserved slot. Explicit flush owns one fresh attempt for remaining
840
844
  // dirty patches instead of leaving them stranded or retrying forever.
841
845
  if (this.#modified.size > 0 && this.#savePromise === observedSave) {
842
846
  if (!this.#pendingSaveSlot) this.#queueSave();
843
847
  this.#releasePendingSaveSlot();
848
+ let retryError: unknown;
844
849
  try {
845
850
  await this.#savePromise;
846
- } catch {
851
+ } catch (error) {
847
852
  // Keep dirty state for a later explicit flush or mutation.
853
+ retryError = error;
848
854
  }
855
+ if (retryError instanceof AtomicYamlRetargetError) return;
849
856
  }
850
857
  await this.#refreshDurableSettings();
851
858
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
@@ -871,6 +878,7 @@ export class Settings implements NotificationSettingsReader {
871
878
  saveError = error;
872
879
  }
873
880
  await this.#refreshDurableSettings();
881
+ if (saveError instanceof AtomicYamlRetargetError) throw saveError;
874
882
  if (this.#modified.size > 0 && !this.#pendingSaveSlot) {
875
883
  this.#queueSave();
876
884
  this.#releasePendingSaveSlot();
@@ -1721,6 +1729,9 @@ export class Settings implements NotificationSettingsReader {
1721
1729
  logger.warn("Settings: refresh after background save failure failed", { error: String(refreshError) });
1722
1730
  }
1723
1731
  throw error;
1732
+ })
1733
+ .finally(() => {
1734
+ if (this.#pendingSaveSlot === slot) this.#pendingSaveSlot = undefined;
1724
1735
  });
1725
1736
  this.#savePromise = save;
1726
1737
  void save.catch(() => {});
@@ -10,11 +10,18 @@
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
12
  import { getConfigRootDir, isCompiledBinary, logger } from "@sayknow-cli/utils";
13
- import { settings } from "./settings";
13
+ import { settings as globalSettings, type Settings } from "./settings";
14
14
  import type { TelegramSettings } from "./settings-schema";
15
15
  import { telegramSettingsToEnv, validateTelegramSettings } from "./telegram-env-bridge";
16
16
 
17
- function readTelegramSettings(): TelegramSettings {
17
+ /**
18
+ * The startup command runs against an explicit Settings instance (tests and
19
+ * embedders inject one), so read through it instead of the global proxy —
20
+ * which may not be initialized at all on those paths.
21
+ */
22
+ type TelegramSettingsSource = Pick<Settings, "get">;
23
+
24
+ function readTelegramSettings(settings: TelegramSettingsSource): TelegramSettings {
18
25
  return {
19
26
  enabled: settings.get("telegram.enabled"),
20
27
  botToken: settings.get("telegram.botToken"),
@@ -69,10 +76,10 @@ function isAlreadyRunning(pidFile: string): boolean {
69
76
  * it in the background. Never throws — failures are logged as warnings so the
70
77
  * main skc session is unaffected.
71
78
  */
72
- export async function maybeAutostartTelegramRemote(): Promise<void> {
79
+ export async function maybeAutostartTelegramRemote(settings: TelegramSettingsSource = globalSettings): Promise<void> {
73
80
  if (!settings.get("telegram.enabled")) return;
74
81
 
75
- const config = readTelegramSettings();
82
+ const config = readTelegramSettings(settings);
76
83
  const errors = validateTelegramSettings(config);
77
84
  if (errors.length > 0) {
78
85
  logger.warn("Telegram Remote enabled but misconfigured, skipping autostart", { errors });
package/src/dap/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import * as fs from "node:fs/promises";
3
- import { logger } from "@sayknow-cli/utils";
3
+ import { isKnownSinkPeerClosedError, logger } from "@sayknow-cli/utils";
4
4
  import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
5
5
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
6
6
  import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
@@ -62,9 +62,9 @@ function parseMessage(
62
62
 
63
63
  async function writeMessage(sink: DapWriteSink, message: DapRequestMessage | DapResponseMessage): Promise<void> {
64
64
  const content = JSON.stringify(message);
65
- sink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
66
- sink.write(content);
67
- await sink.flush();
65
+ const frame = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n${content}`;
66
+ await Promise.resolve(sink.write(frame));
67
+ await Promise.resolve(sink.flush());
68
68
  }
69
69
 
70
70
  function toErrorMessage(value: unknown): string {
@@ -98,6 +98,9 @@ export class DapClient {
98
98
  #messageBuffer = Buffer.alloc(0);
99
99
  #isReading = false;
100
100
  #disposed = false;
101
+ #terminalError: Error | undefined;
102
+ #disposePromise: Promise<void> | undefined;
103
+ #writeQueue: Promise<void> = Promise.resolve();
101
104
  #lastActivity = Date.now();
102
105
  #capabilities?: DapCapabilities;
103
106
  #eventHandlers = new Map<string, Set<DapEventHandler>>();
@@ -119,6 +122,18 @@ export class DapClient {
119
122
  this.#socket = options?.socket;
120
123
  }
121
124
 
125
+ #queueWriteMessage(message: DapRequestMessage | DapResponseMessage): Promise<void> {
126
+ const write = this.#writeQueue
127
+ .catch(() => {})
128
+ .then(() => {
129
+ if (this.#terminalError) throw this.#terminalError;
130
+ if (this.#disposed) throw new Error(`DAP adapter ${this.adapter.name} is not running`);
131
+ return writeMessage(this.#writeSink, message);
132
+ });
133
+ this.#writeQueue = write.catch(() => {});
134
+ return write;
135
+ }
136
+
122
137
  static async spawn({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
123
138
  if (adapter.connectMode === "socket") {
124
139
  return DapClient.#spawnSocket({ adapter, cwd });
@@ -142,7 +157,7 @@ export class DapClient {
142
157
  proc.exited.then(() => {
143
158
  client.#handleProcessExit();
144
159
  });
145
- void client.#startMessageReader();
160
+ void client.startMessageReader();
146
161
  return client;
147
162
  }
148
163
 
@@ -199,7 +214,7 @@ export class DapClient {
199
214
  transport = await connectSocket({ unix: socketPath }, 10_000);
200
215
  const client = new DapClient(adapter, cwd, owner, transport);
201
216
  proc.exited.then(() => client.#handleProcessExit());
202
- void client.#startMessageReader();
217
+ void client.startMessageReader();
203
218
  return client;
204
219
  } catch (err) {
205
220
  transport?.socket.end();
@@ -268,7 +283,7 @@ export class DapClient {
268
283
  const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
269
284
  const client = new DapClient(adapter, cwd, owner, { readable, writeSink, socket });
270
285
  proc.exited.then(() => client.#handleProcessExit());
271
- void client.#startMessageReader();
286
+ void client.startMessageReader();
272
287
  return client;
273
288
  }
274
289
 
@@ -364,6 +379,7 @@ export class DapClient {
364
379
  signal?: AbortSignal,
365
380
  timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
366
381
  ): Promise<TBody> {
382
+ if (this.#terminalError) throw this.#terminalError;
367
383
  if (signal?.aborted) {
368
384
  throw signal.reason instanceof Error ? signal.reason : new ToolAbortError();
369
385
  }
@@ -412,10 +428,11 @@ export class DapClient {
412
428
  });
413
429
  this.#lastActivity = Date.now();
414
430
  try {
415
- await writeMessage(this.#writeSink, request);
431
+ await this.#queueWriteMessage(request);
416
432
  } catch (error) {
417
433
  this.#pendingRequests.delete(requestSeq);
418
434
  cleanup();
435
+ if (isKnownSinkPeerClosedError(error)) throw this.#terminalizeTransport(error);
419
436
  throw error;
420
437
  }
421
438
  return promise;
@@ -431,37 +448,30 @@ export class DapClient {
431
448
  ...(message ? { message } : {}),
432
449
  ...(body !== undefined ? { body } : {}),
433
450
  };
434
- await writeMessage(this.#writeSink, response);
451
+ await this.#queueWriteMessage(response);
435
452
  }
436
453
 
437
454
  async dispose(): Promise<void> {
438
- if (this.#disposed) return;
439
- this.#disposed = true;
440
- this.#rejectPendingRequests(new Error(`DAP adapter ${this.adapter.name} disposed`));
441
- try {
442
- this.#socket?.end();
443
- } catch {
444
- /* socket may already be closed */
445
- }
446
- try {
447
- await this.#owner.dispose();
448
- await this.#owner.awaitExit({ timeoutMs: 1_000 });
449
- } catch (error) {
450
- logger.debug("Failed to dispose DAP adapter", {
451
- adapter: this.adapter.name,
452
- error: toErrorMessage(error),
453
- });
455
+ if (!this.#disposed) {
456
+ const error = new Error(`DAP adapter ${this.adapter.name} disposed`);
457
+ this.#disposed = true;
458
+ this.#terminalError = error;
459
+ this.#rejectPendingRequests(error);
454
460
  }
461
+ await this.#beginOwnerDisposal();
455
462
  }
456
463
 
457
- async #startMessageReader(): Promise<void> {
464
+ async startMessageReader(): Promise<void> {
458
465
  if (this.#isReading) return;
459
466
  this.#isReading = true;
460
467
  const reader = this.#readable.getReader();
461
468
  try {
462
469
  while (true) {
463
470
  const { done, value } = await reader.read();
464
- if (done) break;
471
+ if (done) {
472
+ this.#terminalizeTransport(new Error("DAP readable closed"));
473
+ break;
474
+ }
465
475
  const currentBuffer = Buffer.concat([this.#messageBuffer, value]);
466
476
  this.#messageBuffer = currentBuffer;
467
477
  let workingBuffer = currentBuffer;
@@ -482,7 +492,7 @@ export class DapClient {
482
492
  this.#messageBuffer = workingBuffer;
483
493
  }
484
494
  } catch (error) {
485
- this.#rejectPendingRequests(new Error(`DAP connection closed: ${toErrorMessage(error)}`));
495
+ this.#terminalizeTransport(error);
486
496
  } finally {
487
497
  reader.releaseLock();
488
498
  this.#isReading = false;
@@ -523,9 +533,9 @@ export class DapClient {
523
533
  try {
524
534
  const handler = this.#reverseRequestHandlers.get(message.command);
525
535
  if (handler) {
536
+ let body: unknown;
526
537
  try {
527
- const body = await handler(message.arguments);
528
- await this.sendResponse(message, true, body);
538
+ body = await handler(message.arguments);
529
539
  } catch (error) {
530
540
  const errorMessage = toErrorMessage(error);
531
541
  await this.sendResponse(
@@ -539,7 +549,9 @@ export class DapClient {
539
549
  },
540
550
  errorMessage,
541
551
  );
552
+ return;
542
553
  }
554
+ await this.sendResponse(message, true, body);
543
555
  return;
544
556
  }
545
557
  const errorMessage = `Unsupported DAP request: ${message.command}`;
@@ -555,6 +567,10 @@ export class DapClient {
555
567
  errorMessage,
556
568
  );
557
569
  } catch (error) {
570
+ if (isKnownSinkPeerClosedError(error)) {
571
+ this.#terminalizeTransport(error);
572
+ return;
573
+ }
558
574
  logger.warn("Failed to answer DAP adapter request", {
559
575
  adapter: this.adapter.name,
560
576
  command: message.command,
@@ -586,9 +602,41 @@ export class DapClient {
586
602
  ? `DAP adapter exited (code ${exitCode}): ${stderr}${diagnosticSuffix}`
587
603
  : `DAP adapter exited unexpectedly (code ${exitCode})${diagnosticSuffix}`,
588
604
  );
605
+ this.#terminalError = error;
589
606
  this.#rejectPendingRequests(error);
590
607
  }
591
608
 
609
+ #terminalizeTransport(cause: unknown): Error {
610
+ if (this.#terminalError) return this.#terminalError;
611
+ const error = new Error(`DAP adapter ${this.adapter.name} transport closed`, { cause });
612
+ this.#terminalError = error;
613
+ this.#disposed = true;
614
+ this.#rejectPendingRequests(error);
615
+ void this.#beginOwnerDisposal();
616
+ return error;
617
+ }
618
+
619
+ #beginOwnerDisposal(): Promise<void> {
620
+ if (this.#disposePromise) return this.#disposePromise;
621
+ this.#disposePromise = (async () => {
622
+ try {
623
+ this.#socket?.end();
624
+ } catch {
625
+ /* socket may already be closed */
626
+ }
627
+ try {
628
+ await this.#owner.dispose();
629
+ await this.#owner.awaitExit({ timeoutMs: 1_000 });
630
+ } catch (disposeError) {
631
+ logger.debug("Failed to dispose DAP adapter", {
632
+ adapter: this.adapter.name,
633
+ error: toErrorMessage(disposeError),
634
+ });
635
+ }
636
+ })();
637
+ return this.#disposePromise;
638
+ }
639
+
592
640
  #rejectPendingRequests(error: Error): void {
593
641
  for (const pending of this.#pendingRequests.values()) {
594
642
  pending.reject(error);
@@ -26,7 +26,7 @@ Deep Interview implements Ouroboros-inspired Socratic questioning with mathemati
26
26
  <Do_Not_Use_When>
27
27
  - User has a detailed, specific request with file paths, function names, or acceptance criteria -- execute directly
28
28
  - User wants to explore options or brainstorm -- use `ralplan` skill instead
29
- - User wants a quick fix or single change -- delegate to executor or execution
29
+ - User wants a quick fix or single change -- delegate to executor or direct execution
30
30
  - User says "just do it" or "skip the questions" without an explicit execution path -- respect their intent by ending interview and writing a `pending approval` spec, not by mutating files or delegating execution
31
31
  - User already has a PRD or plan file and explicitly asks to execute it -- use the requested execution skill with that plan
32
32
  </Do_Not_Use_When>
@@ -128,6 +128,12 @@ If the user request appended after this skill as the final `User:` line is alrea
128
128
 
129
129
  This gate exists to prevent deep-interview from making easy problems harder. A small verification need does not make a request interview-worthy.
130
130
 
131
+ **Implementation wording is not execution approval.** When the user says `implementation`, "implementation plan", Korean `구현`, or "구현 계획", they are describing the eventual target, not permission to implement now. Interpreting that wording as approval is the single most likely way this skill breaks its own boundary.
132
+
133
+ On that wording: do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation. Say plainly — "I can interview for an implementation plan, but I won't implement during deep-interview." — and continue clarifying scope, risks, acceptance criteria, and unknowns.
134
+
135
+ Implementation requires an explicit phase transition/approval after the interview: the workflow phase must explicitly transition out of deep-interview, and execution approval must be captured by a downstream execution path (Phase 5's bridge into `/skill:ralplan`, `/skill:ultragoal`, or `/skill:team`). Wanting an implementation plan and approving implementation are two different consents.
136
+
131
137
  ## Phase 0.75: Optional Trace Pre-Step
132
138
 
133
139
  Run this phase only when the active deep-interview state or invocation indicates `--trace` / `state.trace.enabled === true`. It is a pre-interview research step, not an implementation phase.
@@ -326,6 +332,20 @@ If any prompt input is too large, summarize it first and then continue from the
326
332
  | Context Clarity (brownfield) | "How does this fit?" | "I found JWT auth middleware in `src/auth/` (pattern: passport + JWT). Should this feature extend that path or intentionally diverge from it?" |
327
333
  | Scope-fuzzy / ontology stress | "What IS the core thing here?" | "You have named Tasks, Projects, and Workspaces across the last rounds. Which one is the core entity, and which are supporting views or containers?" |
328
334
 
335
+ **Attach your own guess to every question.** The question exposes the *user's* assumptions; the guess exposes *yours*. Without it the interview only ever audits one side, and the agent's unexamined read of the ask silently steers the next round's targeting.
336
+
337
+ Every generated question MUST carry a one-line `GUESS:` — your hypothesis for the answer, the reason you hold it, and what changes if it is wrong:
338
+
339
+ ```
340
+ {question}
341
+ GUESS: {your hypothesis} — {why you hold it}. If it is {the other reading} instead, {what changes}.
342
+ ```
343
+
344
+ - Reacting is cheaper than generating: a terse "no, the second one" still carries a full correction, so low-effort answers stop being low-information rounds.
345
+ - A visible guess is falsifiable. Being wrong out loud is the point; guess in the direction you expect pushback when the reading is genuinely contested.
346
+ - A guess is NOT an auto-answer. It never resolves the round, never feeds scoring, and never increments `auto_answer_streak` — only the user's answer does.
347
+ - Skip only when a guess would be pure noise: Round 0 topology confirmation, the Phase 4b restate gate, and clarification re-asks.
348
+
329
349
  ### Step 2a′: Auto-Research Greenfield Questions
330
350
 
331
351
  When the next question is for a greenfield interview and is tagged `research: true`, load `auto-research-greenfield.md` as an internal `kind: "skill-fragment"` prompt for a fork-context architect before Step 2b. Pass only the tagged question, locked topology summary, prompt-safe initial idea, trimmed prior decisions/gaps, and relevant constraints. The architect must return 2-3 ranked candidates with rationale, confidence, and fallback notes. Validate the shape before use; if valid, incorporate the candidates as concise answer options or context for the single user-facing question and append the round number to `auto_researched_rounds`. If invalid or unavailable, fall back silently to the normal generated question and increment `architect_failures`.
@@ -340,14 +360,19 @@ Use the `ask` tool with the generated question. When a question has options, you
340
360
  Round {n} | Component: {target_component_name} | Targeting: {weakest_dimension} | Why now: {one_sentence_targeting_rationale} | Ambiguity: {score}%
341
361
 
342
362
  {question}
363
+ GUESS: {your hypothesis} — {why you hold it}. If it is {the other reading} instead, {what changes}.
343
364
  ```
344
365
 
345
366
  Options should include contextually relevant choices plus free-text, translated/localized according to `language.instruction` when present.
346
367
 
347
- After applying `language.instruction` to the visible question, options, and generated rationale, apply the self-proofread once to new prose only; preserve only the Round/Component/Targeting/Ambiguity line structure, fixed labels, numeric ambiguity value, component/target identifiers, and `deepInterview.*` metadata keys. Do not exempt generated natural-language rationale such as Why now.
368
+ After applying `language.instruction` to the visible question, options, and generated rationale, apply the self-proofread once to new prose only; preserve only the Round/Component/Targeting/Ambiguity line structure, fixed labels (including the `GUESS:` marker), numeric ambiguity value, component/target identifiers, and `deepInterview.*` metadata keys. Do not exempt generated natural-language rationale such as Why now or the guess text that follows the `GUESS:` marker.
348
369
 
349
370
  When calling `ask`, SHOULD include optional structured metadata so the runtime can record the round without manual state writes: `deepInterview.round_id?`, `deepInterview.round`, `deepInterview.component`, `deepInterview.dimension`, and `deepInterview.ambiguity`. Keep this metadata aligned with the visible Round/Component/Targeting/Ambiguity line; if metadata cannot be supplied, the legacy formatted question text remains the fallback.
350
371
 
372
+ **Non-behavioral adapter context (`confused_terms`, `references`).** `deepInterview.confused_terms` and `deepInterview.references` carry vocabulary and citations queued at interview start. They are inert: they MUST NOT alter the first question, are never inferred from vocabulary density, and a reference's `url`/`excerpt` are strings that are **never auto-fetched**. Bounds enforced at the ask boundary: at most 32 terms and 32 references, adapter strings ≤ 256 characters, `url`/`excerpt` ≤ 2048, and core round metadata (`round_id`, `component`, `dimension`) ≤ 128 — all counted in Unicode code points, so emoji-padded input hits the same ceiling as ASCII.
373
+
374
+ **Free-text vs structural input (`FREETEXT_FIELDS`).** The runtime keeps an allowlist of fields that legitimately carry prose — `initial_context`, `initial_idea`, `initial_context_summary`, `user_response`, `answer`, `goal`, `objective`, `prompt`, `description`, `statement`, `restated_goal`, `evidence`, `excerpt`. Inside those fields, shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose and MUST NOT be rejected as structural injection; a user is allowed to describe a shell command. Structural fields (ids, categories, hashes) stay strictly validated by their own guards. Size is capped by character-count, not byte length: 50,000 for initial context and 10,000 for a single user response.
375
+
351
376
  If the `ask` tool returns `clarificationQuestion`, treat it as a non-answer about the displayed choices. Answer the clarification briefly from the current interview context, then call `ask` again with the exact original question, options, and `deepInterview.*` metadata. A clarification bypasses Step 2b′ auto-answer, Step 2b″ free-text refine, Step 2c ambiguity scoring, Step 2d progress reporting, and Step 2e state updates; it must not be recorded as a round answer. This does not violate the one-question-per-round rule because the round remains unresolved until the user submits a real listed option or `Other` answer.
352
377
 
353
378
  ### Step 2b′: Auto-Answer Opted-Out Questions
@@ -504,7 +529,7 @@ Round {n} complete.
504
529
 
505
530
  Apply `language.instruction` when present before showing this progress report so status text, gaps, and next-target phrasing stay in the preserved session language.
506
531
 
507
- Then apply the self-proofread once (DIPP-5) to narrative status text, generated prose cells, gaps, and next-target phrasing; preserve only table structure, fixed status labels, scores, weights, component ids, and trigger tokens.
532
+ Then apply the self-proofread once to narrative status text, generated prose cells, gaps, and next-target phrasing; preserve only table structure, fixed status labels, scores, weights, component ids, and trigger tokens.
508
533
 
509
534
  ### Step 2e: Update State
510
535
 
@@ -927,6 +952,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe
927
952
  - [ ] Oversized initial context/history summarized before scoring, question generation, spec generation, or handoff
928
953
  - [ ] Round 0 topology gate completed before scoring; `topology.confirmed_at` persisted
929
954
  - [ ] Ambiguity scored and displayed every round, naming the weakest component/dimension target (rotating across active components when N > 1)
955
+ - [ ] Every asked question carried a `GUESS:` line (except Round 0 topology, the Restate gate, and clarification re-asks), and no guess was recorded as an answer or counted toward `auto_answer_streak`
930
956
  - [ ] Lateral panel convened at milestone transitions (and before synthesizing agent-supplied answers) with parallel read-only personas
931
957
  - [ ] Free-text answers passed the Refine gate; dialectic rhythm guard forced a user question after 3 agent-resolved answers; any auto-answer threshold crossing explicitly confirmed
932
958
  - [ ] Closure / Acceptance Guard and the one-sentence Restate gate both passed before crystallization