@gajae-code/coding-agent 0.7.1 → 0.7.3

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 (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/notify-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/config/keybindings.d.ts +2 -2
  7. package/dist/types/config/settings-schema.d.ts +39 -2
  8. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +1 -1
  11. package/dist/types/lsp/types.d.ts +2 -0
  12. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  13. package/dist/types/modes/components/model-selector.d.ts +2 -0
  14. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  15. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  16. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  17. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  18. package/dist/types/notifications/config.d.ts +9 -1
  19. package/dist/types/notifications/engine.d.ts +59 -0
  20. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  21. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  22. package/dist/types/notifications/telegram-daemon.d.ts +73 -16
  23. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  24. package/dist/types/notifications/threaded-render.d.ts +6 -1
  25. package/dist/types/notifications/topic-registry.d.ts +2 -0
  26. package/dist/types/session/agent-session.d.ts +2 -0
  27. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  28. package/dist/types/tools/fetch.d.ts +23 -0
  29. package/dist/types/tools/index.d.ts +1 -0
  30. package/dist/types/tools/telegram-send.d.ts +32 -0
  31. package/dist/types/web/insane/bridge.d.ts +103 -0
  32. package/dist/types/web/insane/url-guard.d.ts +25 -0
  33. package/dist/types/web/scrapers/types.d.ts +5 -0
  34. package/dist/types/web/scrapers/utils.d.ts +7 -1
  35. package/dist/types/web/search/provider.d.ts +18 -1
  36. package/dist/types/web/search/providers/insane.d.ts +53 -0
  37. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  38. package/dist/types/web/search/types.d.ts +12 -4
  39. package/package.json +10 -8
  40. package/scripts/verify-insane-vendor.ts +132 -0
  41. package/src/cli/args.ts +1 -1
  42. package/src/cli/fast-help.ts +1 -1
  43. package/src/cli/mcp-cli.ts +272 -0
  44. package/src/cli/notify-cli.ts +152 -5
  45. package/src/cli.ts +6 -2
  46. package/src/commands/mcp.ts +117 -0
  47. package/src/commands/team.ts +1 -1
  48. package/src/config/keybindings.ts +2 -2
  49. package/src/config/settings-schema.ts +30 -1
  50. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  51. package/src/defaults/gjc/skills/deep-interview/SKILL.md +4 -3
  52. package/src/defaults/gjc/skills/ralplan/SKILL.md +11 -4
  53. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  54. package/src/extensibility/extensions/runner.ts +1 -0
  55. package/src/extensibility/shared-events.ts +1 -0
  56. package/src/gjc-runtime/launch-tmux.ts +17 -3
  57. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  58. package/src/gjc-runtime/ralplan-runtime.ts +2 -2
  59. package/src/gjc-runtime/tmux-common.ts +3 -1
  60. package/src/gjc-runtime/ultragoal-guard.ts +25 -8
  61. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  62. package/src/gjc-runtime/workflow-manifest.ts +7 -2
  63. package/src/hooks/skill-state.ts +57 -0
  64. package/src/internal-urls/docs-index.generated.ts +14 -11
  65. package/src/lsp/config.ts +16 -3
  66. package/src/lsp/defaults.json +7 -0
  67. package/src/lsp/types.ts +2 -0
  68. package/src/modes/bridge/bridge-mode.ts +11 -0
  69. package/src/modes/components/custom-editor.ts +2 -0
  70. package/src/modes/components/footer.ts +2 -3
  71. package/src/modes/components/model-selector.ts +12 -0
  72. package/src/modes/components/status-line/git-utils.ts +25 -0
  73. package/src/modes/components/status-line.ts +10 -11
  74. package/src/modes/components/welcome.ts +2 -3
  75. package/src/modes/controllers/event-controller.ts +15 -0
  76. package/src/modes/controllers/selector-controller.ts +3 -0
  77. package/src/modes/interactive-mode.ts +48 -3
  78. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  79. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  80. package/src/modes/theme/defaults/index.ts +2 -0
  81. package/src/modes/utils/context-usage.ts +2 -2
  82. package/src/notifications/attachment-registry.ts +23 -0
  83. package/src/notifications/chat-adapters.ts +147 -0
  84. package/src/notifications/config.ts +23 -2
  85. package/src/notifications/engine.ts +100 -0
  86. package/src/notifications/index.ts +180 -38
  87. package/src/notifications/managed-daemon.ts +163 -0
  88. package/src/notifications/operator-runtime.ts +171 -0
  89. package/src/notifications/telegram-daemon.ts +553 -236
  90. package/src/notifications/threaded-inbound.ts +60 -4
  91. package/src/notifications/threaded-render.ts +20 -2
  92. package/src/notifications/topic-registry.ts +5 -0
  93. package/src/session/agent-session.ts +82 -51
  94. package/src/slash-commands/helpers/parse.ts +2 -1
  95. package/src/tools/bash.ts +9 -0
  96. package/src/tools/composer-bash-policy.ts +96 -0
  97. package/src/tools/fetch.ts +94 -1
  98. package/src/tools/index.ts +3 -0
  99. package/src/tools/telegram-send.ts +137 -0
  100. package/src/web/insane/bridge.ts +350 -0
  101. package/src/web/insane/url-guard.ts +159 -0
  102. package/src/web/scrapers/types.ts +143 -45
  103. package/src/web/scrapers/utils.ts +70 -19
  104. package/src/web/search/provider.ts +77 -18
  105. package/src/web/search/providers/anthropic.ts +70 -3
  106. package/src/web/search/providers/codex.ts +1 -119
  107. package/src/web/search/providers/gemini.ts +99 -0
  108. package/src/web/search/providers/insane.ts +551 -0
  109. package/src/web/search/providers/openai-compatible.ts +66 -32
  110. package/src/web/search/providers/text-citations.ts +111 -0
  111. package/src/web/search/types.ts +13 -2
  112. package/vendor/insane-search/LICENSE +21 -0
  113. package/vendor/insane-search/MANIFEST.json +24 -0
  114. package/vendor/insane-search/engine/__init__.py +23 -0
  115. package/vendor/insane-search/engine/__main__.py +128 -0
  116. package/vendor/insane-search/engine/bias_check.py +183 -0
  117. package/vendor/insane-search/engine/executor.py +254 -0
  118. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  119. package/vendor/insane-search/engine/learning.py +175 -0
  120. package/vendor/insane-search/engine/phase0.py +214 -0
  121. package/vendor/insane-search/engine/safety.py +91 -0
  122. package/vendor/insane-search/engine/templates/package.json +11 -0
  123. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  124. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  125. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  126. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  127. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  128. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  129. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  130. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  131. package/vendor/insane-search/engine/transport.py +211 -0
  132. package/vendor/insane-search/engine/url_transforms.py +98 -0
  133. package/vendor/insane-search/engine/validators.py +331 -0
  134. package/vendor/insane-search/engine/waf_detector.py +214 -0
  135. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -217,6 +217,7 @@ export interface AutoCompactionEndEvent {
217
217
  errorMessage?: string;
218
218
  /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
219
219
  skipped?: boolean;
220
+ continuationSkipReason?: "auto_continue_disabled_non_resumable_tail";
220
221
  }
221
222
 
222
223
  /** Fired when auto-retry starts */
@@ -93,6 +93,10 @@ function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolea
93
93
  return session?.version === VERSION;
94
94
  }
95
95
 
96
+ function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean {
97
+ return Boolean(parsed.continue || parsed.resume || explicitTmuxSessionName(env));
98
+ }
99
+
96
100
  function findExistingSessionForLaunch(context: {
97
101
  env: NodeJS.ProcessEnv;
98
102
  project: string;
@@ -352,6 +356,11 @@ function readCurrentBranch(cwd: string): string | null {
352
356
  function cleanupCreatedTmuxSession(plan: TmuxLaunchPlan, spawnSync: TmuxSpawnSync, options: TmuxSpawnOptions): void {
353
357
  spawnSync(plan.tmuxCommand, ["kill-session", "-t", `=${plan.sessionName}`], options);
354
358
  }
359
+ function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
360
+ if (result.signalCode === "SIGHUP") return true;
361
+ const stderr = result.stderr?.toLowerCase() ?? "";
362
+ return stderr.includes("eio") || stderr.includes("input/output error");
363
+ }
355
364
 
356
365
  export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaunchPlan | undefined {
357
366
  const env = context.env ?? process.env;
@@ -377,14 +386,15 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
377
386
  tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
378
387
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
379
388
  if (!tmuxAvailable) return undefined;
380
- const existingSessionName =
381
- "existingBranchSessionName" in context
389
+ const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
390
+ ? "existingBranchSessionName" in context
382
391
  ? (context.existingBranchSessionName ?? undefined)
383
392
  : findExistingSessionForLaunch({
384
393
  env,
385
394
  project,
386
395
  branch,
387
- });
396
+ })
397
+ : undefined;
388
398
  const innerCommand = buildInnerCommand(
389
399
  {
390
400
  cwd,
@@ -478,6 +488,10 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
478
488
  if (created.exitCode !== 0) return false;
479
489
  const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
480
490
  if (attached.exitCode === 0) return true;
491
+ if (isTmuxAttachDisconnectError(attached)) {
492
+ (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
493
+ return true;
494
+ }
481
495
  cleanupCreatedTmuxSession(plan, spawnSync, options);
482
496
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
483
497
  return true;
@@ -93,6 +93,7 @@ const RALPLAN_STAGE_CODES: Record<string, string> = {
93
93
  architect: "A",
94
94
  critic: "C",
95
95
  adr: "D",
96
+ "post-interview": "I",
96
97
  final: "F",
97
98
  };
98
99
 
@@ -37,7 +37,7 @@ import {
37
37
  *
38
38
  * 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N> --artifact
39
39
  * <path-or-string> [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
40
- * / Critic / revision / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
40
+ * / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
41
41
  * an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
42
42
  * the HUD chip to reflect the latest persisted stage.
43
43
  */
@@ -48,7 +48,7 @@ export interface RalplanCommandResult {
48
48
  stderr?: string;
49
49
  }
50
50
 
51
- const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "adr", "final"] as const;
51
+ const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"] as const;
52
52
  type RalplanStage = (typeof KNOWN_STAGES)[number];
53
53
 
54
54
  const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
@@ -55,7 +55,9 @@ export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
55
55
  return (
56
56
  `the active multiplexer "${tmuxCommand}" lists this session but did not return GJC's ${GJC_TMUX_PROFILE_OPTION} ownership tag; ` +
57
57
  "GJC-managed sessions and `gjc team` require a tmux provider that round-trips tmux user options. " +
58
- "Alternative multiplexers such as psmux on Windows do not persist user options yet, so the Windows-native psmux path is not fully supported; " +
58
+ "For psmux on Windows, cwd/start-directory flags such as `-c` do not isolate the server namespace; psmux uses the tmux-compatible global `-L <namespace>` flag for that. " +
59
+ "GJC_TMUX_COMMAND and GJC_TEAM_TMUX_COMMAND are binary overrides, not shell command lines, so `psmux -L name` is not a supported value. " +
60
+ "Alternative multiplexers such as psmux on Windows do not reliably persist user options yet, so the Windows-native psmux path is not fully supported; " +
59
61
  "use real tmux for GJC-managed session and team flows."
60
62
  );
61
63
  }
@@ -65,15 +65,17 @@ function isKnownUltragoalObjective(currentObjective: string): boolean {
65
65
  );
66
66
  }
67
67
 
68
- async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
68
+ async function ultragoalReadPaths(cwd: string): Promise<{ paths: UltragoalPaths; sessionId: string | null }> {
69
69
  const envSessionId = process.env.GJC_SESSION_ID?.trim();
70
- if (envSessionId) return getUltragoalPaths(cwd, envSessionId);
70
+ if (envSessionId) return { paths: getUltragoalPaths(cwd, envSessionId), sessionId: envSessionId };
71
71
  try {
72
72
  const session = await resolveGjcSessionForRead(cwd, { envSessionId: process.env.GJC_SESSION_ID });
73
- return getUltragoalPaths(cwd, session.gjcSessionId);
73
+ return { paths: getUltragoalPaths(cwd, session.gjcSessionId), sessionId: session.gjcSessionId };
74
74
  } catch (error) {
75
75
  if (error instanceof SessionResolutionError && error.code === "no_session") {
76
- return getUltragoalPaths(cwd, null);
76
+ // No session could be resolved (no env, no auto-detectable active session).
77
+ // Surface the null session id so callers can decide; ask-guard treats it as inactive.
78
+ return { paths: getUltragoalPaths(cwd, null), sessionId: null };
77
79
  }
78
80
  throw error;
79
81
  }
@@ -82,7 +84,7 @@ async function ultragoalReadPaths(cwd: string): Promise<UltragoalPaths> {
82
84
  async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
83
85
  let paths: UltragoalPaths;
84
86
  try {
85
- paths = await ultragoalReadPaths(cwd);
87
+ ({ paths } = await ultragoalReadPaths(cwd));
86
88
  } catch (error) {
87
89
  if (error instanceof SessionResolutionError) return true;
88
90
  throw error;
@@ -368,14 +370,26 @@ export async function readUltragoalVerificationState(input: {
368
370
 
369
371
  export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
370
372
  let paths: UltragoalPaths;
373
+ let sessionId: string | null;
371
374
  try {
372
- paths = await ultragoalReadPaths(cwd);
375
+ ({ paths, sessionId } = await ultragoalReadPaths(cwd));
373
376
  } catch (error) {
374
377
  return activeAskDiagnostic({
375
378
  reason: `Unable to resolve durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
376
379
  source: "durable_state_unreadable",
377
380
  });
378
381
  }
382
+ // Ultragoal state is session-scoped. When no session can be resolved (no env,
383
+ // no auto-detectable active session) there is no active run to protect, so the
384
+ // ask guard must fall open rather than block on legacy/global durable state.
385
+ if (sessionId === null) {
386
+ return inactiveAskDiagnostic({
387
+ reason: "No active GJC session resolved; ultragoal is inactive.",
388
+ source: "absent",
389
+ goalsPath: paths.goalsPath,
390
+ ledgerPath: paths.ledgerPath,
391
+ });
392
+ }
379
393
  try {
380
394
  await fs.stat(paths.dir);
381
395
  } catch (error) {
@@ -398,8 +412,8 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
398
412
  let plan: UltragoalPlan | null;
399
413
  let ledger: UltragoalLedgerEvent[];
400
414
  try {
401
- plan = await readUltragoalPlan(cwd);
402
- ledger = await readUltragoalLedger(cwd);
415
+ plan = await readUltragoalPlan(cwd, sessionId);
416
+ ledger = await readUltragoalLedger(cwd, sessionId);
403
417
  } catch (error) {
404
418
  return activeAskDiagnostic({
405
419
  reason: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
@@ -409,6 +423,9 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
409
423
  });
410
424
  }
411
425
  if (!plan) {
426
+ // goals.json absent or empty while the state dir exists is an inconsistent
427
+ // durable state, not a clean "no run". Fail closed so the pause guard (which
428
+ // relies on this `durable_state_unreadable` signal) keeps blocking give-ups.
412
429
  return activeAskDiagnostic({
413
430
  reason: "Durable .gjc/ultragoal state exists but goals.json is missing or empty.",
414
431
  source: "durable_state_unreadable",
@@ -369,6 +369,9 @@
369
369
  {
370
370
  "id": "revision"
371
371
  },
372
+ {
373
+ "id": "post-interview"
374
+ },
372
375
  {
373
376
  "id": "adr"
374
377
  },
@@ -401,6 +404,26 @@
401
404
  "to": "revision",
402
405
  "verb": "write-artifact"
403
406
  },
407
+ {
408
+ "from": "revision",
409
+ "to": "post-interview",
410
+ "verb": "write-artifact"
411
+ },
412
+ {
413
+ "from": "critic",
414
+ "to": "post-interview",
415
+ "verb": "write-artifact"
416
+ },
417
+ {
418
+ "from": "post-interview",
419
+ "to": "revision",
420
+ "verb": "write-artifact"
421
+ },
422
+ {
423
+ "from": "post-interview",
424
+ "to": "adr",
425
+ "verb": "write-artifact"
426
+ },
404
427
  {
405
428
  "from": "revision",
406
429
  "to": "adr",
@@ -435,6 +458,11 @@
435
458
  "from": "adr",
436
459
  "to": "handoff",
437
460
  "verb": "handoff"
461
+ },
462
+ {
463
+ "from": "post-interview",
464
+ "to": "handoff",
465
+ "verb": "handoff"
438
466
  }
439
467
  ],
440
468
  "typedArgs": [
@@ -592,6 +620,7 @@
592
620
  "architect",
593
621
  "critic",
594
622
  "revision",
623
+ "post-interview",
595
624
  "adr",
596
625
  "final"
597
626
  ],
@@ -172,12 +172,16 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
172
172
  }),
173
173
  ralplan: manifest({
174
174
  skill: "ralplan",
175
- states: ["planner", "architect", "critic", "revision", "adr", "final", "handoff"],
175
+ states: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final", "handoff"],
176
176
  terminalStates: ["final", "handoff"],
177
177
  transitions: [
178
178
  { from: "planner", to: "architect", verb: "write-artifact" },
179
179
  { from: "architect", to: "critic", verb: "write-artifact" },
180
180
  { from: "critic", to: "revision", verb: "write-artifact" },
181
+ { from: "revision", to: "post-interview", verb: "write-artifact" },
182
+ { from: "critic", to: "post-interview", verb: "write-artifact" },
183
+ { from: "post-interview", to: "revision", verb: "write-artifact" },
184
+ { from: "post-interview", to: "adr", verb: "write-artifact" },
181
185
  { from: "revision", to: "adr", verb: "write-artifact" },
182
186
  { from: "adr", to: "final", verb: "write-artifact" },
183
187
  { from: "planner", to: "handoff", verb: "handoff" },
@@ -185,6 +189,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
185
189
  { from: "critic", to: "handoff", verb: "handoff" },
186
190
  { from: "revision", to: "handoff", verb: "handoff" },
187
191
  { from: "adr", to: "handoff", verb: "handoff" },
192
+ { from: "post-interview", to: "handoff", verb: "handoff" },
188
193
  ],
189
194
  verbs: [...stateVerbs(), ...flagVerbs(["kickoff", "write-artifact"]), ...plannedVerbs(PLANNED_ADMIN_VERBS)],
190
195
  typedArgs: [
@@ -196,7 +201,7 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
196
201
  {
197
202
  name: "stage",
198
203
  type: "enum",
199
- enumValues: ["planner", "architect", "critic", "revision", "adr", "final"],
204
+ enumValues: ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"],
200
205
  appliesToVerbs: ["write-artifact"],
201
206
  },
202
207
  { name: "stage_n", type: "number", appliesToVerbs: ["write-artifact"] },
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { logger } from "@gajae-code/utils";
4
4
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
5
+ import { detectDeepInterviewPlaintextAskLeak } from "../deep-interview/plaintext-gate-guard";
5
6
  import { activeSnapshotPath, modeStatePath as sessionModeStatePath } from "../gjc-runtime/session-layout";
6
7
  import { resolveGjcSessionForRead } from "../gjc-runtime/session-resolution";
7
8
  import { ModeStateSchema, SkillActiveStateSchema } from "../gjc-runtime/state-schema";
@@ -675,6 +676,52 @@ async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | und
675
676
  return typeof objective === "string" && objective.trim().length > 0 ? objective.trim() : null;
676
677
  }
677
678
 
679
+ async function readLatestAssistantTextFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
680
+ const trimmed = sessionFile?.trim();
681
+ if (!trimmed) return null;
682
+ let entries: SessionEntry[];
683
+ try {
684
+ entries = (await loadEntriesFromFile(trimmed)).filter((entry): entry is SessionEntry => entry.type !== "session");
685
+ } catch {
686
+ return null;
687
+ }
688
+ if (entries.length === 0) return null;
689
+ const context = buildSessionContext(entries);
690
+ for (let index = context.messages.length - 1; index >= 0; index--) {
691
+ const message = context.messages[index];
692
+ if (message?.role !== "assistant") continue;
693
+ const text = message.content
694
+ .filter(block => block.type === "text")
695
+ .map(block => block.text)
696
+ .join("");
697
+ const trimmedText = text.trim();
698
+ return trimmedText.length > 0 ? trimmedText : null;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ async function shouldRescueDeepInterviewPlaintextAskLeak(
704
+ skill: GjcWorkflowSkill,
705
+ state: ModeState | null,
706
+ cwd: string,
707
+ sessionFile: string | undefined,
708
+ ): Promise<boolean> {
709
+ if (skill !== "deep-interview") return false;
710
+ if (state?.active !== true) return false;
711
+ const phase = String(state.current_phase ?? "")
712
+ .trim()
713
+ .toLowerCase();
714
+ if (DEEP_INTERVIEW_ABORT_PHASES.has(phase)) return false;
715
+ if (await deepInterviewSpecCrystallized(state, cwd)) return false;
716
+ const latestAssistantText = await readLatestAssistantTextFromSessionFile(sessionFile);
717
+ if (!latestAssistantText) return false;
718
+ return detectDeepInterviewPlaintextAskLeak(latestAssistantText) !== null;
719
+ }
720
+
721
+ function buildDeepInterviewPlaintextAskLeakMessage(statePath: string): string {
722
+ return `GJC deep-interview emitted a Deep Interview question/options block as plain text (${statePath}). It must not wait for a prose answer. Continue immediately by calling the ask tool with the Restate gate question and options: Yes, crystallize; Adjust wording; Missing scope; plus free text/custom input.`;
723
+ }
724
+
678
725
  export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitStateInput): Promise<string | null> {
679
726
  const resolvedSessionId = await resolveBoundarySessionId(input.cwd, input.sessionId);
680
727
  const visibleModeState = await readVisibleModeState(input.cwd, "ultragoal", resolvedSessionId, input.stateDir);
@@ -756,6 +803,16 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
756
803
  systemMessage: recoveryMessage,
757
804
  };
758
805
  }
806
+ if (await shouldRescueDeepInterviewPlaintextAskLeak(entry.skill, modeState, input.cwd, input.sessionFile)) {
807
+ const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
808
+ const rescueMessage = buildDeepInterviewPlaintextAskLeakMessage(statePath);
809
+ return {
810
+ decision: "block",
811
+ reason: rescueMessage,
812
+ stopReason: "gjc_skill_deep_interview_plaintext_ask_leak",
813
+ systemMessage: rescueMessage,
814
+ };
815
+ }
759
816
  if (modeStateReleasesStop(modeState, handoffRequired)) {
760
817
  // A mode-state that claims it releases the Stop block must agree with
761
818
  // authoritative durable state. If a stale/incoherent mode-state would