@gajae-code/coding-agent 0.15.4 → 0.15.6

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 (174) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  4. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  5. package/dist/types/cli/update-cli.d.ts +27 -5
  6. package/dist/types/commands/auth-gateway.d.ts +3 -0
  7. package/dist/types/commands/gc.d.ts +13 -0
  8. package/dist/types/commands/model-presets.d.ts +21 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +99 -0
  10. package/dist/types/config/file-lock.d.ts +49 -2
  11. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  12. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  13. package/dist/types/config/keybindings.d.ts +3 -2
  14. package/dist/types/config/model-preset-registry.d.ts +368 -0
  15. package/dist/types/config/model-profile-activation.d.ts +39 -0
  16. package/dist/types/config/model-profiles.d.ts +5 -3
  17. package/dist/types/config/model-registry.d.ts +11 -5
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  20. package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +4 -4
  22. package/dist/types/extensibility/shared-events.d.ts +2 -0
  23. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  25. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  26. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/main.d.ts +3 -1
  29. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  30. package/dist/types/modes/jobs-observer.d.ts +36 -0
  31. package/dist/types/sdk/broker/broker.d.ts +3 -0
  32. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  33. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  34. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  35. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  36. package/dist/types/sdk/client/discovery.d.ts +1 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  39. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  40. package/dist/types/sdk/host/session-runtime.d.ts +19 -1
  41. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  42. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  43. package/dist/types/sdk/router/session-router.d.ts +9 -0
  44. package/dist/types/session/account-inventory.d.ts +1 -0
  45. package/dist/types/session/agent-session.d.ts +19 -5
  46. package/dist/types/session/fold-coordinator.d.ts +201 -0
  47. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  48. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  49. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  50. package/dist/types/telemetry/control.d.ts +11 -0
  51. package/dist/types/telemetry/events.d.ts +20 -0
  52. package/dist/types/telemetry/index.d.ts +3 -0
  53. package/dist/types/telemetry/transport.d.ts +22 -0
  54. package/dist/types/tools/bash-interactive.d.ts +23 -0
  55. package/dist/types/tools/index.d.ts +6 -5
  56. package/dist/types/tools/path-utils.d.ts +5 -6
  57. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  58. package/dist/types/web/search/providers/base.d.ts +9 -4
  59. package/dist/types/web/search/providers/xai.d.ts +2 -1
  60. package/dist/types/web/search/types.d.ts +14 -0
  61. package/package.json +10 -7
  62. package/scripts/generate-hotkeys-docs.ts +4 -0
  63. package/scripts/generate-sdk-operation-inventory.ts +3 -3
  64. package/src/async/job-manager.ts +671 -39
  65. package/src/cli/auth-broker-cli.ts +40 -12
  66. package/src/cli/auth-gateway-cli.ts +273 -97
  67. package/src/cli/config-cli.ts +2 -2
  68. package/src/cli/fast-help.ts +3 -0
  69. package/src/cli/model-presets-cli.ts +100 -0
  70. package/src/cli/update-cli.ts +142 -6
  71. package/src/cli.ts +6 -2
  72. package/src/commands/auth-gateway.ts +12 -8
  73. package/src/commands/gc.ts +19 -0
  74. package/src/commands/model-presets.ts +24 -0
  75. package/src/commands/sdk.ts +5 -0
  76. package/src/config/autorouting-tier-map.ts +33 -0
  77. package/src/config/file-lock-gc.ts +9 -4
  78. package/src/config/file-lock.ts +775 -59
  79. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  80. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  81. package/src/config/keybindings.ts +6 -2
  82. package/src/config/model-preset-registry.ts +2799 -0
  83. package/src/config/model-profile-activation.ts +115 -28
  84. package/src/config/model-profile-contract.ts +4 -1
  85. package/src/config/model-profiles.ts +56 -14
  86. package/src/config/model-registry.ts +1028 -123
  87. package/src/config/settings-schema.ts +11 -0
  88. package/src/config/settings.ts +6 -1
  89. package/src/coordinator-mcp/model-preset.ts +32 -17
  90. package/src/coordinator-mcp/policy.ts +1 -1
  91. package/src/coordinator-mcp/projection-scan.ts +571 -0
  92. package/src/coordinator-mcp/server.ts +19 -24
  93. package/src/coordinator-mcp/session-reaper.ts +9 -3
  94. package/src/extensibility/extensions/loader.ts +0 -1
  95. package/src/extensibility/extensions/types.ts +4 -2
  96. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  97. package/src/extensibility/shared-events.ts +2 -0
  98. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  99. package/src/gjc-runtime/gc-render.ts +29 -0
  100. package/src/gjc-runtime/gc-runtime.ts +174 -11
  101. package/src/gjc-runtime/launch-worktree.ts +44 -7
  102. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  103. package/src/gjc-runtime/session-state-lock.ts +1242 -141
  104. package/src/gjc-runtime/session-state-sidecar.ts +76 -3
  105. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  106. package/src/index.ts +1 -0
  107. package/src/internal-urls/docs-index.generated.ts +14 -13
  108. package/src/lsp/config.ts +4 -3
  109. package/src/lsp/path-trust.ts +12 -6
  110. package/src/main.ts +9 -2
  111. package/src/modes/acp/acp-agent.ts +11 -5
  112. package/src/modes/components/jobs-overlay-model.ts +99 -16
  113. package/src/modes/components/model-selector.ts +158 -18
  114. package/src/modes/components/status-line/segments.ts +6 -1
  115. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  116. package/src/modes/controllers/input-controller.ts +23 -6
  117. package/src/modes/interactive-mode.ts +14 -1
  118. package/src/modes/jobs-observer.ts +149 -16
  119. package/src/modes/prompt-action-autocomplete.ts +26 -47
  120. package/src/modes/runtime-init.ts +1 -1
  121. package/src/modes/utils/injected-user-submission.ts +15 -1
  122. package/src/prompts/tools/bash.md +2 -2
  123. package/src/prompts/tools/fold-receipt.md +3 -0
  124. package/src/prompts/tools/job.md +1 -1
  125. package/src/sdk/acp/adapter.ts +17 -2
  126. package/src/sdk/broker/broker.ts +48 -21
  127. package/src/sdk/broker/lifecycle.ts +32 -6
  128. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  129. package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
  130. package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
  131. package/src/sdk/bus/prompt-reconciliation.ts +33 -6
  132. package/src/sdk/bus/slack-conversation.ts +5 -1
  133. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  134. package/src/sdk/cli/session-cli.ts +161 -5
  135. package/src/sdk/client/discovery.ts +3 -1
  136. package/src/sdk/host/control/dispatch.ts +3 -0
  137. package/src/sdk/host/host.ts +7 -0
  138. package/src/sdk/host/model-pin.ts +185 -25
  139. package/src/sdk/host/sdk-run-capability.ts +1 -0
  140. package/src/sdk/host/session-runtime.test.ts +1240 -107
  141. package/src/sdk/host/session-runtime.ts +1029 -115
  142. package/src/sdk/prompt-deadline-manager.ts +16 -1
  143. package/src/sdk/protocol/adapter-validation.ts +27 -0
  144. package/src/sdk/protocol/operation-inventory.generated.json +2 -2
  145. package/src/sdk/protocol/operation-registry.ts +4 -1
  146. package/src/sdk/router/session-router.ts +297 -111
  147. package/src/sdk/session.ts +83 -12
  148. package/src/session/account-inventory.ts +27 -11
  149. package/src/session/agent-session.ts +887 -95
  150. package/src/session/fold-coordinator.ts +433 -0
  151. package/src/session/internal/managed-session-storage.ts +64 -26
  152. package/src/session/sdk-run-capability-internal.ts +15 -0
  153. package/src/session/sdk-run-capability.ts +2 -0
  154. package/src/session/session-manager.ts +29 -10
  155. package/src/setup/provider-presets.json +1 -1
  156. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  157. package/src/task/executor.ts +19 -3
  158. package/src/telemetry/control.ts +26 -0
  159. package/src/telemetry/events.ts +142 -0
  160. package/src/telemetry/index.ts +3 -0
  161. package/src/telemetry/transport.ts +87 -0
  162. package/src/tools/bash-interactive.ts +182 -85
  163. package/src/tools/bash.ts +786 -67
  164. package/src/tools/index.ts +7 -5
  165. package/src/tools/path-utils.ts +77 -29
  166. package/src/tools/tmux-self-injection-guard.ts +359 -0
  167. package/src/tools/tool-catalog.generated.ts +1 -1
  168. package/src/web/search/provider.ts +120 -11
  169. package/src/web/search/providers/anthropic.ts +89 -17
  170. package/src/web/search/providers/base.ts +9 -4
  171. package/src/web/search/providers/gemini.ts +87 -15
  172. package/src/web/search/providers/openai-compatible.ts +23 -8
  173. package/src/web/search/providers/xai.ts +98 -12
  174. package/src/web/search/types.ts +16 -0
@@ -31,7 +31,12 @@ import {
31
31
  removeCanonicalBlob,
32
32
  } from "../session/blob-store";
33
33
  import { FileSessionStorage, probeSessionRetirement, retireSessionTranscript } from "../session/session-storage";
34
-
34
+ import {
35
+ collectEmptyDeleteReceipts,
36
+ type EmptyDeleteGcRecord,
37
+ type EmptyDeleteGcReport,
38
+ runEmptyDeleteGc,
39
+ } from "./empty-delete-gc";
35
40
  import { buildGcReportText } from "./gc-render";
36
41
  import { collectSessionScopeUsage, type GcSessionScopeUsage, shouldReportSessionScope } from "./gc-session-scope";
37
42
 
@@ -168,6 +173,7 @@ export interface GcReport {
168
173
  * PID-liveness axis above is unchanged by its absence or presence.
169
174
  */
170
175
  disk?: GcDiskReport;
176
+ empty_delete_receipts?: EmptyDeleteGcReport;
171
177
  }
172
178
 
173
179
  export interface GcRunResult {
@@ -274,9 +280,11 @@ interface ParsedGcArgs {
274
280
  json: boolean;
275
281
  prune: boolean;
276
282
  repairSessionIndex: boolean;
277
- /** Opt-in second axis: report (and with `--prune`, reclaim) on-disk retention. */
278
283
  disk: boolean;
279
284
  help: boolean;
285
+ emptyDeleteReceipts: boolean;
286
+ emptyDeleteRoots: string[];
287
+ emptyDeleteManifests: string[];
280
288
  }
281
289
 
282
290
  class GcUsageError extends Error {}
@@ -286,10 +294,13 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
286
294
  let prune = false;
287
295
  let repairSessionIndex = false;
288
296
  let disk = false;
289
-
290
297
  let dryRun = false;
291
298
  let help = false;
292
- for (const arg of argv) {
299
+ let emptyDeleteReceipts = false;
300
+ const emptyDeleteRoots: string[] = [];
301
+ const emptyDeleteManifests: string[] = [];
302
+ for (let i = 0; i < argv.length; i++) {
303
+ const arg = argv[i]!;
293
304
  switch (arg) {
294
305
  case "--json":
295
306
  case "-j":
@@ -305,7 +316,6 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
305
316
  case "--disk":
306
317
  disk = true;
307
318
  break;
308
-
309
319
  case "--dry-run":
310
320
  dryRun = true;
311
321
  break;
@@ -313,15 +323,34 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
313
323
  case "-h":
314
324
  help = true;
315
325
  break;
326
+ case "--empty-delete-receipts":
327
+ emptyDeleteReceipts = true;
328
+ break;
329
+ case "--root": {
330
+ const value = argv[++i];
331
+ // A following option token is a missing operand, not a root path. Any
332
+ // dash-prefixed value is rejected (short flags like -j included); spell a
333
+ // genuinely dash-prefixed path as ./-name so it is unambiguous.
334
+ if (!value || value.startsWith("-")) throw new GcUsageError("missing_root");
335
+ emptyDeleteRoots.push(value);
336
+ break;
337
+ }
338
+ case "--manifest": {
339
+ const value = argv[++i];
340
+ if (!value || value.startsWith("-")) throw new GcUsageError("missing_manifest");
341
+ emptyDeleteManifests.push(value);
342
+ break;
343
+ }
316
344
  default:
317
345
  throw new GcUsageError(`unknown_flag:${arg}`);
318
346
  }
319
347
  }
320
348
  if (repairSessionIndex && prune) throw new GcUsageError("repair_session_index_cannot_combine_with_prune");
349
+ if (!emptyDeleteReceipts && (emptyDeleteRoots.length > 0 || emptyDeleteManifests.length > 0))
350
+ throw new GcUsageError("empty_delete_operands_require_feature_flag");
321
351
  if (repairSessionIndex && dryRun) throw new GcUsageError("repair_session_index_cannot_combine_with_dry_run");
322
- // Explicit --dry-run always wins over --prune/--force.
323
352
  if (dryRun) prune = false;
324
- return { json, prune, repairSessionIndex, disk, help };
353
+ return { json, prune, repairSessionIndex, disk, help, emptyDeleteReceipts, emptyDeleteRoots, emptyDeleteManifests };
325
354
  }
326
355
 
327
356
  /**
@@ -404,6 +433,7 @@ export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext
404
433
  export function computeExitCode(report: GcReport): number {
405
434
  if (report.errors.length > 0) return 1;
406
435
  if (!report.dry_run && report.counts.failed > 0) return 1;
436
+ if (report.empty_delete_receipts?.errors.length) return 1;
407
437
  if (report.disk) {
408
438
  if (report.disk.errors.length > 0) return 1;
409
439
  if (!report.disk.dry_run && report.disk.totals.failed > 0) return 1;
@@ -415,6 +445,63 @@ function resolveGcAgentDir(env: NodeJS.ProcessEnv): string {
415
445
  return env.GJC_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || getAgentDir();
416
446
  }
417
447
 
448
+ /**
449
+ * Validate operator-supplied empty-delete roots before any ordinary GC mutation.
450
+ *
451
+ * These roots are explicit operands, so a missing, symlink, non-directory, or
452
+ * unreadable root is a usage failure rather than a partial GC report. The
453
+ * collector repeats its own race-safe checks after this preflight.
454
+ */
455
+ async function preflightEmptyDeleteRoot(root: string): Promise<void> {
456
+ const resolvedRoot = path.resolve(root);
457
+ let stat: Stats;
458
+ try {
459
+ stat = await fsp.lstat(resolvedRoot);
460
+ } catch (error) {
461
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
462
+ throw new GcUsageError(`${root}: missing_root`);
463
+ }
464
+ throw new GcUsageError(`${root}: ${error instanceof Error ? error.message : String(error)}`);
465
+ }
466
+ if (stat.isSymbolicLink()) throw new GcUsageError(`${root}: symlink_root`);
467
+ if (!stat.isDirectory()) throw new GcUsageError(`${root}: not_directory`);
468
+ try {
469
+ await fsp.readdir(resolvedRoot);
470
+ } catch (error) {
471
+ throw new GcUsageError(`${root}: ${error instanceof Error ? error.message : String(error)}`);
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Collect every explicit empty-delete root once before starting any destructive
477
+ * operation. The second pass reuses those identities so a later root race cannot
478
+ * leave an earlier root pruned before the race is discovered.
479
+ */
480
+ async function collectEmptyDeleteReceiptsForGc(
481
+ roots: string[],
482
+ ): Promise<{ report: EmptyDeleteGcReport; records: Map<string, EmptyDeleteGcRecord[]> }> {
483
+ const collected = new Map<string, EmptyDeleteGcRecord[]>();
484
+ const collect = async (root: string): Promise<EmptyDeleteGcRecord[]> => {
485
+ const records = await collectEmptyDeleteReceipts(root);
486
+ collected.set(root, records);
487
+ return records.map(record => ({
488
+ ...record,
489
+ identity: record.identity ? { ...record.identity } : undefined,
490
+ retainedPaths: record.retainedPaths ? { ...record.retainedPaths } : undefined,
491
+ }));
492
+ };
493
+ const report = await runEmptyDeleteGc({ roots, prune: false }, { collect });
494
+ return { report, records: collected };
495
+ }
496
+
497
+ function cloneEmptyDeleteRecords(records: EmptyDeleteGcRecord[]): EmptyDeleteGcRecord[] {
498
+ return records.map(record => ({
499
+ ...record,
500
+ identity: record.identity ? { ...record.identity } : undefined,
501
+ retainedPaths: record.retainedPaths ? { ...record.retainedPaths } : undefined,
502
+ }));
503
+ }
504
+
418
505
  /**
419
506
  * Locate and measure the managed scope for `cwd`.
420
507
  *
@@ -482,9 +569,77 @@ export async function runGjcGcCommand(
482
569
  return { stdout: gcHelpText(), stderr: "", status: 0 };
483
570
  }
484
571
 
572
+ // Resolve and validate every empty-delete operand BEFORE any store collection or
573
+ // prune can mutate: a malformed manifest or missing operand must fail with status 2
574
+ // while every candidate is still untouched.
575
+ let emptyDeleteRoots: string[] = [];
576
+ if (parsed.emptyDeleteReceipts) {
577
+ emptyDeleteRoots = [...parsed.emptyDeleteRoots];
578
+ if (emptyDeleteRoots.some(root => root.includes("\0"))) {
579
+ return { stdout: "", stderr: "gjc gc: root_invalid\n", status: 2 };
580
+ }
581
+ // Every supplied manifest is validated, not just the last: a malformed earlier
582
+ // manifest must fail the run before any store collection or prune can mutate.
583
+ for (const manifestPath of parsed.emptyDeleteManifests) {
584
+ let parsedManifest: unknown;
585
+ try {
586
+ const raw = await fsp.readFile(manifestPath, "utf8");
587
+ parsedManifest = JSON.parse(raw);
588
+ } catch (error) {
589
+ const message = error instanceof Error ? error.message : String(error);
590
+ return { stdout: "", stderr: `gjc gc: manifest_invalid: ${message}\n`, status: 2 };
591
+ }
592
+ if (typeof parsedManifest !== "object" || parsedManifest === null || Array.isArray(parsedManifest)) {
593
+ return { stdout: "", stderr: "gjc gc: manifest_shape_invalid\n", status: 2 };
594
+ }
595
+ const manifestRoots = (parsedManifest as { roots?: unknown }).roots;
596
+ if (!Array.isArray(manifestRoots)) {
597
+ return { stdout: "", stderr: "gjc gc: manifest_roots_required\n", status: 2 };
598
+ }
599
+ for (const root of manifestRoots) {
600
+ if (typeof root !== "string" || root.length === 0 || root.includes("\0")) {
601
+ return { stdout: "", stderr: "gjc gc: manifest_root_invalid\n", status: 2 };
602
+ }
603
+ emptyDeleteRoots.push(root);
604
+ }
605
+ }
606
+ if (emptyDeleteRoots.length === 0) {
607
+ return { stdout: "", stderr: "gjc gc: empty_delete_receipts_requires_root_or_manifest\n", status: 2 };
608
+ }
609
+ try {
610
+ for (const root of emptyDeleteRoots) await preflightEmptyDeleteRoot(root);
611
+ } catch (error) {
612
+ const message = error instanceof GcUsageError ? error.message : String(error);
613
+ return { stdout: "", stderr: `gjc gc: ${message}\n`, status: 2 };
614
+ }
615
+ }
616
+ let emptyDeleteReceipts: EmptyDeleteGcReport | undefined;
617
+ let emptyDeleteRecords: Map<string, EmptyDeleteGcRecord[]> | undefined;
618
+ if (parsed.emptyDeleteReceipts) {
619
+ // Discovery is deliberately read-only and covers every root before either the
620
+ // empty-delete unlink or an ordinary --prune adapter can mutate anything.
621
+ const collected = await collectEmptyDeleteReceiptsForGc(emptyDeleteRoots);
622
+ emptyDeleteReceipts = collected.report;
623
+ emptyDeleteRecords = collected.records;
624
+ if (parsed.prune && emptyDeleteReceipts.errors.length === 0) {
625
+ emptyDeleteReceipts = await runEmptyDeleteGc(
626
+ { roots: emptyDeleteRoots, prune: true },
627
+ {
628
+ collect: async root => {
629
+ const records = emptyDeleteRecords?.get(root);
630
+ if (!records) throw new Error(`${root}: empty_delete_collection_missing`);
631
+ return cloneEmptyDeleteRecords(records);
632
+ },
633
+ },
634
+ );
635
+ }
636
+ }
485
637
  const resolvedAdapters = adapters ?? (await defaultGcAdapters());
486
638
  const ctx: GcContext = { probe: gcPidProbe, force: parsed.prune, env, cwd };
487
- const report = await collectGcReport(resolvedAdapters, ctx, parsed.prune);
639
+ // A failed explicit empty-delete discovery or cleanup is a safety gate: report
640
+ // ordinary candidates, but never perform their destructive prune in that run.
641
+ const ordinaryPrune = parsed.prune && (emptyDeleteReceipts?.errors.length ?? 0) === 0;
642
+ const report = await collectGcReport(resolvedAdapters, ctx, ordinaryPrune);
488
643
  report.operation = parsed.repairSessionIndex ? "repair_session_index" : parsed.prune ? "prune" : "dry_run";
489
644
  report.session_index = await collectSessionIndexHealth(parsed.repairSessionIndex, resolveGcAgentDir(env));
490
645
  if (parsed.disk) {
@@ -492,9 +647,10 @@ export async function runGjcGcCommand(
492
647
  agentDir: resolveGcAgentDir(env),
493
648
  env,
494
649
  policy: resolveGcDiskPolicy(diskPolicy),
495
- prune: parsed.prune,
650
+ prune: ordinaryPrune,
496
651
  });
497
652
  }
653
+ if (emptyDeleteReceipts) report.empty_delete_receipts = emptyDeleteReceipts;
498
654
  const scopeUsage = await collectGcSessionScope(cwd, resolveGcAgentDir(env));
499
655
  if (scopeUsage && shouldReportSessionScope(scopeUsage)) report.session_scope = scopeUsage;
500
656
  const sessionIndexFailed =
@@ -503,7 +659,7 @@ export async function runGjcGcCommand(
503
659
  report.session_index?.status === "repair_failed";
504
660
  const status = sessionIndexFailed ? 1 : computeExitCode(report);
505
661
  const stdout = parsed.json
506
- ? `${JSON.stringify(report, null, 2)}\n`
662
+ ? `${JSON.stringify(report, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2)}\n`
507
663
  : `${buildGcReportText(report)}${report.disk ? buildGcDiskReportText(report.disk) : ""}`;
508
664
  return { stdout, stderr: "", status };
509
665
  }
@@ -513,7 +669,7 @@ export function gcHelpText(): string {
513
669
  "gjc gc - garbage-collect stale GJC session/PID records",
514
670
  "",
515
671
  "USAGE",
516
- " $ gjc gc [--prune|--force] [--disk] [--repair-session-index] [--json]",
672
+ " $ gjc gc [--prune|--force] [--disk] [--repair-session-index] [--empty-delete-receipts --root <dir>] [--json]",
517
673
 
518
674
  "",
519
675
  "FLAGS",
@@ -522,6 +678,13 @@ export function gcHelpText(): string {
522
678
  " -j, --json Emit machine-readable JSON",
523
679
  " --repair-session-index Explicitly quarantine a corrupt session-index suffix and retain its valid prefix",
524
680
  " --disk Also report on-disk retention (sessions, blobs, artifacts, natives, backups)",
681
+ " --empty-delete-receipts Report/prune empty .gjc-delete-* under --root / --manifest",
682
+ " --root <dir> Operand root for --empty-delete-receipts (repeatable)",
683
+ ' --manifest <file> JSON {"roots":[...]} for --empty-delete-receipts (repeatable)',
684
+ "",
685
+ " Operand values starting with '-' are rejected as missing operands; spell a",
686
+ " dash-prefixed path as ./-name. --root/--manifest without --empty-delete-receipts",
687
+ " is a usage error, and every supplied manifest is validated before any prune.",
525
688
  "",
526
689
  "Liveness-only: a record is removed only when its owning process is dead",
527
690
  "(ESRCH). Live / permission-denied / unknown processes are always kept.",
@@ -78,7 +78,7 @@ const WORKTREE_BUCKET_ENV = "GJC_WORKTREE_DIR";
78
78
  * what keeps two repositories that share a branch name from resolving to one worktree.
79
79
  */
80
80
  const REPO_NAME_PLACEHOLDER = "{repo}";
81
- const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}.gajae-code-worktrees`;
81
+ const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}/.worktrees`;
82
82
 
83
83
  function expandHomePrefix(value: string, home: string, pathApi: typeof path.posix): string {
84
84
  if (value === "~") return home;
@@ -108,14 +108,36 @@ export function resolveWorktreeBucketForPath(
108
108
  /**
109
109
  * Directory that holds this repository's launch worktrees.
110
110
  *
111
- * A relative override resolves against the repository's PARENT directory, matching the
112
- * default's own shape, so `{repo}.worktrees` adopts an existing sibling bucket and
113
- * `.worktrees` parks a hidden bucket beside the repository. An absolute override is used verbatim.
111
+ * A relative override resolves against the repository's parent directory. The default
112
+ * `{repo}/.worktrees` template places managed worktrees inside the repository, while
113
+ * `{repo}.worktrees` adopts an existing sibling bucket. An absolute override is used verbatim.
114
114
  */
115
115
  function resolveWorktreeBucket(repoRoot: string): string {
116
116
  return resolveWorktreeBucketForPath(repoRoot, process.env[WORKTREE_BUCKET_ENV], os.homedir(), path);
117
117
  }
118
118
 
119
+ function ensureRepositoryBucketIgnored(repoRoot: string, bucketPath: string): void {
120
+ const relativeBucket = path.relative(repoRoot, bucketPath);
121
+ if (relativeBucket.startsWith(`..${path.sep}`) || relativeBucket === ".." || path.isAbsolute(relativeBucket)) return;
122
+ const ignoreProbe = path.join(relativeBucket, ".gjc-worktree-probe");
123
+ for (const candidate of [relativeBucket, ignoreProbe]) {
124
+ const result = Bun.spawnSync(["git", "check-ignore", "--quiet", "--", candidate], {
125
+ cwd: repoRoot,
126
+ stdout: "ignore",
127
+ stderr: "pipe",
128
+ });
129
+ if (result.exitCode === 0) return;
130
+ }
131
+ throw new Error(
132
+ [
133
+ "worktree_bucket_not_ignored",
134
+ "The GJC launch worktree bucket is inside the repository but is not ignored by Git.",
135
+ `Path: ${formatBucketPath(bucketPath)}`,
136
+ `Safe remediation: add /${relativeBucket.replaceAll(path.sep, "/")} to ${path.join(repoRoot, ".gitignore")}, then relaunch.`,
137
+ ].join("\n"),
138
+ );
139
+ }
140
+
119
141
  function resolveSourceBranchSlug(repoRoot: string, baseRef: string): string {
120
142
  const branch = tryRunGit(repoRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
121
143
  if (branch) return sanitizePathToken(branch);
@@ -301,6 +323,7 @@ function pruneStaleWorktreePath(repoRoot: string): void {
301
323
 
302
324
  function readWorktreeEntryFromPath(repoRoot: string, worktreePath: string): GitWorktreeEntry | null {
303
325
  if (!fs.existsSync(worktreePath)) return null;
326
+ if (!fs.existsSync(path.join(worktreePath, ".git"))) return null;
304
327
  const repoCommonDir = tryRunGit(repoRoot, ["rev-parse", "--git-common-dir"]);
305
328
  const worktreeCommonDir = tryRunGit(worktreePath, ["rev-parse", "--git-common-dir"]);
306
329
  if (!repoCommonDir || !worktreeCommonDir) return null;
@@ -379,6 +402,7 @@ export function planLaunchWorktree(
379
402
  if (branchName) validateBranchName(repoRoot, branchName);
380
403
  const worktreeSlug = mode.detached ? resolveSourceBranchSlug(repoRoot, baseRef) : sanitizePathToken(mode.name);
381
404
  const worktreePath = path.join(resolveWorktreeBucket(repoRoot), worktreeSlug);
405
+ if (path.resolve(worktreePath) === path.resolve(repoRoot)) throw new Error(`worktree_path_conflict:${worktreePath}`);
382
406
  return { enabled: true, repoRoot, worktreePath, detached: mode.detached, baseRef, branchName };
383
407
  }
384
408
 
@@ -386,6 +410,9 @@ export function ensureLaunchWorktree(
386
410
  plan: GjcLaunchWorktreePlan | { enabled: false },
387
411
  ): GjcLaunchWorktreeResult | { enabled: false } {
388
412
  if (!plan.enabled) return { enabled: false };
413
+ const bucketPath = path.dirname(plan.worktreePath);
414
+ inspectBucketDir(bucketPath);
415
+ ensureRepositoryBucketIgnored(plan.repoRoot, bucketPath);
389
416
  let allWorktrees = listWorktrees(plan.repoRoot);
390
417
  const staleAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
391
418
  if (staleAtPath && !fs.existsSync(staleAtPath.path)) {
@@ -393,9 +420,19 @@ export function ensureLaunchWorktree(
393
420
  allWorktrees = listWorktrees(plan.repoRoot);
394
421
  }
395
422
 
396
- const existingAtPath =
397
- findWorktreeByPath(allWorktrees, plan.worktreePath) ??
398
- readWorktreeEntryFromPath(plan.repoRoot, plan.worktreePath);
423
+ const listedAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
424
+ const existingAtPath = readWorktreeEntryFromPath(plan.repoRoot, plan.worktreePath);
425
+ if (listedAtPath && !existingAtPath) {
426
+ if (fs.existsSync(plan.worktreePath)) throw new Error(`worktree_path_conflict:${plan.worktreePath}`);
427
+ throw new Error(
428
+ [
429
+ "worktree_path_unavailable",
430
+ "The requested launch worktree is still registered by Git but its directory is unavailable or locked.",
431
+ `Path: ${formatBucketPath(plan.worktreePath)}`,
432
+ "Safe remediation: inspect the worktree lock and remove or repair it with git worktree remove/prune when it is no longer needed, then relaunch. GJC did not delete or replace the entry.",
433
+ ].join("\n"),
434
+ );
435
+ }
399
436
  const expectedBranchRef = plan.branchName ? `refs/heads/${plan.branchName}` : null;
400
437
 
401
438
  if (existingAtPath) {
@@ -756,9 +756,22 @@ export async function resolveRalplanTargetRoot(
756
756
  stdout: "pipe",
757
757
  stderr: "pipe",
758
758
  });
759
- if ((await verified.exited) !== 0) {
759
+ const [verifiedStatus, verifiedOutput] = await Promise.all([verified.exited, new Response(verified.stdout).text()]);
760
+ const headObject = verifiedOutput.trim();
761
+ if (verifiedStatus !== 0 || !/^[0-9a-f]{40,64}$/u.test(headObject)) {
760
762
  throw new RalplanCommandError(2, `ralplan --worktree-root is not a valid git worktree: ${canonical}`);
761
763
  }
764
+ const objectType = Bun.spawn(["git", "-C", canonical, "cat-file", "-t", headObject], {
765
+ stdout: "pipe",
766
+ stderr: "pipe",
767
+ });
768
+ const [objectTypeStatus, objectTypeOutput] = await Promise.all([
769
+ objectType.exited,
770
+ new Response(objectType.stdout).text(),
771
+ ]);
772
+ if (objectTypeStatus !== 0 || objectTypeOutput.trim() !== "commit") {
773
+ throw new RalplanCommandError(2, `ralplan --worktree-root is not a commit worktree: ${canonical}`);
774
+ }
762
775
  let worktreeRoot: string;
763
776
  try {
764
777
  worktreeRoot = await fs.realpath(repository.repoRoot);