@gajae-code/coding-agent 0.5.0 → 0.5.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 (194) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +26 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/list-models.d.ts +6 -0
  6. package/dist/types/cli/setup-cli.d.ts +8 -1
  7. package/dist/types/commands/gc.d.ts +26 -0
  8. package/dist/types/commands/setup.d.ts +7 -0
  9. package/dist/types/config/file-lock-gc.d.ts +5 -0
  10. package/dist/types/config/file-lock.d.ts +29 -0
  11. package/dist/types/config/model-registry.d.ts +4 -0
  12. package/dist/types/config/models-config-schema.d.ts +5 -0
  13. package/dist/types/config/settings-schema.d.ts +62 -0
  14. package/dist/types/coordinator/contract.d.ts +1 -1
  15. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  25. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  26. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  27. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  28. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  29. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  31. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  32. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  33. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  34. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  42. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  43. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  46. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -1
  49. package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
  50. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  51. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  52. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  53. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  54. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  55. package/dist/types/modes/theme/theme.d.ts +1 -0
  56. package/dist/types/modes/types.d.ts +1 -1
  57. package/dist/types/session/agent-session.d.ts +1 -1
  58. package/dist/types/session/blob-store.d.ts +39 -3
  59. package/dist/types/session/history-storage.d.ts +2 -2
  60. package/dist/types/session/session-manager.d.ts +10 -1
  61. package/dist/types/setup/credential-import.d.ts +79 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/executor.d.ts +1 -0
  64. package/dist/types/task/render.d.ts +1 -1
  65. package/dist/types/tools/ask.d.ts +15 -1
  66. package/dist/types/tools/subagent-render.d.ts +7 -1
  67. package/dist/types/tools/subagent.d.ts +27 -0
  68. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  69. package/dist/types/web/search/index.d.ts +4 -4
  70. package/dist/types/web/search/provider.d.ts +16 -20
  71. package/dist/types/web/search/providers/base.d.ts +2 -1
  72. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  73. package/dist/types/web/search/types.d.ts +14 -2
  74. package/package.json +7 -7
  75. package/scripts/build-binary.ts +7 -0
  76. package/src/async/job-manager.ts +52 -0
  77. package/src/cli/args.ts +5 -0
  78. package/src/cli/auth-broker-cli.ts +1 -0
  79. package/src/cli/fast-help.ts +2 -0
  80. package/src/cli/list-models.ts +13 -1
  81. package/src/cli/setup-cli.ts +138 -3
  82. package/src/cli.ts +1 -0
  83. package/src/commands/gc.ts +22 -0
  84. package/src/commands/harness.ts +7 -3
  85. package/src/commands/setup.ts +5 -1
  86. package/src/commands/ultragoal.ts +3 -1
  87. package/src/config/file-lock-gc.ts +193 -0
  88. package/src/config/file-lock.ts +66 -10
  89. package/src/config/model-profile-activation.ts +15 -3
  90. package/src/config/model-profiles.ts +39 -30
  91. package/src/config/model-registry.ts +21 -1
  92. package/src/config/models-config-schema.ts +1 -0
  93. package/src/config/settings-schema.ts +62 -0
  94. package/src/coordinator/contract.ts +1 -0
  95. package/src/coordinator-mcp/server.ts +459 -3
  96. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  97. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  106. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  107. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  108. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  109. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  110. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  111. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  113. package/src/defaults/gjc-defaults.ts +7 -0
  114. package/src/defaults/gjc-grok-cli.ts +22 -0
  115. package/src/extensibility/extensions/index.ts +1 -0
  116. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  117. package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
  118. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  119. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  120. package/src/gjc-runtime/gc-render.ts +70 -0
  121. package/src/gjc-runtime/gc-runtime.ts +403 -0
  122. package/src/gjc-runtime/launch-tmux.ts +3 -4
  123. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  124. package/src/gjc-runtime/ralplan-runtime.ts +232 -19
  125. package/src/gjc-runtime/state-renderer.ts +12 -3
  126. package/src/gjc-runtime/state-runtime.ts +48 -30
  127. package/src/gjc-runtime/state-writer.ts +254 -7
  128. package/src/gjc-runtime/team-gc.ts +49 -0
  129. package/src/gjc-runtime/team-runtime.ts +179 -2
  130. package/src/gjc-runtime/tmux-common.ts +14 -0
  131. package/src/gjc-runtime/tmux-gc.ts +177 -0
  132. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  133. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  134. package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
  135. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  136. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  137. package/src/harness-control-plane/gc-adapter.ts +184 -0
  138. package/src/harness-control-plane/owner.ts +14 -2
  139. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  140. package/src/harness-control-plane/storage.ts +70 -0
  141. package/src/hooks/skill-state.ts +121 -2
  142. package/src/internal-urls/docs-index.generated.ts +22 -12
  143. package/src/lsp/defaults.json +1 -0
  144. package/src/main.ts +18 -3
  145. package/src/modes/acp/acp-agent.ts +4 -2
  146. package/src/modes/bridge/bridge-mode.ts +2 -1
  147. package/src/modes/components/history-search.ts +5 -2
  148. package/src/modes/components/hook-selector.ts +19 -0
  149. package/src/modes/components/model-selector.ts +51 -8
  150. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  151. package/src/modes/components/status-line/segments.ts +1 -1
  152. package/src/modes/controllers/command-controller.ts +25 -6
  153. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  154. package/src/modes/controllers/selector-controller.ts +81 -1
  155. package/src/modes/interactive-mode.ts +11 -1
  156. package/src/modes/rpc/rpc-mode.ts +266 -34
  157. package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
  158. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  159. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  160. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  161. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  162. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  163. package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
  164. package/src/modes/theme/defaults/claude-code.json +100 -0
  165. package/src/modes/theme/defaults/codex.json +100 -0
  166. package/src/modes/theme/defaults/index.ts +6 -0
  167. package/src/modes/theme/defaults/opencode.json +102 -0
  168. package/src/modes/theme/theme.ts +2 -2
  169. package/src/modes/types.ts +1 -1
  170. package/src/prompts/agents/executor.md +5 -2
  171. package/src/sdk.ts +29 -4
  172. package/src/session/agent-session.ts +99 -19
  173. package/src/session/blob-store.ts +59 -3
  174. package/src/session/history-storage.ts +32 -11
  175. package/src/session/session-manager.ts +72 -20
  176. package/src/setup/credential-import.ts +429 -0
  177. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  178. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  179. package/src/skill-state/workflow-hud.ts +106 -10
  180. package/src/slash-commands/builtin-registry.ts +3 -2
  181. package/src/task/executor.ts +16 -1
  182. package/src/task/render.ts +18 -7
  183. package/src/tools/ask.ts +59 -2
  184. package/src/tools/cron.ts +1 -1
  185. package/src/tools/job.ts +3 -2
  186. package/src/tools/monitor.ts +36 -1
  187. package/src/tools/subagent-render.ts +128 -29
  188. package/src/tools/subagent.ts +173 -9
  189. package/src/tools/ultragoal-ask-guard.ts +39 -0
  190. package/src/web/search/index.ts +25 -25
  191. package/src/web/search/provider.ts +178 -87
  192. package/src/web/search/providers/base.ts +2 -1
  193. package/src/web/search/providers/openai-compatible.ts +151 -0
  194. package/src/web/search/types.ts +47 -22
@@ -21,6 +21,8 @@ interface RalplanHudState extends WorkflowGateHudState {
21
21
  stage?: string;
22
22
  waiting?: string;
23
23
  iteration?: number;
24
+ iterationFromIndex?: number;
25
+ stages?: string;
24
26
  verdict?: string;
25
27
  latestSummary?: string;
26
28
  pendingApproval?: boolean;
@@ -100,6 +102,95 @@ export function buildDeepInterviewHudSummary(state: DeepInterviewHudState): Work
100
102
  };
101
103
  }
102
104
 
105
+ export interface DeepInterviewHudDeriveOptions {
106
+ phase?: string;
107
+ specStatus?: string;
108
+ updatedAt?: string;
109
+ }
110
+
111
+ function diIsPlainObject(value: unknown): value is Record<string, unknown> {
112
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
113
+ }
114
+
115
+ function latestScoredAmbiguity(rounds: unknown): number | undefined {
116
+ if (!Array.isArray(rounds)) return undefined;
117
+ for (let index = rounds.length - 1; index >= 0; index--) {
118
+ const round = rounds[index];
119
+ if (diIsPlainObject(round) && round.lifecycle === "scored" && typeof round.ambiguity === "number") {
120
+ return round.ambiguity;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ function weakestDimensionFromTopology(
127
+ topology: Record<string, unknown>,
128
+ targetComponent: string | undefined,
129
+ ): string | undefined {
130
+ if (!Array.isArray(topology.components)) return undefined;
131
+ const components = topology.components.filter(diIsPlainObject);
132
+ const dimensionOf = (component: Record<string, unknown>): string | undefined =>
133
+ typeof component.weakest_dimension === "string" && component.weakest_dimension.trim()
134
+ ? component.weakest_dimension
135
+ : undefined;
136
+ if (targetComponent) {
137
+ const targeted = components.find(component => component.id === targetComponent && dimensionOf(component));
138
+ if (targeted) return dimensionOf(targeted);
139
+ }
140
+ const active = components.find(component => component.status !== "deferred" && dimensionOf(component));
141
+ if (active) return dimensionOf(active);
142
+ const any = components.find(component => dimensionOf(component));
143
+ return any ? dimensionOf(any) : undefined;
144
+ }
145
+
146
+ /**
147
+ * Single source of deep-interview HUD derivation. Reads a complete (normalized)
148
+ * mode-state envelope so recorder, `gjc state write`, reconcile, seed, and handoff
149
+ * all produce identical chips. Topology-aware `target`/`weakest` come from
150
+ * `state.topology`; `legacy_missing` topology omits those chips (no synthetic values).
151
+ */
152
+ export function deriveDeepInterviewHud(
153
+ payload: Record<string, unknown>,
154
+ options: DeepInterviewHudDeriveOptions = {},
155
+ ): WorkflowHudSummary {
156
+ const stateField = diIsPlainObject(payload.state) ? payload.state : {};
157
+ const isNumber = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
158
+ const isArray = (value: unknown): value is unknown[] => Array.isArray(value);
159
+ const pick = <T>(key: string, guard: (value: unknown) => value is T): T | undefined => {
160
+ const value = stateField[key] ?? payload[key];
161
+ return guard(value) ? value : undefined;
162
+ };
163
+
164
+ const phase = options.phase ?? (typeof payload.current_phase === "string" ? payload.current_phase : undefined);
165
+ const rounds = pick("rounds", isArray);
166
+ const ambiguity = pick("current_ambiguity", isNumber) ?? latestScoredAmbiguity(rounds);
167
+ const threshold = pick("threshold", isNumber);
168
+ const rawTopology = diIsPlainObject(stateField.topology)
169
+ ? stateField.topology
170
+ : diIsPlainObject(payload.topology)
171
+ ? payload.topology
172
+ : undefined;
173
+ // `legacy_missing` topology was never confirmed: omit target/weakest even if stale fields linger.
174
+ const topology = rawTopology && rawTopology.status !== "legacy_missing" ? rawTopology : undefined;
175
+ const targetComponent =
176
+ topology && typeof topology.last_targeted_component_id === "string"
177
+ ? topology.last_targeted_component_id
178
+ : undefined;
179
+ const weakestDimension = topology ? weakestDimensionFromTopology(topology, targetComponent) : undefined;
180
+ const specStatus = options.specStatus ?? (typeof payload.spec_status === "string" ? payload.spec_status : undefined);
181
+
182
+ return buildDeepInterviewHudSummary({
183
+ phase,
184
+ ambiguity,
185
+ threshold,
186
+ roundCount: rounds?.length,
187
+ targetComponent,
188
+ weakestDimension,
189
+ specStatus,
190
+ updatedAt: options.updatedAt ?? new Date().toISOString(),
191
+ });
192
+ }
193
+
103
194
  export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSummary {
104
195
  const verdict = state.verdict?.toUpperCase();
105
196
  const verdictSeverity =
@@ -118,7 +209,14 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
118
209
  ...gateChips(state, 6),
119
210
  chip("stage", state.stage, 10),
120
211
  chip("waiting", state.waiting, 20),
121
- chip("iter", state.iteration === undefined ? undefined : String(state.iteration), 30),
212
+ chip(
213
+ "iter",
214
+ (state.iterationFromIndex ?? state.iteration) === undefined
215
+ ? undefined
216
+ : String(state.iterationFromIndex ?? state.iteration),
217
+ 30,
218
+ ),
219
+ chip("stages", state.stages, 35),
122
220
  chip("verdict", verdict, 40, verdictSeverity),
123
221
  ]),
124
222
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
@@ -136,17 +234,15 @@ export function buildUltragoalHudSummary(state: UltragoalHudState): WorkflowHudS
136
234
  chip("goals", `${complete}/${total}`, 10),
137
235
  chip("current", state.currentGoal ? `${state.currentGoal.id}:${state.currentGoal.title}` : state.status, 20),
138
236
  chip("status", state.status, 30, state.status === "complete" ? "success" : undefined),
237
+ chip(
238
+ "ledger",
239
+ state.latestLedgerEvent?.event
240
+ ? [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":")
241
+ : undefined,
242
+ 35,
243
+ ),
139
244
  ...gateChips(state, 40),
140
245
  ]),
141
- details: state.latestLedgerEvent
142
- ? compactChips([
143
- chip(
144
- "ledger",
145
- [state.latestLedgerEvent.event, state.latestLedgerEvent.goalId].filter(Boolean).join(":"),
146
- 100,
147
- ),
148
- ])
149
- : undefined,
150
246
  ...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
151
247
  };
152
248
  }
@@ -4,6 +4,7 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
6
  import { setProjectDir } from "@gajae-code/utils";
7
+ import { jobElapsedMs } from "../async";
7
8
  import {
8
9
  GJC_MODEL_ASSIGNMENT_TARGET_IDS,
9
10
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -506,14 +507,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
506
507
  if (snapshot.running.length > 0) {
507
508
  lines.push("", "Running Jobs");
508
509
  for (const job of snapshot.running) {
509
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
510
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
510
511
  lines.push(` ${job.label}`);
511
512
  }
512
513
  }
513
514
  if (snapshot.recent.length > 0) {
514
515
  lines.push("", "Recent Jobs");
515
516
  for (const job of snapshot.recent) {
516
- lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(now - job.startTime)}`);
517
+ lines.push(` [${job.id}] ${job.type} (${job.status}) — ${formatDuration(jobElapsedMs(job, now))}`);
517
518
  lines.push(` ${job.label}`);
518
519
  }
519
520
  }
@@ -482,11 +482,17 @@ function getUsageTokens(usage: unknown): number {
482
482
  return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
483
483
  }
484
484
 
485
- function createSubagentSettings(baseSettings: Settings): Settings {
485
+ export function createSubagentSettings(baseSettings: Settings): Settings {
486
486
  const snapshot: Partial<Record<SettingPath, unknown>> = {};
487
487
  for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
488
488
  snapshot[key] = baseSettings.get(key);
489
489
  }
490
+ // Subagent-scoped service-tier override: "inherit" keeps the snapshotted main
491
+ // session tier; any explicit value applies only to subagent sessions.
492
+ const taskServiceTier = baseSettings.get("task.serviceTier");
493
+ if (taskServiceTier !== "inherit") {
494
+ snapshot.serviceTier = taskServiceTier;
495
+ }
490
496
  return Settings.isolated({
491
497
  ...snapshot,
492
498
  "async.enabled": false,
@@ -1154,6 +1160,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1154
1160
  resolvedModel: model.id,
1155
1161
  });
1156
1162
  }
1163
+ // Record which model the subagent actually runs on (and any auth fallback,
1164
+ // see #985) so the subagent panel can surface it to the user.
1165
+ if (model) {
1166
+ AsyncJobManager.instance()?.updateSubagentModel?.(options.subagentId ?? id, {
1167
+ requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
1168
+ effectiveModel: resolvedModelString,
1169
+ modelFellBack: authFallbackUsed === true,
1170
+ });
1171
+ }
1157
1172
  if (model?.contextWindow && model.contextWindow > 0) {
1158
1173
  progress.contextWindow = model.contextWindow;
1159
1174
  }
@@ -525,6 +525,10 @@ function renderAgentProgress(
525
525
  expanded: boolean,
526
526
  theme: Theme,
527
527
  spinnerFrame?: number,
528
+ /** When true, omit wall-clock-derived displays (current-tool elapsed, retry
529
+ * countdown) so the output is a pure function of `progress` — required when the
530
+ * caller caches these lines (the `subagent` await panel). */
531
+ staticTime = false,
528
532
  ): string[] {
529
533
  const lines: string[] = [];
530
534
  const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
@@ -587,7 +591,7 @@ function renderAgentProgress(
587
591
  if (toolDetail) {
588
592
  toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
589
593
  }
590
- if (progress.currentToolStartMs) {
594
+ if (!staticTime && progress.currentToolStartMs) {
591
595
  const elapsed = Date.now() - progress.currentToolStartMs;
592
596
  if (elapsed > 5000) {
593
597
  toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
@@ -610,12 +614,17 @@ function renderAgentProgress(
610
614
  // long until the next attempt. Without this, the parent UI would just
611
615
  // keep spinning while a child sleeps on a 3-hour provider rate-limit.
612
616
  if (progress.retryState && progress.status === "running") {
613
- const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
614
- const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
615
617
  const attemptLabel = progress.retryState.unbounded
616
618
  ? `attempt ${progress.retryState.attempt}`
617
619
  : `${progress.retryState.attempt}/${progress.retryState.maxAttempts}`;
618
- const summary = `retrying ${attemptLabel} ${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
620
+ // `staticTime` omits the wall-clock countdown so a cached await body stays a
621
+ // pure function of its key (the producer already drops time-only churn).
622
+ let waitLabel = "";
623
+ if (!staticTime) {
624
+ const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
625
+ waitLabel = remainingMs > 0 ? ` in ${formatDuration(remainingMs)}` : " now";
626
+ }
627
+ const summary = `retrying ${attemptLabel}${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
619
628
  lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
620
629
  } else if (progress.retryFailure && progress.status !== "running") {
621
630
  const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
@@ -687,7 +696,7 @@ function renderAgentProgress(
687
696
  const inflight = progress.inflightTaskDetails;
688
697
  if (completedTaskCalls.length > 0 || inflight) {
689
698
  const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
690
- const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame);
699
+ const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame, staticTime);
691
700
  for (const line of nestedLines) {
692
701
  lines.push(`${continuePrefix}${line}`);
693
702
  }
@@ -712,8 +721,9 @@ export function renderSubagentLiveProgress(
712
721
  expanded: boolean,
713
722
  theme: Theme,
714
723
  spinnerFrame?: number,
724
+ staticTime = false,
715
725
  ): string[] {
716
- return renderAgentProgress(progress, true, expanded, theme, spinnerFrame);
726
+ return renderAgentProgress(progress, true, expanded, theme, spinnerFrame, staticTime);
717
727
  }
718
728
 
719
729
  /**
@@ -1051,6 +1061,7 @@ function renderNestedTaskTree(
1051
1061
  expanded: boolean,
1052
1062
  theme: Theme,
1053
1063
  spinnerFrame?: number,
1064
+ staticTime = false,
1054
1065
  ): string[] {
1055
1066
  const lines: string[] = [];
1056
1067
  for (const details of detailsList) {
@@ -1066,7 +1077,7 @@ function renderNestedTaskTree(
1066
1077
  if (inflight && inflight.length > 0) {
1067
1078
  inflight.forEach((prog, index) => {
1068
1079
  const isLast = index === inflight.length - 1;
1069
- lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame));
1080
+ lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame, staticTime));
1070
1081
  });
1071
1082
  }
1072
1083
  }
package/src/tools/ask.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  visibleWidth,
27
27
  wrapTextWithAnsi,
28
28
  } from "@gajae-code/tui";
29
- import { prompt, untilAborted } from "@gajae-code/utils";
29
+ import { logger, prompt, untilAborted } from "@gajae-code/utils";
30
30
  import * as z from "zod/v4";
31
31
  import {
32
32
  formatDeepInterviewSelectorPrompt,
@@ -34,6 +34,8 @@ import {
34
34
  renderDeepInterviewAskQuestion,
35
35
  } from "../deep-interview/render-middleware";
36
36
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
37
+ import { appendOrMergeDeepInterviewRound } from "../gjc-runtime/deep-interview-recorder";
38
+ import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
37
39
  import { gateAnswerToResult, questionToGate } from "../modes/shared/agent-wire/deep-interview-gate";
38
40
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
39
41
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
@@ -41,6 +43,7 @@ import { renderStatusLine } from "../tui";
41
43
  import type { ToolSession } from ".";
42
44
  import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
43
45
  import { ToolAbortError } from "./tool-errors";
46
+ import { assertUltragoalAskAllowed } from "./ultragoal-ask-guard";
44
47
 
45
48
  // =============================================================================
46
49
  // Types
@@ -50,15 +53,25 @@ const OptionItem = z.object({
50
53
  label: z.string().describe("display label"),
51
54
  });
52
55
 
56
+ /** Optional structured deep-interview round metadata; when present the round is recorded automatically. */
57
+ const DeepInterviewMeta = z.object({
58
+ round_id: z.string().describe("stable optional round identity").optional(),
59
+ round: z.number().int().nonnegative().describe("round number"),
60
+ component: z.string().min(1).describe("targeted topology component"),
61
+ dimension: z.string().min(1).describe("targeted clarity dimension"),
62
+ ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
63
+ });
64
+
53
65
  const QuestionItem = z.object({
54
66
  id: z.string().describe("question id"),
55
67
  question: z.string().describe("question text"),
56
68
  options: z.array(OptionItem).describe("available options"),
57
69
  multi: z.boolean().describe("allow multiple selections").optional(),
58
70
  recommended: z.number().describe("recommended option index").optional(),
71
+ deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
59
72
  });
60
73
 
61
- const askSchema = z.object({
74
+ export const askSchema = z.object({
62
75
  questions: z.array(QuestionItem).min(1).describe("questions to ask"),
63
76
  });
64
77
 
@@ -456,6 +469,45 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
456
469
  TERMINAL.sendNotification("Waiting for input");
457
470
  }
458
471
 
472
+ /**
473
+ * Record a resolved deep-interview round when the question carries structured
474
+ * metadata. The runtime owns durable record/merge semantics; this tool is only the
475
+ * caller. Best-effort: a state-write hiccup must not break the user's answer flow.
476
+ */
477
+ async #recordDeepInterviewRound(
478
+ q: AskParams["questions"][number],
479
+ selectedOptions: string[],
480
+ customInput: string | undefined,
481
+ ): Promise<void> {
482
+ const meta = q.deepInterview;
483
+ if (!meta) return;
484
+ try {
485
+ const cwd = this.session.cwd;
486
+ const sessionId = this.session.getSessionId?.() ?? undefined;
487
+ const statePath = deepInterviewStatePath(cwd, sessionId);
488
+ await appendOrMergeDeepInterviewRound(
489
+ cwd,
490
+ statePath,
491
+ {
492
+ round: meta.round,
493
+ round_id: meta.round_id,
494
+ questionId: q.id,
495
+ questionText: q.question,
496
+ component: meta.component,
497
+ dimension: meta.dimension,
498
+ ambiguity: meta.ambiguity,
499
+ selectedOptions,
500
+ customInput,
501
+ },
502
+ { sessionId },
503
+ );
504
+ } catch (error) {
505
+ logger.warn(
506
+ `ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
507
+ );
508
+ }
509
+ }
510
+
459
511
  async execute(
460
512
  _toolCallId: string,
461
513
  params: AskParams,
@@ -463,6 +515,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
463
515
  _onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
464
516
  context?: AgentToolContext,
465
517
  ): Promise<AgentToolResult<AskToolDetails>> {
518
+ await assertUltragoalAskAllowed(this.session.cwd);
466
519
  const gateEmitter = this.session.getWorkflowGateEmitter?.();
467
520
  const canUseWorkflowGate = gateEmitter?.isUnattended() === true;
468
521
 
@@ -515,6 +568,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
515
568
  options: q.options,
516
569
  multi: q.multi,
517
570
  recommended: q.recommended,
571
+ deepInterview: q.deepInterview,
518
572
  };
519
573
  const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
520
574
  const decoded = gateAnswerToResult(gateQuestion, answer);
@@ -582,6 +636,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
582
636
  context?.abort();
583
637
  throw new ToolAbortError("Ask tool was cancelled by the user");
584
638
  }
639
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
585
640
  const details: AskToolDetails = {
586
641
  question: q.question,
587
642
  options: optionLabels,
@@ -644,6 +699,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
644
699
  customInput,
645
700
  };
646
701
 
702
+ await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
703
+
647
704
  if (navAction === "back") {
648
705
  questionIndex = Math.max(0, questionIndex - 1);
649
706
  continue;
package/src/tools/cron.ts CHANGED
@@ -391,7 +391,7 @@ export function calculateCronFireTimeMs(params: {
391
391
  }
392
392
 
393
393
  function setCronTimeout(callback: () => void, delayMs: number): CronTimerHandle {
394
- let handle: ReturnType<typeof setTimeout> | undefined;
394
+ let handle: NodeJS.Timeout | undefined;
395
395
  let cleared = false;
396
396
  const schedule = (remainingMs: number) => {
397
397
  if (cleared) return;
package/src/tools/job.ts CHANGED
@@ -3,7 +3,7 @@ import type { Component } from "@gajae-code/tui";
3
3
  import { Text } from "@gajae-code/tui";
4
4
  import { prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
6
+ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
9
  import jobDescription from "../prompts/tools/job.md" with { type: "text" };
@@ -257,6 +257,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
257
257
  status: string;
258
258
  label: string;
259
259
  startTime: number;
260
+ endTime?: number;
260
261
  resultText?: string;
261
262
  errorText?: string;
262
263
  }[],
@@ -270,7 +271,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
270
271
  type: latest.type,
271
272
  status: latest.status as JobSnapshot["status"],
272
273
  label: latest.label,
273
- durationMs: Math.max(0, now - latest.startTime),
274
+ durationMs: jobElapsedMs(latest, now),
274
275
  ...(latest.resultText ? { resultText: latest.resultText } : {}),
275
276
  ...(latest.errorText ? { errorText: latest.errorText } : {}),
276
277
  };
@@ -3,6 +3,7 @@ import { logger, prompt } from "@gajae-code/utils";
3
3
  import * as z from "zod/v4";
4
4
  import { AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
5
5
  import monitorDescription from "../prompts/tools/monitor.md" with { type: "text" };
6
+ import { truncateTail } from "../session/streaming-output";
6
7
  import { BashTool } from "./bash";
7
8
  import type { ToolSession } from "./index";
8
9
  import { ToolError } from "./tool-errors";
@@ -48,6 +49,8 @@ export interface MonitorToolDetails {
48
49
 
49
50
  const MONITOR_LABEL_MAX = 120;
50
51
  const MAX_PENDING_MONITOR_NOTIFICATIONS = 3;
52
+ const MONITOR_NOTIFICATION_LINE_MAX_BYTES = 16 * 1024;
53
+ const MONITOR_NOTIFICATION_LINE_MAX_LINES = 20;
51
54
 
52
55
  function buildMonitorLabel(params: MonitorParams): string {
53
56
  const base = `[monitor:${params.kind}] ${params.description}`;
@@ -55,6 +58,34 @@ function buildMonitorLabel(params: MonitorParams): string {
55
58
  return `${base.slice(0, MONITOR_LABEL_MAX - 3)}...`;
56
59
  }
57
60
 
61
+ function formatMonitorNotificationLine(line: string): {
62
+ content: string;
63
+ truncated: boolean;
64
+ totalBytes: number;
65
+ outputBytes: number;
66
+ } {
67
+ const truncation = truncateTail(line, {
68
+ maxBytes: MONITOR_NOTIFICATION_LINE_MAX_BYTES,
69
+ maxLines: MONITOR_NOTIFICATION_LINE_MAX_LINES,
70
+ });
71
+ const outputBytes = truncation.outputBytes ?? truncation.totalBytes;
72
+ if (!truncation.truncated) {
73
+ return {
74
+ content: truncation.content,
75
+ truncated: false,
76
+ totalBytes: truncation.totalBytes,
77
+ outputBytes,
78
+ };
79
+ }
80
+ const notice = `[Monitor output truncated: showing last ${outputBytes} of ${truncation.totalBytes} bytes]`;
81
+ return {
82
+ content: `${truncation.content}\n${notice}`,
83
+ truncated: true,
84
+ totalBytes: truncation.totalBytes,
85
+ outputBytes,
86
+ };
87
+ }
88
+
58
89
  export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolDetails> {
59
90
  readonly name = "monitor";
60
91
  readonly label = "Monitor";
@@ -130,7 +161,8 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
130
161
  if (controller.closed) return;
131
162
  const notificationId = `${jobId}:${sequence}`;
132
163
  const suffix = count > 0 ? `\n(+${count} earlier lines)` : "";
133
- const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted latest state:\n${line}${suffix}\n</task-notification>`;
164
+ const notificationLine = formatMonitorNotificationLine(line);
165
+ const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted latest state:\n${notificationLine.content}${suffix}\n</task-notification>`;
134
166
  const details = {
135
167
  taskId: jobId,
136
168
  kind: params.kind,
@@ -139,6 +171,9 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
139
171
  notificationId,
140
172
  sequence,
141
173
  coalescedCount: count,
174
+ outputTruncated: notificationLine.truncated,
175
+ outputTotalBytes: notificationLine.totalBytes,
176
+ outputBytes: notificationLine.outputBytes,
142
177
  };
143
178
  pendingNotifications += 1;
144
179
  if (pendingNotifications > MAX_PENDING_MONITOR_NOTIFICATIONS) {