@gajae-code/coding-agent 0.5.0 → 0.5.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 +19 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/config/file-lock-gc.d.ts +5 -0
  7. package/dist/types/config/file-lock.d.ts +7 -0
  8. package/dist/types/coordinator/contract.d.ts +1 -1
  9. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  10. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  11. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  12. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  13. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  14. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  15. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  19. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  22. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  23. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  24. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  25. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  26. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  27. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  28. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  29. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  30. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  31. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  32. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  33. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  34. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  35. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  36. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  37. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  38. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  40. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  41. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  42. package/dist/types/session/agent-session.d.ts +1 -1
  43. package/dist/types/session/blob-store.d.ts +39 -3
  44. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  45. package/dist/types/tools/ask.d.ts +15 -1
  46. package/dist/types/tools/subagent.d.ts +6 -0
  47. package/package.json +7 -7
  48. package/src/async/job-manager.ts +52 -0
  49. package/src/cli/args.ts +3 -0
  50. package/src/cli/auth-broker-cli.ts +1 -0
  51. package/src/cli/list-models.ts +13 -1
  52. package/src/cli.ts +1 -0
  53. package/src/commands/gc.ts +22 -0
  54. package/src/commands/harness.ts +7 -3
  55. package/src/config/file-lock-gc.ts +181 -0
  56. package/src/config/file-lock.ts +14 -0
  57. package/src/config/model-profiles.ts +24 -15
  58. package/src/coordinator/contract.ts +1 -0
  59. package/src/coordinator-mcp/server.ts +459 -3
  60. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  61. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  62. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  63. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  66. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  67. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  68. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  69. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  70. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  71. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  72. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  73. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  75. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  76. package/src/defaults/gjc-defaults.ts +7 -0
  77. package/src/defaults/gjc-grok-cli.ts +22 -0
  78. package/src/extensibility/extensions/index.ts +1 -0
  79. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  80. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  81. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  82. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  83. package/src/gjc-runtime/gc-render.ts +70 -0
  84. package/src/gjc-runtime/gc-runtime.ts +403 -0
  85. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  86. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  87. package/src/gjc-runtime/state-renderer.ts +12 -3
  88. package/src/gjc-runtime/state-runtime.ts +46 -29
  89. package/src/gjc-runtime/team-gc.ts +49 -0
  90. package/src/gjc-runtime/team-runtime.ts +179 -2
  91. package/src/gjc-runtime/tmux-common.ts +14 -0
  92. package/src/gjc-runtime/tmux-gc.ts +176 -0
  93. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  94. package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
  95. package/src/harness-control-plane/gc-adapter.ts +184 -0
  96. package/src/harness-control-plane/owner.ts +11 -0
  97. package/src/harness-control-plane/storage.ts +70 -0
  98. package/src/internal-urls/docs-index.generated.ts +14 -8
  99. package/src/main.ts +7 -2
  100. package/src/modes/components/hook-selector.ts +19 -0
  101. package/src/modes/components/model-selector.ts +25 -8
  102. package/src/modes/components/status-line/segments.ts +1 -1
  103. package/src/modes/controllers/command-controller.ts +25 -6
  104. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  105. package/src/modes/controllers/selector-controller.ts +1 -0
  106. package/src/modes/rpc/rpc-mode.ts +151 -33
  107. package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
  108. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  109. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  110. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  111. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  112. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  113. package/src/sdk.ts +17 -3
  114. package/src/session/agent-session.ts +77 -8
  115. package/src/session/blob-store.ts +59 -3
  116. package/src/session/session-manager.ts +4 -4
  117. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  118. package/src/skill-state/workflow-hud.ts +106 -10
  119. package/src/slash-commands/builtin-registry.ts +3 -2
  120. package/src/task/executor.ts +9 -0
  121. package/src/tools/ask.ts +56 -1
  122. package/src/tools/job.ts +3 -2
  123. package/src/tools/monitor.ts +36 -1
  124. package/src/tools/subagent-render.ts +9 -0
  125. package/src/tools/subagent.ts +26 -2
@@ -13,6 +13,14 @@ export interface AsyncJob {
13
13
  type: "bash" | "task";
14
14
  status: "running" | "completed" | "failed" | "cancelled" | "paused";
15
15
  startTime: number;
16
+
17
+ /**
18
+ * Wall-clock ms when the job left the `running` state (completed, failed,
19
+ * cancelled, or paused). Undefined while running. Frozen on the first
20
+ * terminal/pause transition so elapsed-time renderers stop counting once a
21
+ * job is no longer active instead of growing forever against `Date.now()`.
22
+ */
23
+ endTime?: number;
16
24
  label: string;
17
25
  abortController: AbortController;
18
26
  promise: Promise<void>;
@@ -28,6 +36,16 @@ export interface AsyncJob {
28
36
  ownerId?: string;
29
37
  }
30
38
 
39
+ /**
40
+ * Elapsed wall-clock ms for a job, frozen once it stops running. While the job
41
+ * is active (`endTime` undefined) this counts against `now`; after it stops it
42
+ * returns the fixed `endTime - startTime` span so status renderers do not keep
43
+ * incrementing a completed job's timer.
44
+ */
45
+ export function jobElapsedMs(job: Pick<AsyncJob, "startTime" | "endTime">, now: number = Date.now()): number {
46
+ return Math.max(0, (job.endTime ?? now) - job.startTime);
47
+ }
48
+
31
49
  export interface AsyncJobMetadata {
32
50
  subagent?: {
33
51
  id: string;
@@ -80,6 +98,12 @@ export interface SubagentRecord {
80
98
  /** False for ephemeral sessions (no persistent artifacts dir). */
81
99
  resumable: boolean;
82
100
  queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
101
+ /** Resolved model the subagent was asked to use, e.g. "openai-codex/gpt-5.5". */
102
+ requestedModel?: string;
103
+ /** Model actually used after auth fallback (#985); equals requestedModel when no fallback. */
104
+ effectiveModel?: string;
105
+ /** True when the requested model lacked credentials and the subagent fell back to the parent model. */
106
+ modelFellBack?: boolean;
83
107
  }
84
108
 
85
109
  /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
@@ -368,6 +392,7 @@ export class AsyncJobManager {
368
392
  // delivery and no eviction scheduling: a paused subagent stays
369
393
  // listed and resumable from its sessionFile.
370
394
  job.status = "paused";
395
+ this.#freezeEndTime(job);
371
396
  if (outcome.note) job.resultText = outcome.note;
372
397
  this.#markRecordPaused(id);
373
398
  this.#drainResumeQueue();
@@ -375,6 +400,7 @@ export class AsyncJobManager {
375
400
  }
376
401
 
377
402
  job.status = "completed";
403
+ this.#freezeEndTime(job);
378
404
  job.resultText = outcome.text;
379
405
  this.#enqueueDelivery(id, outcome.text);
380
406
  this.#runLifecycle(id, "terminal");
@@ -393,6 +419,7 @@ export class AsyncJobManager {
393
419
  this.#runLifecycle(id, "terminal");
394
420
  const errorText = error instanceof Error ? error.message : String(error);
395
421
  job.status = "failed";
422
+ this.#freezeEndTime(job);
396
423
  job.errorText = errorText;
397
424
  this.#enqueueDelivery(id, errorText);
398
425
  this.#scheduleEviction(id);
@@ -428,10 +455,22 @@ export class AsyncJobManager {
428
455
  if (job.status !== "running") return false;
429
456
  this.#runLifecycle(id, "cancel");
430
457
  job.status = "cancelled";
458
+ this.#freezeEndTime(job);
431
459
  job.abortController.abort();
432
460
  return true;
433
461
  }
434
462
 
463
+ /**
464
+ * Freeze the wall-clock instant a job stopped running. Idempotent: the
465
+ * first stop (completed/failed/cancelled/paused) wins so elapsed-time
466
+ * renderers report a stable duration instead of counting against
467
+ * `Date.now()` forever. A resumed subagent registers a brand-new job with
468
+ * its own `startTime`, so a paused job's frozen `endTime` is never reused.
469
+ */
470
+ #freezeEndTime(job: AsyncJob): void {
471
+ job.endTime ??= Date.now();
472
+ }
473
+
435
474
  #runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
436
475
  const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
437
476
  if (fired.has(phase)) return;
@@ -503,6 +542,18 @@ export class AsyncJobManager {
503
542
  this.#subagentRecords.set(record.subagentId, record);
504
543
  }
505
544
 
545
+ /** Patch model metadata onto an existing subagent record (best-effort; no-op if unknown). */
546
+ updateSubagentModel(
547
+ subagentId: string,
548
+ model: { requestedModel?: string; effectiveModel?: string; modelFellBack?: boolean },
549
+ ): void {
550
+ const record = this.#subagentRecords.get(subagentId);
551
+ if (!record) return;
552
+ record.requestedModel = model.requestedModel;
553
+ record.effectiveModel = model.effectiveModel;
554
+ record.modelFellBack = model.modelFellBack;
555
+ }
556
+
506
557
  getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
507
558
  const rec = this.#subagentRecords.get(subagentId.trim());
508
559
  if (!rec) return undefined;
@@ -978,6 +1029,7 @@ export class AsyncJobManager {
978
1029
  for (const job of this.getRunningJobs(filter)) {
979
1030
  this.#runLifecycle(job.id, "cancel");
980
1031
  job.status = "cancelled";
1032
+ this.#freezeEndTime(job);
981
1033
  job.abortController.abort();
982
1034
  this.#scheduleEviction(job.id);
983
1035
  }
package/src/cli/args.ts CHANGED
@@ -30,6 +30,7 @@ export interface Args {
30
30
  mode?: Mode;
31
31
  noSession?: boolean;
32
32
  sessionDir?: string;
33
+ rpcListen?: string;
33
34
  providerSessionId?: string;
34
35
  fork?: string;
35
36
  models?: string[];
@@ -145,6 +146,8 @@ export function parseArgs(args: string[]): Args {
145
146
  result.noSession = true;
146
147
  } else if (arg === "--session-dir" && i + 1 < args.length) {
147
148
  result.sessionDir = args[++i];
149
+ } else if (arg === "--listen" && i + 1 < args.length) {
150
+ result.rpcListen = args[++i];
148
151
  } else if (arg === "--models" && i + 1 < args.length) {
149
152
  result.models = args[++i].split(",").map(s => s.trim());
150
153
  } else if (arg === "--no-tools") {
@@ -70,6 +70,7 @@ const CALLBACK_PORTS: Record<string, number> = {
70
70
  "google-antigravity": 51121,
71
71
  "gitlab-duo": 8080,
72
72
  xai: 56121,
73
+ "grok-build": 56121,
73
74
  };
74
75
 
75
76
  function getTokenFilePath(): string {
@@ -5,7 +5,12 @@ import { type Api, getSupportedEfforts, type Model } from "@gajae-code/ai";
5
5
  import { fuzzyFilter } from "@gajae-code/tui";
6
6
  import { formatNumber } from "@gajae-code/utils";
7
7
  import type { ModelRegistry } from "../config/model-registry";
8
- import { discoverAndLoadExtensions, loadExtensions } from "../extensibility/extensions";
8
+ import {
9
+ discoverAndLoadExtensions,
10
+ type ExtensionFactory,
11
+ loadExtensionFromFactory,
12
+ loadExtensions,
13
+ } from "../extensibility/extensions";
9
14
  import { EventBus } from "../utils/event-bus";
10
15
 
11
16
  interface ProviderRow {
@@ -137,6 +142,8 @@ export interface RunListModelsOptions {
137
142
  cwd: string;
138
143
  /** CLI-supplied extension paths (e.g. from `-e <path>`). */
139
144
  additionalExtensionPaths?: string[];
145
+ /** In-process extension factories to load without filesystem discovery. */
146
+ extensionFactories?: Array<{ factory: ExtensionFactory; name: string }>;
140
147
  /** Extension paths configured under `extensions:` in user settings. */
141
148
  settingsExtensions?: string[];
142
149
  /** Disabled extension ids from settings (`disabledExtensions`). */
@@ -159,6 +166,7 @@ export async function runListModelsCommand(options: RunListModelsOptions): Promi
159
166
  modelRegistry,
160
167
  cwd,
161
168
  additionalExtensionPaths = [],
169
+ extensionFactories = [],
162
170
  settingsExtensions = [],
163
171
  disabledExtensionIds = [],
164
172
  disableExtensionDiscovery = false,
@@ -174,6 +182,10 @@ export async function runListModelsCommand(options: RunListModelsOptions): Promi
174
182
  eventBus,
175
183
  disabledExtensionIds,
176
184
  );
185
+ for (const { factory, name } of extensionFactories) {
186
+ const extension = await loadExtensionFromFactory(factory, cwd, eventBus, extensionsResult.runtime, name);
187
+ extensionsResult.extensions.push(extension);
188
+ }
177
189
 
178
190
  for (const { path: extPath, error } of extensionsResult.errors) {
179
191
  process.stderr.write(`Failed to load extension: ${extPath}: ${error}\n`);
package/src/cli.ts CHANGED
@@ -32,6 +32,7 @@ const commands: CommandEntry[] = [
32
32
  { name: "coordinator", load: () => import("./commands/coordinator").then(m => m.default) },
33
33
  { name: "team", load: () => import("./commands/team").then(m => m.default) },
34
34
  { name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
35
+ { name: "gc", load: () => import("./commands/gc").then(m => m.default) },
35
36
  { name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
36
37
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
37
38
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
@@ -0,0 +1,22 @@
1
+ import { Command, Flags } from "@gajae-code/utils/cli";
2
+ import { runGjcGcCommand } from "../gjc-runtime/gc-runtime";
3
+
4
+ export default class Gc extends Command {
5
+ static description = "Garbage-collect stale GJC session/PID records (dry-run by default)";
6
+ static strict = false;
7
+ static flags = {
8
+ json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
9
+ prune: Flags.boolean({ description: "Remove stale records (default: report only)", default: false }),
10
+ force: Flags.boolean({ description: "Alias for --prune (eligible records only)", default: false }),
11
+ "dry-run": Flags.boolean({ description: "Force report-only mode", default: false }),
12
+ };
13
+
14
+ static examples = ["gjc gc", "gjc gc --json", "gjc gc --prune", "gjc gc --prune --json"];
15
+
16
+ async run(): Promise<void> {
17
+ const result = await runGjcGcCommand(this.argv, process.cwd(), process.env);
18
+ if (result.stdout) process.stdout.write(result.stdout);
19
+ if (result.stderr) process.stderr.write(result.stderr);
20
+ process.exitCode = result.status;
21
+ }
22
+ }
@@ -16,7 +16,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
16
16
  import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
17
17
  import { classifyRecovery } from "../harness-control-plane/classifier";
18
18
  import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
19
- import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
19
+ import { type ResolvedOwner, RuntimeOwner, resolveOwner, resolveOwnerLive } from "../harness-control-plane/owner";
20
20
  import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
21
21
  import { RECEIPT_SPOOL_DIR_ENV } from "../harness-control-plane/receipt-spool";
22
22
  import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
@@ -948,10 +948,14 @@ export default class Harness extends Command {
948
948
  let observation = input.observation as Partial<Observation> | undefined;
949
949
  let stateView: SessionState | null = null;
950
950
  const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
951
+ // Session-backed classify derives owner liveness from the same lease/socket probe observe
952
+ // uses for routing, so a live (e.g. manual) owner is never misread as vanished/restart-clean.
953
+ let ownerLive = false;
951
954
  if (sessionId) {
952
955
  stateView = await loadState(root, sessionId);
956
+ ownerLive = await resolveOwnerLive(root, sessionId);
953
957
  if (!observation) {
954
- const built = await buildObservation(root, stateView, ownerLiveFor(stateView));
958
+ const built = await buildObservation(root, stateView, ownerLive);
955
959
  observation = built.observation;
956
960
  stateView = await markVanishedOwnerBlocked(
957
961
  root,
@@ -975,7 +979,7 @@ export default class Harness extends Command {
975
979
  const decision = classifyRecovery({ observation: full, retryBudget: budget });
976
980
  if (stateView) {
977
981
  writeJson(
978
- buildResponse(stateView, ownerLiveFor(stateView), {
982
+ buildResponse(stateView, ownerLive, {
979
983
  decision,
980
984
  observation: { ...full, lifecycle: stateView.lifecycle },
981
985
  }),
@@ -0,0 +1,181 @@
1
+ /**
2
+ * GC adapter for config file-locks (`<file>.lock` dirs holding `{pid, timestamp}`).
3
+ */
4
+
5
+ import * as fs from "node:fs/promises";
6
+ import * as path from "node:path";
7
+ import { getAgentDir, getConfigRootDir, isEnoent } from "@gajae-code/utils";
8
+ import type {
9
+ GcCollectResult,
10
+ GcContext,
11
+ GcError,
12
+ GcPruneOutcome,
13
+ GcRecord,
14
+ GcStoreAdapter,
15
+ } from "../gjc-runtime/gc-runtime";
16
+ import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
17
+ import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
18
+ import { readFileLockInfoForGc, removeFileLockDirForGc } from "./file-lock";
19
+
20
+ const MAX_WALK_DEPTH = 6;
21
+ const MAX_WALK_ENTRIES = 20_000;
22
+
23
+ // High-cardinality, lock-free subtrees we never descend into. `.lock` dirs are
24
+ // created next to config files, never inside these.
25
+ const PRUNED_DIR_NAMES = new Set(["sessions", "node_modules", ".git", "blobs", "artifacts", "receipts", "events"]);
26
+
27
+ interface WalkState {
28
+ entries: number;
29
+ truncated: boolean;
30
+ }
31
+
32
+ // Global, env-aware GJC lock roots. Per the approved scope this covers the
33
+ // user config root, the agent dir (honors GJC_CODING_AGENT_DIR), and the
34
+ // configured receipt-spool dir — NOT the invocation cwd's project `.gjc`.
35
+ function knownFileLockRoots(ctx: GcContext): string[] {
36
+ const roots = [getConfigRootDir(), getAgentDir()];
37
+ const spoolDir = resolveReceiptSpoolDir(ctx.env);
38
+ if (spoolDir) roots.push(spoolDir);
39
+ return Array.from(new Set(roots.map(root => path.resolve(root))));
40
+ }
41
+
42
+ function errorMessage(error: unknown): string {
43
+ return error instanceof Error ? error.message : String(error);
44
+ }
45
+
46
+ function keptMalformedRecord(lockDir: string): GcRecord {
47
+ return {
48
+ store: "file_locks",
49
+ id: lockDir,
50
+ path: lockDir,
51
+ pid_status: "none",
52
+ status: "malformed",
53
+ stale: false,
54
+ removable: false,
55
+ action: "none",
56
+ reason: "missing_or_malformed_file_lock_info",
57
+ };
58
+ }
59
+
60
+ async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
61
+ const info = await readFileLockInfoForGc(lockDir);
62
+ if (!info) return keptMalformedRecord(lockDir);
63
+
64
+ const probeResult = ctx.probe(info.pid);
65
+ const pidStatus = gcPidStatusLabel(probeResult);
66
+ const removable = probeResult.status === "dead";
67
+
68
+ return {
69
+ store: "file_locks",
70
+ id: lockDir,
71
+ path: lockDir,
72
+ pid: info.pid,
73
+ pid_status: pidStatus,
74
+ status: pidStatus,
75
+ stale: removable,
76
+ removable,
77
+ action: "none",
78
+ reason: removable ? "file_lock_owner_pid_dead" : `file_lock_owner_pid_${pidStatus}`,
79
+ detail: `timestamp=${info.timestamp}`,
80
+ };
81
+ }
82
+
83
+ async function walkForLockDirs(
84
+ dir: string,
85
+ depth: number,
86
+ state: WalkState,
87
+ lockDirs: Set<string>,
88
+ errors: GcError[],
89
+ ): Promise<void> {
90
+ if (state.entries >= MAX_WALK_ENTRIES) {
91
+ state.truncated = true;
92
+ return;
93
+ }
94
+
95
+ let stat: Awaited<ReturnType<typeof fs.lstat>>;
96
+ try {
97
+ stat = await fs.lstat(dir);
98
+ } catch (error) {
99
+ if (isEnoent(error)) return;
100
+ errors.push({ store: "file_locks", scope: dir, message: errorMessage(error) });
101
+ return;
102
+ }
103
+
104
+ state.entries++;
105
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
106
+
107
+ if (path.basename(dir).endsWith(".lock")) {
108
+ lockDirs.add(dir);
109
+ return;
110
+ }
111
+
112
+ if (depth >= MAX_WALK_DEPTH) return;
113
+
114
+ let entries: string[];
115
+ try {
116
+ entries = await fs.readdir(dir);
117
+ } catch (error) {
118
+ if (isEnoent(error)) return;
119
+ errors.push({ store: "file_locks", scope: dir, message: errorMessage(error) });
120
+ return;
121
+ }
122
+
123
+ for (const entry of entries) {
124
+ if (state.entries >= MAX_WALK_ENTRIES) {
125
+ state.truncated = true;
126
+ return;
127
+ }
128
+ if (PRUNED_DIR_NAMES.has(entry)) continue;
129
+ await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors);
130
+ }
131
+ }
132
+
133
+ export const fileLocksGcAdapter: GcStoreAdapter = {
134
+ store: "file_locks",
135
+ async collect(ctx: GcContext): Promise<GcCollectResult> {
136
+ const records: GcRecord[] = [];
137
+ const errors: GcError[] = [];
138
+ const lockDirs = new Set<string>();
139
+ const state: WalkState = { entries: 0, truncated: false };
140
+
141
+ for (const root of knownFileLockRoots(ctx)) {
142
+ await walkForLockDirs(root, 0, state, lockDirs, errors);
143
+ if (state.truncated) break;
144
+ }
145
+
146
+ if (state.truncated) {
147
+ errors.push({
148
+ store: "file_locks",
149
+ scope: "discovery",
150
+ message: `file lock discovery capped at ${MAX_WALK_ENTRIES} entries`,
151
+ });
152
+ }
153
+
154
+ for (const lockDir of lockDirs) {
155
+ try {
156
+ records.push(await collectLockRecord(lockDir, ctx));
157
+ } catch (error) {
158
+ errors.push({ store: "file_locks", scope: lockDir, message: errorMessage(error) });
159
+ }
160
+ }
161
+
162
+ return { records, errors };
163
+ },
164
+ async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
165
+ const lockDir = record.path ?? record.id;
166
+ const info = await readFileLockInfoForGc(lockDir);
167
+ if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
168
+
169
+ const probeResult = ctx.probe(info.pid);
170
+ if (probeResult.status !== "dead") {
171
+ return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
172
+ }
173
+
174
+ try {
175
+ await removeFileLockDirForGc(lockDir);
176
+ return { removed: true };
177
+ } catch (error) {
178
+ return { removed: false, error: errorMessage(error) };
179
+ }
180
+ },
181
+ };
@@ -36,6 +36,20 @@ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
36
36
  }
37
37
  }
38
38
 
39
+ /** @internal */
40
+ export async function readFileLockInfoForGc(lockDir: string): Promise<{ pid: number; timestamp: number } | null> {
41
+ const info = await readLockInfo(lockDir);
42
+ if (!info) return null;
43
+ if (!Number.isFinite(info.pid) || info.pid <= 0) return null;
44
+ if (!Number.isFinite(info.timestamp)) return null;
45
+ return info;
46
+ }
47
+
48
+ /** @internal */
49
+ export async function removeFileLockDirForGc(lockDir: string): Promise<void> {
50
+ await fs.rm(lockDir, { recursive: true, force: true });
51
+ }
52
+
39
53
  function isProcessAlive(pid: number): boolean {
40
54
  try {
41
55
  process.kill(pid, 0);
@@ -90,25 +90,25 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
90
90
  architect: "anthropic/claude-opus-4-8:xhigh",
91
91
  }),
92
92
  profile("glm-eco", ["zai"], {
93
- default: "zai/glm-5.1:low",
94
- executor: "zai/glm-5.1:minimal",
95
- planner: "zai/glm-5.1:low",
96
- critic: "zai/glm-5.1:medium",
97
- architect: "zai/glm-5.1:high",
93
+ default: "zai/glm-5.2:low",
94
+ executor: "zai/glm-5.2:minimal",
95
+ planner: "zai/glm-5.2:low",
96
+ critic: "zai/glm-5.2:medium",
97
+ architect: "zai/glm-5.2:high",
98
98
  }),
99
99
  profile("glm-medium", ["zai"], {
100
- default: "zai/glm-5.1:medium",
101
- executor: "zai/glm-5.1:low",
102
- planner: "zai/glm-5.1:medium",
103
- critic: "zai/glm-5.1:high",
104
- architect: "zai/glm-5.1:xhigh",
100
+ default: "zai/glm-5.2:medium",
101
+ executor: "zai/glm-5.2:low",
102
+ planner: "zai/glm-5.2:medium",
103
+ critic: "zai/glm-5.2:high",
104
+ architect: "zai/glm-5.2:xhigh",
105
105
  }),
106
106
  profile("glm-pro", ["zai"], {
107
- default: "zai/glm-5.1:xhigh",
108
- executor: "zai/glm-5.1:medium",
109
- planner: "zai/glm-5.1:high",
110
- critic: "zai/glm-5.1:xhigh",
111
- architect: "zai/glm-5.1:xhigh",
107
+ default: "zai/glm-5.2:xhigh",
108
+ executor: "zai/glm-5.2:medium",
109
+ planner: "zai/glm-5.2:high",
110
+ critic: "zai/glm-5.2:xhigh",
111
+ architect: "zai/glm-5.2:xhigh",
112
112
  }),
113
113
  profile("kimi-coding-plan-eco", ["kimi-code"], {
114
114
  default: "kimi-code/kimi-k2.7-code:low",
@@ -173,6 +173,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
173
173
  critic: "xai/grok-4.3:xhigh",
174
174
  architect: "xai/grok-4.3:xhigh",
175
175
  }),
176
+ profile("grok-build-pro", ["grok-build"], {
177
+ default: "grok-build/grok-composer-2.5-fast",
178
+ executor: "grok-build/grok-build",
179
+ planner: "grok-build/grok-composer-2.5-fast",
180
+ critic: "grok-build/grok-composer-2.5-fast",
181
+ architect: "grok-build/grok-build",
182
+ }),
176
183
  profile("cursor-eco", ["cursor"], {
177
184
  default: "cursor/composer-1.5:low",
178
185
  executor: "cursor/composer-1.5:minimal",
@@ -254,6 +261,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
254
261
  "grok-eco": { displayName: "Grok Eco", providerGroup: "GROK" },
255
262
  "grok-medium": { displayName: "Grok Medium", providerGroup: "GROK" },
256
263
  "grok-pro": { displayName: "Grok Pro", providerGroup: "GROK" },
264
+ "grok-build-pro": { displayName: "Grok Build Pro", providerGroup: "GROK" },
257
265
  "cursor-eco": { displayName: "Cursor Eco", providerGroup: "CURSOR" },
258
266
  "cursor-medium": { displayName: "Cursor Medium", providerGroup: "CURSOR" },
259
267
  "cursor-pro": { displayName: "Cursor Pro", providerGroup: "CURSOR" },
@@ -285,6 +293,7 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
285
293
  "kimi-code": "kimi-coding-plan-medium",
286
294
  xiaomi: "mimo-medium",
287
295
  xai: "grok-medium",
296
+ "grok-build": "grok-build-pro",
288
297
  cursor: "cursor-medium",
289
298
  "minimax-code": "minimax-medium",
290
299
  };
@@ -9,6 +9,7 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
9
9
  "gjc_coordinator_list_artifacts",
10
10
  "gjc_coordinator_read_artifact",
11
11
  "gjc_coordinator_read_coordination_status",
12
+ "gjc_coordinator_watch_events",
12
13
  "gjc_coordinator_register_session",
13
14
  "gjc_coordinator_start_session",
14
15
  "gjc_coordinator_send_prompt",