@gajae-code/coding-agent 0.8.0 → 0.8.2

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 (116) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/completion-cli.d.ts +41 -0
  3. package/dist/types/cli/config-cli.d.ts +1 -0
  4. package/dist/types/cli/notify-cli.d.ts +7 -0
  5. package/dist/types/cli.d.ts +124 -1
  6. package/dist/types/commands/completion.d.ts +29 -0
  7. package/dist/types/commands/config.d.ts +3 -0
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +7 -6
  10. package/dist/types/config/keybindings.d.ts +2 -2
  11. package/dist/types/config/model-profile-activation.d.ts +6 -0
  12. package/dist/types/config/settings.d.ts +8 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  14. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  17. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
  19. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  20. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  21. package/dist/types/main.d.ts +4 -1
  22. package/dist/types/modes/components/model-selector.d.ts +4 -0
  23. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  26. package/dist/types/modes/print-mode.d.ts +13 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  28. package/dist/types/notifications/config.d.ts +8 -1
  29. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  30. package/dist/types/notifications/index.d.ts +6 -3
  31. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  32. package/dist/types/notifications/telegram-daemon.d.ts +18 -3
  33. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  34. package/dist/types/runtime-api-key.d.ts +7 -0
  35. package/dist/types/session/agent-session.d.ts +10 -0
  36. package/dist/types/session/agent-storage.d.ts +1 -0
  37. package/dist/types/task/executor.d.ts +2 -0
  38. package/dist/types/tools/computer.d.ts +2 -2
  39. package/dist/types/tools/skill.d.ts +1 -1
  40. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  41. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  42. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  43. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  44. package/package.json +7 -7
  45. package/src/cli/completion-cli.ts +234 -0
  46. package/src/cli/config-cli.ts +37 -9
  47. package/src/cli/fast-help.ts +1 -0
  48. package/src/cli/notify-cli.ts +91 -7
  49. package/src/cli.ts +3 -1
  50. package/src/commands/completion.ts +84 -0
  51. package/src/commands/config.ts +2 -0
  52. package/src/commands/deep-interview.ts +2 -1
  53. package/src/config/file-lock.ts +73 -34
  54. package/src/config/keybindings.ts +2 -2
  55. package/src/config/model-profile-activation.ts +65 -0
  56. package/src/config/model-registry.ts +14 -2
  57. package/src/config/settings.ts +22 -0
  58. package/src/coordinator-mcp/server.ts +67 -12
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
  61. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  62. package/src/extensibility/extensions/runner.ts +2 -0
  63. package/src/extensibility/extensions/types.ts +9 -0
  64. package/src/extensibility/skills.ts +30 -2
  65. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  66. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  67. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  68. package/src/gjc-runtime/deep-interview-state.ts +13 -0
  69. package/src/gjc-runtime/session-state-sidecar.ts +59 -11
  70. package/src/gjc-runtime/state-runtime.ts +12 -2
  71. package/src/gjc-runtime/team-runtime.ts +23 -0
  72. package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
  73. package/src/hooks/native-skill-hook.ts +29 -0
  74. package/src/internal-urls/docs-index.generated.ts +2 -2
  75. package/src/main.ts +52 -41
  76. package/src/modes/acp/acp-agent.ts +7 -6
  77. package/src/modes/components/model-selector.ts +54 -11
  78. package/src/modes/components/queued-message-selector.ts +92 -0
  79. package/src/modes/components/settings-selector.ts +4 -0
  80. package/src/modes/components/welcome.ts +160 -44
  81. package/src/modes/controllers/command-controller.ts +4 -4
  82. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  83. package/src/modes/controllers/input-controller.ts +186 -25
  84. package/src/modes/controllers/selector-controller.ts +122 -27
  85. package/src/modes/interactive-mode.ts +43 -16
  86. package/src/modes/print-mode.ts +43 -4
  87. package/src/modes/prompt-action-autocomplete.ts +24 -13
  88. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  89. package/src/modes/utils/ui-helpers.ts +2 -2
  90. package/src/notifications/config.ts +9 -1
  91. package/src/notifications/daemon-paths.ts +2 -0
  92. package/src/notifications/index.ts +39 -9
  93. package/src/notifications/lifecycle-commands.ts +37 -7
  94. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  95. package/src/notifications/telegram-daemon-control.ts +3 -1
  96. package/src/notifications/telegram-daemon.ts +231 -43
  97. package/src/notifications/telegram-reference.ts +0 -16
  98. package/src/prompts/system/system-prompt.md +3 -0
  99. package/src/prompts/tools/computer.md +2 -2
  100. package/src/runtime-api-key.ts +16 -0
  101. package/src/sdk.ts +20 -6
  102. package/src/session/agent-session.ts +225 -31
  103. package/src/session/agent-storage.ts +4 -0
  104. package/src/session/session-manager.ts +44 -2
  105. package/src/slash-commands/builtin-registry.ts +153 -71
  106. package/src/task/executor.ts +7 -4
  107. package/src/task/index.ts +9 -3
  108. package/src/task/output-manager.ts +16 -6
  109. package/src/task/receipt.ts +26 -7
  110. package/src/tools/computer.ts +246 -43
  111. package/src/tools/skill.ts +1 -1
  112. package/src/tools/telegram-send.ts +6 -0
  113. package/src/workflow/workflow-intent-diff.ts +29 -6
  114. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  115. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  116. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -1,3 +1,4 @@
1
+ import type { Stats } from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import { isEnoent } from "@gajae-code/utils/fs-error";
3
4
 
@@ -22,9 +23,10 @@ function getLockPath(filePath: string): string {
22
23
  return `${filePath}.lock`;
23
24
  }
24
25
 
25
- async function writeLockInfo(lockPath: string): Promise<void> {
26
+ async function writeLockInfo(lockPath: string): Promise<LockInfo> {
26
27
  const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
27
28
  await Bun.write(`${lockPath}/info`, JSON.stringify(info));
29
+ return info;
28
30
  }
29
31
 
30
32
  async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
@@ -51,17 +53,30 @@ export interface FileLockOwnerToken {
51
53
  timestamp: number;
52
54
  }
53
55
 
54
- /** Outcome of a guarded GC removal attempt (`removeFileLockDirForGc`). */
56
+ /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
55
57
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
56
58
 
59
+ interface LockDirStatToken {
60
+ dev: number;
61
+ ino: number;
62
+ mtimeMs: number;
63
+ ctimeMs: number;
64
+ }
65
+
66
+ type LockStaleSnapshot =
67
+ | { stale: false }
68
+ | { stale: true; owner: FileLockOwnerToken }
69
+ | { stale: true; owner: null; stat: LockDirStatToken };
70
+
57
71
  /**
58
72
  * @internal
59
- * Fail-closed removal of a dead lock dir for GC. Re-reads the on-disk owner
60
- * token as close to the unlink as possible and only deletes the dir when it
61
- * STILL holds the exact `{pid, timestamp}` identity the caller observed dead.
73
+ * Fail-closed removal of a lock dir whose owner is expected to be dead or
74
+ * finished. Re-reads the on-disk owner token as close to the unlink as possible
75
+ * and only deletes the dir when it STILL holds the exact `{pid, timestamp}`
76
+ * identity the caller observed.
62
77
  *
63
- * Closes the prune-time TOCTOU window (#606): between GC's dead re-read/probe
64
- * and the unlink, a live process can reclaim a stale lock at the same path
78
+ * Closes stale-cleanup TOCTOU windows (#606): between a dead/stale re-read and
79
+ * the unlink, a live process can reclaim a stale lock at the same path
65
80
  * (`acquireLock` rms the stale dir, then re-`mkdir`s and rewrites `info` with a
66
81
  * fresh pid+timestamp). Deleting by path alone would reap that LIVE lock. Any
67
82
  * mismatch (`owner_changed`) or absent/unreadable info (`missing` — e.g. a
@@ -99,14 +114,28 @@ function ownerLiveness(pid: number): OwnerLiveness {
99
114
  }
100
115
  }
101
116
 
102
- async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
117
+ function statToken(stats: Stats): LockDirStatToken {
118
+ return {
119
+ dev: stats.dev,
120
+ ino: stats.ino,
121
+ mtimeMs: stats.mtimeMs,
122
+ ctimeMs: stats.ctimeMs,
123
+ };
124
+ }
125
+
126
+ function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
127
+ return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
128
+ }
129
+
130
+ async function staleLockSnapshot(lockPath: string, staleMs: number): Promise<LockStaleSnapshot> {
103
131
  const info = await readLockInfo(lockPath);
104
132
  if (!info) {
105
133
  try {
106
134
  const stats = await fs.stat(lockPath);
107
- return Date.now() - stats.mtimeMs > staleMs;
135
+ if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
136
+ return { stale: true, owner: null, stat: statToken(stats) };
108
137
  } catch (err) {
109
- if (isEnoent(err)) return false;
138
+ if (isEnoent(err)) return { stale: false };
110
139
  throw err;
111
140
  }
112
141
  }
@@ -115,53 +144,63 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
115
144
  // not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
116
145
  // liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
117
146
  const liveness = ownerLiveness(info.pid);
118
- if (liveness === "dead") return true;
119
- if (liveness === "alive") return false;
120
- return Date.now() - info.timestamp > staleMs;
147
+ if (liveness === "alive") return { stale: false };
148
+ if (liveness === "dead" || Date.now() - info.timestamp > staleMs) {
149
+ return { stale: true, owner: { pid: info.pid, timestamp: info.timestamp } };
150
+ }
151
+ return { stale: false };
121
152
  }
122
153
 
123
- async function tryAcquireLock(lockPath: string): Promise<boolean> {
154
+ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
155
+ if (!snapshot.stale) return false;
156
+ if (snapshot.owner) {
157
+ return (await removeFileLockDirForGc(lockPath, snapshot.owner)) === "removed";
158
+ }
159
+
160
+ const currentInfo = await readLockInfo(lockPath);
161
+ if (currentInfo) return false;
124
162
  try {
125
- await fs.mkdir(lockPath);
126
- await writeLockInfo(lockPath);
163
+ const currentStats = await fs.stat(lockPath);
164
+ if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
165
+ await fs.rm(lockPath, { recursive: true, force: true });
127
166
  return true;
167
+ } catch (err) {
168
+ if (isEnoent(err)) return false;
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
174
+ try {
175
+ await fs.mkdir(lockPath);
176
+ return await writeLockInfo(lockPath);
128
177
  } catch (error) {
129
178
  if ((error as NodeJS.ErrnoException).code === "EEXIST") {
130
- return false;
179
+ return null;
131
180
  }
132
181
  throw error;
133
182
  }
134
183
  }
135
184
 
136
- async function releaseLock(lockPath: string): Promise<void> {
185
+ async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise<void> {
137
186
  try {
138
- await fs.rm(lockPath, { recursive: true });
187
+ await removeFileLockDirForGc(lockPath, owner);
139
188
  } catch {
140
189
  // Ignore errors on release
141
190
  }
142
191
  }
143
-
144
- async function lockExists(lockPath: string): Promise<boolean> {
145
- try {
146
- await fs.stat(lockPath);
147
- return true;
148
- } catch (err) {
149
- if (isEnoent(err)) return false;
150
- throw err;
151
- }
152
- }
153
-
154
192
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
155
193
  const opts = { ...DEFAULT_OPTIONS, ...options };
156
194
  const lockPath = getLockPath(filePath);
157
195
 
158
196
  for (let attempt = 0; attempt < opts.retries; attempt++) {
159
- if (await tryAcquireLock(lockPath)) {
160
- return () => releaseLock(lockPath);
197
+ const owner = await tryAcquireLock(lockPath);
198
+ if (owner) {
199
+ return () => releaseLock(lockPath, owner);
161
200
  }
162
201
 
163
- if ((await lockExists(lockPath)) && (await isLockStale(lockPath, opts.staleMs))) {
164
- await releaseLock(lockPath);
202
+ const stale = await staleLockSnapshot(lockPath, opts.staleMs);
203
+ if (await removeStaleLockForAcquire(lockPath, stale)) {
165
204
  continue;
166
205
  }
167
206
 
@@ -134,8 +134,8 @@ export const KEYBINDINGS = {
134
134
  description: "Queue message for next turn",
135
135
  },
136
136
  "app.message.dequeue": {
137
- defaultKeys: "alt+up",
138
- description: "Dequeue message",
137
+ defaultKeys: ["alt+up", "alt+down"],
138
+ description: "Select queued message to edit",
139
139
  },
140
140
  "app.clipboard.pasteImage": {
141
141
  defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
@@ -76,6 +76,34 @@ export interface MaterializeModelProfileAssignmentOptions {
76
76
  selector: string;
77
77
  }
78
78
 
79
+ export interface MaterializeModelProfileAssignmentsOptions {
80
+ session: Pick<
81
+ ModelProfileActivationSession,
82
+ "model" | "thinkingLevel" | "setActiveModelProfile" | "getActiveModelProfile"
83
+ >;
84
+ settings: Pick<Settings, "clearOverride" | "get" | "override" | "set">;
85
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
86
+ }
87
+
88
+ function isReadonlyAssignmentMap(
89
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
90
+ ): assignments is ReadonlyMap<GjcModelAssignmentTargetId, string> {
91
+ return typeof (assignments as { entries?: unknown }).entries === "function";
92
+ }
93
+
94
+ function getMaterializedAssignments(
95
+ assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>,
96
+ ): Array<[GjcModelAssignmentTargetId, string]> {
97
+ if (isReadonlyAssignmentMap(assignments)) return [...assignments.entries()];
98
+ const assignmentRecord: Partial<Record<GjcModelAssignmentTargetId, string>> = assignments;
99
+ const result: Array<[GjcModelAssignmentTargetId, string]> = [];
100
+ for (const role of Object.keys(assignmentRecord) as GjcModelAssignmentTargetId[]) {
101
+ const selector = assignmentRecord[role];
102
+ if (selector !== undefined) result.push([role, selector]);
103
+ }
104
+ return result;
105
+ }
106
+
79
107
  export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
80
108
  const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
81
109
  if (!activeProfile) return false;
@@ -109,6 +137,43 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
109
137
  return true;
110
138
  }
111
139
 
140
+ export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
141
+ const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
142
+ if (!activeProfile) return false;
143
+
144
+ const materializedAssignments = getMaterializedAssignments(options.assignments);
145
+ if (materializedAssignments.length === 0) return true;
146
+
147
+ const nextModelRoles = { ...options.settings.get("modelRoles") };
148
+ const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
149
+ const includesDefault = materializedAssignments.some(([role]) => role === "default");
150
+
151
+ if (!includesDefault && !nextModelRoles.default && options.session.model) {
152
+ nextModelRoles.default = formatModelSelectorValue(
153
+ `${options.session.model.provider}/${options.session.model.id}`,
154
+ options.session.thinkingLevel,
155
+ );
156
+ }
157
+
158
+ for (const [role, selector] of materializedAssignments) {
159
+ const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
160
+ if (target.settingsPath === "modelRoles") {
161
+ nextModelRoles[role] = selector;
162
+ } else {
163
+ nextAgentModelOverrides[role] = selector;
164
+ }
165
+ }
166
+
167
+ options.settings.set("modelRoles", nextModelRoles);
168
+ options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
169
+ options.settings.set("modelProfile.default", undefined);
170
+ options.settings.clearOverride("modelProfile.default");
171
+ options.settings.override("modelRoles", nextModelRoles);
172
+ options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
173
+ options.session.setActiveModelProfile?.(undefined);
174
+ return true;
175
+ }
176
+
112
177
  export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
113
178
  return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
114
179
  }
@@ -923,10 +923,22 @@ function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
923
923
  }
924
924
 
925
925
  function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
926
- if (model.id !== "gpt-5.4" || model.provider === "github-copilot" || model.contextWindow !== undefined) {
926
+ if (model.contextWindow !== undefined) {
927
927
  return model;
928
928
  }
929
- return { ...model, contextWindow: 1_000_000 };
929
+ if (model.id === "gpt-5.4" && model.provider !== "github-copilot") {
930
+ return { ...model, contextWindow: 1_000_000 };
931
+ }
932
+ // Custom GPT-5.5 endpoints that are not first-party `openai-responses` are
933
+ // typically Codex passthrough proxies (e.g. CLIProxyAPI fronting
934
+ // chatgpt.com/backend-api/codex), where the request path enforces the 272K
935
+ // prompt budget even though the model advertises a 1M total window.
936
+ // Without this default the bundled reference resolves to 1M, compaction
937
+ // never fires in time, and requests die with context_length_exceeded.
938
+ if (model.id === "gpt-5.5" && model.api !== "openai-responses") {
939
+ return { ...model, contextWindow: 272_000 };
940
+ }
941
+ return model;
930
942
  }
931
943
 
932
944
  function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
@@ -475,6 +475,28 @@ export class Settings {
475
475
  this.override("modelRoles", { ...shallowStringRecord(runtimeOverrides), [role]: modelId });
476
476
  }
477
477
  }
478
+ /**
479
+ * Set an agent model override while keeping any live runtime override aligned.
480
+ *
481
+ * Runtime model profiles override `task.agentModelOverrides` for the current
482
+ * session. A user-selected role assignment must win immediately in that same
483
+ * session, but only the explicit agent change should be persisted.
484
+ */
485
+ setAgentModelOverride(agentName: string, modelId: string): void {
486
+ const current = shallowStringRecord(getByPath(this.#global, ["task", "agentModelOverrides"]));
487
+ const runtimeOverrides = getByPath(this.#overrides, ["task", "agentModelOverrides"]);
488
+ const updateRuntimeOverride =
489
+ !!runtimeOverrides && typeof runtimeOverrides === "object" && !Array.isArray(runtimeOverrides);
490
+
491
+ this.set("task.agentModelOverrides", { ...current, [agentName]: modelId });
492
+
493
+ if (updateRuntimeOverride) {
494
+ this.override("task.agentModelOverrides", {
495
+ ...shallowStringRecord(runtimeOverrides),
496
+ [agentName]: modelId,
497
+ });
498
+ }
499
+ }
478
500
 
479
501
  /**
480
502
  * Get a model role (helper for modelRoles record).
@@ -175,6 +175,7 @@ type CoordinatorEventKind =
175
175
  | "turn.queued"
176
176
  | "turn.delivering"
177
177
  | "turn.active"
178
+ | "turn.acknowledged"
178
179
  | "turn.waiting_for_answer"
179
180
  | "turn.completed"
180
181
  | "turn.failed"
@@ -966,24 +967,53 @@ function hasTmuxIdentity(session: Record<string, unknown>): boolean {
966
967
  );
967
968
  }
968
969
 
970
+ function unavailableSessionReason(turn: TurnRecord, reason: string): string {
971
+ if (
972
+ reason === "tmux_session_missing" &&
973
+ turn.delivery.tmux_keys_sent === true &&
974
+ turn.delivery.prompt_acknowledged === true
975
+ ) {
976
+ return "tmux_session_missing_after_prompt_acknowledgement";
977
+ }
978
+ return reason;
979
+ }
980
+
981
+ function unavailableSessionEvidence(turn: TurnRecord, reason: string, timestamp: string): Record<string, unknown>[] {
982
+ if (reason !== "tmux_session_missing_after_prompt_acknowledgement") return turn.evidence;
983
+ return [
984
+ ...turn.evidence,
985
+ {
986
+ type: reason,
987
+ message:
988
+ "The tmux session disappeared after GJC runtime acknowledged the prompt, before any terminal final_response or error was recorded. Treat this as an in-flight vanished session and inspect/restart with recovery evidence rather than resubmitting blindly.",
989
+ tmux_keys_sent: true,
990
+ prompt_acknowledged: true,
991
+ prior_status: turn.status,
992
+ created_at: timestamp,
993
+ },
994
+ ];
995
+ }
996
+
969
997
  async function markTurnFailedForUnavailableSession(
970
998
  namespaceDir: string,
971
999
  turn: TurnRecord,
972
1000
  reason: string,
973
1001
  ): Promise<TurnRecord> {
974
1002
  const timestamp = new Date().toISOString();
1003
+ const durableReason = unavailableSessionReason(turn, reason);
975
1004
  const failed: TurnRecord = {
976
1005
  ...turn,
977
1006
  status: "failed",
978
1007
  final_response: {
979
- text: `Coordinator session unavailable: ${reason}`,
1008
+ text: `Coordinator session unavailable: ${durableReason}`,
980
1009
  format: "markdown",
981
1010
  source: "coordinator_liveness",
982
1011
  artifact_path: null,
983
1012
  truncated: false,
984
1013
  },
985
- error: { code: "session_unavailable", message: reason, recoverable: true },
986
- liveness: { checked_at: timestamp, live: false, reason },
1014
+ evidence: unavailableSessionEvidence(turn, durableReason, timestamp),
1015
+ error: { code: "session_unavailable", message: durableReason, recoverable: true },
1016
+ liveness: { checked_at: timestamp, live: false, reason: durableReason },
987
1017
  updated_at: timestamp,
988
1018
  completed_at: timestamp,
989
1019
  };
@@ -992,7 +1022,7 @@ async function markTurnFailedForUnavailableSession(
992
1022
  await writeSessionState(namespaceDir, failed.session_id, "stale", {
993
1023
  lastTurnId: failed.turn_id,
994
1024
  live: false,
995
- reason,
1025
+ reason: durableReason,
996
1026
  });
997
1027
  return failed;
998
1028
  }
@@ -1092,6 +1122,18 @@ async function markTurnAcknowledgedFromRuntimeState(
1092
1122
  };
1093
1123
  await writeTurnRecord(namespaceDir, acknowledged);
1094
1124
  await writeActiveTurn(namespaceDir, acknowledged);
1125
+ await appendCoordinatorEvent(namespaceDir, {
1126
+ kind: "turn.acknowledged",
1127
+ sessionId: acknowledged.session_id,
1128
+ turnId: acknowledged.turn_id,
1129
+ summary: `Turn ${acknowledged.turn_id} was acknowledged by the GJC runtime`,
1130
+ payloadRef: path.relative(namespaceDir, turnFile(namespaceDir, acknowledged.turn_id)),
1131
+ metadata: {
1132
+ status: acknowledged.status,
1133
+ tmux_keys_sent: acknowledged.delivery.tmux_keys_sent ?? null,
1134
+ prompt_acknowledged: true,
1135
+ },
1136
+ });
1095
1137
  return acknowledged;
1096
1138
  }
1097
1139
 
@@ -1504,14 +1546,27 @@ async function waitForCoordinatorEvents(namespaceDir: string, timeoutMs: number)
1504
1546
  };
1505
1547
  const timer = setTimeout(finish, Math.max(timeoutMs, 0));
1506
1548
  timer.unref?.();
1507
- await ensureDir(eventsDir(namespaceDir));
1508
- try {
1509
- const watcher = nodeFs.watch(eventsDir(namespaceDir), (_eventType, filename) => {
1510
- if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
1511
- });
1512
- watchers.push(watcher);
1513
- } catch {
1514
- // Directory may not exist yet; the timeout remains a bounded fallback.
1549
+ const eventDir = eventsDir(namespaceDir);
1550
+ const watchedDirs = [
1551
+ eventDir,
1552
+ turnsDir(namespaceDir),
1553
+ path.join(namespaceDir, "active-turns"),
1554
+ path.join(namespaceDir, "session-states"),
1555
+ ];
1556
+ for (const dir of watchedDirs) {
1557
+ await ensureDir(dir);
1558
+ try {
1559
+ const watcher = nodeFs.watch(dir, (_eventType, filename) => {
1560
+ if (dir === eventDir) {
1561
+ if (filename === "event-journal.jsonl" || filename === "latest-seq.json") finish();
1562
+ return;
1563
+ }
1564
+ if (typeof filename === "string" && filename.endsWith(".json")) finish();
1565
+ });
1566
+ watchers.push(watcher);
1567
+ } catch {
1568
+ // Directory may not be watchable on this platform; the timeout remains a bounded fallback.
1569
+ }
1515
1570
  }
1516
1571
  return deferred.promise;
1517
1572
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: deep-interview
3
3
  description: Socratic deep interview with mathematical ambiguity gating before explicit execution approval
4
- argument-hint: "[--quick|--standard|--deep] <idea or vague description>"
4
+ argument-hint: "[--trace] [--quick|--standard|--deep] <idea or vague description>"
5
5
  pipeline: [deep-interview, plan]
6
6
  handoff-policy: approval-required
7
7
  handoff: .gjc/_session-{sessionid}/specs/deep-interview-{slug}.md
@@ -20,12 +20,15 @@ Deep Interview implements Ouroboros-inspired Socratic questioning with mathemati
20
20
  - User says "ouroboros", "socratic", "I have a vague idea", "not sure exactly what I want"
21
21
  - User wants to avoid "that's not what I meant" outcomes from autonomous execution
22
22
  - Task is complex enough that jumping to code would waste cycles on scope discovery
23
+ - User asks for implementation but the target, scope, acceptance criteria, or safety boundary is ambiguous enough that mutation would require guessing
23
24
  - User wants mathematically-validated clarity before committing to execution
24
25
  - User explicitly requests deep-interview even after being told the request is already clear, bounded, and low-risk
26
+ - User requests a trace/research pre-step before the interview, e.g. `/skill:deep-interview --trace <idea>`
25
27
  </Use_When>
26
28
 
27
29
  <Do_Not_Use_When>
28
30
  - User has a detailed, specific request with file paths, function names, or acceptance criteria -- execute directly
31
+ - User has an explicit concrete low-risk implementation request with enough target, scope, and acceptance criteria to execute safely -- execute directly
29
32
  - User wants to explore options or brainstorm -- use `ralplan` skill instead
30
33
  - User wants a quick fix or single change -- use direct execution, not deep-interview or role-agent delegation
31
34
  - User says "just do it" or "skip the questions" without an explicit execution path -- respect their intent by exiting deep-interview, not by writing a `pending approval` spec
@@ -51,6 +54,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
51
54
  - When the locked topology has multiple active components, score and target each component explicitly so depth-first clarity on one component cannot hide ambiguity in siblings
52
55
  - Keep prompt payloads budgeted: summarize or trim oversized initial context/history before composing question, scoring, spec, or handoff prompts
53
56
  - If the user's initial context is oversized, create a concise prompt-safe summary first and wait for that summary before ambiguity scoring, question generation, or downstream execution handoff
57
+ - Route ambiguous implementation asks to clarification, deep-interview, or downstream `ralplan` before mutation; do not infer missing target, scope, acceptance criteria, or safety boundary just to start coding.
54
58
  - Do not proceed to execution until ambiguity ≤ the resolved threshold for this run and the user explicitly approves a scoped execution path
55
59
  - Treat user wording such as `implementation`, "implementation plan", Korean `구현`, or "구현 계획" as describing the eventual target, not permission to implement now.
56
60
  - While still in deep-interview, do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation; continue interviewing for scope, risks, acceptance criteria, and unknowns.
@@ -62,6 +66,7 @@ Inspired by the [Ouroboros project](https://github.com/Q00/ouroboros) which demo
62
66
  - Refine free-text answers into a structured interpretation and confirm nothing is lost before scoring
63
67
  - After 3 consecutive agent-resolved answers (accepted auto-research candidates or auto-answers), route the next question to the user (dialectic rhythm guard)
64
68
  - Run an independent closure audit and a one-sentence goal restatement, each requiring explicit user confirmation, before crystallizing the spec
69
+ - When `--trace` is active, use the bounded trace evidence summary as pre-question context; never dump raw logs, raw files, or unbounded search output into questions, scoring, specs, or handoffs
65
70
  </Execution_Policy>
66
71
 
67
72
  <Internal_Auto_Mode_Protocol>
@@ -133,6 +138,16 @@ If `{{ARGUMENTS}}` is already clear, bounded, low-risk, and asks for a quick fix
133
138
 
134
139
  This gate exists to prevent deep-interview from making easy problems harder. A small verification need does not make a request interview-worthy.
135
140
 
141
+ ## Phase 0.75: Optional Trace Pre-Step
142
+
143
+ 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.
144
+
145
+ 1. Read the native trace summary from active deep-interview state (`trace`, `state.trace`, or `state.trace_summary`). The native seed must have produced this summary before any interview question.
146
+ 2. Treat the summary as compact evidence: project hints, relevant paths, and path-level findings only. Do not expand it by dumping raw files, raw logs, or unbounded command output.
147
+ 3. Store or preserve it under `state.trace_summary` and fold it into `codebase_context` with citations to the summarized paths.
148
+ 4. Use trace findings to influence Round 0 topology, Phase 2 question targeting, requirement wording, acceptance criteria, and final Technical Context. Normal no-trace interviews must behave exactly as before.
149
+ 5. If `--trace` was requested but no valid bounded summary exists, increment `architect_failures` or record an internal audit note, then continue with the normal no-trace path without surfacing tool noise.
150
+
136
151
  ## Phase 1: Initialize
137
152
 
138
153
  1. **Parse the user's idea** from `{{ARGUMENTS}}`
@@ -177,6 +192,7 @@ This gate exists to prevent deep-interview from making easy problems harder. A s
177
192
  "threshold": <resolvedThreshold>,
178
193
  "threshold_source": "<resolvedThresholdSource>",
179
194
  "language": "<existing language object from active state, if present>",
195
+ "trace_summary": "<bounded trace summary when --trace is active, else null>",
180
196
  "codebase_context": null,
181
197
  "topology": {
182
198
  "status": "pending|confirmed|legacy_missing",
@@ -222,6 +238,7 @@ Run this gate exactly once after Phase 1 initialization and before any Phase 2 a
222
238
  - Extract top-level verbs/nouns, workstreams, surfaces, integrations, or deliverables that can succeed or fail independently.
223
239
  - Prefer 1-6 components. If more than 6 candidates appear, group siblings at the highest useful level and note the grouping rationale.
224
240
  - Do not treat implementation tasks, fields, or sub-features as top-level components unless the user framed them as independent outcomes.
241
+ - When `--trace` is active, include trace-summarized paths as topology evidence, but do not add implementation sub-tasks as top-level components solely because trace found files.
225
242
  2. **Ask one confirmation question** before Round 1:
226
243
 
227
244
  ```
@@ -289,6 +306,7 @@ Build the question generation prompt with:
289
306
  - Current clarity scores per dimension (which is weakest?)
290
307
  - Lateral-review panel findings (if convened this round -- see Phase 3)
291
308
  - Brownfield codebase context (if applicable), summarized to cited paths/symbols/patterns instead of raw dumps
309
+ - Bounded trace summary (when `--trace` is active): project hints, relevant paths, and findings only; cite paths instead of raw content
292
310
  - Locked topology from Round 0, including active components, deferred components, prior per-component scores, and `last_targeted_component_id`
293
311
 
294
312
  - `language` from active state when present; apply `language.instruction` to all natural-language user-facing question text, rationale, and options
@@ -370,11 +388,22 @@ Ambiguity-raising triggers:
370
388
 
371
389
  Use **mechanism A** for every ambiguity rise: a trigger LOWERS the affected component/dimension clarity score, and the existing weighted formula raises ambiguity. There is **no separate penalty term**; ambiguity remains bounded by the same greenfield/brownfield formula.
372
390
 
391
+ **Deterministic ambiguity floor (runtime-enforced).** The runtime independently computes a code-level floor from persisted state and clamps every reported ambiguity to `max(reported, floor)` at write time — the scorer cannot under-report below what code can objectively measure:
392
+
393
+ - `+0.10` per established fact marked `disputed` that has no `superseded_by` resolution (contradiction pressure)
394
+ - `+0.05` per active topology component whose goal/constraints/criteria clarity is still unscored (gap pressure — persist `topology.components[].clarity_scores` every round or the floor blocks convergence)
395
+ - `+0.05 × (auto-answered rounds / scored rounds)` (assumption dilution)
396
+
397
+ Cooperate with the floor rather than fight it:
398
+ - Replacing an already-scored answer for the same round (a retraction/pivot) automatically marks that round's established facts as disputed; ambiguity rises mechanically even when no trigger is reported. Treat a floor-driven rise as trigger evidence and score the affected dimensions accordingly.
399
+ - A disputed fact keeps the floor at or above `0.10` — above the default threshold — so convergence is blocked until the dispute is resolved: either the user re-confirms the original fact (set `disputed: false`) or the superseding decision is recorded as a new established fact and the old fact gets `superseded_by: <new fact id>`. Never delete the contradicted fact.
400
+ - When the effective score was clamped upward, the persisted round carries `reported_ambiguity` (your raw score) and `ambiguity_floor`; report the floor and its dominant cause in the Step 2d table instead of pretending the raw score held.
401
+
373
402
  The rise is SILENT: no modal, no forced-resolution step, and no dedicated conflict UI. Surface it through the normal per-round report and by targeting the next question at the affected component/dimension.
374
403
 
375
404
  Structured scorer output is required. Include `triggers`, `trigger_status`, `affected_component`, `affected_dimension`, `prior_dimension_score`, `new_dimension_score`, `prior_ambiguity`, `new_ambiguity`, `evidence`, `contradicted_established_fact` when relevant, and `disputed_unresolved_rationale` when applicable.
376
405
 
377
- Established-facts maintenance: promote stable confirmed decisions into `state.established_facts` with source/evidence; when a new answer contradicts an established fact, mark the fact disputed and preserve the contradicted fact instead of deleting it.
406
+ Established-facts maintenance: promote stable confirmed decisions into `state.established_facts` with source/evidence; when a new answer contradicts an established fact, mark the fact disputed and preserve the contradicted fact instead of deleting it. When the user later confirms the new direction, record the superseding decision as a new established fact and set `superseded_by: <new fact id>` on the disputed fact — that is the only way to release the deterministic floor pressure while keeping the audit trail.
378
407
 
379
408
  TRANSITION VALIDATION: if a trigger is present, the affected dimension must not improve and overall ambiguity must rise vs the prior scored round, unless the trigger is explicitly marked disputed or unresolved with rationale.
380
409
 
@@ -396,6 +425,9 @@ Locked topology:
396
425
  Established facts:
397
426
  {state.established_facts}
398
427
 
428
+ Trace summary:
429
+ {state.trace_summary if --trace was active, else "not requested"}
430
+
399
431
  Score each active component on each dimension, then provide the overall dimension scores as the minimum or coverage-weighted weakest score across active components. Deferred components are excluded from ambiguity math but must remain listed in topology and the final spec.
400
432
 
401
433
  Score each dimension:
@@ -465,6 +497,7 @@ Round {n} complete.
465
497
  | Success Criteria | {s} | {w} | {s*w} | {gap or "Clear"} |
466
498
  | Context (brownfield) | {s} | {w} | {s*w} | {gap or "Clear"} |
467
499
  | **Ambiguity** | | | **{prior_score}% -> {score}% {up|down|flat}** | {if up: trigger name such as "A direct contradiction"} |
500
+ | **Floor** (only when clamped) | | | **{floor}%** | {dominant cause: disputed fact / unscored component / auto-answer dilution} |
468
501
 
469
502
  **Topology:** Targeted {target_component_name} | Active: {active_component_count} | Deferred: {deferred_component_count} | Next rotation after: {last_targeted_component_id}
470
503
 
@@ -525,7 +558,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
525
558
 
526
559
  **Before generating the spec, two gates must pass, in order:**
527
560
 
528
- **4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
561
+ **4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, no disputed established fact lacks a `superseded_by` resolution, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
529
562
 
530
563
  **4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask`: "If someone read only this line, would they reach the same outcome you have in mind?" Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
531
564
 
@@ -615,8 +648,8 @@ Spec structure:
615
648
  | {assumption} | {how it was questioned} | {what was decided} |
616
649
 
617
650
  ## Technical Context
618
- {brownfield: relevant codebase findings from focused repo inspection or canonical role-agent fact-finding}
619
- {greenfield: technology choices and constraints}
651
+ {brownfield: relevant codebase findings from focused repo inspection, canonical role-agent fact-finding, and bounded trace summary when --trace was active}
652
+ {greenfield: technology choices and constraints, plus bounded trace findings when --trace was active and relevant}
620
653
 
621
654
  ## Ontology (Key Entities)
622
655
  {Fill from the FINAL round's ontology extraction, not just crystallization-time generation}