@gajae-code/coding-agent 0.3.0 → 0.3.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 (213) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
@@ -13,10 +13,16 @@ declare const subagentSchema: z.ZodObject<{
13
13
  steer: "steer";
14
14
  }>;
15
15
  ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
+ id: z.ZodOptional<z.ZodString>;
16
17
  message: z.ZodOptional<z.ZodString>;
17
18
  pause: z.ZodOptional<z.ZodBoolean>;
18
19
  timeout_ms: z.ZodOptional<z.ZodNumber>;
19
20
  limit: z.ZodOptional<z.ZodNumber>;
21
+ verbosity: z.ZodOptional<z.ZodEnum<{
22
+ full: "full";
23
+ preview: "preview";
24
+ receipt: "receipt";
25
+ }>>;
20
26
  }, z.core.$strip>;
21
27
  type SubagentParams = z.infer<typeof subagentSchema>;
22
28
  type SubagentStatus = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled" | "not_found" | "already_completed";
@@ -32,6 +38,9 @@ export interface SubagentSnapshot {
32
38
  durationMs: number;
33
39
  resultText?: string;
34
40
  errorText?: string;
41
+ resultPreview?: string;
42
+ outputRef?: string;
43
+ truncated?: boolean;
35
44
  guidance?: string;
36
45
  }
37
46
  export interface SubagentToolDetails {
@@ -55,10 +64,16 @@ export declare class SubagentTool implements AgentTool<typeof subagentSchema, Su
55
64
  steer: "steer";
56
65
  }>;
57
66
  ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
67
+ id: z.ZodOptional<z.ZodString>;
58
68
  message: z.ZodOptional<z.ZodString>;
59
69
  pause: z.ZodOptional<z.ZodBoolean>;
60
70
  timeout_ms: z.ZodOptional<z.ZodNumber>;
61
71
  limit: z.ZodOptional<z.ZodNumber>;
72
+ verbosity: z.ZodOptional<z.ZodEnum<{
73
+ full: "full";
74
+ preview: "preview";
75
+ receipt: "receipt";
76
+ }>>;
62
77
  }, z.core.$strip>;
63
78
  readonly strict = true;
64
79
  readonly loadMode = "discoverable";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -48,12 +48,12 @@
48
48
  "@agentclientprotocol/sdk": "0.21.0",
49
49
  "@babel/parser": "^7.29.3",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@gajae-code/stats": "0.3.0",
52
- "@gajae-code/agent-core": "0.3.0",
53
- "@gajae-code/ai": "0.3.0",
54
- "@gajae-code/natives": "0.3.0",
55
- "@gajae-code/tui": "0.3.0",
56
- "@gajae-code/utils": "0.3.0",
51
+ "@gajae-code/stats": "0.3.2",
52
+ "@gajae-code/agent-core": "0.3.2",
53
+ "@gajae-code/ai": "0.3.2",
54
+ "@gajae-code/natives": "0.3.2",
55
+ "@gajae-code/tui": "0.3.2",
56
+ "@gajae-code/utils": "0.3.2",
57
57
  "@puppeteer/browsers": "^2.13.0",
58
58
  "@types/turndown": "5.0.6",
59
59
  "@xterm/headless": "^6.0.0",
@@ -4,6 +4,7 @@ import * as path from "node:path";
4
4
 
5
5
  const packageDir = path.join(import.meta.dir, "..");
6
6
  const outputPath = path.join(packageDir, "dist", "gjc");
7
+ const nativeDir = path.join(packageDir, "..", "natives", "native");
7
8
 
8
9
  function shouldAdhocSignDarwinBinary(): boolean {
9
10
  return process.platform === "darwin";
@@ -21,6 +22,11 @@ async function runCommand(command: string[], env: NodeJS.ProcessEnv = Bun.env):
21
22
  throw new Error(`Command failed with exit code ${exitCode}: ${command.join(" ")}`);
22
23
  }
23
24
  }
25
+ async function stageWorkspaceNativeAddons(): Promise<void> {
26
+ await Array.fromAsync(new Bun.Glob("pi_natives.*.node").scan({ cwd: nativeDir }), async filename => {
27
+ await Bun.write(path.join(packageDir, "dist", filename), Bun.file(path.join(nativeDir, filename)));
28
+ });
29
+ }
24
30
 
25
31
  async function main(): Promise<void> {
26
32
  await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
@@ -62,6 +68,7 @@ async function main(): Promise<void> {
62
68
  buildEnv,
63
69
  );
64
70
 
71
+ await stageWorkspaceNativeAddons();
65
72
  // Bun 1.3.12 emits a truncated Mach-O signature on darwin builds.
66
73
  if (shouldAdhocSignDarwinBinary()) {
67
74
  await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
@@ -35,6 +35,8 @@ export interface AsyncJobMetadata {
35
35
  description?: string;
36
36
  assignment?: string;
37
37
  };
38
+ /** True when this bash job was started by the `monitor` tool (vs plain async bash). */
39
+ monitor?: boolean;
38
40
  }
39
41
 
40
42
  /**
@@ -224,6 +226,12 @@ export class AsyncJobManager {
224
226
  #resumeSeq = 0;
225
227
  #resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
226
228
  readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
229
+ /**
230
+ * Change listeners notified on any mutation that can alter the live job set
231
+ * (register, terminal/eviction transitions, dispose). Used by the status-line
232
+ * jobs widget / overlay to refresh event-driven without polling.
233
+ */
234
+ readonly #changeListeners = new Set<() => void>();
227
235
 
228
236
  #filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
229
237
  const ownerId = filter?.ownerId;
@@ -241,6 +249,29 @@ export class AsyncJobManager {
241
249
  this.#retentionMs = Math.max(0, Math.floor(options.retentionMs ?? DEFAULT_RETENTION_MS));
242
250
  }
243
251
 
252
+ /**
253
+ * Subscribe to live-job-set change events. Returns an unsubscribe function.
254
+ * Listener errors are isolated so one bad subscriber cannot break others.
255
+ */
256
+ onChange(cb: () => void): () => void {
257
+ this.#changeListeners.add(cb);
258
+ return () => {
259
+ this.#changeListeners.delete(cb);
260
+ };
261
+ }
262
+
263
+ #notifyChange(): void {
264
+ for (const cb of this.#changeListeners) {
265
+ try {
266
+ cb();
267
+ } catch (error) {
268
+ logger.warn("Async job change listener failed", {
269
+ error: error instanceof Error ? error.message : String(error),
270
+ });
271
+ }
272
+ }
273
+ }
274
+
244
275
  register(
245
276
  type: "bash" | "task",
246
277
  label: string,
@@ -336,6 +367,7 @@ export class AsyncJobManager {
336
367
  })();
337
368
 
338
369
  this.#jobs.set(id, job);
370
+ this.#notifyChange();
339
371
  return id;
340
372
  }
341
373
 
@@ -880,6 +912,8 @@ export class AsyncJobManager {
880
912
  this.#liveHandles.clear();
881
913
  this.#resumeDescriptors.clear();
882
914
  this.#resumeQueue.length = 0;
915
+ this.#notifyChange();
916
+ this.#changeListeners.clear();
883
917
  return drained;
884
918
  }
885
919
 
@@ -909,6 +943,7 @@ export class AsyncJobManager {
909
943
  }
910
944
 
911
945
  #scheduleEviction(jobId: string): void {
946
+ this.#notifyChange();
912
947
  if (this.#retentionMs <= 0) {
913
948
  this.#jobs.delete(jobId);
914
949
  this.#suppressedDeliveries.delete(jobId);
@@ -926,6 +961,7 @@ export class AsyncJobManager {
926
961
  this.#suppressedDeliveries.delete(jobId);
927
962
  this.#watchedJobs.delete(jobId);
928
963
  this.#outputState.delete(jobId);
964
+ this.#notifyChange();
929
965
  }, this.#retentionMs);
930
966
  timer.unref();
931
967
  this.#evictionTimers.set(jobId, timer);
package/src/cli/args.ts CHANGED
@@ -7,7 +7,7 @@ import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
8
8
  import { BUILTIN_TOOLS } from "../tools";
9
9
 
10
- export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
10
+ export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui" | "bridge";
11
11
 
12
12
  export interface Args {
13
13
  cwd?: string;
@@ -17,6 +17,8 @@ export interface Args {
17
17
  smol?: string;
18
18
  slow?: string;
19
19
  plan?: string;
20
+ mpreset?: string;
21
+ default?: boolean;
20
22
  apiKey?: string;
21
23
  systemPrompt?: string;
22
24
  appendSystemPrompt?: string;
@@ -96,7 +98,14 @@ export function parseArgs(args: string[]): Args {
96
98
  result.allowHome = true;
97
99
  } else if (arg === "--mode" && i + 1 < args.length) {
98
100
  const mode = args[++i];
99
- if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp" || mode === "rpc-ui") {
101
+ if (
102
+ mode === "text" ||
103
+ mode === "json" ||
104
+ mode === "rpc" ||
105
+ mode === "acp" ||
106
+ mode === "rpc-ui" ||
107
+ mode === "bridge"
108
+ ) {
100
109
  result.mode = mode;
101
110
  }
102
111
  } else if (arg === "--continue" || arg === "-c") {
@@ -120,6 +129,10 @@ export function parseArgs(args: string[]): Args {
120
129
  result.slow = args[++i];
121
130
  } else if (arg === "--plan" && i + 1 < args.length) {
122
131
  result.plan = args[++i];
132
+ } else if (arg === "--mpreset" && i + 1 < args.length) {
133
+ result.mpreset = args[++i];
134
+ } else if (arg === "--default") {
135
+ result.default = true;
123
136
  } else if (arg === "--api-key" && i + 1 < args.length) {
124
137
  result.apiKey = args[++i];
125
138
  } else if (arg === "--system-prompt" && i + 1 < args.length) {
@@ -192,6 +205,10 @@ export function parseArgs(args: string[]): Args {
192
205
  }
193
206
  }
194
207
 
208
+ if (result.default && !result.mpreset) {
209
+ throw new Error("--default requires --mpreset <name>");
210
+ }
211
+
195
212
  return result;
196
213
  }
197
214
 
@@ -21,6 +21,7 @@ export default class DeepInterview extends Command {
21
21
  deliberate: Flags.boolean({
22
22
  description: "Shortcut for --write handoff to ralplan in deliberate consensus mode",
23
23
  }),
24
+ force: Flags.boolean({ description: "Overwrite corrupt existing deep-interview state during --write" }),
24
25
  json: Flags.boolean({ description: "Output JSON" }),
25
26
  };
26
27
  static examples = [
@@ -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,