@gajae-code/coding-agent 0.3.2 → 0.4.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 (125) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
  58. package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
  59. package/src/discovery/claude-plugins.ts +25 -5
  60. package/src/edit/diff.ts +64 -1
  61. package/src/edit/modes/replace.ts +60 -2
  62. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  63. package/src/extensibility/gjc-plugins/index.ts +9 -0
  64. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  65. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  66. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  67. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  68. package/src/extensibility/gjc-plugins/state.ts +29 -0
  69. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  70. package/src/extensibility/gjc-plugins/types.ts +97 -0
  71. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  72. package/src/extensibility/skills.ts +39 -7
  73. package/src/gjc-runtime/state-runtime.ts +93 -2
  74. package/src/gjc-runtime/state-writer.ts +17 -1
  75. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  76. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  77. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  78. package/src/harness-control-plane/storage.ts +144 -2
  79. package/src/hashline/hash.ts +23 -0
  80. package/src/hooks/skill-state.ts +2 -0
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/lsp/client.ts +7 -0
  83. package/src/modes/acp/acp-agent.ts +25 -2
  84. package/src/modes/bridge/bridge-mode.ts +124 -2
  85. package/src/modes/controllers/input-controller.ts +14 -2
  86. package/src/modes/prompt-action-autocomplete.ts +49 -10
  87. package/src/modes/rpc/rpc-client.ts +79 -3
  88. package/src/modes/rpc/rpc-mode.ts +67 -0
  89. package/src/modes/rpc/rpc-types.ts +224 -2
  90. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  91. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  92. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  93. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  94. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  95. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  96. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  97. package/src/modes/shared/agent-wire/responses.ts +2 -2
  98. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  99. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  100. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  101. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  102. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  103. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  104. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  105. package/src/modes/theme/theme.ts +6 -0
  106. package/src/prompts/system/system-prompt.md +9 -0
  107. package/src/runtime-mcp/client.ts +7 -4
  108. package/src/runtime-mcp/manager.ts +45 -13
  109. package/src/runtime-mcp/transports/http.ts +40 -14
  110. package/src/runtime-mcp/transports/stdio.ts +11 -10
  111. package/src/sdk.ts +47 -0
  112. package/src/session/agent-session.ts +211 -2
  113. package/src/session/blob-store.ts +84 -0
  114. package/src/session/messages.ts +3 -0
  115. package/src/session/session-manager.ts +390 -33
  116. package/src/session/session-storage.ts +26 -0
  117. package/src/setup/provider-onboarding.ts +2 -2
  118. package/src/skill-state/active-state.ts +89 -1
  119. package/src/task/discovery.ts +7 -1
  120. package/src/task/executor.ts +16 -2
  121. package/src/thinking.ts +8 -2
  122. package/src/tools/ask.ts +39 -9
  123. package/src/tools/index.ts +3 -0
  124. package/src/tools/skill.ts +15 -3
  125. package/src/utils/edit-mode.ts +1 -1
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.2",
4
+ "version": "0.4.1",
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",
@@ -36,6 +36,8 @@
36
36
  "check:types": "tsgo -p tsconfig.json --noEmit",
37
37
  "lint": "biome lint .",
38
38
  "test": "bun test",
39
+ "generate-schemas": "bun ../../scripts/generate-json-schemas.ts",
40
+ "check:schemas": "bun ../../scripts/generate-json-schemas.ts --check",
39
41
  "fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index",
40
42
  "fmt": "biome format --write . && bun run format-prompts",
41
43
  "format-prompts": "bun scripts/format-prompts.ts",
@@ -48,12 +50,12 @@
48
50
  "@agentclientprotocol/sdk": "0.21.0",
49
51
  "@babel/parser": "^7.29.3",
50
52
  "@mozilla/readability": "^0.6.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",
53
+ "@gajae-code/stats": "0.4.1",
54
+ "@gajae-code/agent-core": "0.4.1",
55
+ "@gajae-code/ai": "0.4.1",
56
+ "@gajae-code/natives": "0.4.1",
57
+ "@gajae-code/tui": "0.4.1",
58
+ "@gajae-code/utils": "0.4.1",
57
59
  "@puppeteer/browsers": "^2.13.0",
58
60
  "@types/turndown": "5.0.6",
59
61
  "@xterm/headless": "^6.0.0",
package/src/cli.ts CHANGED
@@ -84,6 +84,20 @@ function isSubcommand(first: string | undefined): boolean {
84
84
  async function runSmokeTest(): Promise<void> {
85
85
  const { smokeTestSyncWorker } = await import("@gajae-code/stats");
86
86
  await smokeTestSyncWorker();
87
+ // Prove the embedded native addon extracts and the new perf exports resolve in
88
+ // the COMPILED single binary (dev runs only load the on-disk .node). Loading the
89
+ // natives module triggers loadNative()/embedded extraction; calling each new
90
+ // export confirms the symbols are present in the shipped binary.
91
+ const { h06FormatHashLines, h02ScoreSequenceFuzzy, h01FindBestFuzzyMatch } = await import(
92
+ "../../natives/native/index.js"
93
+ );
94
+ const hashed = h06FormatHashLines("a\nb", 1);
95
+ if (hashed.split("\n").length !== 2) {
96
+ throw new Error(`smoke-test: h06FormatHashLines returned unexpected output: ${JSON.stringify(hashed)}`);
97
+ }
98
+ if (typeof h02ScoreSequenceFuzzy !== "function" || typeof h01FindBestFuzzyMatch !== "function") {
99
+ throw new Error("smoke-test: native fuzzy exports missing from embedded addon");
100
+ }
87
101
  process.stdout.write("smoke-test: ok\n");
88
102
  }
89
103
 
@@ -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
  ),
@@ -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;
@@ -2,6 +2,7 @@ import * as path from "node:path";
2
2
  import {
3
3
  type Api,
4
4
  type AssistantMessageEventStream,
5
+ type CacheRetention,
5
6
  type Context,
6
7
  createModelManager,
7
8
  enrichModelThinking,
@@ -240,6 +241,7 @@ interface ProviderValidationConfig {
240
241
  compat?: Model<Api>["compat"];
241
242
  requestTransform?: ModelRequestTransform;
242
243
  disableStrictTools?: boolean;
244
+ cacheRetention?: CacheRetention;
243
245
  modelOverrides?: Record<string, unknown>;
244
246
  models: ProviderValidationModel[];
245
247
  }
@@ -267,11 +269,12 @@ function validateProviderConfiguration(
267
269
  !config.apiKeyEnv &&
268
270
  !config.disableStrictTools &&
269
271
  !config.requestTransform &&
272
+ !config.cacheRetention &&
270
273
  !hasModelOverrides &&
271
274
  !config.discovery
272
275
  ) {
273
276
  throw new Error(
274
- `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
277
+ `Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
275
278
  );
276
279
  }
277
280
  }
@@ -384,6 +387,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
384
387
  compat: providerConfig.compat,
385
388
  requestTransform: providerConfig.requestTransform,
386
389
  disableStrictTools: providerConfig.disableStrictTools,
390
+ cacheRetention: providerConfig.cacheRetention,
387
391
  modelOverrides: providerConfig.modelOverrides,
388
392
  models: (providerConfig.models ?? []) as ProviderValidationModel[],
389
393
  },
@@ -402,6 +406,7 @@ interface ProviderOverride {
402
406
  compat?: Model<Api>["compat"];
403
407
  transport?: Model<Api>["transport"];
404
408
  requestTransform?: ModelRequestTransform;
409
+ cacheRetention?: CacheRetention;
405
410
  }
406
411
 
407
412
  const PROVIDER_BASE_URL_ENV_ALIASES: Record<string, readonly string[]> = {
@@ -449,7 +454,10 @@ function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
449
454
  export function mergeDiscoveredModel<TApi extends Api>(
450
455
  model: Model<TApi>,
451
456
  existing: Model<Api> | undefined,
452
- providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "transport" | "requestTransform">,
457
+ providerOverride?: Pick<
458
+ ProviderOverride,
459
+ "baseUrl" | "headers" | "transport" | "requestTransform" | "cacheRetention"
460
+ >,
453
461
  ): Model<TApi> {
454
462
  if (existing) {
455
463
  return {
@@ -460,6 +468,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
460
468
  mergeRequestTransform(existing.requestTransform, model.requestTransform),
461
469
  providerOverride?.requestTransform,
462
470
  ),
471
+ cacheRetention: model.cacheRetention ?? existing.cacheRetention ?? providerOverride?.cacheRetention,
463
472
  };
464
473
  }
465
474
  if (providerOverride) {
@@ -481,6 +490,7 @@ interface DiscoveryProviderConfig {
481
490
  headers?: Record<string, string>;
482
491
  compat?: Model<Api>["compat"];
483
492
  requestTransform?: ModelRequestTransform;
493
+ cacheRetention?: CacheRetention;
484
494
  discovery: ProviderDiscovery;
485
495
  optional?: boolean;
486
496
  }
@@ -678,6 +688,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
678
688
  if (override.reasoning !== undefined) result.reasoning = override.reasoning;
679
689
  if (override.thinking !== undefined) result.thinking = override.thinking as ThinkingConfig;
680
690
  if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
691
+ if (override.cacheRetention !== undefined) result.cacheRetention = override.cacheRetention;
681
692
  if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
682
693
  if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
683
694
  if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
@@ -716,6 +727,7 @@ interface CustomModelDefinitionLike {
716
727
  premiumMultiplier?: number;
717
728
  wireModelId?: string;
718
729
  requestTransform?: ModelRequestTransform;
730
+ cacheRetention?: CacheRetention;
719
731
  }
720
732
 
721
733
  interface CustomModelBuildOptions {
@@ -740,6 +752,7 @@ type CustomModelOverlay = {
740
752
  premiumMultiplier?: number;
741
753
  wireModelId?: string;
742
754
  requestTransform?: ModelRequestTransform;
755
+ cacheRetention?: CacheRetention;
743
756
  isOAuth?: boolean;
744
757
  };
745
758
 
@@ -791,6 +804,7 @@ function buildCustomModelOverlay(
791
804
  providerCompat: Model<Api>["compat"] | undefined,
792
805
  providerRequestTransform: ModelRequestTransform | undefined,
793
806
  providerAuth: ProviderAuthMode | undefined,
807
+ providerCacheRetention: CacheRetention | undefined,
794
808
  modelDef: CustomModelDefinitionLike,
795
809
  ): CustomModelOverlay | undefined {
796
810
  const api = modelDef.api ?? providerApi;
@@ -813,6 +827,7 @@ function buildCustomModelOverlay(
813
827
  wireModelId: modelDef.wireModelId,
814
828
  contextPromotionTarget: modelDef.contextPromotionTarget,
815
829
  premiumMultiplier: modelDef.premiumMultiplier,
830
+ cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
816
831
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
817
832
  };
818
833
  }
@@ -913,6 +928,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
913
928
  contextPromotionTarget: resolvedModel.contextPromotionTarget,
914
929
  wireModelId: resolvedModel.wireModelId,
915
930
  requestTransform: resolvedModel.requestTransform,
931
+ cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
916
932
  premiumMultiplier: resolvedModel.premiumMultiplier,
917
933
  isOAuth: resolvedModel.isOAuth,
918
934
  } as Model<Api>);
@@ -1139,6 +1155,7 @@ export class ModelRegistry {
1139
1155
  return {
1140
1156
  ...withTransportOverride,
1141
1157
  compat: mergeCompat(m.compat, providerOverride.compat),
1158
+ cacheRetention: m.cacheRetention ?? providerOverride.cacheRetention,
1142
1159
  };
1143
1160
  });
1144
1161
  });
@@ -1295,6 +1312,7 @@ export class ModelRegistry {
1295
1312
  requestTransform: providerConfig.requestTransform
1296
1313
  ? mergeRequestTransform(undefined, providerConfig.requestTransform)
1297
1314
  : undefined,
1315
+ cacheRetention: normalized.cacheRetention ?? providerConfig.cacheRetention,
1298
1316
  };
1299
1317
  });
1300
1318
  }
@@ -1383,7 +1401,8 @@ export class ModelRegistry {
1383
1401
  providerConfig.compat ||
1384
1402
  providerConfig.disableStrictTools ||
1385
1403
  providerConfig.requestTransform ||
1386
- providerConfig.transport
1404
+ providerConfig.transport ||
1405
+ providerConfig.cacheRetention
1387
1406
  ) {
1388
1407
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
1389
1408
  overrides.set(providerName, {
@@ -1394,6 +1413,7 @@ export class ModelRegistry {
1394
1413
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1395
1414
  transport: providerConfig.transport,
1396
1415
  requestTransform: providerConfig.requestTransform,
1416
+ cacheRetention: providerConfig.cacheRetention,
1397
1417
  });
1398
1418
  }
1399
1419
 
@@ -1410,6 +1430,7 @@ export class ModelRegistry {
1410
1430
  headers: providerConfig.headers,
1411
1431
  compat: providerConfig.compat,
1412
1432
  requestTransform: providerConfig.requestTransform,
1433
+ cacheRetention: providerConfig.cacheRetention,
1413
1434
  discovery: providerConfig.discovery,
1414
1435
  optional: false,
1415
1436
  });
@@ -2101,13 +2122,16 @@ export class ModelRegistry {
2101
2122
  compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
2102
2123
  transport: override.transport ?? baseOverride?.transport,
2103
2124
  requestTransform: mergeRequestTransform(baseOverride?.requestTransform, override.requestTransform),
2125
+ cacheRetention: override.cacheRetention ?? baseOverride?.cacheRetention,
2104
2126
  };
2105
2127
  }
2106
- #applyProviderTransportOverride<T extends { baseUrl?: string; headers?: Record<string, string> }>(
2128
+ #applyProviderTransportOverride<
2129
+ T extends { baseUrl?: string; headers?: Record<string, string>; cacheRetention?: CacheRetention },
2130
+ >(
2107
2131
  entry: T,
2108
2132
  override: Pick<
2109
2133
  ProviderOverride,
2110
- "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport" | "requestTransform"
2134
+ "baseUrl" | "headers" | "authHeader" | "apiKey" | "transport" | "requestTransform" | "cacheRetention"
2111
2135
  >,
2112
2136
  ): T {
2113
2137
  const headers = mergeAuthHeader(
@@ -2126,6 +2150,7 @@ export class ModelRegistry {
2126
2150
  (entry as { requestTransform?: ModelRequestTransform }).requestTransform,
2127
2151
  override.requestTransform,
2128
2152
  ),
2153
+ cacheRetention: entry.cacheRetention ?? override.cacheRetention,
2129
2154
  };
2130
2155
  }
2131
2156
  #applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
@@ -2214,6 +2239,7 @@ export class ModelRegistry {
2214
2239
  providerCompat,
2215
2240
  providerConfig.requestTransform,
2216
2241
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
2242
+ providerConfig.cacheRetention,
2217
2243
  modelDef as CustomModelDefinitionLike,
2218
2244
  );
2219
2245
  if (!model) continue;
@@ -2576,6 +2602,7 @@ export class ModelRegistry {
2576
2602
  config.compat,
2577
2603
  config.requestTransform,
2578
2604
  undefined,
2605
+ undefined,
2579
2606
  modelDef as CustomModelDefinitionLike,
2580
2607
  );
2581
2608
  if (!overlay) {
@@ -16,6 +16,7 @@ const ReasoningEffortMapSchema = z.object({
16
16
  medium: z.string().optional(),
17
17
  high: z.string().optional(),
18
18
  xhigh: z.string().optional(),
19
+ max: z.string().optional(),
19
20
  });
20
21
 
21
22
  export const OpenAICompatSchema = z.object({
@@ -45,7 +46,8 @@ export const OpenAICompatSchema = z.object({
45
46
  toolStrictMode: z.enum(["all_strict", "none"]).optional(),
46
47
  });
47
48
 
48
- const EffortSchema = z.enum(["minimal", "low", "medium", "high", "xhigh"]);
49
+ const EffortSchema = z.enum(["minimal", "low", "medium", "high", "xhigh", "max"]);
50
+ const CacheRetentionSchema = z.enum(["none", "short", "long"]);
49
51
 
50
52
  const ThinkingControlModeSchema = z.enum([
51
53
  "effort",
@@ -89,7 +91,7 @@ function isValidProfileModelSelector(value: string): boolean {
89
91
  if (parts.length > 2) return false;
90
92
  const [base, suffix] = parts;
91
93
  if (!base) return false;
92
- return suffix === undefined || ["minimal", "low", "medium", "high", "xhigh"].includes(suffix);
94
+ return suffix === undefined || ["minimal", "low", "medium", "high", "xhigh", "max"].includes(suffix);
93
95
  }
94
96
 
95
97
  export const ProfileModelSelectorSchema = z
@@ -149,6 +151,7 @@ const ModelDefinitionSchema = z
149
151
  contextPromotionTarget: z.string().min(1).optional(),
150
152
  wireModelId: z.string().min(1).optional(),
151
153
  requestTransform: RequestTransformSchema.optional(),
154
+ cacheRetention: CacheRetentionSchema.optional(),
152
155
  })
153
156
  .strict();
154
157
 
@@ -174,6 +177,7 @@ export const ModelOverrideSchema = z
174
177
  contextPromotionTarget: z.string().min(1).optional(),
175
178
  wireModelId: z.string().min(1).optional(),
176
179
  requestTransform: RequestTransformSchema.optional(),
180
+ cacheRetention: CacheRetentionSchema.optional(),
177
181
  })
178
182
  .strict();
179
183
 
@@ -225,6 +229,7 @@ const ProviderConfigSchema = z
225
229
  * and `apiKey` must carry the gateway bearer.
226
230
  */
227
231
  transport: z.literal("pi-native").optional(),
232
+ cacheRetention: CacheRetentionSchema.optional(),
228
233
  })
229
234
  .strict();
230
235