@gajae-code/coding-agent 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +105 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  18. package/dist/types/goals/runtime.d.ts +1 -0
  19. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  21. package/dist/types/modes/components/status-line.d.ts +9 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  24. package/dist/types/modes/interactive-mode.d.ts +2 -0
  25. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  26. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  27. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  28. package/dist/types/modes/types.d.ts +1 -0
  29. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  30. package/dist/types/notifications/config.d.ts +6 -0
  31. package/dist/types/notifications/telegram-daemon.d.ts +8 -0
  32. package/dist/types/notifications/threaded-render.d.ts +6 -0
  33. package/dist/types/sdk.d.ts +3 -0
  34. package/dist/types/session/agent-session.d.ts +43 -1
  35. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  36. package/dist/types/session/messages.d.ts +4 -0
  37. package/dist/types/session/session-manager.d.ts +17 -1
  38. package/dist/types/skill-state/active-state.d.ts +18 -1
  39. package/dist/types/system-prompt.d.ts +18 -2
  40. package/dist/types/task/executor.d.ts +1 -0
  41. package/dist/types/task/fork-context-budget.d.ts +7 -0
  42. package/dist/types/task/token-log.d.ts +30 -0
  43. package/dist/types/task/token-log.test.d.ts +1 -0
  44. package/dist/types/tools/bisect.d.ts +81 -0
  45. package/dist/types/tools/index.d.ts +1 -0
  46. package/dist/types/tools/search.d.ts +2 -2
  47. package/dist/types/utils/file-mentions.d.ts +14 -0
  48. package/dist/types/utils/git.d.ts +32 -0
  49. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  50. package/package.json +7 -7
  51. package/src/append-only-mode.ts +58 -0
  52. package/src/autoresearch/prompt.md +1 -1
  53. package/src/autoresearch/tools/log-experiment.ts +1 -1
  54. package/src/capability/rule.ts +9 -0
  55. package/src/cli/config-cli.ts +27 -3
  56. package/src/cli/fixture-report.test.ts +230 -0
  57. package/src/cli/fixture-report.ts +171 -0
  58. package/src/cli/nofile-limit.ts +48 -0
  59. package/src/cli/update-cli.ts +85 -9
  60. package/src/cli.ts +26 -0
  61. package/src/commands/contribution-prep.ts +1 -1
  62. package/src/config/settings-schema.ts +68 -1
  63. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  64. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  65. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  66. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  67. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  68. package/src/defaults/gjc-defaults.ts +9 -0
  69. package/src/discovery/helpers.ts +10 -0
  70. package/src/discovery/index.ts +1 -0
  71. package/src/export/html/template.generated.ts +1 -1
  72. package/src/export/html/template.js +60 -2
  73. package/src/export/ttsr.ts +96 -18
  74. package/src/extensibility/extensions/runner.ts +181 -219
  75. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  76. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  77. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  78. package/src/gjc-runtime/state-writer.ts +15 -0
  79. package/src/gjc-runtime/team-runtime.ts +164 -15
  80. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  82. package/src/gjc-runtime/ultragoal-runtime.ts +1593 -143
  83. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  85. package/src/goals/runtime.ts +10 -2
  86. package/src/hindsight/backend.ts +1 -3
  87. package/src/hooks/native-skill-hook.ts +95 -4
  88. package/src/internal-urls/docs-index.generated.ts +14 -5
  89. package/src/main.ts +43 -2
  90. package/src/memories/index.ts +12 -1
  91. package/src/modes/components/assistant-message.ts +241 -82
  92. package/src/modes/components/custom-editor.ts +1 -0
  93. package/src/modes/components/status-line.ts +167 -54
  94. package/src/modes/components/tool-execution.ts +44 -19
  95. package/src/modes/controllers/command-controller.ts +39 -2
  96. package/src/modes/controllers/event-controller.ts +66 -47
  97. package/src/modes/controllers/input-controller.ts +1 -1
  98. package/src/modes/interactive-mode.ts +9 -1
  99. package/src/modes/rpc/rpc-mode.ts +29 -3
  100. package/src/modes/rpc/rpc-types.ts +9 -0
  101. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  102. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  103. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  104. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  105. package/src/modes/types.ts +1 -0
  106. package/src/modes/utils/ui-helpers.ts +17 -4
  107. package/src/notifications/config.ts +16 -4
  108. package/src/notifications/index.ts +78 -5
  109. package/src/notifications/telegram-daemon-cli.ts +2 -2
  110. package/src/notifications/telegram-daemon-control.ts +2 -2
  111. package/src/notifications/telegram-daemon.ts +85 -8
  112. package/src/notifications/threaded-render.ts +17 -1
  113. package/src/prompts/agents/architect.md +19 -10
  114. package/src/prompts/agents/critic.md +16 -18
  115. package/src/prompts/agents/executor.md +14 -11
  116. package/src/prompts/agents/planner.md +18 -13
  117. package/src/prompts/ci-green-request.md +3 -8
  118. package/src/prompts/goals/goal-continuation.md +0 -5
  119. package/src/prompts/goals/goal-mode-active.md +0 -5
  120. package/src/prompts/system/agent-creation-architect.md +2 -2
  121. package/src/prompts/system/plan-mode-active.md +1 -1
  122. package/src/prompts/system/plan-mode-subagent.md +3 -3
  123. package/src/prompts/system/project-prompt.md +0 -13
  124. package/src/prompts/system/subagent-system-prompt.md +3 -2
  125. package/src/prompts/system/system-prompt.md +26 -19
  126. package/src/prompts/system/volatile-project-context.md +10 -0
  127. package/src/prompts/system/web-search.md +4 -7
  128. package/src/prompts/tools/apply-patch.md +6 -0
  129. package/src/prompts/tools/ast-edit.md +1 -0
  130. package/src/prompts/tools/bash.md +1 -1
  131. package/src/prompts/tools/bisect.md +22 -0
  132. package/src/prompts/tools/browser.md +1 -1
  133. package/src/prompts/tools/cron.md +0 -2
  134. package/src/prompts/tools/eval.md +2 -2
  135. package/src/prompts/tools/find.md +1 -1
  136. package/src/prompts/tools/github.md +6 -6
  137. package/src/prompts/tools/goal.md +1 -3
  138. package/src/prompts/tools/image-gen.md +12 -5
  139. package/src/prompts/tools/irc.md +5 -4
  140. package/src/prompts/tools/job.md +2 -2
  141. package/src/prompts/tools/lsp.md +1 -1
  142. package/src/prompts/tools/monitor.md +2 -2
  143. package/src/prompts/tools/patch.md +2 -2
  144. package/src/prompts/tools/replace.md +2 -21
  145. package/src/prompts/tools/rewind.md +1 -1
  146. package/src/prompts/tools/search-tool-bm25.md +1 -11
  147. package/src/prompts/tools/search.md +1 -1
  148. package/src/prompts/tools/skill.md +3 -2
  149. package/src/prompts/tools/ssh.md +2 -1
  150. package/src/prompts/tools/subagent.md +2 -2
  151. package/src/prompts/tools/task.md +4 -5
  152. package/src/prompts/tools/todo-write.md +1 -1
  153. package/src/prompts/tools/vim.md +1 -0
  154. package/src/prompts/tools/web-search.md +2 -0
  155. package/src/prompts/tools/write.md +8 -2
  156. package/src/sdk.ts +32 -22
  157. package/src/session/agent-session.ts +732 -107
  158. package/src/session/file-mention-pruning.ts +64 -0
  159. package/src/session/messages.ts +4 -0
  160. package/src/session/session-manager.ts +59 -2
  161. package/src/skill-state/active-state.ts +141 -5
  162. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  163. package/src/slash-commands/builtin-registry.ts +65 -0
  164. package/src/system-prompt.ts +57 -16
  165. package/src/task/executor.ts +125 -20
  166. package/src/task/fork-context-advisory.ts +2 -7
  167. package/src/task/fork-context-budget.ts +9 -0
  168. package/src/task/index.ts +4 -3
  169. package/src/task/output-manager.ts +4 -2
  170. package/src/task/token-log.test.ts +188 -0
  171. package/src/task/token-log.ts +179 -0
  172. package/src/tools/bisect.ts +367 -0
  173. package/src/tools/index.ts +3 -0
  174. package/src/tools/output-meta.ts +116 -4
  175. package/src/tools/search.ts +8 -2
  176. package/src/tools/skill.ts +15 -0
  177. package/src/utils/file-mentions.ts +39 -7
  178. package/src/utils/git.ts +73 -0
  179. package/src/web/scrapers/youtube.ts +24 -12
@@ -304,6 +304,18 @@ function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope)
304
304
  return layoutActiveStateDir(cwd, requireSessionId(sessionScope, "activeStateDir"));
305
305
  }
306
306
 
307
+ type ActiveStateCacheInvalidator = (cwd?: string, sessionId?: string) => void;
308
+ var activeStateCacheInvalidator: ActiveStateCacheInvalidator | undefined;
309
+
310
+ export function setActiveStateCacheInvalidator(invalidator: ActiveStateCacheInvalidator): void {
311
+ activeStateCacheInvalidator = invalidator;
312
+ }
313
+
314
+ function invalidateActiveStateCacheForScope(cwd: string, sessionScope?: string | ActiveSessionScope): void {
315
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
316
+ activeStateCacheInvalidator?.(cwd, sessionId);
317
+ }
318
+
307
319
  function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
308
320
  return layoutActiveSnapshotPath(cwd, requireSessionId(sessionScope, "activeSnapshotPath"));
309
321
  }
@@ -979,6 +991,7 @@ export async function writeActiveEntry(
979
991
  persistedSourceRevision(entry) || persistedSourceRevision(await readJsonIfPresent(filePath)) + 1,
980
992
  },
981
993
  );
994
+ invalidateActiveStateCacheForScope(cwd, sessionScope);
982
995
  return filePath;
983
996
  }
984
997
 
@@ -1003,6 +1016,7 @@ export async function removeActiveEntry(
1003
1016
  }
1004
1017
  const deleted = await atomicRemove(filePath);
1005
1018
  if (deleted) await maybeAudit(filePath, options);
1019
+ if (deleted) invalidateActiveStateCacheForScope(cwd, sessionScope);
1006
1020
  return { path: filePath, deleted };
1007
1021
  },
1008
1022
  options?.lock,
@@ -1049,6 +1063,7 @@ export async function rebuildActiveSnapshot(
1049
1063
  ...entries.map(entry => persistedSourceRevision(entry)),
1050
1064
  ),
1051
1065
  });
1066
+ invalidateActiveStateCacheForScope(cwd, sessionScope);
1052
1067
  return snapshotPath;
1053
1068
  }
1054
1069
 
@@ -204,6 +204,7 @@ export type GjcTeamNotificationDeliveryState =
204
204
  | "acknowledged";
205
205
 
206
206
  export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
207
+ export type GjcTeamMailboxDeliveryTransportKind = "notifications_sdk" | "pane";
207
208
 
208
209
  export interface GjcTeamNotification {
209
210
  id: string;
@@ -220,6 +221,36 @@ export interface GjcTeamNotification {
220
221
  updated_at: string;
221
222
  replay_count: number;
222
223
  }
224
+ export interface GjcTeamMailboxDeliveryInput {
225
+ team_name: string;
226
+ state_dir: string;
227
+ config: GjcTeamConfig;
228
+ notification: GjcTeamNotification;
229
+ message: GjcTeamMailboxMessage;
230
+ cwd: string;
231
+ env: NodeJS.ProcessEnv;
232
+ }
233
+ export type GjcTeamMailboxDeliveryResult =
234
+ | { transport: "notifications_sdk"; state: GjcTeamNotificationDeliveryState; reason?: string }
235
+ | { transport: "pane"; state: GjcTeamPaneAttemptResult; reason?: string };
236
+ export interface GjcTeamMailboxDeliveryTransport {
237
+ deliverMailboxMessage(input: GjcTeamMailboxDeliveryInput): Promise<GjcTeamMailboxDeliveryResult | null>;
238
+ }
239
+
240
+ let gjcTeamMailboxDeliveryTransport: GjcTeamMailboxDeliveryTransport | undefined;
241
+
242
+ export function setGjcTeamMailboxDeliveryTransport(transport: GjcTeamMailboxDeliveryTransport | undefined): () => void {
243
+ const previous = gjcTeamMailboxDeliveryTransport;
244
+ gjcTeamMailboxDeliveryTransport = transport;
245
+ return () => {
246
+ gjcTeamMailboxDeliveryTransport = previous;
247
+ };
248
+ }
249
+ export function setGjcTeamMailboxDeliveryTransportForTest(
250
+ transport: GjcTeamMailboxDeliveryTransport | undefined,
251
+ ): () => void {
252
+ return setGjcTeamMailboxDeliveryTransport(transport);
253
+ }
223
254
 
224
255
  export interface GjcTeamNotificationSummary {
225
256
  total: number;
@@ -256,6 +287,7 @@ export interface GjcTeamStartOptions {
256
287
  cwd?: string;
257
288
  env?: NodeJS.ProcessEnv;
258
289
  dryRun?: boolean;
290
+ mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
259
291
  }
260
292
 
261
293
  export interface GjcTeamApiClaimResult {
@@ -1056,6 +1088,22 @@ async function readConfig(dir: string): Promise<GjcTeamConfig> {
1056
1088
  worker_cli_plan: config.worker_cli_plan ?? Array.from({ length: config.worker_count }, () => "gjc"),
1057
1089
  };
1058
1090
  }
1091
+ const WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS = 100;
1092
+ type WorkerIntegrationConfigCacheEntry = { checkedAt: number; mtimeMs: number; config: GjcTeamConfig };
1093
+ const workerIntegrationConfigCache = new Map<string, WorkerIntegrationConfigCacheEntry>();
1094
+
1095
+ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfig> {
1096
+ const configPath = path.join(dir, "config.json");
1097
+ const nowMs = Date.now();
1098
+ const stat = await fs.stat(configPath);
1099
+ const cached = workerIntegrationConfigCache.get(configPath);
1100
+ if (cached && cached.mtimeMs === stat.mtimeMs && nowMs - cached.checkedAt <= WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS)
1101
+ return cached.config;
1102
+ const config = await readConfig(dir);
1103
+ workerIntegrationConfigCache.set(configPath, { checkedAt: nowMs, mtimeMs: stat.mtimeMs, config });
1104
+
1105
+ return config;
1106
+ }
1059
1107
  async function readPhase(dir: string): Promise<GjcTeamPhase> {
1060
1108
  const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
1061
1109
  return phase?.current_phase ?? "running";
@@ -1704,6 +1752,23 @@ function runGitResult(cwd: string, args: string[]): GitResult {
1704
1752
  stderr: result.stderr.toString().trim(),
1705
1753
  };
1706
1754
  }
1755
+ async function runGitResultAsync(cwd: string, args: string[]): Promise<GitResult> {
1756
+ const result = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
1757
+ const [exitCode, stdout, stderr] = await Promise.all([
1758
+ result.exited,
1759
+ new Response(result.stdout).text(),
1760
+ new Response(result.stderr).text(),
1761
+ ]);
1762
+ return {
1763
+ ok: exitCode === 0,
1764
+ stdout: stdout.trim(),
1765
+ stderr: stderr.trim(),
1766
+ };
1767
+ }
1768
+ async function tryRunGitAsync(cwd: string, args: string[]): Promise<string | null> {
1769
+ const result = await runGitResultAsync(cwd, args);
1770
+ return result.ok ? result.stdout : null;
1771
+ }
1707
1772
  function runGit(cwd: string, args: string[]): string {
1708
1773
  const result = runGitResult(cwd, args);
1709
1774
  if (result.ok) return result.stdout;
@@ -2318,6 +2383,9 @@ async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTea
2318
2383
  function resolveHead(cwd: string): string | null {
2319
2384
  return tryRunGit(cwd, ["rev-parse", "HEAD"]);
2320
2385
  }
2386
+ async function resolveHeadAsync(cwd: string): Promise<string | null> {
2387
+ return tryRunGitAsync(cwd, ["rev-parse", "HEAD"]);
2388
+ }
2321
2389
  function isAncestor(cwd: string, ancestor: string, descendant: string): boolean {
2322
2390
  return runGitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok;
2323
2391
  }
@@ -2337,6 +2405,14 @@ function listConflictFiles(cwd: string): string[] {
2337
2405
  .map(line => line.trim())
2338
2406
  .filter(Boolean);
2339
2407
  }
2408
+ async function listConflictFilesAsync(cwd: string): Promise<string[]> {
2409
+ const result = await runGitResultAsync(cwd, ["diff", "--name-only", "--diff-filter=U"]);
2410
+ if (!result.ok || !result.stdout) return [];
2411
+ return result.stdout
2412
+ .split(/\r?\n/)
2413
+ .map(line => line.trim())
2414
+ .filter(Boolean);
2415
+ }
2340
2416
 
2341
2417
  export type GjcWorkerCheckpointClassification =
2342
2418
  | { kind: "clean"; files: string[] }
@@ -2346,17 +2422,14 @@ export type GjcWorkerCheckpointClassification =
2346
2422
  | { kind: "git_error"; files: string[]; detail: string };
2347
2423
 
2348
2424
  const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
2349
- const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [
2350
- ".gjc/_session-*/state/",
2351
- ".gjc/_session-*/logs/",
2352
- ".gjc/_session-*/reports/",
2353
- ".gjc/_session-*/runtime/",
2354
- ".gjc/_session-*/ultragoal/",
2355
- ".gjc/_session-*/plans/",
2356
- ".gjc/_session-*/specs/",
2357
- ".gjc/_session-*/rlm/",
2358
- ".gjc/_session-*/audit/",
2359
- ];
2425
+ // Every generated/runtime artifact for a GJC session lives under
2426
+ // `.gjc/_session-{id}/...` (see session-layout.ts), so worker auto-checkpoints
2427
+ // exclude the whole session subtree instead of enumerating its subdirectories.
2428
+ // The enumerated form drifted: subtrees outside the list (for example the
2429
+ // extragoal gate receipts from docs/extragoal-skill-template.md, or the
2430
+ // session-root `.session-activity.json` marker) were auto-committed and merged
2431
+ // into the leader branch on projects that do not gitignore `.gjc/_session-*/`.
2432
+ const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [".gjc/_session-*/"];
2360
2433
 
2361
2434
  function parsePorcelainStatusFiles(stdout: string): string[] {
2362
2435
  return stdout
@@ -2407,6 +2480,26 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
2407
2480
  return { kind: "protected_only", files: classified.protected };
2408
2481
  return { kind: "eligible", files: classified.eligible };
2409
2482
  }
2483
+ export async function classifyWorkerCheckpointStatusAsync(cwd: string): Promise<GjcWorkerCheckpointClassification> {
2484
+ const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"]);
2485
+ if (!status.ok) {
2486
+ return { kind: "git_error", files: [], detail: status.stderr || status.stdout || "git status failed" };
2487
+ }
2488
+ if (!status.stdout.trim()) return { kind: "clean", files: [] };
2489
+ const files = parsePorcelainStatusFiles(status.stdout);
2490
+ const hasUnmergedStatus = status.stdout
2491
+ .split(/\r?\n/)
2492
+ .filter(Boolean)
2493
+ .some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
2494
+ const conflictFiles = await listConflictFilesAsync(cwd);
2495
+ if (hasUnmergedStatus || conflictFiles.length > 0) {
2496
+ return { kind: "conflicted", files: conflictFiles.length > 0 ? conflictFiles : files };
2497
+ }
2498
+ const classified = classifyGjcTeamCheckpointFiles(files);
2499
+ if (classified.eligible.length === 0 && classified.protected.length > 0)
2500
+ return { kind: "protected_only", files: classified.protected };
2501
+ return { kind: "eligible", files: classified.eligible };
2502
+ }
2410
2503
  async function appendIntegrationEvent(
2411
2504
  dir: string,
2412
2505
  type: string,
@@ -2884,6 +2977,7 @@ async function initializeStateDirs(dir: string, workers: GjcTeamWorker[]): Promi
2884
2977
  export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTeamSnapshot> {
2885
2978
  const cwd = options.cwd ?? process.cwd();
2886
2979
  const env = options.env ?? process.env;
2980
+ if (options.mailboxDeliveryTransport) setGjcTeamMailboxDeliveryTransport(options.mailboxDeliveryTransport);
2887
2981
  if (!Number.isInteger(options.workerCount) || options.workerCount < 1 || options.workerCount > GJC_TEAM_MAX_WORKERS)
2888
2982
  throw new Error(`invalid_team_worker_count:${options.workerCount}:expected_1_${GJC_TEAM_MAX_WORKERS}`);
2889
2983
  const workerCliPlan = resolveGjcTeamWorkerCliPlan(options.workerCount, env);
@@ -3065,13 +3159,15 @@ export async function requestGjcWorkerIntegrationAttempt(
3065
3159
  const worker = env.GJC_TEAM_WORKER_ID?.trim() || env.GJC_TEAM_INTERNAL_WORKER?.split("/").pop()?.trim();
3066
3160
  if (!teamName || !worker) return { requested: false, reason: "not_worker" };
3067
3161
  const dir = await findTeamDir(teamName, cwd, env);
3068
- const config = await readConfig(dir);
3162
+ const config = await readConfigForWorkerIntegration(dir);
3069
3163
  const configuredWorker = config.workers.find(candidate => candidate.id === worker);
3070
3164
  const worktreePath = env.GJC_TEAM_WORKTREE_PATH?.trim() || configuredWorker?.worktree_path;
3071
3165
  if (!worktreePath || !(await pathExists(worktreePath)))
3072
3166
  return { requested: false, reason: "missing_worktree", worker, team_name: teamName };
3073
- const classification = classifyWorkerCheckpointStatus(worktreePath);
3074
- const head = resolveHead(worktreePath);
3167
+ const [classification, head] = await Promise.all([
3168
+ classifyWorkerCheckpointStatusAsync(worktreePath),
3169
+ resolveHeadAsync(worktreePath),
3170
+ ]);
3075
3171
  if (classification.kind === "git_error") {
3076
3172
  return { requested: false, reason: "git_error", worker, team_name: teamName, head, status: classification.kind };
3077
3173
  }
@@ -3839,12 +3935,59 @@ async function reconcileTeamNotifications(dir: string, config: GjcTeamConfig): P
3839
3935
  }
3840
3936
  return summarizeNotifications(await listNotificationRecords(dir));
3841
3937
  }
3938
+ async function messageForNotification(
3939
+ dir: string,
3940
+ notification: GjcTeamNotification,
3941
+ ): Promise<GjcTeamMailboxMessage | null> {
3942
+ if (notification.kind !== "mailbox_message" || notification.source.type !== "message") return null;
3943
+ const mailbox = await readMailbox(dir, notification.recipient);
3944
+ return mailbox.messages.find(message => message.message_id === notification.source.id) ?? null;
3945
+ }
3946
+
3947
+ async function attemptConfiguredMailboxTransport(
3948
+ dir: string,
3949
+ config: GjcTeamConfig,
3950
+ notification: GjcTeamNotification,
3951
+ cwd: string,
3952
+ env: NodeJS.ProcessEnv,
3953
+ ): Promise<GjcTeamNotification | null> {
3954
+ if (!gjcTeamMailboxDeliveryTransport) return null;
3955
+ const message = await messageForNotification(dir, notification);
3956
+ if (!message) return null;
3957
+ try {
3958
+ const result = await gjcTeamMailboxDeliveryTransport.deliverMailboxMessage({
3959
+ team_name: config.team_name,
3960
+ state_dir: dir,
3961
+ config,
3962
+ notification,
3963
+ message,
3964
+ cwd,
3965
+ env,
3966
+ });
3967
+ if (!result) return null;
3968
+ if (result.transport === "notifications_sdk" && result.state === "failed") return null;
3969
+ return writeNotificationRecord(dir, {
3970
+ ...notification,
3971
+ delivery_state: result.state,
3972
+ pane_attempt_result: result.transport === "pane" ? result.state : undefined,
3973
+ pane_attempt_reason: result.reason ?? result.transport,
3974
+ pane_attempt_at: now(),
3975
+ updated_at: now(),
3976
+ });
3977
+ } catch {
3978
+ return null;
3979
+ }
3980
+ }
3981
+
3842
3982
  async function attemptPaneNotification(
3843
3983
  dir: string,
3844
3984
  config: GjcTeamConfig,
3845
3985
  notification: GjcTeamNotification,
3846
3986
  env: NodeJS.ProcessEnv,
3987
+ cwd = process.cwd(),
3847
3988
  ): Promise<GjcTeamNotification> {
3989
+ const transported = await attemptConfiguredMailboxTransport(dir, config, notification, cwd, env);
3990
+ if (transported) return transported;
3848
3991
  const paneId =
3849
3992
  notification.recipient === "leader-fixed"
3850
3993
  ? config.leader.pane_id
@@ -3891,6 +4034,7 @@ export async function replayGjcTeamNotifications(
3891
4034
  replay_count: (notification.replay_count ?? 0) + 1,
3892
4035
  },
3893
4036
  env,
4037
+ cwd,
3894
4038
  );
3895
4039
  next.push(attempted);
3896
4040
  }
@@ -3919,8 +4063,13 @@ export async function sendGjcTeamMessage(
3919
4063
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
3920
4064
  };
3921
4065
  const written = await writeMailboxMessage(dir, toWorker, message);
4066
+ const existingNotification = await readJsonFile<GjcTeamNotification>(
4067
+ notificationPath(dir, messageNotificationId(config.team_name, toWorker, written.message_id)),
4068
+ );
3922
4069
  const notification = await createMessageNotification(dir, config.team_name, written);
3923
- await attemptPaneNotification(dir, config, notification, env);
4070
+ if (!existingNotification) {
4071
+ await attemptPaneNotification(dir, config, notification, env, cwd);
4072
+ }
3924
4073
  await appendEvent(dir, {
3925
4074
  type: "message_sent",
3926
4075
  worker: fromWorker,
@@ -1,17 +1,15 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
3
3
  import { resolveGjcSessionForRead, SessionResolutionError } from "./session-resolution";
4
+ import { validateDeferredMemberReceiptFresh, validateReceiptFreshBase } from "./ultragoal-receipt-freshness";
4
5
  import {
5
- computeUltragoalPlanGeneration,
6
6
  getUltragoalPaths,
7
7
  getUltragoalRunCompletionState,
8
- hashStructuredValue,
9
8
  readUltragoalLedger,
10
9
  readUltragoalPlan,
11
10
  recordUltragoalNudgeIfBudgetRemaining,
12
11
  resolveUltragoalNudgeBudget,
13
12
  selectUltragoalNudgeTarget,
14
- type UltragoalCompletionVerification,
15
13
  type UltragoalGoal,
16
14
  type UltragoalLedgerEvent,
17
15
  type UltragoalNudgeSurface,
@@ -176,26 +174,6 @@ function findReceiptGoal(
176
174
  return storyGoal ? { goal: storyGoal, receiptKind: "per-goal" } : null;
177
175
  }
178
176
 
179
- function findLedgerReceiptEvent(
180
- ledger: readonly UltragoalLedgerEvent[],
181
- receipt: UltragoalCompletionVerification,
182
- ): UltragoalLedgerEvent | null {
183
- return (
184
- ledger.find(event => {
185
- if (event.eventId !== receipt.checkpointLedgerEventId) return false;
186
- if (event.event !== "goal_checkpointed") return false;
187
- if (event.goalId !== receipt.goalId) return false;
188
- const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
189
- return (
190
- event.status === "complete" &&
191
- eventReceipt?.receiptId === receipt.receiptId &&
192
- eventReceipt.receiptKind === receipt.receiptKind &&
193
- eventReceipt.planGeneration === receipt.planGeneration
194
- );
195
- }) ?? null
196
- );
197
- }
198
-
199
177
  export function validateCompletionReceipt(input: {
200
178
  plan: UltragoalPlan;
201
179
  ledger: readonly UltragoalLedgerEvent[];
@@ -210,55 +188,66 @@ export function validateCompletionReceipt(input: {
210
188
  goalId: input.goal.id,
211
189
  };
212
190
  }
213
- if (
214
- receipt.schemaVersion !== 1 ||
215
- receipt.goalId !== input.goal.id ||
216
- receipt.receiptKind !== input.receiptKind ||
217
- !receipt.planGeneration ||
218
- !receipt.checkpointLedgerEventId
219
- ) {
220
- return {
221
- state: "active_stale_receipt",
222
- message: `Ultragoal ${input.goal.id} receipt is malformed or stale.`,
223
- goalId: input.goal.id,
224
- };
225
- }
226
- const event = findLedgerReceiptEvent(input.ledger, receipt);
227
- if (!event) {
228
- return {
229
- state: "active_stale_receipt",
230
- message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
231
- goalId: input.goal.id,
232
- };
191
+ if (receipt.validationBatch?.role === "deferred-member") {
192
+ return validateDeferredMemberReceiptFresh({
193
+ plan: input.plan,
194
+ ledger: input.ledger,
195
+ goal: input.goal,
196
+ receipt,
197
+ receiptKind: input.receiptKind,
198
+ requireClose: true,
199
+ });
233
200
  }
234
- const generation = computeUltragoalPlanGeneration({
201
+ const baseDiagnostic = validateReceiptFreshBase({
235
202
  plan: input.plan,
236
203
  ledger: input.ledger,
237
204
  goal: input.goal,
205
+ receipt,
238
206
  receiptKind: input.receiptKind,
239
- beforeStatus: receipt.goalStatusBeforeCheckpoint,
240
- excludeEventId: receipt.checkpointLedgerEventId,
241
207
  });
242
- if (generation.planGeneration !== receipt.planGeneration) {
243
- return {
244
- state: "active_stale_receipt",
245
- message: `Ultragoal ${input.goal.id} receipt generation is stale.`,
246
- goalId: input.goal.id,
247
- };
248
- }
249
- if (hashStructuredValue(event.qualityGateJson) !== receipt.qualityGateHash) {
250
- return {
251
- state: "active_dirty_quality_gate",
252
- message: `Ultragoal ${input.goal.id} receipt quality-gate hash does not match ledger.`,
253
- goalId: input.goal.id,
254
- };
255
- }
256
- if (input.goal.updatedAt !== receipt.verifiedAt) {
257
- return {
258
- state: "active_stale_receipt",
259
- message: `Ultragoal ${input.goal.id} receipt target changed after verification.`,
260
- goalId: input.goal.id,
261
- };
208
+ if (baseDiagnostic) return baseDiagnostic;
209
+ if (receipt.validationBatch?.role === "batch-close") {
210
+ for (const memberId of receipt.validationBatch.memberIds) {
211
+ const member = input.plan.goals.find(goal => goal.id === memberId);
212
+ if (
213
+ !member?.validationBatch ||
214
+ member.validationBatch.metadataHash !== receipt.validationBatch.memberMetadataHashes[memberId]
215
+ ) {
216
+ return {
217
+ state: "active_stale_receipt",
218
+ message: `Ultragoal ${input.goal.id} batch-close receipt has stale member metadata for ${memberId}.`,
219
+ goalId: input.goal.id,
220
+ };
221
+ }
222
+ if (memberId === receipt.validationBatch.finalGoalId) continue;
223
+ const memberReceipt = member.completionVerification;
224
+ if (memberReceipt?.validationBatch?.role !== "deferred-member") {
225
+ return {
226
+ state: "active_missing_final_receipt",
227
+ message: `Ultragoal ${input.goal.id} batch-close receipt requires deferred member receipt for ${memberId}.`,
228
+ goalId: input.goal.id,
229
+ };
230
+ }
231
+ const memberDiagnostic = validateDeferredMemberReceiptFresh({
232
+ plan: input.plan,
233
+ ledger: input.ledger,
234
+ goal: member,
235
+ receipt: memberReceipt,
236
+ receiptKind: "per-goal",
237
+ requireClose: false,
238
+ });
239
+ if (memberDiagnostic.state !== "active_verified_complete") return memberDiagnostic;
240
+ if (
241
+ receipt.validationBatch.memberReceiptIds[memberId] !== memberReceipt.receiptId ||
242
+ receipt.validationBatch.memberChangeSetHashes[memberId] !== memberReceipt.validationBatch.changeSetHash
243
+ ) {
244
+ return {
245
+ state: "active_stale_receipt",
246
+ message: `Ultragoal ${input.goal.id} batch-close receipt is stale for deferred member ${memberId}.`,
247
+ goalId: input.goal.id,
248
+ };
249
+ }
250
+ }
262
251
  }
263
252
  if (input.receiptKind === "final-aggregate") {
264
253
  const incomplete = requiredGoals(input.plan).filter(goal => goal.status !== "complete");