@gajae-code/coding-agent 0.5.1 → 0.5.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 (165) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +6 -0
  4. package/dist/types/cli/setup-cli.d.ts +8 -1
  5. package/dist/types/commands/setup.d.ts +7 -0
  6. package/dist/types/config/file-lock.d.ts +24 -2
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +62 -0
  10. package/dist/types/dap/client.d.ts +2 -1
  11. package/dist/types/edit/read-file.d.ts +6 -0
  12. package/dist/types/eval/js/context-manager.d.ts +3 -0
  13. package/dist/types/eval/js/executor.d.ts +1 -0
  14. package/dist/types/exec/bash-executor.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  19. package/dist/types/lsp/types.d.ts +2 -0
  20. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  23. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  24. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  25. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
  28. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  29. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  30. package/dist/types/modes/theme/theme.d.ts +1 -0
  31. package/dist/types/modes/types.d.ts +1 -1
  32. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  33. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  34. package/dist/types/runtime-mcp/types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +17 -1
  36. package/dist/types/session/artifacts.d.ts +4 -1
  37. package/dist/types/session/history-storage.d.ts +2 -2
  38. package/dist/types/session/session-manager.d.ts +10 -1
  39. package/dist/types/session/streaming-output.d.ts +5 -0
  40. package/dist/types/setup/credential-import.d.ts +79 -0
  41. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  42. package/dist/types/task/executor.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +1 -1
  44. package/dist/types/tools/bash.d.ts +1 -0
  45. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  46. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  47. package/dist/types/tools/subagent-render.d.ts +7 -1
  48. package/dist/types/tools/subagent.d.ts +21 -0
  49. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  50. package/dist/types/web/search/index.d.ts +4 -4
  51. package/dist/types/web/search/provider.d.ts +16 -20
  52. package/dist/types/web/search/providers/base.d.ts +2 -1
  53. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  54. package/dist/types/web/search/types.d.ts +14 -2
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +7 -0
  57. package/src/async/job-manager.ts +153 -39
  58. package/src/cli/args.ts +2 -0
  59. package/src/cli/fast-help.ts +2 -0
  60. package/src/cli/setup-cli.ts +138 -3
  61. package/src/commands/setup.ts +5 -1
  62. package/src/commands/ultragoal.ts +3 -1
  63. package/src/config/file-lock-gc.ts +14 -2
  64. package/src/config/file-lock.ts +63 -13
  65. package/src/config/model-profile-activation.ts +15 -3
  66. package/src/config/model-profiles.ts +15 -15
  67. package/src/config/model-registry.ts +21 -1
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +62 -0
  70. package/src/dap/client.ts +105 -64
  71. package/src/dap/session.ts +44 -7
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  73. package/src/edit/read-file.ts +19 -1
  74. package/src/eval/js/context-manager.ts +228 -65
  75. package/src/eval/js/executor.ts +2 -0
  76. package/src/eval/js/index.ts +1 -0
  77. package/src/eval/js/worker-core.ts +10 -6
  78. package/src/eval/py/executor.ts +68 -19
  79. package/src/eval/py/kernel.ts +46 -22
  80. package/src/eval/py/runner.py +68 -14
  81. package/src/exec/bash-executor.ts +49 -13
  82. package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
  83. package/src/gjc-runtime/launch-tmux.ts +3 -4
  84. package/src/gjc-runtime/ralplan-runtime.ts +174 -12
  85. package/src/gjc-runtime/state-runtime.ts +2 -1
  86. package/src/gjc-runtime/state-writer.ts +254 -7
  87. package/src/gjc-runtime/tmux-gc.ts +88 -38
  88. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  89. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  90. package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
  91. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  92. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  93. package/src/harness-control-plane/owner.ts +3 -2
  94. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  95. package/src/hooks/skill-state.ts +121 -2
  96. package/src/internal-urls/artifact-protocol.ts +10 -1
  97. package/src/internal-urls/docs-index.generated.ts +14 -10
  98. package/src/lsp/client.ts +64 -26
  99. package/src/lsp/defaults.json +1 -0
  100. package/src/lsp/index.ts +2 -1
  101. package/src/lsp/lspmux.ts +33 -9
  102. package/src/lsp/types.ts +2 -0
  103. package/src/main.ts +14 -4
  104. package/src/modes/acp/acp-agent.ts +4 -2
  105. package/src/modes/bridge/bridge-mode.ts +23 -1
  106. package/src/modes/components/assistant-message.ts +10 -2
  107. package/src/modes/components/bash-execution.ts +5 -1
  108. package/src/modes/components/eval-execution.ts +5 -1
  109. package/src/modes/components/history-search.ts +5 -2
  110. package/src/modes/components/model-selector.ts +60 -2
  111. package/src/modes/components/oauth-selector.ts +5 -0
  112. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  113. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  114. package/src/modes/components/skill-message.ts +24 -16
  115. package/src/modes/components/tool-execution.ts +6 -0
  116. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  117. package/src/modes/controllers/input-controller.ts +5 -0
  118. package/src/modes/controllers/selector-controller.ts +86 -2
  119. package/src/modes/interactive-mode.ts +11 -1
  120. package/src/modes/rpc/rpc-mode.ts +132 -18
  121. package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
  122. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  123. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  124. package/src/modes/theme/defaults/claude-code.json +100 -0
  125. package/src/modes/theme/defaults/codex.json +100 -0
  126. package/src/modes/theme/defaults/index.ts +6 -0
  127. package/src/modes/theme/defaults/opencode.json +102 -0
  128. package/src/modes/theme/theme.ts +2 -2
  129. package/src/modes/types.ts +1 -1
  130. package/src/modes/utils/ui-helpers.ts +5 -2
  131. package/src/prompts/agents/executor.md +5 -2
  132. package/src/runtime/process-lifecycle.ts +400 -0
  133. package/src/runtime-mcp/manager.ts +164 -50
  134. package/src/runtime-mcp/transports/http.ts +12 -11
  135. package/src/runtime-mcp/transports/stdio.ts +64 -38
  136. package/src/runtime-mcp/types.ts +3 -0
  137. package/src/sdk.ts +39 -1
  138. package/src/session/agent-session.ts +190 -33
  139. package/src/session/artifacts.ts +17 -2
  140. package/src/session/blob-store.ts +36 -2
  141. package/src/session/history-storage.ts +32 -11
  142. package/src/session/session-manager.ts +99 -31
  143. package/src/session/streaming-output.ts +54 -3
  144. package/src/setup/credential-import.ts +429 -0
  145. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  146. package/src/slash-commands/builtin-registry.ts +30 -3
  147. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  148. package/src/task/executor.ts +7 -1
  149. package/src/task/render.ts +18 -7
  150. package/src/tools/archive-reader.ts +10 -1
  151. package/src/tools/ask.ts +4 -2
  152. package/src/tools/bash.ts +11 -4
  153. package/src/tools/browser/tab-supervisor.ts +22 -0
  154. package/src/tools/browser.ts +38 -4
  155. package/src/tools/cron.ts +1 -1
  156. package/src/tools/read.ts +11 -12
  157. package/src/tools/sqlite-reader.ts +19 -5
  158. package/src/tools/subagent-render.ts +119 -29
  159. package/src/tools/subagent.ts +147 -7
  160. package/src/tools/ultragoal-ask-guard.ts +39 -0
  161. package/src/web/search/index.ts +25 -25
  162. package/src/web/search/provider.ts +178 -87
  163. package/src/web/search/providers/base.ts +2 -1
  164. package/src/web/search/providers/openai-compatible.ts +151 -0
  165. package/src/web/search/types.ts +47 -22
@@ -7,6 +7,11 @@ const DELIVERY_RETRY_JITTER_MS = 200;
7
7
  const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
8
8
  const DEFAULT_MAX_RUNNING_JOBS = 15;
9
9
  const MONITOR_TOMBSTONE_TTL_MS = 5 * 60_000;
10
+ const DEFAULT_MAX_DELIVERY_QUEUE = 100;
11
+ const DELIVERY_MAX_TEXT_BYTES = 64 * 1024;
12
+ const DELIVERY_PREVIEW_HEAD_BYTES = 32 * 1024;
13
+ const DELIVERY_PREVIEW_TAIL_BYTES = 32 * 1024;
14
+ const DELIVERY_MAX_ATTEMPTS = 3;
10
15
 
11
16
  export interface AsyncJob {
12
17
  id: string;
@@ -128,9 +133,16 @@ export interface AsyncJobManagerOptions {
128
133
  retentionMs?: number;
129
134
  }
130
135
 
136
+ export interface AsyncJobDisposeDiagnostics {
137
+ stuckJobIds: string[];
138
+ deliveriesDrained: boolean;
139
+ }
140
+
131
141
  interface AsyncJobDelivery {
132
142
  jobId: string;
133
143
  text: string;
144
+ originalBytes?: number;
145
+ truncated?: boolean;
134
146
  attempt: number;
135
147
  nextAttemptAt: number;
136
148
  lastError?: string;
@@ -143,6 +155,7 @@ export interface AsyncJobDeliveryState {
143
155
  delivering: boolean;
144
156
  nextRetryAt?: number;
145
157
  pendingJobIds: string[];
158
+ deadLettered: number;
146
159
  }
147
160
 
148
161
  export interface AsyncJobLifecycleCleanup {
@@ -198,6 +211,32 @@ function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string
198
211
  return text.slice(codeUnitIndex);
199
212
  }
200
213
 
214
+ function sliceTextAfterUtf8ByteOffset(text: string, offsetBytes: number): string {
215
+ if (offsetBytes <= 0) return text;
216
+ let consumedBytes = 0;
217
+ let codeUnitIndex = 0;
218
+ for (const char of text) {
219
+ const charBytes = Buffer.byteLength(char, "utf8");
220
+ consumedBytes += charBytes;
221
+ codeUnitIndex += char.length;
222
+ if (consumedBytes >= offsetBytes) break;
223
+ }
224
+ return text.slice(codeUnitIndex);
225
+ }
226
+
227
+ function sliceTextToUtf8ByteLength(text: string, maxBytes: number): string {
228
+ if (maxBytes <= 0) return "";
229
+ let consumedBytes = 0;
230
+ let codeUnitIndex = 0;
231
+ for (const char of text) {
232
+ const charBytes = Buffer.byteLength(char, "utf8");
233
+ if (consumedBytes + charBytes > maxBytes) break;
234
+ consumedBytes += charBytes;
235
+ codeUnitIndex += char.length;
236
+ }
237
+ return text.slice(0, codeUnitIndex);
238
+ }
239
+
201
240
  /**
202
241
  * A slice of process-stream output for a background job, as recorded by
203
242
  * `appendOutput` / read by `readOutputSince`.
@@ -277,6 +316,8 @@ export class AsyncJobManager {
277
316
  #resumeSeq = 0;
278
317
  #resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
279
318
  readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
319
+ readonly #deadLetteredDeliveries = new Map<string, AsyncJobDelivery>();
320
+ #lastDisposeDiagnostics: AsyncJobDisposeDiagnostics = { stuckJobIds: [], deliveriesDrained: true };
280
321
  /**
281
322
  * Change listeners notified on any mutation that can alter the live job set
282
323
  * (register, terminal/eviction transitions, dispose). Used by the status-line
@@ -381,7 +422,7 @@ export class AsyncJobManager {
381
422
 
382
423
  if (job.status === "cancelled") {
383
424
  job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
384
- this.#runLifecycle(id, "terminal");
425
+ this.#runLifecycle(id, "terminal", job);
385
426
  this.#scheduleEviction(id);
386
427
  this.#markRecordTerminal(id, "cancelled");
387
428
  this.#drainResumeQueue();
@@ -403,20 +444,20 @@ export class AsyncJobManager {
403
444
  this.#freezeEndTime(job);
404
445
  job.resultText = outcome.text;
405
446
  this.#enqueueDelivery(id, outcome.text);
406
- this.#runLifecycle(id, "terminal");
447
+ this.#runLifecycle(id, "terminal", job);
407
448
  this.#scheduleEviction(id);
408
449
  this.#markRecordTerminal(id, "completed");
409
450
  this.#drainResumeQueue();
410
451
  } catch (error) {
411
452
  if (job.status === "cancelled") {
412
453
  job.errorText = error instanceof Error ? error.message : String(error);
413
- this.#runLifecycle(id, "terminal");
454
+ this.#runLifecycle(id, "terminal", job);
414
455
  this.#scheduleEviction(id);
415
456
  this.#markRecordTerminal(id, "cancelled");
416
457
  this.#drainResumeQueue();
417
458
  return;
418
459
  }
419
- this.#runLifecycle(id, "terminal");
460
+ this.#runLifecycle(id, "terminal", job);
420
461
  const errorText = error instanceof Error ? error.message : String(error);
421
462
  job.status = "failed";
422
463
  this.#freezeEndTime(job);
@@ -471,14 +512,14 @@ export class AsyncJobManager {
471
512
  job.endTime ??= Date.now();
472
513
  }
473
514
 
474
- #runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
515
+ #runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict", jobOverride?: AsyncJob): void {
516
+ const lifecycle = this.#lifecycles.get(jobId);
517
+ const job = jobOverride ?? this.#jobs.get(jobId);
518
+ if (!lifecycle || !job) return;
475
519
  const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
476
520
  if (fired.has(phase)) return;
477
521
  fired.add(phase);
478
522
  this.#lifecyclePhases.set(jobId, fired);
479
- const lifecycle = this.#lifecycles.get(jobId);
480
- const job = this.#jobs.get(jobId);
481
- if (!lifecycle || !job) return;
482
523
  try {
483
524
  if (phase === "cancel") lifecycle.onCancel?.(job);
484
525
  else if (phase === "terminal") lifecycle.onTerminal?.(job);
@@ -649,6 +690,16 @@ export class AsyncJobManager {
649
690
  }
650
691
  }
651
692
 
693
+ #purgeTerminalSubagentStateForJob(jobId: string): void {
694
+ const rec = this.#recordByJobId(jobId);
695
+ if (!rec) return;
696
+ if (rec.status === "paused" || rec.status === "queued") return;
697
+ this.#liveHandles.delete(rec.subagentId);
698
+ this.#subagentProgress.delete(rec.subagentId);
699
+ this.#resumeDescriptors.delete(rec.subagentId);
700
+ this.#subagentRecords.delete(rec.subagentId);
701
+ }
702
+
652
703
  #markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
653
704
  const rec = this.#recordByJobId(jobId);
654
705
  if (!rec) return;
@@ -967,6 +1018,10 @@ export class AsyncJobManager {
967
1018
  getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
968
1019
  const deliveries = this.#filterDeliveries(filter);
969
1020
  const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
1021
+ const ownerId = filter?.ownerId;
1022
+ const deadLettered = Array.from(this.#deadLetteredDeliveries.values()).filter(
1023
+ delivery => !ownerId || delivery.ownerId === ownerId,
1024
+ ).length;
970
1025
  const nextRetryAt = deliveries.reduce<number | undefined>((next, delivery) => {
971
1026
  if (next === undefined) return delivery.nextAttemptAt;
972
1027
  return Math.min(next, delivery.nextAttemptAt);
@@ -977,6 +1032,7 @@ export class AsyncJobManager {
977
1032
  delivering: inFlightDeliveries.length > 0 || (this.#deliveryLoop !== undefined && deliveries.length > 0),
978
1033
  nextRetryAt,
979
1034
  pendingJobIds: deliveries.concat(inFlightDeliveries).map(delivery => delivery.jobId),
1035
+ deadLettered,
980
1036
  };
981
1037
  }
982
1038
 
@@ -1035,6 +1091,29 @@ export class AsyncJobManager {
1035
1091
  }
1036
1092
  }
1037
1093
 
1094
+ getLastDisposeDiagnostics(): AsyncJobDisposeDiagnostics {
1095
+ return { ...this.#lastDisposeDiagnostics, stuckJobIds: [...this.#lastDisposeDiagnostics.stuckJobIds] };
1096
+ }
1097
+
1098
+ async #waitForAllWithDeadline(timeoutMs: number): Promise<{ completed: boolean; stuckJobIds: string[] }> {
1099
+ const jobs = Array.from(this.#jobs.values());
1100
+ if (jobs.length === 0) return { completed: true, stuckJobIds: [] };
1101
+ let timedOut = false;
1102
+ await Promise.race([
1103
+ Promise.allSettled(jobs.map(job => job.promise)),
1104
+ Bun.sleep(Math.max(0, timeoutMs)).then(() => {
1105
+ timedOut = true;
1106
+ }),
1107
+ ]);
1108
+ if (!timedOut) return { completed: true, stuckJobIds: [] };
1109
+ return {
1110
+ completed: false,
1111
+ stuckJobIds: Array.from(this.#jobs.values())
1112
+ .filter(job => job.status === "running" || job.status === "cancelled")
1113
+ .map(job => job.id),
1114
+ };
1115
+ }
1116
+
1038
1117
  async waitForAll(): Promise<void> {
1039
1118
  await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
1040
1119
  }
@@ -1102,12 +1181,18 @@ export class AsyncJobManager {
1102
1181
  }
1103
1182
  }
1104
1183
  this.#monitorTombstones.clear();
1105
- await this.waitForAll();
1106
- const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
1184
+ const timeoutMs = options?.timeoutMs ?? 3_000;
1185
+ const waitResult = await this.#waitForAllWithDeadline(timeoutMs);
1186
+ const drained = waitResult.completed ? await this.drainDeliveries({ timeoutMs }) : false;
1187
+ this.#lastDisposeDiagnostics = { stuckJobIds: waitResult.stuckJobIds, deliveriesDrained: drained };
1188
+ if (waitResult.stuckJobIds.length > 0) {
1189
+ logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds: waitResult.stuckJobIds });
1190
+ }
1107
1191
  this.#clearEvictionTimers();
1108
1192
  this.#jobs.clear();
1109
1193
  this.#deliveries.length = 0;
1110
1194
  this.#inFlightDeliveries.length = 0;
1195
+ this.#deadLetteredDeliveries.clear();
1111
1196
  this.#suppressedDeliveries.clear();
1112
1197
  this.#watchedJobs.clear();
1113
1198
  this.#outputState.clear();
@@ -1119,7 +1204,7 @@ export class AsyncJobManager {
1119
1204
  this.#resumeQueue.length = 0;
1120
1205
  this.#notifyChange();
1121
1206
  this.#changeListeners.clear();
1122
- return drained;
1207
+ return drained && waitResult.completed;
1123
1208
  }
1124
1209
 
1125
1210
  #resolveJobId(preferredId?: string): string {
@@ -1148,16 +1233,10 @@ export class AsyncJobManager {
1148
1233
  }
1149
1234
 
1150
1235
  #scheduleEviction(jobId: string): void {
1236
+ if (this.#disposed) return;
1151
1237
  this.#notifyChange();
1152
1238
  if (this.#retentionMs <= 0) {
1153
- this.#recordMonitorTombstone(jobId);
1154
- this.#runLifecycle(jobId, "evict");
1155
- this.#jobs.delete(jobId);
1156
- this.#lifecycles.delete(jobId);
1157
- this.#lifecyclePhases.delete(jobId);
1158
- this.#suppressedDeliveries.delete(jobId);
1159
- this.#watchedJobs.delete(jobId);
1160
- this.#outputState.delete(jobId);
1239
+ this.#evictJob(jobId);
1161
1240
  return;
1162
1241
  }
1163
1242
  const existing = this.#evictionTimers.get(jobId);
@@ -1166,20 +1245,25 @@ export class AsyncJobManager {
1166
1245
  }
1167
1246
  const timer = setTimeout(() => {
1168
1247
  this.#evictionTimers.delete(jobId);
1169
- this.#recordMonitorTombstone(jobId);
1170
- this.#runLifecycle(jobId, "evict");
1171
- this.#jobs.delete(jobId);
1172
- this.#lifecycles.delete(jobId);
1173
- this.#lifecyclePhases.delete(jobId);
1174
- this.#suppressedDeliveries.delete(jobId);
1175
- this.#watchedJobs.delete(jobId);
1176
- this.#outputState.delete(jobId);
1248
+ this.#evictJob(jobId);
1177
1249
  this.#notifyChange();
1178
1250
  }, this.#retentionMs);
1179
1251
  timer.unref();
1180
1252
  this.#evictionTimers.set(jobId, timer);
1181
1253
  }
1182
1254
 
1255
+ #evictJob(jobId: string): void {
1256
+ this.#recordMonitorTombstone(jobId);
1257
+ this.#runLifecycle(jobId, "evict");
1258
+ this.#purgeTerminalSubagentStateForJob(jobId);
1259
+ this.#jobs.delete(jobId);
1260
+ this.#lifecycles.delete(jobId);
1261
+ this.#lifecyclePhases.delete(jobId);
1262
+ this.#suppressedDeliveries.delete(jobId);
1263
+ this.#watchedJobs.delete(jobId);
1264
+ this.#outputState.delete(jobId);
1265
+ }
1266
+
1183
1267
  #clearEvictionTimers(): void {
1184
1268
  for (const timer of this.#evictionTimers.values()) {
1185
1269
  clearTimeout(timer);
@@ -1245,17 +1329,38 @@ export class AsyncJobManager {
1245
1329
  if (this.isDeliverySuppressed(jobId)) {
1246
1330
  return;
1247
1331
  }
1332
+ const deliveryText = this.#boundedDeliveryText(text);
1248
1333
  this.#deliveries.push({
1249
1334
  jobId,
1250
- text,
1335
+ text: deliveryText.text,
1336
+ originalBytes: deliveryText.originalBytes,
1337
+ truncated: deliveryText.truncated,
1251
1338
  attempt: 0,
1252
1339
  nextAttemptAt: Date.now(),
1253
1340
  ownerId: this.#jobs.get(jobId)?.ownerId,
1254
1341
  });
1342
+ while (this.#deliveries.length > DEFAULT_MAX_DELIVERY_QUEUE) {
1343
+ const dropped = this.#deliveries.shift();
1344
+ if (dropped) this.#deadLetteredDeliveries.set(dropped.jobId, dropped);
1345
+ }
1255
1346
  this.#ensureDeliveryLoop();
1256
1347
  }
1257
1348
 
1349
+ #boundedDeliveryText(text: string): { text: string; originalBytes?: number; truncated?: boolean } {
1350
+ const bytes = Buffer.byteLength(text, "utf8");
1351
+ if (bytes <= DELIVERY_MAX_TEXT_BYTES) return { text };
1352
+ const head = sliceTextToUtf8ByteLength(text, DELIVERY_PREVIEW_HEAD_BYTES);
1353
+ const tailStart = Math.max(0, bytes - DELIVERY_PREVIEW_TAIL_BYTES);
1354
+ const tail = sliceTextAfterUtf8ByteOffset(text, tailStart);
1355
+ return {
1356
+ text: `${head}\n\n[async delivery output truncated from ${bytes} bytes]\n\n${tail}`,
1357
+ originalBytes: bytes,
1358
+ truncated: true,
1359
+ };
1360
+ }
1361
+
1258
1362
  #ensureDeliveryLoop(): void {
1363
+ if (this.#disposed) return;
1259
1364
  if (this.#deliveryLoop) {
1260
1365
  return;
1261
1366
  }
@@ -1266,7 +1371,7 @@ export class AsyncJobManager {
1266
1371
  })
1267
1372
  .finally(() => {
1268
1373
  this.#deliveryLoop = undefined;
1269
- if (this.#deliveries.length > 0) {
1374
+ if (!this.#disposed && this.#deliveries.length > 0) {
1270
1375
  this.#ensureDeliveryLoop();
1271
1376
  }
1272
1377
  });
@@ -1304,20 +1409,29 @@ export class AsyncJobManager {
1304
1409
  } catch (error) {
1305
1410
  delivery.attempt += 1;
1306
1411
  delivery.lastError = error instanceof Error ? error.message : String(error);
1307
- delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
1308
- if (!this.isDeliverySuppressed(delivery.jobId)) {
1309
- this.#deliveries.push(delivery);
1412
+ if (delivery.attempt >= DELIVERY_MAX_ATTEMPTS) {
1413
+ this.#deadLetteredDeliveries.set(delivery.jobId, delivery);
1414
+ logger.warn("Async job completion delivery reached retry cap", {
1415
+ jobId: delivery.jobId,
1416
+ attempt: delivery.attempt,
1417
+ error: delivery.lastError,
1418
+ });
1419
+ } else {
1420
+ delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
1421
+ if (!this.isDeliverySuppressed(delivery.jobId)) {
1422
+ this.#deliveries.push(delivery);
1423
+ }
1424
+ logger.warn("Async job completion delivery failed", {
1425
+ jobId: delivery.jobId,
1426
+ attempt: delivery.attempt,
1427
+ nextRetryAt: delivery.nextAttemptAt,
1428
+ error: delivery.lastError,
1429
+ });
1310
1430
  }
1311
- logger.warn("Async job completion delivery failed", {
1312
- jobId: delivery.jobId,
1313
- attempt: delivery.attempt,
1314
- nextRetryAt: delivery.nextAttemptAt,
1315
- error: delivery.lastError,
1316
- });
1317
1431
  } finally {
1318
1432
  const index = this.#inFlightDeliveries.indexOf(delivery);
1319
1433
  if (index !== -1) this.#inFlightDeliveries.splice(index, 1);
1320
- if (this.#deliveries.length > 0) this.#ensureDeliveryLoop();
1434
+ if (!this.#disposed && this.#deliveries.length > 0) this.#ensureDeliveryLoop();
1321
1435
  }
1322
1436
  })();
1323
1437
  delivery.promise = promise;
package/src/cli/args.ts CHANGED
@@ -269,6 +269,8 @@ export function getExtraHelpText(): string {
269
269
  gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
270
270
  GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
271
271
  GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
272
+ GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
273
+ GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
272
274
 
273
275
  For complete environment variable reference, see:
274
276
  ${chalk.dim("docs/environment-variables.md")}
@@ -54,6 +54,8 @@ export function getExtraHelpText(): string {
54
54
  gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
55
55
  GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
56
56
  GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
57
+ GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
58
+ GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
57
59
 
58
60
  For complete environment variable reference, see:
59
61
  docs/environment-variables.md
@@ -3,8 +3,11 @@
3
3
  *
4
4
  * Handles `gjc setup [component]` to install the normal defaults or optional feature dependencies.
5
5
  */
6
+
6
7
  import * as path from "node:path";
7
- import { $which, APP_NAME, getPythonEnvDir } from "@gajae-code/utils";
8
+ import { createInterface } from "node:readline/promises";
9
+ import { SqliteAuthCredentialStore } from "@gajae-code/ai";
10
+ import { $which, APP_NAME, getAgentDbPath, getPythonEnvDir } from "@gajae-code/utils";
8
11
  import { $ } from "bun";
9
12
  import chalk from "chalk";
10
13
  import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
@@ -14,6 +17,7 @@ import {
14
17
  readGjcManagedCodexHooksStatus,
15
18
  } from "../hooks/codex-native-hooks-config";
16
19
  import { theme } from "../modes/theme/theme";
20
+ import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../setup/credential-import";
17
21
  import {
18
22
  formatHermesSetupResult,
19
23
  type HermesSetupFlags,
@@ -27,7 +31,7 @@ import {
27
31
  parseProviderCompatibility,
28
32
  } from "../setup/provider-onboarding";
29
33
 
30
- export type SetupComponent = "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
34
+ export type SetupComponent = "credentials" | "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
31
35
 
32
36
  export interface SetupCommandArgs {
33
37
  component: SetupComponent;
@@ -57,10 +61,12 @@ export interface SetupCommandArgs {
57
61
  gjcCommand?: string;
58
62
  target?: string;
59
63
  profileDir?: string;
64
+ yes?: boolean;
65
+ dryRun?: boolean;
60
66
  };
61
67
  }
62
68
 
63
- const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
69
+ const VALID_COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
64
70
 
65
71
  function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
66
72
  return (
@@ -113,6 +119,10 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
113
119
  flags.smoke = true;
114
120
  } else if (arg === "--install") {
115
121
  flags.install = true;
122
+ } else if (arg === "--yes" || arg === "-y") {
123
+ flags.yes = true;
124
+ } else if (arg === "--dry-run") {
125
+ flags.dryRun = true;
116
126
  } else if (arg === "--root") {
117
127
  flags.root = [...(flags.root ?? []), args[++i] ?? ""];
118
128
  } else if (arg === "--repo") {
@@ -243,6 +253,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
243
253
  case "stt":
244
254
  await handleSttSetup(cmd.flags);
245
255
  break;
256
+ case "credentials":
257
+ await handleCredentialsSetup(cmd.flags);
258
+ break;
246
259
  }
247
260
  }
248
261
 
@@ -472,6 +485,122 @@ async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promi
472
485
  process.exit(1);
473
486
  }
474
487
  }
488
+ async function confirmImport(count: number): Promise<boolean> {
489
+ if (!process.stdin.isTTY) return false;
490
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
491
+ try {
492
+ const answer = (await rl.question(`Import ${count} credential(s) into ${getAgentDbPath()}? [y/N] `))
493
+ .trim()
494
+ .toLowerCase();
495
+ return answer === "y" || answer === "yes";
496
+ } finally {
497
+ rl.close();
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Discover existing Claude Code / Codex CLI credentials and import them into the
503
+ * gjc credential store after a redacted preview + confirmation. Falls back to
504
+ * manual-setup guidance when nothing importable is found.
505
+ */
506
+ async function handleCredentialsSetup(flags: { json?: boolean; yes?: boolean; dryRun?: boolean }): Promise<void> {
507
+ const result = await discoverExternalCredentials();
508
+ const redactedPlan = {
509
+ importable: result.importable.map(c => ({
510
+ provider: c.provider,
511
+ kind: c.kind,
512
+ source: c.source,
513
+ identity: c.identity,
514
+ expiresAt: c.expiresAt,
515
+ redactedToken: c.redactedToken,
516
+ })),
517
+ skipped: result.skipped,
518
+ environment: result.environment,
519
+ };
520
+
521
+ if (result.importable.length === 0) {
522
+ if (flags.json) {
523
+ process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
524
+ return;
525
+ }
526
+ for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
527
+ process.stdout.write(
528
+ chalk.yellow(
529
+ `\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
530
+ ` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
531
+ ` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
532
+ ),
533
+ );
534
+ return;
535
+ }
536
+
537
+ if (!flags.json) {
538
+ process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
539
+ for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
540
+ }
541
+
542
+ if (flags.dryRun) {
543
+ if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
544
+ else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
545
+ return;
546
+ }
547
+
548
+ const confirmed = flags.yes || (await confirmImport(result.importable.length));
549
+ if (!confirmed) {
550
+ if (flags.json) {
551
+ process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
552
+ return;
553
+ }
554
+ process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
555
+ return;
556
+ }
557
+
558
+ const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
559
+ let summary: Awaited<ReturnType<typeof importCredentials>>;
560
+ try {
561
+ summary = await importCredentials(result.importable, (provider, credential) =>
562
+ store.upsertAuthCredentialForProvider(provider, credential),
563
+ );
564
+ } finally {
565
+ store.close();
566
+ }
567
+
568
+ if (flags.json) {
569
+ process.stdout.write(
570
+ `${JSON.stringify({
571
+ ...redactedPlan,
572
+ imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
573
+ failed: summary.failed.map(f => ({
574
+ provider: f.credential.provider,
575
+ source: f.credential.source,
576
+ error: f.error,
577
+ })),
578
+ })}\n`,
579
+ );
580
+ if (summary.failed.length > 0) process.exitCode = 1;
581
+ return;
582
+ }
583
+
584
+ for (const credential of summary.imported) {
585
+ process.stdout.write(
586
+ `${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
587
+ );
588
+ }
589
+ for (const failure of summary.failed) {
590
+ process.stdout.write(
591
+ `${chalk.red(`${theme.status.error} failed`)} ${failure.credential.provider} (${failure.credential.source}): ${failure.error}\n`,
592
+ );
593
+ }
594
+ if (summary.failed.length > 0) {
595
+ process.exitCode = 1;
596
+ return;
597
+ }
598
+ process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
599
+ }
600
+
601
+ function formatCredentialSummaryLine(credential: { provider: string; kind: string; source: string }): string {
602
+ return `${credential.provider} · ${credential.kind} (from ${credential.source})`;
603
+ }
475
604
 
476
605
  /**
477
606
  * Print setup command help.
@@ -489,6 +618,7 @@ ${chalk.bold("Components:")}
489
618
  provider Optional: add a preset, OpenAI-compatible, or Anthropic-compatible API provider
490
619
  python Optional: verify a Python 3 interpreter is reachable for code execution
491
620
  stt Optional: install speech-to-text dependencies (openai-whisper, recording tools)
621
+ credentials Optional: import existing Claude Code / Codex CLI credentials
492
622
 
493
623
 
494
624
  ${chalk.bold("Provider example:")}
@@ -524,6 +654,8 @@ ${chalk.bold("Options:")}
524
654
  --mutation Hermes MCP mutation classes: sessions,questions,reports,all
525
655
  --target Hermes config file target for config-only install
526
656
  --profile-dir Hermes profile directory for full setup install
657
+ --dry-run Preview discovered credentials without importing (credentials)
658
+ -y, --yes Import discovered credentials without an interactive prompt (credentials)
527
659
 
528
660
  ${chalk.bold("Examples:")}
529
661
  ${APP_NAME} setup Install bundled GJC default workflow skills
@@ -536,5 +668,8 @@ ${chalk.bold("Examples:")}
536
668
  ${APP_NAME} setup stt Install speech-to-text dependencies
537
669
  ${APP_NAME} setup stt --check Check if STT dependencies are available
538
670
  ${APP_NAME} setup python --check Check if Python execution is available
671
+ ${APP_NAME} setup credentials Discover & import existing Claude/Codex credentials
672
+ ${APP_NAME} setup credentials --dry-run Preview importable credentials (redacted)
673
+ ${APP_NAME} setup credentials --yes Import without an interactive prompt
539
674
  `);
540
675
  }
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
5
5
  import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
6
6
  import { initTheme } from "../modes/theme/theme";
7
7
 
8
- const COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
8
+ const COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
9
9
 
10
10
  export default class Setup extends Command {
11
11
  static description = "Install GJC defaults or optional feature dependencies";
@@ -47,6 +47,8 @@ export default class Setup extends Command {
47
47
  "api-key-env": Flags.string({ description: "Read provider API key from this environment variable" }),
48
48
  model: Flags.string({ description: "Model id to add (repeat or comma-separate)", multiple: true }),
49
49
  "models-path": Flags.string({ description: "Override models config path" }),
50
+ yes: Flags.boolean({ char: "y", description: "Import discovered credentials without an interactive prompt" }),
51
+ "dry-run": Flags.boolean({ description: "Preview discovered credentials without importing" }),
50
52
  };
51
53
 
52
54
  async run(): Promise<void> {
@@ -79,6 +81,8 @@ export default class Setup extends Command {
79
81
  gjcCommand: flags["gjc-command"],
80
82
  target: flags.target,
81
83
  profileDir: flags["profile-dir"],
84
+ yes: flags.yes,
85
+ dryRun: flags["dry-run"],
82
86
  },
83
87
  };
84
88
  await initTheme();
@@ -16,12 +16,14 @@ export default class Ultragoal extends Command {
16
16
  static delegateHelp = true;
17
17
 
18
18
  async run(): Promise<void> {
19
+ const isReviewStart = this.argv.includes("review") && this.argv.includes("review-start");
19
20
  const shouldActivateGoalMode = isUltragoalCreateGoalsInvocation(this.argv);
20
21
  const result = await runNativeUltragoalCommand(this.argv);
21
22
  if (result.stdout) process.stdout.write(result.stdout);
22
23
  if (result.stderr) process.stderr.write(result.stderr);
23
24
  process.exitCode = result.status;
24
- if (result.status !== 0 || !shouldActivateGoalMode) return;
25
+ if (result.status !== 0 || (!shouldActivateGoalMode && !isReviewStart)) return;
26
+ if (isReviewStart && !result.createdReviewPlan && (result.reviewBlockerGoalIds?.length ?? 0) === 0) return;
25
27
 
26
28
  const cwd = process.cwd();
27
29
  const { objective, goalsPath } = await readUltragoalGjcObjective(cwd);
@@ -2,6 +2,7 @@
2
2
  * GC adapter for config file-locks (`<file>.lock` dirs holding `{pid, timestamp}`).
3
3
  */
4
4
 
5
+ import type { Stats } from "node:fs";
5
6
  import * as fs from "node:fs/promises";
6
7
  import * as path from "node:path";
7
8
  import { getAgentDir, getConfigRootDir, isEnoent } from "@gajae-code/utils";
@@ -92,7 +93,7 @@ async function walkForLockDirs(
92
93
  return;
93
94
  }
94
95
 
95
- let stat: Awaited<ReturnType<typeof fs.lstat>>;
96
+ let stat: Stats;
96
97
  try {
97
98
  stat = await fs.lstat(dir);
98
99
  } catch (error) {
@@ -171,8 +172,19 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
171
172
  return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
172
173
  }
173
174
 
175
+ // Fail-closed owner-token guard (#606): we observed `info` (pid+timestamp)
176
+ // dead, but a fresh owner can reclaim a stale lock dir at this same path
177
+ // between the probe above and the unlink below. Pass the exact owner token
178
+ // so removal re-verifies the on-disk identity under the unlink and refuses
179
+ // to delete a recreated LIVE lock (TOCTOU).
174
180
  try {
175
- await removeFileLockDirForGc(lockDir);
181
+ const removal = await removeFileLockDirForGc(lockDir, info);
182
+ if (removal === "owner_changed") {
183
+ return { removed: false, skipped: "file_lock_owner_changed_before_delete" };
184
+ }
185
+ if (removal === "missing") {
186
+ return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
187
+ }
176
188
  return { removed: true };
177
189
  } catch (error) {
178
190
  return { removed: false, error: errorMessage(error) };