@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
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { YAML } from "bun";
5
6
  import { syncSkillActiveState } from "../skill-state/active-state";
6
7
  import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
7
8
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
@@ -19,7 +20,7 @@ export * from "./deep-interview-recorder";
19
20
  *
20
21
  * The CLI itself does not run the Socratic interview; that lives inside the `/skill:deep-interview`
21
22
  * skill executed by the agent. This handler validates the documented argument-hint surface
22
- * (`[--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
23
+ * (`[--trace] [--quick|--standard|--deep] <idea>`), seeds `.gjc/state/deep-interview-state.json`, and
23
24
  * updates the shared HUD rail via `syncSkillActiveState` so the active interview is visible to
24
25
  * the TUI.
25
26
  */
@@ -40,6 +41,68 @@ const RESOLUTION_THRESHOLDS = {
40
41
  deep: 0.35,
41
42
  } as const;
42
43
 
44
+ const TRACE_MAX_RELEVANT_PATHS = 12;
45
+ const TRACE_MAX_PACKAGE_HINTS = 8;
46
+ const TRACE_MAX_DIRECTORY_VISITS = 1200;
47
+ const TRACE_MAX_ENTRY_VISITS = 5000;
48
+ const TRACE_MAX_PENDING_DIRECTORIES = 1200;
49
+ const TRACE_SKIP_DIRS = new Set([
50
+ ".git",
51
+ ".gjc",
52
+ "node_modules",
53
+ "dist",
54
+ "build",
55
+ "coverage",
56
+ ".next",
57
+ ".turbo",
58
+ ".cache",
59
+ "vendor",
60
+ "target",
61
+ ".venv",
62
+ "venv",
63
+ "__pycache__",
64
+ ".pytest_cache",
65
+ "tmp",
66
+ "temp",
67
+ "logs",
68
+ "out",
69
+ ]);
70
+ const TRACE_SOURCE_EXTENSIONS = new Set([
71
+ ".ts",
72
+ ".tsx",
73
+ ".js",
74
+ ".jsx",
75
+ ".mts",
76
+ ".cts",
77
+ ".py",
78
+ ".rs",
79
+ ".go",
80
+ ".java",
81
+ ".kt",
82
+ ".swift",
83
+ ".md",
84
+ ".json",
85
+ ".yml",
86
+ ".yaml",
87
+ ]);
88
+
89
+ interface DeepInterviewTraceSummary {
90
+ enabled: true;
91
+ generated_at: string;
92
+ bounded: true;
93
+ limits: {
94
+ max_relevant_paths: number;
95
+ max_package_hints: number;
96
+ max_directory_visits: number;
97
+ max_entry_visits: number;
98
+ max_pending_directories: number;
99
+ };
100
+ idea_terms: string[];
101
+ project_hints: string[];
102
+ relevant_paths: Array<{ path: string; reason: string }>;
103
+ findings: string[];
104
+ }
105
+
43
106
  type DeepInterviewResolution = keyof typeof RESOLUTION_THRESHOLDS;
44
107
 
45
108
  class DeepInterviewCommandError extends Error {
@@ -107,6 +170,134 @@ async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string>
107
170
  return rawSpec;
108
171
  }
109
172
 
173
+ function traceTerms(idea: string): string[] {
174
+ const terms = new Set<string>();
175
+ for (const match of idea.toLowerCase().matchAll(/[a-z0-9][a-z0-9_-]{2,}/g)) {
176
+ const value = match[0];
177
+ if (["the", "and", "for", "with", "that", "this", "from", "into", "should", "would"].includes(value)) continue;
178
+ terms.add(value);
179
+ if (terms.size >= 12) break;
180
+ }
181
+ return [...terms];
182
+ }
183
+
184
+ function relativePathReason(relativePath: string, terms: readonly string[]): string | undefined {
185
+ const normalized = relativePath.toLowerCase();
186
+ const matched = terms.find(term => normalized.includes(term));
187
+ if (matched) return `path matches idea term "${matched}"`;
188
+ if (/deep[-_]?interview/i.test(relativePath)) return "path matches deep-interview workflow surface";
189
+ if (/skill|workflow|runtime|state/i.test(relativePath)) return "path matches workflow/runtime surface";
190
+ return undefined;
191
+ }
192
+
193
+ async function readPackageHints(cwd: string): Promise<string[]> {
194
+ const packagePath = path.join(cwd, "package.json");
195
+ let parsed: unknown;
196
+ try {
197
+ parsed = JSON.parse(await fs.readFile(packagePath, "utf-8"));
198
+ } catch {
199
+ return [];
200
+ }
201
+ const manifest = parsed as {
202
+ name?: unknown;
203
+ workspaces?: unknown;
204
+ scripts?: Record<string, unknown>;
205
+ dependencies?: Record<string, unknown>;
206
+ devDependencies?: Record<string, unknown>;
207
+ };
208
+ const hints: string[] = [];
209
+ if (typeof manifest.name === "string") hints.push(`package: ${manifest.name}`);
210
+ if (manifest.workspaces) hints.push("workspace: package.json declares workspaces");
211
+ const scripts = Object.keys(manifest.scripts ?? {}).slice(0, TRACE_MAX_PACKAGE_HINTS);
212
+ if (scripts.length > 0) hints.push(`scripts: ${scripts.join(", ")}`);
213
+ const deps = [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
214
+ .filter(name => /typescript|bun|react|vite|zod|winston|commander|oclif/i.test(name))
215
+ .slice(0, TRACE_MAX_PACKAGE_HINTS);
216
+ if (deps.length > 0) hints.push(`notable dependencies: ${deps.join(", ")}`);
217
+ return hints.slice(0, TRACE_MAX_PACKAGE_HINTS);
218
+ }
219
+
220
+ async function collectRelevantTracePaths(
221
+ cwd: string,
222
+ terms: readonly string[],
223
+ ): Promise<Array<{ path: string; reason: string }>> {
224
+ const results: Array<{ path: string; reason: string; score: number }> = [];
225
+ const pending: Array<{ absolutePath: string; depth: number }> = [{ absolutePath: cwd, depth: 0 }];
226
+ let visitedDirectories = 0;
227
+ let visitedEntries = 0;
228
+ while (
229
+ pending.length > 0 &&
230
+ visitedDirectories < TRACE_MAX_DIRECTORY_VISITS &&
231
+ visitedEntries < TRACE_MAX_ENTRY_VISITS
232
+ ) {
233
+ const current = pending.shift();
234
+ if (!current) break;
235
+ visitedDirectories += 1;
236
+ try {
237
+ const directory = await fs.opendir(current.absolutePath);
238
+ for await (const entry of directory) {
239
+ visitedEntries += 1;
240
+ if (visitedEntries > TRACE_MAX_ENTRY_VISITS) break;
241
+ if (TRACE_SKIP_DIRS.has(entry.name)) continue;
242
+ const absolutePath = path.join(current.absolutePath, entry.name);
243
+ const relativePath = path.relative(cwd, absolutePath).split(path.sep).join("/");
244
+ if (entry.isDirectory()) {
245
+ if (current.depth < 6 && pending.length < TRACE_MAX_PENDING_DIRECTORIES) {
246
+ pending.push({ absolutePath, depth: current.depth + 1 });
247
+ }
248
+ continue;
249
+ }
250
+ if (!entry.isFile() || !TRACE_SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue;
251
+ const reason = relativePathReason(relativePath, terms);
252
+ if (!reason) continue;
253
+ const termScore = terms.reduce(
254
+ (score, term) => score + (relativePath.toLowerCase().includes(term) ? 2 : 0),
255
+ 0,
256
+ );
257
+ const surfaceScore = /deep[-_]?interview|skill|workflow|runtime|state/i.test(relativePath) ? 1 : 0;
258
+ results.push({ path: relativePath, reason, score: termScore + surfaceScore });
259
+ }
260
+ } catch {}
261
+ }
262
+ return results
263
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
264
+ .slice(0, TRACE_MAX_RELEVANT_PATHS)
265
+ .map(({ path: relativePath, reason }) => ({ path: relativePath, reason }));
266
+ }
267
+
268
+ async function buildDeepInterviewTraceSummary(cwd: string, idea: string): Promise<DeepInterviewTraceSummary> {
269
+ const terms = traceTerms(idea);
270
+ const [projectHints, relevantPaths] = await Promise.all([
271
+ readPackageHints(cwd),
272
+ collectRelevantTracePaths(cwd, terms),
273
+ ]);
274
+ const findings = [
275
+ projectHints.length > 0
276
+ ? "Project manifest was summarized into bounded package/script/dependency hints."
277
+ : "No readable package.json manifest was found at the project root.",
278
+ relevantPaths.length > 0
279
+ ? `Relevant path scan captured ${relevantPaths.length} bounded path hint(s) before interview questions.`
280
+ : "Relevant path scan found no matching source/documentation paths before interview questions.",
281
+ "Trace summary intentionally stores path-level evidence only; raw files and logs are excluded.",
282
+ ];
283
+ return {
284
+ enabled: true,
285
+ generated_at: new Date().toISOString(),
286
+ bounded: true,
287
+ limits: {
288
+ max_relevant_paths: TRACE_MAX_RELEVANT_PATHS,
289
+ max_package_hints: TRACE_MAX_PACKAGE_HINTS,
290
+ max_directory_visits: TRACE_MAX_DIRECTORY_VISITS,
291
+ max_entry_visits: TRACE_MAX_ENTRY_VISITS,
292
+ max_pending_directories: TRACE_MAX_PENDING_DIRECTORIES,
293
+ },
294
+ idea_terms: terms,
295
+ project_hints: projectHints,
296
+ relevant_paths: relevantPaths,
297
+ findings,
298
+ };
299
+ }
300
+
110
301
  interface ResolvedDeepInterviewArgs {
111
302
  resolution: DeepInterviewResolution;
112
303
  threshold: number;
@@ -114,6 +305,7 @@ interface ResolvedDeepInterviewArgs {
114
305
  sessionId: string;
115
306
  idea: string;
116
307
  language?: DeepInterviewLanguagePreference;
308
+ trace?: DeepInterviewTraceSummary;
117
309
  json: boolean;
118
310
  }
119
311
 
@@ -199,7 +391,7 @@ async function readModernSettingsAmbiguityThreshold(): Promise<{ threshold: numb
199
391
  const modernConfigPath = modernSettingsPath();
200
392
  let parsed: unknown;
201
393
  try {
202
- parsed = (await import("bun")).YAML.parse(await fs.readFile(modernConfigPath, "utf-8"));
394
+ parsed = YAML.parse(await fs.readFile(modernConfigPath, "utf-8"));
203
395
  } catch {
204
396
  return undefined;
205
397
  }
@@ -371,6 +563,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
371
563
  skipNext = true;
372
564
  continue;
373
565
  }
566
+ if (arg === "--trace") continue;
374
567
  if (arg === "--quick" || arg === "--standard" || arg === "--deep" || arg === "--json") continue;
375
568
  if (arg.startsWith("-")) {
376
569
  throw new DeepInterviewCommandError(2, `unknown flag for gjc deep-interview: ${arg}`);
@@ -379,6 +572,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
379
572
  }
380
573
  const idea = ideaParts.join(" ").trim();
381
574
  const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
575
+ const trace = hasFlag(args, "--trace") && idea ? await buildDeepInterviewTraceSummary(cwd, idea) : undefined;
382
576
  return {
383
577
  resolution: effectiveResolution,
384
578
  threshold,
@@ -386,6 +580,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
386
580
  sessionId,
387
581
  idea,
388
582
  language: resolveDeepInterviewLanguagePreference(idea),
583
+ trace,
389
584
  json: hasFlag(args, "--json"),
390
585
  };
391
586
  }
@@ -507,6 +702,17 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
507
702
  },
508
703
  updated_at: now,
509
704
  };
705
+ if (resolved.trace) {
706
+ payload.trace = resolved.trace;
707
+ (payload.state as Record<string, unknown>).trace = resolved.trace;
708
+ (payload.state as Record<string, unknown>).trace_summary = resolved.trace;
709
+ (payload.state as Record<string, unknown>).codebase_context = {
710
+ source: "trace",
711
+ summary: resolved.trace.findings,
712
+ relevant_paths: resolved.trace.relevant_paths,
713
+ project_hints: resolved.trace.project_hints,
714
+ };
715
+ }
510
716
  if (resolved.language) {
511
717
  payload.language = resolved.language;
512
718
  (payload.state as Record<string, unknown>).language = resolved.language;
@@ -646,6 +852,7 @@ export async function runNativeDeepInterviewCommand(
646
852
  threshold_source: resolved.thresholdSource,
647
853
  idea: resolved.idea,
648
854
  language: resolved.language,
855
+ trace: resolved.trace,
649
856
  state_path: statePath,
650
857
  handoff: "/skill:deep-interview",
651
858
  };
@@ -654,6 +861,7 @@ export async function runNativeDeepInterviewCommand(
654
861
  : [
655
862
  `deep-interview seed state_path=${statePath}`,
656
863
  `resolution=${resolved.resolution} threshold=${resolved.threshold} threshold_source=${resolved.thresholdSource}`,
864
+ resolved.trace ? `trace=enabled bounded_paths=${resolved.trace.relevant_paths.length}` : undefined,
657
865
  "handoff=/skill:deep-interview",
658
866
  "",
659
867
  ].join("\n");
@@ -31,6 +31,13 @@ export interface DeepInterviewEstablishedFact {
31
31
  dimension?: string;
32
32
  evidence?: string;
33
33
  disputed: boolean;
34
+ /**
35
+ * Resolution pointer for a disputed fact: the id of the fact that replaced it
36
+ * after the user confirmed a pivot. A disputed fact without `superseded_by`
37
+ * keeps the deterministic ambiguity floor elevated; setting it releases the
38
+ * pressure while preserving the contradicted fact for audit.
39
+ */
40
+ superseded_by?: string;
34
41
  }
35
42
 
36
43
  export interface DeepInterviewTriggerMetadata {
@@ -66,7 +73,12 @@ export interface DeepInterviewRoundRecord {
66
73
  answered_at: string;
67
74
  scored_at?: string;
68
75
  scores?: Record<string, number>;
76
+ /** Effective ambiguity after the deterministic floor clamp (`max(reported, floor)`). */
69
77
  ambiguity?: number;
78
+ /** Original LLM-reported ambiguity, preserved for audit when the floor clamped it. */
79
+ reported_ambiguity?: number;
80
+ /** Deterministic floor in effect when this round was scored, when it clamped. */
81
+ ambiguity_floor?: number;
70
82
  triggers?: DeepInterviewTriggerMetadata[];
71
83
  }
72
84
 
@@ -125,6 +137,7 @@ const TRANSCRIPT_STATE_FIELDS = [
125
137
  "rounds",
126
138
  "established_facts",
127
139
  "current_ambiguity",
140
+ "ambiguity_floor",
128
141
  "topology",
129
142
  "ontology_snapshots",
130
143
  "auto_researched_rounds",
@@ -3,6 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import type { AssistantMessage } from "@gajae-code/ai";
5
5
  import { logger, postmortem } from "@gajae-code/utils";
6
+ import { sessionRuntimeDir } from "./session-layout";
6
7
 
7
8
  export const GJC_COORDINATOR_SESSION_STATE_FILE_ENV = "GJC_COORDINATOR_SESSION_STATE_FILE";
8
9
  export const GJC_COORDINATOR_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
@@ -148,6 +149,12 @@ function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): bo
148
149
  return source === "agent_end" || source === "launch_error";
149
150
  }
150
151
 
152
+ function runtimeStateFileForContext(context: RuntimeStateContext): string | null {
153
+ const explicit = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
154
+ if (explicit) return explicit;
155
+ if (!context.sessionId.trim()) return null;
156
+ return path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json");
157
+ }
151
158
  function branchForContext(context: RuntimeStateContext): string | null {
152
159
  return context.branch ?? (process.env[GJC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
153
160
  }
@@ -160,10 +167,11 @@ function basePayload(input: {
160
167
  source: string;
161
168
  event: string;
162
169
  reason: string | null;
170
+ sessionId: string;
163
171
  }): Record<string, unknown> {
164
172
  return {
165
173
  schema_version: 1,
166
- session_id: process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || input.context.sessionId,
174
+ session_id: input.sessionId,
167
175
  state: input.state,
168
176
  ready_for_input: input.state === "completed" || input.state === "ready_for_input",
169
177
  updated_at: input.now,
@@ -193,30 +201,51 @@ function numericProcessExitCode(defaultCode: number | null): number | null {
193
201
  return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
194
202
  }
195
203
 
196
- function postmortemExitDetails(reason: postmortem.Reason): {
204
+ function postmortemExitDetails(
205
+ reason: postmortem.Reason,
206
+ previous: RuntimeStateSidecarPayload,
207
+ ): {
197
208
  state: RuntimeState;
198
209
  reason: string;
199
210
  exitKind: string;
200
211
  exitCode: number | null;
201
212
  signal: string | null;
202
213
  error?: { code: string; message: string; recoverable: true };
214
+ recovery?: { action: string; reason: string };
203
215
  } {
204
216
  if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
205
217
  const exitCode = numericProcessExitCode(0) ?? 0;
206
- const state: RuntimeState = exitCode === 0 ? "completed" : "errored";
218
+ const exitedBeforeTerminalState =
219
+ exitCode === 0 && reason === postmortem.Reason.EXIT && previous.state === "running";
220
+ const state: RuntimeState = exitCode === 0 && !exitedBeforeTerminalState ? "completed" : "errored";
221
+ const exitReason = exitedBeforeTerminalState
222
+ ? "process_exit_before_terminal_state"
223
+ : reason === postmortem.Reason.EXIT
224
+ ? "process_exit"
225
+ : "manual_cleanup";
207
226
  return {
208
227
  state,
209
- reason: reason === postmortem.Reason.EXIT ? "process_exit" : "manual_cleanup",
228
+ reason: exitReason,
210
229
  exitKind: reason,
211
230
  exitCode,
212
231
  signal: null,
213
232
  ...(state === "errored"
214
233
  ? {
215
234
  error: {
216
- code: "process_exit",
217
- message: publicSafeErrorMessage(`GJC process exited with code ${exitCode}`),
235
+ code: exitReason,
236
+ message: publicSafeErrorMessage(
237
+ exitedBeforeTerminalState
238
+ ? "GJC process exited before emitting terminal agent state"
239
+ : `GJC process exited with code ${exitCode}`,
240
+ ),
218
241
  recoverable: true,
219
242
  },
243
+ recovery: {
244
+ action: "recover_or_resume_session",
245
+ reason: exitedBeforeTerminalState
246
+ ? "previous runtime state was non-terminal; preserve the worktree and inspect the session before retrying"
247
+ : "process exited with a non-zero status",
248
+ },
220
249
  }
221
250
  : {}),
222
251
  };
@@ -233,6 +262,7 @@ function postmortemExitDetails(reason: postmortem.Reason): {
233
262
  exitCode: numericProcessExitCode(null),
234
263
  signal: signalByReason[reason] ?? null,
235
264
  error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
265
+ recovery: { action: "recover_or_resume_session", reason: "process cleanup ran before terminal agent state" },
236
266
  };
237
267
  }
238
268
 
@@ -250,15 +280,27 @@ export async function persistCoordinatorRuntimeStateFromEvent(
250
280
  event: RuntimeStateEvent,
251
281
  context: RuntimeStateContext,
252
282
  ): Promise<void> {
253
- const stateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
283
+ const stateFile = runtimeStateFileForContext(context);
254
284
  if (!stateFile) return;
255
285
  const state = stateForEvent(event);
256
286
  if (!state) return;
257
287
  const now = new Date().toISOString();
258
288
  const previous = readPreviousPayload(stateFile);
259
289
  const finalResponse = finalResponseForEvent(event);
290
+ const sessionId = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
291
+ ? process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
292
+ : context.sessionId;
260
293
  const payload = {
261
- ...basePayload({ context, previous, state, now, source: "agent_session_event", event: event.type, reason: null }),
294
+ ...basePayload({
295
+ context,
296
+ previous,
297
+ state,
298
+ now,
299
+ source: "agent_session_event",
300
+ event: event.type,
301
+ reason: null,
302
+ sessionId,
303
+ }),
262
304
  ...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
263
305
  ...(finalResponse ? { final_response: finalResponse } : {}),
264
306
  ...(state === "errored"
@@ -282,12 +324,15 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
282
324
  reason: postmortem.Reason,
283
325
  context: RuntimeStateContext,
284
326
  ): void {
285
- const stateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
327
+ const stateFile = runtimeStateFileForContext(context);
286
328
  if (!stateFile) return;
287
329
  const previous = readPreviousPayload(stateFile);
288
330
  if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
289
331
  const now = new Date().toISOString();
290
- const details = postmortemExitDetails(reason);
332
+ const details = postmortemExitDetails(reason, previous as RuntimeStateSidecarPayload);
333
+ const sessionId = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()
334
+ ? process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || context.sessionId
335
+ : context.sessionId;
291
336
  const payload = {
292
337
  ...basePayload({
293
338
  context,
@@ -297,6 +342,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
297
342
  source: "process_postmortem",
298
343
  event: "process_exit",
299
344
  reason: details.reason,
345
+ sessionId,
300
346
  }),
301
347
  ended_at: now,
302
348
  detected_at: now,
@@ -304,6 +350,8 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
304
350
  exit_code: details.exitCode,
305
351
  signal: details.signal,
306
352
  ...(details.error ? { error: details.error } : {}),
353
+ ...(details.recovery ? { recovery: details.recovery } : {}),
354
+ previous_runtime_state: typeof previous.state === "string" ? previous.state : null,
307
355
  };
308
356
  try {
309
357
  writeStateFileSync(stateFile, payload);
@@ -313,7 +361,7 @@ export function persistCoordinatorRuntimeStateFromPostmortem(
313
361
  }
314
362
 
315
363
  export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
316
- if (!process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()) return () => {};
364
+ if (!runtimeStateFileForContext(context)) return () => {};
317
365
  return postmortem.register("coordinator-runtime-state", reason => {
318
366
  persistCoordinatorRuntimeStateFromPostmortem(reason, context);
319
367
  });
@@ -26,6 +26,7 @@ import {
26
26
  type WorkflowStateReceipt,
27
27
  } from "../skill-state/workflow-state-contract";
28
28
  import { renderCliWriteReceipt } from "./cli-write-receipt";
29
+ import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
29
30
  import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
30
31
  import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
31
32
  import {
@@ -1158,7 +1159,12 @@ export async function reconcileWorkflowSkillState(options: {
1158
1159
 
1159
1160
  const merged =
1160
1161
  mode === "deep-interview"
1161
- ? (mergeDeepInterviewEnvelope(existingPayload, payload) as Record<string, unknown>)
1162
+ ? // Enforce the deterministic ambiguity floor on every reconcile so a
1163
+ // self-reported score can never undercut persisted contradiction evidence.
1164
+ (applyAmbiguityFloorToEnvelope(mergeDeepInterviewEnvelope(existingPayload, payload)).envelope as Record<
1165
+ string,
1166
+ unknown
1167
+ >)
1162
1168
  : mergeWithNullDelete(existingPayload, payload);
1163
1169
  merged.skill = mode;
1164
1170
  merged.current_phase = trimmedPhase;
@@ -1355,7 +1361,11 @@ async function handleWrite(
1355
1361
  if (mode === "deep-interview") {
1356
1362
  // Deep-interview keeps interview data nested under `state` and merges rounds
1357
1363
  // losslessly by durable key; never flatten or delete `state` (that drops recorder history).
1358
- merged = mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") });
1364
+ // The deterministic ambiguity floor is applied after the merge so a reported
1365
+ // score written through the CLI can never undercut persisted contradiction evidence.
1366
+ merged = applyAmbiguityFloorToEnvelope(
1367
+ mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") }),
1368
+ ).envelope;
1359
1369
  } else if (hasFlag(args, "--replace")) {
1360
1370
  merged = { ...payload };
1361
1371
  } else {
@@ -1896,16 +1896,39 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
1896
1896
  }
1897
1897
  return { sessionName, windowIndex, leaderPaneId, target: `${sessionName}:${windowIndex}` };
1898
1898
  }
1899
+ type CommandPathResolver = (command: string) => string | null;
1900
+
1901
+ function isBunVirtualPath(candidate: string | undefined): boolean {
1902
+ const normalized = candidate?.trim().replace(/\\/g, "/");
1903
+ return normalized === "/$bunfs" || normalized?.startsWith("/$bunfs/") === true;
1904
+ }
1905
+
1906
+ function resolveFallbackGjcWorkerCommand(
1907
+ platform: NodeJS.Platform,
1908
+ execPath: string,
1909
+ which: CommandPathResolver,
1910
+ ): string {
1911
+ const pathModule = platform === "win32" ? path.win32 : path;
1912
+ const quote = platform === "win32" ? powershellQuote : shellQuote;
1913
+ const executable = execPath.trim();
1914
+ if (executable && !isBunVirtualPath(executable) && pathModule.isAbsolute(executable)) return quote(executable);
1915
+ const gjcPath = which("gjc")?.trim();
1916
+ if (gjcPath && !isBunVirtualPath(gjcPath)) return quote(gjcPath);
1917
+ return "gjc";
1918
+ }
1919
+
1899
1920
  export function resolveGjcWorkerCommand(
1900
1921
  cwd = process.cwd(),
1901
1922
  env: NodeJS.ProcessEnv = process.env,
1902
1923
  platform: NodeJS.Platform = process.platform,
1903
1924
  argv: string[] = process.argv,
1904
1925
  execPath = process.execPath,
1926
+ which: CommandPathResolver = Bun.which,
1905
1927
  ): string {
1906
1928
  const explicit = env.GJC_TEAM_WORKER_COMMAND?.trim();
1907
1929
  if (explicit) return explicit;
1908
1930
  const entrypoint = argv[1];
1931
+ if (isBunVirtualPath(entrypoint)) return resolveFallbackGjcWorkerCommand(platform, execPath, which);
1909
1932
  if (!entrypoint) return "gjc";
1910
1933
  const pathModule = platform === "win32" ? path.win32 : path;
1911
1934
  const resolvedEntrypoint = pathModule.isAbsolute(entrypoint) ? entrypoint : pathModule.resolve(cwd, entrypoint);