@gajae-code/coding-agent 0.3.0 → 0.3.1

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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -27,6 +27,7 @@ import {
27
27
  } from "../harness-control-plane/storage";
28
28
  import {
29
29
  DEFAULT_RETRY_BUDGET,
30
+ type EventEnvelope,
30
31
  type GitDelta,
31
32
  type Harness as HarnessKind,
32
33
  type Observation,
@@ -76,27 +77,191 @@ function gitDeltaFor(workspace: string): { gitDelta: GitDelta; branch: string |
76
77
  return { gitDelta: "unknown", branch, deleted: false };
77
78
  }
78
79
  }
80
+ interface HarnessPreflight {
81
+ ok: boolean;
82
+ blockers: string[];
83
+ workspace: string;
84
+ actualBranch: string | null;
85
+ declaredBranch: string | null;
86
+ normalizedIssueOrPr: string | null;
87
+ }
88
+
89
+ function normalizeIssueOrPr(value: unknown): string | null {
90
+ if (value === undefined || value === null) return null;
91
+ if (typeof value === "number") {
92
+ if (Number.isSafeInteger(value) && value > 0) return String(value);
93
+ throw new Error(`invalid_issue_or_pr:${value}`);
94
+ }
95
+ if (typeof value !== "string") throw new Error("invalid_issue_or_pr:not-string-or-number");
96
+ const trimmed = value.trim();
97
+ if (!trimmed) return null;
98
+ const patterns = [
99
+ /^#?(\d+)$/i,
100
+ /^(?:pr|pull|issue)[-_#]?(\d+)$/i,
101
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#(\d+)$/,
102
+ /^(?:https?:\/\/github\.com\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/(?:pull|issues)\/(\d+)\/?$/i,
103
+ ];
104
+ for (const pattern of patterns) {
105
+ const match = trimmed.match(pattern);
106
+ if (match?.[1]) return match[1];
107
+ }
108
+ throw new Error(`invalid_issue_or_pr:${trimmed}`);
109
+ }
110
+
111
+ function gitOutput(workspace: string, args: string[]): string | null {
112
+ try {
113
+ return execFileSync("git", args, {
114
+ cwd: workspace,
115
+ encoding: "utf8",
116
+ stdio: ["ignore", "pipe", "ignore"],
117
+ }).trim();
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function buildPreflight(input: Record<string, unknown>): HarnessPreflight {
124
+ const workspace = typeof input.workspace === "string" ? input.workspace : process.cwd();
125
+ const declaredBranch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : null;
126
+ const blockers: string[] = [];
127
+ const gitRoot = gitOutput(workspace, ["rev-parse", "--show-toplevel"]);
128
+ const actualBranch = gitRoot ? gitOutput(workspace, ["rev-parse", "--abbrev-ref", "HEAD"]) : null;
129
+ let normalizedIssueOrPr: string | null = null;
130
+
131
+ if (!gitRoot) blockers.push("workspace-not-git-repo");
132
+ if (gitRoot && actualBranch === "HEAD") blockers.push("detached-head");
133
+ if (declaredBranch && actualBranch && actualBranch !== "HEAD" && declaredBranch !== actualBranch) {
134
+ blockers.push("branch-mismatch");
135
+ }
136
+ try {
137
+ normalizedIssueOrPr = normalizeIssueOrPr(input.issueOrPr ?? input.pr ?? input.issue);
138
+ } catch (error) {
139
+ blockers.push(error instanceof Error ? error.message : String(error));
140
+ }
141
+
142
+ return {
143
+ ok: blockers.length === 0,
144
+ blockers,
145
+ workspace,
146
+ actualBranch: actualBranch === "HEAD" ? null : actualBranch,
147
+ declaredBranch,
148
+ normalizedIssueOrPr,
149
+ };
150
+ }
79
151
 
80
- /** Owner liveness always false in the foundation build (RuntimeOwner is M3). */
152
+ function startFatalPreflightBlockers(input: Record<string, unknown>, preflight: HarnessPreflight): string[] {
153
+ const strict = input.strictPreflight === true || typeof input.branch === "string";
154
+ return preflight.blockers.filter(blocker => {
155
+ if (blocker === "branch-mismatch") return true;
156
+ if (blocker.startsWith("invalid_issue_or_pr:")) return true;
157
+ if (strict && (blocker === "workspace-not-git-repo" || blocker === "detached-head")) return true;
158
+ return false;
159
+ });
160
+ }
161
+
162
+ /** Fallback liveness after owner routing failed: no reachable owner handled this CLI call. */
81
163
  function ownerLiveFor(_state: SessionState): boolean {
82
164
  return false;
83
165
  }
84
166
 
85
- function buildObservation(state: SessionState, ownerLive: boolean): Observation {
167
+ function pushUnique(out: string[], value: unknown): void {
168
+ if (typeof value === "string" && !out.includes(value)) out.push(value);
169
+ }
170
+
171
+ interface CompletedTerminalEvent {
172
+ cursor: number;
173
+ createdAt: string;
174
+ kind: string;
175
+ }
176
+
177
+ function completedTerminalEvent(events: EventEnvelope[]): CompletedTerminalEvent | null {
178
+ for (const event of [...events].reverse()) {
179
+ const signal = (event.evidence as { signal?: unknown } | undefined)?.signal;
180
+ if (event.kind === "rpc_agent_completed" || signal === "completed") {
181
+ return { cursor: event.cursor, createdAt: event.createdAt, kind: event.kind };
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+
187
+ async function buildObservation(
188
+ root: string,
189
+ state: SessionState,
190
+ ownerLive: boolean,
191
+ ): Promise<{
192
+ observation: Observation;
193
+ completedTerminalEvent: CompletedTerminalEvent | null;
194
+ }> {
86
195
  const workspace = state.handle.workspace;
87
196
  const { gitDelta, branch, deleted } = gitDeltaFor(workspace);
197
+ const events = await readEvents(root, state.sessionId, 0);
198
+ const observedSignals = ["SessionStart"];
199
+ for (const event of events.slice(-200)) {
200
+ pushUnique(observedSignals, (event.evidence as { signal?: unknown } | undefined)?.signal);
201
+ }
202
+ const terminalEvent = completedTerminalEvent(events);
203
+ const lastEventAt = events.at(-1)?.createdAt;
88
204
  return {
89
- lifecycle: state.lifecycle,
90
- ownerLive,
91
- cwd: workspace,
92
- branch: branch ?? state.handle.branch,
93
- gitDelta,
94
- lastActivityAt: state.updatedAt,
95
- observedSignals: ["SessionStart"],
96
- risk: deleted ? "deleted-worktree" : "normal",
205
+ observation: {
206
+ lifecycle: state.lifecycle,
207
+ ownerLive,
208
+ cwd: workspace,
209
+ branch: branch ?? state.handle.branch,
210
+ gitDelta,
211
+ lastActivityAt: lastEventAt ?? state.updatedAt,
212
+ observedSignals,
213
+ risk: deleted ? "deleted-worktree" : !ownerLive && gitDelta === "dirty" ? "vanished-dirty" : "normal",
214
+ },
215
+ completedTerminalEvent: terminalEvent,
97
216
  };
98
217
  }
99
218
 
219
+ function isOwnerLivenessBlocker(blocker: string): boolean {
220
+ return blocker === "detached-owner-not-live" || blocker.startsWith("owner-vanished:");
221
+ }
222
+
223
+ async function reconcileCompletedOwnerExited(
224
+ root: string,
225
+ state: SessionState,
226
+ observation: Observation,
227
+ completedTerminal: CompletedTerminalEvent | null,
228
+ ): Promise<SessionState> {
229
+ if (!completedTerminal || observation.ownerLive || observation.gitDelta !== "clean") return state;
230
+ if (state.lifecycle === "completed" || state.lifecycle === "retired") return state;
231
+ state.lifecycle = "completed";
232
+ state.blockers = state.blockers.filter(blocker => !isOwnerLivenessBlocker(blocker));
233
+ state.updatedAt = nowIso();
234
+ await writeSessionState(root, state);
235
+ return state;
236
+ }
237
+
238
+ function needsVanishedOwnerBlock(
239
+ state: SessionState,
240
+ observation: Observation,
241
+ completedTerminal: CompletedTerminalEvent | null,
242
+ ): boolean {
243
+ if (observation.ownerLive || state.lifecycle !== "observing") return false;
244
+ if (completedTerminal || observation.observedSignals.includes("completed")) return false;
245
+ return observation.observedSignals.some(
246
+ signal => signal === "prompt-accepted" || signal === "tool-call" || signal === "streaming",
247
+ );
248
+ }
249
+
250
+ async function markVanishedOwnerBlocked(
251
+ root: string,
252
+ state: SessionState,
253
+ observation: Observation,
254
+ completedTerminal: CompletedTerminalEvent | null,
255
+ ): Promise<SessionState> {
256
+ if (!needsVanishedOwnerBlock(state, observation, completedTerminal)) return state;
257
+ const blocker = `owner-vanished:${observation.gitDelta}`;
258
+ state.lifecycle = "blocked";
259
+ state.blockers = state.blockers.includes(blocker) ? state.blockers : [...state.blockers, blocker];
260
+ state.updatedAt = nowIso();
261
+ await writeSessionState(root, state);
262
+ return state;
263
+ }
264
+
100
265
  function resolveRetryBudget(input: Record<string, unknown>): RetryBudget {
101
266
  const supplied = input.retryBudget;
102
267
  if (supplied && typeof supplied === "object" && !Array.isArray(supplied)) {
@@ -139,7 +304,7 @@ export default class Harness extends Command {
139
304
 
140
305
  static args = {
141
306
  verb: Args.string({
142
- description: "start|submit|observe|classify|recover|validate|finalize|retire|events|monitor|operate",
307
+ description: "start|preflight|submit|observe|classify|recover|validate|finalize|retire|events|monitor|operate",
143
308
  required: true,
144
309
  }),
145
310
  };
@@ -168,6 +333,8 @@ export default class Harness extends Command {
168
333
  switch (verb) {
169
334
  case "start":
170
335
  return await this.#start(root, input);
336
+ case "preflight":
337
+ return this.#preflight(input);
171
338
  case "observe":
172
339
  return await this.#observe(root, input, flags.session);
173
340
  case "classify":
@@ -196,6 +363,20 @@ export default class Harness extends Command {
196
363
  }
197
364
  }
198
365
 
366
+ #preflight(input: Record<string, unknown>): void {
367
+ const preflight = buildPreflight(input);
368
+ writeJson({
369
+ ok: preflight.ok,
370
+ evidence: {
371
+ preflight,
372
+ guidance: preflight.ok
373
+ ? "workspace metadata is normalized"
374
+ : "fix blockers before gjc harness start; branch must match the actual checkout and issueOrPr must be numeric or a recognized PR/issue form",
375
+ },
376
+ });
377
+ if (!preflight.ok) process.exitCode = 1;
378
+ }
379
+
199
380
  async #finalizeVerb(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
200
381
  const sessionId = requireSessionId(input, flagSession);
201
382
  if (await this.#tryOwnerRoute(root, sessionId, "finalize", { ...input, sessionId })) return;
@@ -214,6 +395,7 @@ export default class Harness extends Command {
214
395
  ): Promise<void> {
215
396
  const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
216
397
  if (sessionId && (await this.#tryOwnerRoute(root, sessionId, verb, { ...input, sessionId }))) return;
398
+ if (verb === "recover" && sessionId) return this.#recoverWithoutOwner(root, sessionId, input);
217
399
  return this.#pending(root, verb, input, flagSession);
218
400
  }
219
401
 
@@ -343,6 +525,21 @@ export default class Harness extends Command {
343
525
  process.exitCode = 1;
344
526
  return;
345
527
  }
528
+ const preflight = buildPreflight(input);
529
+ const fatalBlockers = startFatalPreflightBlockers(input, preflight);
530
+ if (fatalBlockers.length > 0) {
531
+ writeJson({
532
+ ok: false,
533
+ error: "harness_preflight_failed",
534
+ evidence: {
535
+ preflight: { ...preflight, blockers: fatalBlockers, ok: false },
536
+ guidance:
537
+ "fix blockers before start; run gjc harness preflight with the same input for branch and issue/PR diagnostics",
538
+ },
539
+ });
540
+ process.exitCode = 1;
541
+ return;
542
+ }
346
543
  const workspace = typeof input.workspace === "string" ? input.workspace : process.cwd();
347
544
  const sessionId = typeof input.sessionId === "string" ? input.sessionId : generateSessionId();
348
545
  const eventsPath = `${root}/sessions/${sessionId}/events.jsonl`;
@@ -353,9 +550,9 @@ export default class Harness extends Command {
353
550
  harness,
354
551
  repo: typeof input.repo === "string" ? input.repo : null,
355
552
  workspace,
356
- branch: typeof input.branch === "string" ? input.branch : null,
553
+ branch: preflight.declaredBranch ?? preflight.actualBranch,
357
554
  base: typeof input.base === "string" ? input.base : null,
358
- issueOrPr: typeof input.issueOrPr === "string" ? input.issueOrPr : null,
555
+ issueOrPr: preflight.normalizedIssueOrPr,
359
556
  processHandle: { kind: "runtime-owner", ownerId: null, pid: null },
360
557
  rpcHandle: { kind: "rpc-subprocess", pid: null, sessionDir: `${root}/sessions/${sessionId}/gjc-session` },
361
558
  ownerHandle: { leasePath, endpoint: null, heartbeatAt: null },
@@ -407,6 +604,25 @@ export default class Harness extends Command {
407
604
  await writeSessionState(root, state);
408
605
  }
409
606
  }
607
+ if (ownerBlockerReason) {
608
+ const resolved = await resolveOwner(root, sessionId);
609
+ if (resolved.live && resolved.socketPath) {
610
+ ownerLive = true;
611
+ ownerBlockerReason = null;
612
+ handle.processHandle = {
613
+ kind: "runtime-owner",
614
+ ownerId: resolved.lease?.ownerId ?? null,
615
+ pid: resolved.lease?.pid ?? null,
616
+ };
617
+ handle.ownerHandle = {
618
+ leasePath,
619
+ endpoint: resolved.socketPath,
620
+ heartbeatAt: resolved.lease?.heartbeatAt ?? null,
621
+ };
622
+ state.handle = handle;
623
+ await writeSessionState(root, state);
624
+ }
625
+ }
410
626
  if (ownerBlockerReason) {
411
627
  state.lifecycle = "blocked";
412
628
  state.blockers = [...state.blockers, ownerBlockerReason];
@@ -421,6 +637,7 @@ export default class Harness extends Command {
421
637
  {
422
638
  handle,
423
639
  ownerRuntime,
640
+ preflight,
424
641
  ...(ownerFallbackReason ? { ownerFallbackReason } : {}),
425
642
  ...(ownerBlockerReason ? { reason: ownerBlockerReason } : {}),
426
643
  },
@@ -453,10 +670,24 @@ export default class Harness extends Command {
453
670
  async #observe(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
454
671
  const sessionId = requireSessionId(input, flagSession);
455
672
  if (await this.#tryOwnerRoute(root, sessionId, "observe", { ...input, sessionId })) return;
456
- const state = await loadState(root, sessionId);
673
+ let state = await loadState(root, sessionId);
457
674
  const ownerLive = ownerLiveFor(state);
458
- const observation = buildObservation(state, ownerLive);
459
- writeJson(buildResponse(state, ownerLive, { observation, readOnly: !ownerLive }));
675
+ const { observation, completedTerminalEvent } = await buildObservation(root, state, ownerLive);
676
+ state = await reconcileCompletedOwnerExited(root, state, observation, completedTerminalEvent);
677
+ const vanishedOwnerBlock = needsVanishedOwnerBlock(state, observation, completedTerminalEvent);
678
+ state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
679
+ writeJson(
680
+ buildResponse(state, ownerLive, {
681
+ observation: { ...observation, lifecycle: state.lifecycle },
682
+ readOnly: !ownerLive,
683
+ ...(vanishedOwnerBlock
684
+ ? { ownerVanished: true, blockerReason: `owner-vanished:${observation.gitDelta}` }
685
+ : {}),
686
+ ...(completedTerminalEvent && !ownerLive
687
+ ? { completedOwnerExited: true, terminalResult: completedTerminalEvent }
688
+ : {}),
689
+ }),
690
+ );
460
691
  }
461
692
 
462
693
  async #classify(root: string, input: Record<string, unknown>, flagSession: string | undefined): Promise<void> {
@@ -466,7 +697,16 @@ export default class Harness extends Command {
466
697
  const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
467
698
  if (sessionId) {
468
699
  stateView = await loadState(root, sessionId);
469
- if (!observation) observation = buildObservation(stateView, ownerLiveFor(stateView));
700
+ if (!observation) {
701
+ const built = await buildObservation(root, stateView, ownerLiveFor(stateView));
702
+ observation = built.observation;
703
+ stateView = await markVanishedOwnerBlocked(
704
+ root,
705
+ stateView,
706
+ built.observation,
707
+ built.completedTerminalEvent,
708
+ );
709
+ }
470
710
  }
471
711
  if (!observation) throw new Error("classify_requires_observation_or_session");
472
712
  const full: Observation = {
@@ -481,7 +721,12 @@ export default class Harness extends Command {
481
721
  };
482
722
  const decision = classifyRecovery({ observation: full, retryBudget: budget });
483
723
  if (stateView) {
484
- writeJson(buildResponse(stateView, ownerLiveFor(stateView), { decision, observation: full }));
724
+ writeJson(
725
+ buildResponse(stateView, ownerLiveFor(stateView), {
726
+ decision,
727
+ observation: { ...full, lifecycle: stateView.lifecycle },
728
+ }),
729
+ );
485
730
  return;
486
731
  }
487
732
  // Pure classify without a session: synthesize a minimal state view.
@@ -531,7 +776,7 @@ export default class Harness extends Command {
531
776
  const sessionId = requireSessionId(input, flagSession);
532
777
  if (await this.#tryOwnerRoute(root, sessionId, "retire", { ...input, sessionId })) return;
533
778
  const state = await loadState(root, sessionId);
534
- const observation = buildObservation(state, ownerLiveFor(state));
779
+ const { observation } = await buildObservation(root, state, ownerLiveFor(state));
535
780
  if (observation.gitDelta === "dirty" || observation.gitDelta === "unknown") {
536
781
  writeJson(
537
782
  buildResponse(
@@ -554,6 +799,31 @@ export default class Harness extends Command {
554
799
  writeJson(buildResponse(state, false, { retired: true }));
555
800
  }
556
801
 
802
+ async #recoverWithoutOwner(root: string, sessionId: string, input: Record<string, unknown>): Promise<void> {
803
+ const budget = resolveRetryBudget(input);
804
+ let state = await loadState(root, sessionId);
805
+ const { observation, completedTerminalEvent } = await buildObservation(root, state, false);
806
+ state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
807
+ const decision = classifyRecovery({
808
+ observation: { ...observation, lifecycle: state.lifecycle },
809
+ retryBudget: budget,
810
+ });
811
+ writeJson(
812
+ buildResponse(
813
+ state,
814
+ false,
815
+ {
816
+ pending: false,
817
+ reason: "owner-not-live",
818
+ decision,
819
+ observation: { ...observation, lifecycle: state.lifecycle },
820
+ },
821
+ false,
822
+ ),
823
+ );
824
+ process.exitCode = 1;
825
+ }
826
+
557
827
  async #pending(
558
828
  root: string,
559
829
  verb: string,
@@ -52,8 +52,8 @@ export default class Index extends Command {
52
52
  description: "Allow starting in ~ without auto-switching to a temp dir",
53
53
  }),
54
54
  mode: Flags.string({
55
- description: "Output mode: text (default), json, rpc, or rpc-ui",
56
- options: ["text", "json", "rpc", "acp", "rpc-ui"],
55
+ description: "Output mode: text (default), json, rpc, acp, rpc-ui, or bridge",
56
+ options: ["text", "json", "rpc", "acp", "rpc-ui", "bridge"],
57
57
  }),
58
58
  print: Flags.boolean({
59
59
  char: "p",
@@ -9,9 +9,10 @@ export default class State extends Command {
9
9
  '$ gjc state write --input \'{"state":{"interview_id":"abc"}}\' --mode deep-interview --json',
10
10
  "$ gjc state clear --mode deep-interview",
11
11
  "$ gjc state deep-interview read --json",
12
- '$ gjc state ralplan write --input \'{"phase":"approval","active":true}\' --json',
12
+ '$ gjc state ralplan write --input \'{"phase":"planner","active":true}\' --json',
13
13
  "$ gjc state team contract",
14
14
  "$ gjc state deep-interview handoff --to ralplan --json",
15
+ "$ gjc state doctor --skill ralplan --json",
15
16
  ];
16
17
 
17
18
  async run(): Promise<void> {
@@ -1,4 +1,5 @@
1
1
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
2
+ import { renderCliWriteReceipt } from "../gjc-runtime/cli-write-receipt";
2
3
  import { renderTeamStatusMarkdown } from "../gjc-runtime/state-renderer";
3
4
  import {
4
5
  buildTeamHudSummary,
@@ -45,6 +46,23 @@ function formatTaskCounts(counts: Record<string, number>): string {
45
46
  .join(" ");
46
47
  }
47
48
 
49
+ function snapshotWriteReceipt(snapshot: GjcTeamSnapshot): Record<string, unknown> {
50
+ return {
51
+ ok: true,
52
+ team_name: snapshot.team_name,
53
+ phase: snapshot.phase,
54
+ state_dir: snapshot.state_dir,
55
+ tmux_session: snapshot.tmux_session,
56
+ tmux_target: snapshot.tmux_target,
57
+ worker_count: snapshot.workers.length,
58
+ task_counts: snapshot.task_counts,
59
+ };
60
+ }
61
+
62
+ function writeReceipt(value: Record<string, unknown>): void {
63
+ process.stdout.write(renderCliWriteReceipt(value));
64
+ }
65
+
48
66
  function parseInputFlag(argv: string[]): Record<string, unknown> {
49
67
  const index = argv.indexOf("--input");
50
68
  if (index < 0) return {};
@@ -124,7 +142,7 @@ export default class Team extends Command {
124
142
  const snapshot = await monitorGjcTeamSnapshot(teamName);
125
143
  await syncTeamHud(snapshot);
126
144
  if (json) {
127
- writeJson(snapshot);
145
+ writeReceipt(snapshotWriteReceipt(snapshot));
128
146
  return;
129
147
  }
130
148
  writeText([
@@ -141,7 +159,7 @@ export default class Team extends Command {
141
159
  const snapshot = await shutdownGjcTeam(teamName);
142
160
  await syncTeamHud(snapshot);
143
161
  if (json) {
144
- writeJson(snapshot);
162
+ writeReceipt(snapshotWriteReceipt(snapshot));
145
163
  return;
146
164
  }
147
165
  writeText([`team: ${snapshot.team_name}`, `phase: ${snapshot.phase}`, `state: ${snapshot.state_dir}`]);
@@ -174,7 +192,7 @@ export default class Team extends Command {
174
192
  // API operations without a resolvable snapshot leave HUD state unchanged.
175
193
  }
176
194
  }
177
- writeJson(result);
195
+ writeReceipt(result as Record<string, unknown>);
178
196
  return;
179
197
  }
180
198
 
@@ -183,7 +201,7 @@ export default class Team extends Command {
183
201
  const snapshot = await startGjcTeam({ ...options, dryRun });
184
202
  await syncTeamHud(snapshot);
185
203
  if (json) {
186
- writeJson(snapshot);
204
+ writeReceipt(snapshotWriteReceipt(snapshot));
187
205
  return;
188
206
  }
189
207
  writeText([
@@ -38,6 +38,7 @@ interface AppKeybindings {
38
38
  "app.session.fork": true;
39
39
  "app.session.resume": true;
40
40
  "app.session.observe": true;
41
+ "app.jobs.open": true;
41
42
  "app.session.togglePath": true;
42
43
  "app.session.toggleSort": true;
43
44
  "app.session.rename": true;
@@ -149,6 +150,11 @@ export const KEYBINDINGS = {
149
150
  defaultKeys: "ctrl+s",
150
151
  description: "Observe subagent sessions",
151
152
  },
153
+
154
+ "app.jobs.open": {
155
+ defaultKeys: "alt+j",
156
+ description: "Open monitor/cron jobs overlay",
157
+ },
152
158
  "app.session.togglePath": {
153
159
  defaultKeys: "ctrl+p",
154
160
  description: "Toggle session path display",
@@ -74,6 +74,7 @@ export type StatusLineSegmentId =
74
74
  | "git"
75
75
  | "pr"
76
76
  | "subagents"
77
+ | "jobs"
77
78
  | "token_in"
78
79
  | "token_out"
79
80
  | "token_total"
@@ -2352,11 +2353,12 @@ export const SETTINGS_SCHEMA = {
2352
2353
 
2353
2354
  "task.maxConcurrency": {
2354
2355
  type: "number",
2355
- default: 32,
2356
+ default: 8,
2356
2357
  ui: {
2357
2358
  tab: "tasks",
2358
2359
  label: "Max Concurrent Tasks",
2359
- description: "Concurrent limit for subagents",
2360
+ description:
2361
+ "Safer concurrent limit for subagents; higher fan-out still requires an explicit plan above 4 tasks.",
2360
2362
  options: [
2361
2363
  { value: "0", label: "Unlimited" },
2362
2364
  { value: "1", label: "1 task" },
@@ -2408,7 +2410,8 @@ export const SETTINGS_SCHEMA = {
2408
2410
  ui: {
2409
2411
  tab: "tasks",
2410
2412
  label: "Fork Context Max Tokens",
2411
- description: "Approximate token cap for fork-context seeds. 0 uses 25% of the target model context window.",
2413
+ description:
2414
+ "Approximate token cap for explicit full fork-context seeds. 0 uses 15% of the target model context window, with a 15k fallback when the window is unknown.",
2412
2415
  },
2413
2416
  },
2414
2417
 
package/src/dap/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { logger, ptree } from "@gajae-code/utils";
2
+ import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
2
3
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
3
4
  import { ToolAbortError } from "../tools/tool-errors";
4
5
  import type {
@@ -532,15 +533,28 @@ export class DapClient {
532
533
  }
533
534
  }
534
535
 
535
- #handleProcessExit(): void {
536
+ async #handleProcessExit(): Promise<void> {
536
537
  if (this.#disposed) return;
537
538
  this.#disposed = true;
538
539
  const stderr = this.proc.peekStderr().trim();
539
540
  const exitCode = this.proc.exitCode;
541
+ const crashNotice = formatCrashDiagnosticNotice(
542
+ await writeCrashReport(
543
+ {
544
+ kind: "dap",
545
+ command: [this.adapter.resolvedCommand, ...this.adapter.args],
546
+ exitCode,
547
+ stderr,
548
+ protocol: this.adapter.connectMode ?? "stdio",
549
+ },
550
+ { cwd: this.cwd },
551
+ ),
552
+ );
553
+ const diagnosticSuffix = crashNotice ? `\n${crashNotice}` : "";
540
554
  const error = new Error(
541
555
  stderr
542
- ? `DAP adapter exited (code ${exitCode}): ${stderr}`
543
- : `DAP adapter exited unexpectedly (code ${exitCode})`,
556
+ ? `DAP adapter exited (code ${exitCode}): ${stderr}${diagnosticSuffix}`
557
+ : `DAP adapter exited unexpectedly (code ${exitCode})${diagnosticSuffix}`,
544
558
  );
545
559
  this.#rejectPendingRequests(error);
546
560
  }