@gajae-code/coding-agent 0.3.1 → 0.4.0

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 (166) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +6 -0
  5. package/dist/types/config/model-profile-activation.d.ts +30 -0
  6. package/dist/types/config/model-profiles.d.ts +19 -0
  7. package/dist/types/config/model-registry.d.ts +25 -10
  8. package/dist/types/config/model-resolver.d.ts +1 -1
  9. package/dist/types/config/models-config-schema.d.ts +84 -0
  10. package/dist/types/config/settings-schema.d.ts +15 -0
  11. package/dist/types/edit/diff.d.ts +16 -0
  12. package/dist/types/edit/modes/replace.d.ts +7 -0
  13. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  14. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  16. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  17. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  18. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  19. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  20. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  22. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  23. package/dist/types/extensibility/skills.d.ts +9 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  25. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  26. package/dist/types/lsp/client.d.ts +1 -0
  27. package/dist/types/main.d.ts +10 -1
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  29. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  30. package/dist/types/modes/components/model-selector.d.ts +6 -1
  31. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  34. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  35. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  36. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  37. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  38. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  39. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  40. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  41. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  42. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  43. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  44. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  45. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  46. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  47. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  49. package/dist/types/modes/theme/theme.d.ts +2 -1
  50. package/dist/types/modes/types.d.ts +1 -0
  51. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  52. package/dist/types/sdk.d.ts +8 -1
  53. package/dist/types/session/agent-session.d.ts +10 -0
  54. package/dist/types/session/blob-store.d.ts +17 -0
  55. package/dist/types/session/messages.d.ts +3 -0
  56. package/dist/types/session/session-storage.d.ts +6 -0
  57. package/dist/types/skill-state/active-state.d.ts +13 -0
  58. package/dist/types/task/executor.d.ts +1 -0
  59. package/dist/types/thinking.d.ts +3 -2
  60. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  61. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  62. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  63. package/dist/types/tools/index.d.ts +7 -4
  64. package/package.json +9 -7
  65. package/src/cli/args.ts +10 -0
  66. package/src/cli.ts +14 -0
  67. package/src/commands/harness.ts +192 -7
  68. package/src/commands/launch.ts +8 -0
  69. package/src/commands/ultragoal.ts +1 -21
  70. package/src/config/model-equivalence.ts +1 -1
  71. package/src/config/model-profile-activation.ts +157 -0
  72. package/src/config/model-profiles.ts +155 -0
  73. package/src/config/model-registry.ts +51 -5
  74. package/src/config/model-resolver.ts +3 -2
  75. package/src/config/models-config-schema.ts +42 -1
  76. package/src/config/settings-schema.ts +14 -1
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +11 -1
  78. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  79. package/src/defaults/gjc-defaults.ts +7 -0
  80. package/src/discovery/claude-plugins.ts +25 -5
  81. package/src/edit/diff.ts +64 -1
  82. package/src/edit/modes/replace.ts +60 -2
  83. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  84. package/src/extensibility/gjc-plugins/index.ts +9 -0
  85. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  86. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  87. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  88. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  89. package/src/extensibility/gjc-plugins/state.ts +29 -0
  90. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  91. package/src/extensibility/gjc-plugins/types.ts +97 -0
  92. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  93. package/src/extensibility/skills.ts +39 -7
  94. package/src/gjc-runtime/state-runtime.ts +93 -2
  95. package/src/gjc-runtime/state-writer.ts +17 -1
  96. package/src/gjc-runtime/ultragoal-runtime.ts +62 -2
  97. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  98. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  99. package/src/harness-control-plane/storage.ts +144 -2
  100. package/src/hashline/hash.ts +23 -0
  101. package/src/hooks/skill-state.ts +2 -0
  102. package/src/internal-urls/docs-index.generated.ts +8 -11
  103. package/src/lsp/client.ts +7 -0
  104. package/src/main.ts +67 -1
  105. package/src/modes/acp/acp-agent.ts +25 -2
  106. package/src/modes/bridge/bridge-mode.ts +124 -2
  107. package/src/modes/components/custom-provider-wizard.ts +318 -0
  108. package/src/modes/components/model-selector.ts +108 -18
  109. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  110. package/src/modes/controllers/input-controller.ts +14 -2
  111. package/src/modes/controllers/selector-controller.ts +57 -1
  112. package/src/modes/prompt-action-autocomplete.ts +49 -10
  113. package/src/modes/rpc/rpc-client.ts +57 -3
  114. package/src/modes/rpc/rpc-mode.ts +67 -0
  115. package/src/modes/rpc/rpc-types.ts +224 -2
  116. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  117. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  118. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  119. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  120. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  121. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  122. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  123. package/src/modes/shared/agent-wire/responses.ts +2 -2
  124. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  125. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  126. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  127. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  128. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  129. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  130. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  131. package/src/modes/theme/theme.ts +6 -0
  132. package/src/modes/types.ts +1 -0
  133. package/src/prompts/memories/consolidation.md +1 -1
  134. package/src/prompts/memories/read-path.md +6 -7
  135. package/src/prompts/memories/unavailable.md +2 -2
  136. package/src/prompts/tools/bash.md +1 -1
  137. package/src/prompts/tools/irc.md +1 -1
  138. package/src/prompts/tools/read.md +2 -2
  139. package/src/prompts/tools/recall.md +1 -0
  140. package/src/prompts/tools/reflect.md +1 -0
  141. package/src/prompts/tools/retain.md +1 -0
  142. package/src/runtime-mcp/client.ts +7 -4
  143. package/src/runtime-mcp/manager.ts +45 -13
  144. package/src/runtime-mcp/transports/http.ts +40 -14
  145. package/src/runtime-mcp/transports/stdio.ts +11 -10
  146. package/src/sdk.ts +48 -1
  147. package/src/session/agent-session.ts +211 -2
  148. package/src/session/blob-store.ts +84 -0
  149. package/src/session/messages.ts +3 -0
  150. package/src/session/session-manager.ts +390 -33
  151. package/src/session/session-storage.ts +26 -0
  152. package/src/setup/provider-onboarding.ts +2 -2
  153. package/src/skill-state/active-state.ts +89 -1
  154. package/src/slash-commands/builtin-registry.ts +1 -1
  155. package/src/task/discovery.ts +7 -1
  156. package/src/task/executor.ts +18 -2
  157. package/src/task/index.ts +2 -0
  158. package/src/thinking.ts +8 -2
  159. package/src/tools/ask.ts +39 -9
  160. package/src/tools/hindsight-recall.ts +0 -2
  161. package/src/tools/hindsight-reflect.ts +0 -2
  162. package/src/tools/hindsight-retain.ts +0 -2
  163. package/src/tools/index.ts +7 -18
  164. package/src/tools/read.ts +3 -3
  165. package/src/tools/skill.ts +15 -3
  166. package/src/utils/edit-mode.ts +1 -1
@@ -9,20 +9,28 @@
9
9
  * until the RuntimeOwner (M3+) lands.
10
10
  */
11
11
  import { execFileSync } from "node:child_process";
12
+ import { randomBytes } from "node:crypto";
12
13
  import { existsSync } from "node:fs";
13
14
  import { Args, Command, Flags } from "@gajae-code/utils/cli";
14
15
  import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
15
16
  import { classifyRecovery } from "../harness-control-plane/classifier";
16
17
  import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
17
- import { RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
18
+ import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
19
+ import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
20
+ import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
18
21
  import { GajaeCodeRpc } from "../harness-control-plane/rpc-adapter";
22
+ import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
19
23
  import { buildResponse, buildStateView } from "../harness-control-plane/state-machine";
20
24
  import {
25
+ canonicalWorkspacePath,
21
26
  generateSessionId,
22
27
  readEvents,
23
28
  readSessionState,
29
+ rememberHarnessSessionRoot,
24
30
  resolveHarnessRoot,
31
+ resolveHarnessSessionRoot,
25
32
  sessionPaths,
33
+ writeReceiptImmutable,
26
34
  writeSessionState,
27
35
  } from "../harness-control-plane/storage";
28
36
  import {
@@ -31,6 +39,7 @@ import {
31
39
  type GitDelta,
32
40
  type Harness as HarnessKind,
33
41
  type Observation,
42
+ type RecoveryClassification,
34
43
  type RetryBudget,
35
44
  SESSION_SCHEMA_VERSION,
36
45
  type SessionHandle,
@@ -120,8 +129,12 @@ function gitOutput(workspace: string, args: string[]): string | null {
120
129
  }
121
130
  }
122
131
 
132
+ function resolveInputWorkspace(input: Record<string, unknown>): string {
133
+ return canonicalWorkspacePath(typeof input.workspace === "string" ? input.workspace : process.cwd());
134
+ }
135
+
123
136
  function buildPreflight(input: Record<string, unknown>): HarnessPreflight {
124
- const workspace = typeof input.workspace === "string" ? input.workspace : process.cwd();
137
+ const workspace = resolveInputWorkspace(input);
125
138
  const declaredBranch = typeof input.branch === "string" && input.branch.trim() ? input.branch.trim() : null;
126
139
  const blockers: string[] = [];
127
140
  const gitRoot = gitOutput(workspace, ["rev-parse", "--show-toplevel"]);
@@ -198,6 +211,7 @@ async function buildObservation(
198
211
  const observedSignals = ["SessionStart"];
199
212
  for (const event of events.slice(-200)) {
200
213
  pushUnique(observedSignals, (event.evidence as { signal?: unknown } | undefined)?.signal);
214
+ if (event.kind === "prompt_accepted") pushUnique(observedSignals, "prompt-accepted");
201
215
  }
202
216
  const terminalEvent = completedTerminalEvent(events);
203
217
  const lastEventAt = events.at(-1)?.createdAt;
@@ -215,6 +229,114 @@ async function buildObservation(
215
229
  completedTerminalEvent: terminalEvent,
216
230
  };
217
231
  }
232
+ interface OwnerExitEvidence {
233
+ reason: string;
234
+ leaseStatus: string;
235
+ pid: number | null;
236
+ endpointPresent: boolean;
237
+ heartbeatAt: string | null;
238
+ expiresAt: string | null;
239
+ lastEventKind: string | null;
240
+ lastEventAt: string | null;
241
+ lastSignal: string | null;
242
+ promptAcceptedSeen: boolean;
243
+ completedSeen: boolean;
244
+ }
245
+
246
+ async function buildOwnerExitEvidence(root: string, state: SessionState): Promise<OwnerExitEvidence> {
247
+ const lease = await readLease(root, state.sessionId);
248
+ const leaseStatus = classifyLeaseStatus(lease);
249
+ const events = await readEvents(root, state.sessionId, 0);
250
+ const lastEvent = events.at(-1) ?? null;
251
+ let lastSignal: string | null = null;
252
+ let promptAcceptedSeen = false;
253
+ let completedSeen = false;
254
+ for (const event of events) {
255
+ const signal = (event.evidence as { signal?: unknown } | undefined)?.signal;
256
+ if (typeof signal === "string") lastSignal = signal;
257
+ if (event.kind === "prompt_accepted" || signal === "prompt-accepted") promptAcceptedSeen = true;
258
+ if (event.kind === "rpc_agent_completed" || signal === "completed") completedSeen = true;
259
+ }
260
+ let reason = "owner-not-live";
261
+ if (!lease) {
262
+ reason = promptAcceptedSeen && !completedSeen ? "owner-exited-after-prompt-acceptance" : "owner-lease-missing";
263
+ } else if (leaseStatus === "dead") {
264
+ reason = promptAcceptedSeen && !completedSeen ? "owner-exited-after-prompt-acceptance" : "owner-process-dead";
265
+ } else if (leaseStatus === "expiredAlive") {
266
+ reason = "owner-lease-expired";
267
+ } else if (leaseStatus === "epermAlive") {
268
+ reason = "owner-liveness-unknown-permission-denied";
269
+ } else {
270
+ reason = "owner-endpoint-unreachable";
271
+ }
272
+ return {
273
+ reason,
274
+ leaseStatus,
275
+ pid: lease?.pid ?? null,
276
+ endpointPresent: Boolean(lease?.endpoint?.path),
277
+ heartbeatAt: lease?.heartbeatAt ?? null,
278
+ expiresAt: lease?.expiresAt ?? null,
279
+ lastEventKind: lastEvent?.kind ?? null,
280
+ lastEventAt: lastEvent?.createdAt ?? null,
281
+ lastSignal,
282
+ promptAcceptedSeen,
283
+ completedSeen,
284
+ };
285
+ }
286
+
287
+ async function writeVanishReceiptForDecision(
288
+ root: string,
289
+ state: SessionState,
290
+ observation: Observation,
291
+ classification: RecoveryClassification,
292
+ ): Promise<string | null> {
293
+ if (!requiresVanishBeforeAction(classification)) return null;
294
+ const dirty = observation.gitDelta === "dirty" || observation.gitDelta === "unknown";
295
+ const preservation = dirty ? preserveDirtyWorktree(observation.cwd) : null;
296
+ const evidence: VanishEvidence = {
297
+ classification,
298
+ gitDelta: observation.gitDelta,
299
+ gitStatusPorcelain: preservation
300
+ ? `tracked:${preservation.trackedDiffSha256};untracked:${preservation.untrackedManifest.length}`
301
+ : observation.observedSignals.join(","),
302
+ untrackedManifest: preservation?.untrackedManifest ?? [],
303
+ preservation: preservation?.stashRef ? "stash" : "snapshot",
304
+ stashRef: preservation?.stashRef ?? null,
305
+ snapshotComplete: preservation?.snapshotComplete ?? true,
306
+ forbiddenActions: dirty ? ["restart-clean", "delete", "reset"] : [],
307
+ };
308
+ const receipt = buildReceipt<VanishEvidence>({
309
+ receiptId: `vanish-${Date.now()}-${randomBytes(4).toString("hex")}`,
310
+ sessionId: state.sessionId,
311
+ family: "vanish",
312
+ source: "cli-recover",
313
+ subject: {
314
+ workspace: observation.cwd,
315
+ branch: observation.branch,
316
+ head: null,
317
+ commit: null,
318
+ },
319
+ evidence,
320
+ });
321
+ await writeReceiptImmutable(root, state.sessionId, "vanish", receipt.receiptId, receipt);
322
+ return receipt.receiptId;
323
+ }
324
+
325
+ function updateStateWithRestoredOwner(state: SessionState, leasePath: string, resolved: ResolvedOwner): void {
326
+ state.lifecycle = "observing";
327
+ state.blockers = state.blockers.filter(blocker => !isOwnerLivenessBlocker(blocker));
328
+ state.handle.processHandle = {
329
+ kind: "runtime-owner",
330
+ ownerId: resolved.lease?.ownerId ?? null,
331
+ pid: resolved.lease?.pid ?? null,
332
+ };
333
+ state.handle.ownerHandle = {
334
+ leasePath,
335
+ endpoint: resolved.socketPath,
336
+ heartbeatAt: resolved.lease?.heartbeatAt ?? null,
337
+ };
338
+ state.updatedAt = nowIso();
339
+ }
218
340
 
219
341
  function isOwnerLivenessBlocker(blocker: string): boolean {
220
342
  return blocker === "detached-owner-not-live" || blocker.startsWith("owner-vanished:");
@@ -327,9 +449,14 @@ export default class Harness extends Command {
327
449
  async run(): Promise<void> {
328
450
  const { args, flags } = await this.parse(Harness);
329
451
  const verb = String(args.verb);
330
- const root = resolveHarnessRoot();
452
+ let root = resolveHarnessRoot();
331
453
  try {
332
454
  const input = parseInput(flags.input);
455
+ const sessionId = flags.session ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
456
+ const expectedWorkspace = typeof input.workspace === "string" ? resolveInputWorkspace(input) : undefined;
457
+ if (verb !== "start" && sessionId) {
458
+ root = await resolveHarnessSessionRoot(root, sessionId, process.env, { expectedWorkspace });
459
+ }
333
460
  switch (verb) {
334
461
  case "start":
335
462
  return await this.#start(root, input);
@@ -468,6 +595,9 @@ export default class Harness extends Command {
468
595
  if (process.env.GJC_HARNESS_RPC_COMMAND) {
469
596
  envAssignments.push(`GJC_HARNESS_RPC_COMMAND=${shellQuote(process.env.GJC_HARNESS_RPC_COMMAND)}`);
470
597
  }
598
+ if (process.env.GJC_HARNESS_TEST_NODE_MODULES) {
599
+ envAssignments.push(`GJC_HARNESS_TEST_NODE_MODULES=${shellQuote(process.env.GJC_HARNESS_TEST_NODE_MODULES)}`);
600
+ }
471
601
  const ownerCommand = this.#buildOwnerCommand(sessionId).map(shellQuote).join(" ");
472
602
  const shellCommand = `exec env ${envAssignments.join(" ")} ${ownerCommand}`;
473
603
  const created = Bun.spawnSync([tmuxCommand, "new-session", "-d", "-s", sessionName, "-c", cwd, shellCommand], {
@@ -498,7 +628,13 @@ export default class Harness extends Command {
498
628
  const cmd = this.#buildOwnerCommand(sessionId);
499
629
  const child = Bun.spawn(cmd, {
500
630
  cwd,
501
- env: { ...process.env, GJC_HARNESS_STATE_ROOT: root },
631
+ env: {
632
+ ...process.env,
633
+ GJC_HARNESS_STATE_ROOT: root,
634
+ ...(process.env.GJC_HARNESS_TEST_NODE_MODULES
635
+ ? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
636
+ : {}),
637
+ },
502
638
  stdout: "ignore",
503
639
  stderr: "ignore",
504
640
  stdin: "ignore",
@@ -540,7 +676,7 @@ export default class Harness extends Command {
540
676
  process.exitCode = 1;
541
677
  return;
542
678
  }
543
- const workspace = typeof input.workspace === "string" ? input.workspace : process.cwd();
679
+ const workspace = resolveInputWorkspace(input);
544
680
  const sessionId = typeof input.sessionId === "string" ? input.sessionId : generateSessionId();
545
681
  const eventsPath = `${root}/sessions/${sessionId}/events.jsonl`;
546
682
  const leasePath = `${root}/sessions/${sessionId}/lease.json`;
@@ -573,6 +709,7 @@ export default class Harness extends Command {
573
709
  updatedAt: startedAt,
574
710
  };
575
711
  await writeSessionState(root, state);
712
+ await rememberHarnessSessionRoot(root, sessionId);
576
713
  let ownerLive = false;
577
714
  let ownerRuntime: OwnerSpawnResult["runtime"] = "manual";
578
715
  let ownerFallbackReason: string | null = null;
@@ -676,6 +813,10 @@ export default class Harness extends Command {
676
813
  state = await reconcileCompletedOwnerExited(root, state, observation, completedTerminalEvent);
677
814
  const vanishedOwnerBlock = needsVanishedOwnerBlock(state, observation, completedTerminalEvent);
678
815
  state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
816
+ const ownerExit =
817
+ !ownerLive && (vanishedOwnerBlock || completedTerminalEvent)
818
+ ? await buildOwnerExitEvidence(root, state)
819
+ : null;
679
820
  writeJson(
680
821
  buildResponse(state, ownerLive, {
681
822
  observation: { ...observation, lifecycle: state.lifecycle },
@@ -686,6 +827,7 @@ export default class Harness extends Command {
686
827
  ...(completedTerminalEvent && !ownerLive
687
828
  ? { completedOwnerExited: true, terminalResult: completedTerminalEvent }
688
829
  : {}),
830
+ ...(ownerExit ? { ownerExit } : {}),
689
831
  }),
690
832
  );
691
833
  }
@@ -767,7 +909,9 @@ export default class Harness extends Command {
767
909
  buildResponse(state, ownerLiveFor(state), {
768
910
  events,
769
911
  cursor: nextCursor,
770
- note: "tail-only; live producer (owner) lands in M3/M5",
912
+ note: "tail-only; events are preserved after owner exit",
913
+ ownerLive: ownerLiveFor(state),
914
+ ownerExit: ownerLiveFor(state) ? null : await buildOwnerExitEvidence(root, state),
771
915
  }),
772
916
  );
773
917
  }
@@ -802,21 +946,62 @@ export default class Harness extends Command {
802
946
  async #recoverWithoutOwner(root: string, sessionId: string, input: Record<string, unknown>): Promise<void> {
803
947
  const budget = resolveRetryBudget(input);
804
948
  let state = await loadState(root, sessionId);
949
+ const beforeExit = await buildOwnerExitEvidence(root, state);
805
950
  const { observation, completedTerminalEvent } = await buildObservation(root, state, false);
806
951
  state = await markVanishedOwnerBlocked(root, state, observation, completedTerminalEvent);
807
952
  const decision = classifyRecovery({
808
953
  observation: { ...observation, lifecycle: state.lifecycle },
809
954
  retryBudget: budget,
810
955
  });
956
+ const vanishReceiptId = await writeVanishReceiptForDecision(root, state, observation, decision.classification);
957
+ const restoredOwner =
958
+ decision.ownerRequired && beforeExit.endpointPresent
959
+ ? await this.#spawnDetachedOwner(root, sessionId, state.handle.workspace)
960
+ : null;
961
+ if (restoredOwner?.live) {
962
+ const resolved = await resolveOwner(root, sessionId);
963
+ if (resolved.live && resolved.socketPath) {
964
+ updateStateWithRestoredOwner(state, state.handle.ownerHandle.leasePath, resolved);
965
+ if (restoredOwner.tmuxSessionName)
966
+ state.handle.viewportHandle.tmuxSessionName = restoredOwner.tmuxSessionName;
967
+ await writeSessionState(root, state);
968
+ writeJson(
969
+ buildResponse(state, true, {
970
+ pending: false,
971
+ restoredOwner: true,
972
+ decision,
973
+ observation: { ...observation, lifecycle: state.lifecycle, ownerLive: true },
974
+ ownerExit: beforeExit,
975
+ ownerRuntime: restoredOwner.runtime,
976
+ ...(restoredOwner.fallbackReason ? { ownerFallbackReason: restoredOwner.fallbackReason } : {}),
977
+ ...(vanishReceiptId ? { vanishReceiptId } : {}),
978
+ }),
979
+ );
980
+ return;
981
+ }
982
+ }
983
+ const afterExit = await buildOwnerExitEvidence(root, state);
811
984
  writeJson(
812
985
  buildResponse(
813
986
  state,
814
987
  false,
815
988
  {
816
989
  pending: false,
817
- reason: "owner-not-live",
990
+ reason: afterExit.reason,
818
991
  decision,
819
992
  observation: { ...observation, lifecycle: state.lifecycle },
993
+ ownerExit: afterExit,
994
+ ...(restoredOwner
995
+ ? {
996
+ restoreAttempt: {
997
+ runtime: restoredOwner.runtime,
998
+ live: restoredOwner.live,
999
+ fallbackReason: restoredOwner.fallbackReason,
1000
+ blockerReason: restoredOwner.blockerReason,
1001
+ },
1002
+ }
1003
+ : {}),
1004
+ ...(vanishReceiptId ? { vanishReceiptId } : {}),
820
1005
  },
821
1006
  false,
822
1007
  ),
@@ -36,6 +36,12 @@ export default class Index extends Command {
36
36
  plan: Flags.string({
37
37
  description: "Plan model for architectural planning (or GJC_PLAN_MODEL env)",
38
38
  }),
39
+ mpreset: Flags.string({
40
+ description: "Model profile preset to activate for this session",
41
+ }),
42
+ default: Flags.boolean({
43
+ description: "Persist --mpreset as the default model profile",
44
+ }),
39
45
  provider: Flags.string({
40
46
  description: "Provider to use (legacy; prefer --model)",
41
47
  }),
@@ -136,6 +142,8 @@ export default class Index extends Command {
136
142
  `# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
137
143
  `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
138
144
  `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
145
+ `# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-standard`,
146
+ `# Persist a model profile as the default\n ${APP_NAME} --mpreset opencode-go-pro --default`,
139
147
  `# Export a session file to HTML\n ${APP_NAME} --export ~/.gjc/agent/sessions/--path--/session.jsonl`,
140
148
  ];
141
149
 
@@ -6,13 +6,7 @@ import {
6
6
  writeCurrentSessionGoalModeState,
7
7
  writePendingGoalModeRequest,
8
8
  } from "../gjc-runtime/goal-mode-request";
9
- import {
10
- buildUltragoalHudSummary,
11
- getUltragoalStatus,
12
- readUltragoalLedger,
13
- runNativeUltragoalCommand,
14
- } from "../gjc-runtime/ultragoal-runtime";
15
- import { syncSkillActiveState } from "../skill-state/active-state";
9
+ import { runNativeUltragoalCommand } from "../gjc-runtime/ultragoal-runtime";
16
10
 
17
11
  export default class Ultragoal extends Command {
18
12
  static description = "Run native GJC Ultragoal workflow commands";
@@ -25,20 +19,6 @@ export default class Ultragoal extends Command {
25
19
  if (result.stdout) process.stdout.write(result.stdout);
26
20
  if (result.stderr) process.stderr.write(result.stderr);
27
21
  process.exitCode = result.status;
28
- try {
29
- const summary = await getUltragoalStatus(process.cwd());
30
- const ledger = await readUltragoalLedger(process.cwd());
31
- await syncSkillActiveState({
32
- cwd: process.cwd(),
33
- skill: "ultragoal",
34
- active: summary.exists && summary.status !== "complete",
35
- phase: summary.status,
36
- hud: buildUltragoalHudSummary(summary, ledger.at(-1)),
37
- source: "gjc-ultragoal",
38
- });
39
- } catch {
40
- // HUD sync is best-effort and must not change command semantics.
41
- }
42
22
  if (result.status !== 0 || !shouldActivateGoalMode) return;
43
23
 
44
24
  const cwd = process.cwd();
@@ -42,7 +42,7 @@ interface ResolvedCanonicalModel {
42
42
  }
43
43
 
44
44
  const TRAILING_MARKER_PATTERN =
45
- /[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4)$/i;
45
+ /[-:](?:thinking|customtools|high|low|medium|minimal|xhigh|max|free|cloud|exacto|nitro|original|optimized|nvfp4|fp8|fp4|bf16|int8|int4)$/i;
46
46
  const WRAPPER_PREFIXES = ["duo-chat-"] as const;
47
47
 
48
48
  let referenceDataCache: CanonicalReferenceData | undefined;
@@ -0,0 +1,157 @@
1
+ import type { ThinkingLevel } from "@gajae-code/agent-core";
2
+ import type { Api, Model } from "@gajae-code/ai";
3
+ import type { AgentSession } from "../session/agent-session";
4
+ import {
5
+ aggregateModelProfileRequiredProviders,
6
+ formatAvailableProfileNames,
7
+ resolveProfileBindings,
8
+ } from "./model-profiles";
9
+ import { type GjcModelAssignmentTargetId, isAuthenticated, type ModelRegistry } from "./model-registry";
10
+ import { resolveModelRoleValue } from "./model-resolver";
11
+ import type { Settings } from "./settings";
12
+
13
+ export interface PrepareModelProfileActivationOptions {
14
+ session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId">;
15
+ modelRegistry: Pick<
16
+ ModelRegistry,
17
+ | "getModelProfile"
18
+ | "getModelProfiles"
19
+ | "getAvailableModelProfileNames"
20
+ | "getApiKeyForProvider"
21
+ | "getAll"
22
+ | "resolveCanonicalModel"
23
+ | "getCanonicalVariants"
24
+ | "getCanonicalId"
25
+ >;
26
+ settings: Pick<Settings, "get">;
27
+ profileName: string;
28
+ }
29
+
30
+ export interface PreparedModelProfileActivation {
31
+ profileName: string;
32
+ session: Pick<AgentSession, "model" | "thinkingLevel" | "sessionId" | "setModelTemporary">;
33
+ settings: Pick<Settings, "get" | "override" | "set" | "flush">;
34
+ previousModel: Model<Api> | undefined;
35
+ previousThinkingLevel: ThinkingLevel | undefined;
36
+ previousAgentModelOverrides: Record<string, string>;
37
+ defaultModel: Model<Api> | undefined;
38
+ defaultThinkingLevel: ThinkingLevel | undefined;
39
+ agentModelOverrides: Record<string, string>;
40
+ }
41
+
42
+ export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
43
+ return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
44
+ }
45
+
46
+ export async function prepareModelProfileActivation(
47
+ options: PrepareModelProfileActivationOptions,
48
+ ): Promise<PreparedModelProfileActivation> {
49
+ const profile = options.modelRegistry.getModelProfile(options.profileName);
50
+ if (!profile) {
51
+ const available = formatAvailableProfileNames(options.modelRegistry.getModelProfiles());
52
+ throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
53
+ }
54
+
55
+ const missingProviders: string[] = [];
56
+ for (const provider of aggregateModelProfileRequiredProviders(profile.requiredProviders, profile)) {
57
+ const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
58
+ if (!isAuthenticated(apiKey)) {
59
+ missingProviders.push(provider);
60
+ }
61
+ }
62
+ if (missingProviders.length > 0) {
63
+ throw new Error(formatModelProfileCredentialError(options.profileName, missingProviders));
64
+ }
65
+
66
+ const availableModels = options.modelRegistry.getAll();
67
+ const bindings = resolveProfileBindings(profile);
68
+ const resolvedDefault = bindings.defaultSelector
69
+ ? resolveModelRoleValue(bindings.defaultSelector, availableModels, {
70
+ settings: options.settings as Settings,
71
+ modelRegistry: options.modelRegistry,
72
+ })
73
+ : undefined;
74
+ if (bindings.defaultSelector && !resolvedDefault?.model) {
75
+ throw new Error(
76
+ `Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
77
+ );
78
+ }
79
+
80
+ const agentModelOverrides: Record<string, string> = {};
81
+ for (const [role, selector] of Object.entries(bindings.agentModelOverrides) as [
82
+ GjcModelAssignmentTargetId,
83
+ string,
84
+ ][]) {
85
+ const resolved = resolveModelRoleValue(selector, availableModels, {
86
+ settings: options.settings as Settings,
87
+ modelRegistry: options.modelRegistry,
88
+ });
89
+ if (!resolved.model) {
90
+ throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
91
+ }
92
+ agentModelOverrides[role] = selector;
93
+ }
94
+
95
+ return {
96
+ profileName: options.profileName,
97
+ session: options.session as PreparedModelProfileActivation["session"],
98
+ settings: options.settings as PreparedModelProfileActivation["settings"],
99
+ previousModel: options.session.model,
100
+ previousThinkingLevel: options.session.thinkingLevel,
101
+ previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
102
+ defaultModel: resolvedDefault?.model,
103
+ defaultThinkingLevel: resolvedDefault?.thinkingLevel,
104
+ agentModelOverrides,
105
+ };
106
+ }
107
+
108
+ export async function applyPreparedModelProfileActivation(
109
+ prepared: PreparedModelProfileActivation,
110
+ options: { persistDefault?: boolean } = {},
111
+ ): Promise<void> {
112
+ const previousModel = prepared.previousModel;
113
+ const previousThinkingLevel = prepared.previousThinkingLevel;
114
+ const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
115
+ const previousPersistedDefault = prepared.settings.get("modelProfile.default");
116
+ let modelChanged = false;
117
+ let overridesChanged = false;
118
+ let defaultChanged = false;
119
+
120
+ try {
121
+ if (prepared.defaultModel) {
122
+ await prepared.session.setModelTemporary(prepared.defaultModel, prepared.defaultThinkingLevel);
123
+ modelChanged = true;
124
+ }
125
+ if (Object.keys(prepared.agentModelOverrides).length > 0) {
126
+ prepared.settings.override("task.agentModelOverrides", {
127
+ ...prepared.settings.get("task.agentModelOverrides"),
128
+ ...prepared.agentModelOverrides,
129
+ });
130
+ overridesChanged = true;
131
+ }
132
+ if (options.persistDefault) {
133
+ prepared.settings.set("modelProfile.default", prepared.profileName);
134
+ defaultChanged = true;
135
+ await prepared.settings.flush();
136
+ }
137
+ } catch (error) {
138
+ if (defaultChanged) {
139
+ prepared.settings.set("modelProfile.default", previousPersistedDefault);
140
+ }
141
+ if (overridesChanged) {
142
+ prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
143
+ }
144
+ if (modelChanged && previousModel) {
145
+ await prepared.session.setModelTemporary(previousModel, previousThinkingLevel);
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ export async function activateModelProfile(
152
+ options: PrepareModelProfileActivationOptions,
153
+ applyOptions: { persistDefault?: boolean } = {},
154
+ ): Promise<void> {
155
+ const prepared = await prepareModelProfileActivation(options);
156
+ await applyPreparedModelProfileActivation(prepared, applyOptions);
157
+ }