@gajae-code/coding-agent 0.12.4 → 0.12.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 (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -0
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  categorizeComputerChangePath,
3
- isSettingsSchemaPath,
4
- normalizeRepoPath,
5
- type UltragoalChangeCategory,
3
+ normalizeChangeSetPath,
6
4
  type UltragoalChangeSet,
7
5
  type UltragoalChangeSetPath,
8
6
  type UltragoalChangeStatus,
@@ -15,12 +13,19 @@ export async function spawnText(
15
13
  try {
16
14
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
17
15
  const timeout = setTimeout(() => proc.kill(), options.timeoutMs ?? 5000);
18
- const [stdout, stderr, exitCode] = await Promise.all([
19
- new Response(proc.stdout).text(),
20
- new Response(proc.stderr).text(),
16
+ const [stdoutBytes, stderrBytes, exitCode] = await Promise.all([
17
+ new Response(proc.stdout).arrayBuffer(),
18
+ new Response(proc.stderr).arrayBuffer(),
21
19
  proc.exited,
22
20
  ]);
23
21
  clearTimeout(timeout);
22
+ let stdout: string;
23
+ try {
24
+ stdout = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(stdoutBytes);
25
+ } catch {
26
+ return { ok: false, stdout: "", stderr: "command stdout was not valid UTF-8" };
27
+ }
28
+ const stderr = new TextDecoder().decode(stderrBytes);
24
29
  return { ok: exitCode === 0, stdout, stderr };
25
30
  } catch (error) {
26
31
  return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
@@ -55,61 +60,79 @@ export async function resolveGitBase(cwd: string, branch?: string): Promise<stri
55
60
  }
56
61
  if (best) return best.ref;
57
62
  }
58
- const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
59
- if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
60
- return "HEAD~1";
63
+ throw new Error("unable to resolve an authoritative integration base");
61
64
  }
62
65
 
63
66
  export function parseGitNameStatus(output: string): UltragoalChangeSetPath[] {
64
67
  const rows: UltragoalChangeSetPath[] = [];
65
- for (const line of output.split("\n")) {
66
- const trimmed = line.trim();
67
- if (!trimmed) continue;
68
- const parts = trimmed.split(/\s+/);
69
- const statusCode = parts[0] ?? "";
68
+ const append = (statusCode: string, pathValue: string | undefined, oldPath: string | undefined): void => {
69
+ if (!pathValue) return;
70
70
  let status: UltragoalChangeStatus = "unknown";
71
71
  if (statusCode.startsWith("A")) status = "added";
72
72
  else if (statusCode.startsWith("M")) status = "modified";
73
73
  else if (statusCode.startsWith("D")) status = "deleted";
74
74
  else if (statusCode.startsWith("R")) status = "renamed";
75
75
  else if (statusCode.startsWith("C")) status = "copied";
76
- const pathValue = status === "renamed" || status === "copied" ? parts[2] : parts[1];
77
- if (!pathValue) continue;
78
- const oldPath = status === "renamed" || status === "copied" ? parts[1] : undefined;
79
76
  rows.push({
80
- path: normalizeRepoPath(pathValue),
81
- oldPath: oldPath ? normalizeRepoPath(oldPath) : undefined,
77
+ path: normalizeChangeSetPath(pathValue),
78
+ oldPath: oldPath ? normalizeChangeSetPath(oldPath) : undefined,
82
79
  status,
83
80
  category: categorizeComputerChangePath(pathValue),
84
81
  });
82
+ };
83
+ if (output.includes("\0")) {
84
+ const tokens = output.split("\0");
85
+ let index = 0;
86
+ while (index < tokens.length) {
87
+ const statusCode = tokens[index++] ?? "";
88
+ if (!statusCode) continue;
89
+ if (statusCode.startsWith("R") || statusCode.startsWith("C")) {
90
+ const oldPath = tokens[index++];
91
+ append(statusCode, tokens[index++], oldPath);
92
+ } else {
93
+ append(statusCode, tokens[index++], undefined);
94
+ }
95
+ }
96
+ return rows;
97
+ }
98
+ for (const line of output.split("\n")) {
99
+ if (!line.trim()) continue;
100
+ const [rawStatus = "", firstPath, secondPath] = line.split("\t");
101
+ const statusCode = rawStatus.trim();
102
+ append(
103
+ statusCode,
104
+ statusCode.startsWith("R") || statusCode.startsWith("C") ? secondPath : firstPath,
105
+ statusCode.startsWith("R") || statusCode.startsWith("C") ? firstPath : undefined,
106
+ );
85
107
  }
86
108
  return rows;
87
109
  }
88
110
 
89
- function categorizeCiChangedPath(value: string): UltragoalChangeCategory {
90
- // CI_DEV_CHANGED_PATHS intentionally carries path names only. Mixed registries
91
- // such as settings-schema.ts require diff-level narrowing; without the diff,
92
- // treating the whole registry as computer-control source forces the mandatory
93
- // computer red-team suite on unrelated settings changes.
94
- if (isSettingsSchemaPath(value)) return "other";
95
- return categorizeComputerChangePath(value);
111
+ export function parseGitUntrackedPaths(output: string): UltragoalChangeSetPath[] {
112
+ const paths = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/);
113
+ return paths
114
+ .filter(pathValue => pathValue.length > 0)
115
+ .map(pathValue => ({
116
+ path: normalizeChangeSetPath(pathValue),
117
+ status: "added" as UltragoalChangeStatus,
118
+ category: categorizeComputerChangePath(pathValue),
119
+ }));
96
120
  }
97
121
 
98
- function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
122
+ export function ciDevChangedPathRows(): UltragoalChangeSetPath[] {
99
123
  const raw = process.env.CI_DEV_CHANGED_PATHS;
100
124
  if (!raw) return [];
101
125
  return raw
102
126
  .split(/\r?\n/)
103
- .map(row => row.trim())
104
- .filter(Boolean)
127
+ .filter(row => row.length > 0)
105
128
  .map(pathValue => ({
106
- path: normalizeRepoPath(pathValue),
129
+ path: normalizeChangeSetPath(pathValue),
107
130
  status: "unknown" as UltragoalChangeStatus,
108
- category: categorizeCiChangedPath(pathValue),
131
+ category: categorizeComputerChangePath(pathValue),
109
132
  }));
110
133
  }
111
134
 
112
- function mergeChangeSetPaths(groups: UltragoalChangeSetPath[][]): UltragoalChangeSetPath[] {
135
+ export function mergeChangeSetPaths(groups: UltragoalChangeSetPath[][]): UltragoalChangeSetPath[] {
113
136
  const byKey = new Map<string, UltragoalChangeSetPath>();
114
137
  for (const row of groups.flat()) byKey.set(`${row.oldPath ?? ""}\u0000${row.path}`, row);
115
138
  return [...byKey.values()];
@@ -119,36 +142,59 @@ export async function computeCheckpointChangeSet(cwd: string): Promise<Ultragoal
119
142
  const ciChangedPaths = ciDevChangedPathRows();
120
143
  const inGit = await spawnText(["git", "rev-parse", "--is-inside-work-tree"], { cwd, timeoutMs: 3000 });
121
144
  if (!inGit.ok || inGit.stdout.trim() !== "true") {
122
- if (ciChangedPaths.length === 0) return undefined;
145
+ if (ciChangedPaths.length === 0)
146
+ return { source: "checkpoint-git", paths: [], captureIncomplete: true, trusted: true };
123
147
  return { source: "checkpoint-git", paths: ciChangedPaths, trusted: true };
124
148
  }
125
149
  const baseRef = await resolveGitBase(cwd);
126
150
  const base = baseRef;
127
151
  const mergeBase = await spawnText(["git", "merge-base", "HEAD", baseRef], { cwd, timeoutMs: 3000 });
128
- const [committed, unstaged, staged, stat, committedDiff, unstagedDiff, stagedDiff] = await Promise.all([
129
- spawnText(["git", "diff", "--name-status", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
130
- spawnText(["git", "diff", "--name-status"], { cwd, timeoutMs: 5000 }),
131
- spawnText(["git", "diff", "--cached", "--name-status"], { cwd, timeoutMs: 5000 }),
152
+ const [committed, unstaged, staged, untracked, stat, committedDiff, unstagedDiff, stagedDiff] = await Promise.all([
153
+ spawnText(["git", "diff", "--name-status", "-z", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
154
+ spawnText(["git", "diff", "--name-status", "-z"], { cwd, timeoutMs: 5000 }),
155
+ spawnText(["git", "diff", "--cached", "--name-status", "-z"], { cwd, timeoutMs: 5000 }),
156
+ spawnText(["git", "ls-files", "--others", "--exclude-standard", "-z"], { cwd, timeoutMs: 5000 }),
132
157
  spawnText(["git", "diff", "--stat", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
133
158
  spawnText(["git", "diff", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
134
159
  spawnText(["git", "diff"], { cwd, timeoutMs: 5000 }),
135
160
  spawnText(["git", "diff", "--cached"], { cwd, timeoutMs: 5000 }),
136
161
  ]);
137
- if (!committed.ok && !unstaged.ok && !staged.ok && ciChangedPaths.length === 0) return undefined;
138
- const gitPaths = mergeChangeSetPaths([
162
+ if (!committed.ok || !unstaged.ok || !staged.ok || !untracked.ok) {
163
+ const paths = mergeChangeSetPaths([
164
+ committed.ok ? parseGitNameStatus(committed.stdout) : [],
165
+ unstaged.ok ? parseGitNameStatus(unstaged.stdout) : [],
166
+ staged.ok ? parseGitNameStatus(staged.stdout) : [],
167
+ untracked.ok ? parseGitUntrackedPaths(untracked.stdout) : [],
168
+ ciChangedPaths,
169
+ ]);
170
+ return {
171
+ source: "checkpoint-git",
172
+ baseRef,
173
+ mergeBase: mergeBase.ok && mergeBase.stdout.trim() ? mergeBase.stdout.trim() : undefined,
174
+ headRef: "HEAD",
175
+ paths,
176
+ captureIncomplete: true,
177
+ trusted: true,
178
+ };
179
+ }
180
+ const paths = mergeChangeSetPaths([
139
181
  parseGitNameStatus(committed.stdout),
140
182
  parseGitNameStatus(unstaged.stdout),
141
183
  parseGitNameStatus(staged.stdout),
184
+ parseGitUntrackedPaths(untracked.stdout),
185
+ ciChangedPaths,
142
186
  ]);
143
- const paths = gitPaths.length > 0 ? gitPaths : ciChangedPaths;
144
187
  return {
145
188
  source: "checkpoint-git",
146
189
  baseRef,
147
190
  mergeBase: mergeBase.ok && mergeBase.stdout.trim() ? mergeBase.stdout.trim() : undefined,
148
191
  headRef: "HEAD",
149
192
  paths,
150
- rawDiffStat: stat.stdout,
151
- rawDiff: [committedDiff.stdout, unstagedDiff.stdout, stagedDiff.stdout].filter(Boolean).join("\n"),
193
+ rawDiffStat: stat.ok ? stat.stdout : undefined,
194
+ rawDiff:
195
+ committedDiff.ok && unstagedDiff.ok && stagedDiff.ok
196
+ ? [committedDiff.stdout, unstagedDiff.stdout, stagedDiff.stdout].filter(Boolean).join("\n")
197
+ : undefined,
152
198
  trusted: true,
153
199
  };
154
200
  }
@@ -159,8 +205,8 @@ export function parseUnifiedDiffPaths(diff: string): UltragoalChangeSetPath[] {
159
205
  if (!line.startsWith("diff --git ")) continue;
160
206
  const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
161
207
  if (!match) continue;
162
- const oldPath = normalizeRepoPath(match[1]!);
163
- const newPath = normalizeRepoPath(match[2]!);
208
+ const oldPath = normalizeChangeSetPath(match[1]!);
209
+ const newPath = normalizeChangeSetPath(match[2]!);
164
210
  paths.push({
165
211
  path: newPath,
166
212
  oldPath: oldPath === newPath ? undefined : oldPath,
@@ -338,6 +338,13 @@ export function validateReceiptFreshBase(input: {
338
338
  message: `Ultragoal ${input.goal.id} receipt ledger event is missing.`,
339
339
  goalId: input.goal.id,
340
340
  };
341
+ const eventReceipt = event.completionVerification as UltragoalCompletionVerification | undefined;
342
+ if (!eventReceipt || hashStructuredValue(eventReceipt) !== hashStructuredValue(input.receipt))
343
+ return {
344
+ state: "active_stale_receipt",
345
+ message: `Ultragoal ${input.goal.id} receipt does not match its ledger event receipt.`,
346
+ goalId: input.goal.id,
347
+ };
341
348
  const generation = computeUltragoalPlanGeneration({
342
349
  plan: input.plan,
343
350
  ledger: input.ledger,
@@ -0,0 +1,378 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import type { RepositoryBinding } from "./repository-binding";
5
+ import { publicRepositoryBinding } from "./repository-binding";
6
+
7
+ async function spawnText(
8
+ command: string[],
9
+ options: { cwd: string; timeoutMs?: number },
10
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
11
+ try {
12
+ const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
13
+ const timeout = setTimeout(() => proc.kill(), options.timeoutMs ?? 5000);
14
+ const [stdout, stderr, exitCode] = await Promise.all([
15
+ new Response(proc.stdout).text(),
16
+ new Response(proc.stderr).text(),
17
+ proc.exited,
18
+ ]);
19
+ clearTimeout(timeout);
20
+ return { ok: exitCode === 0, stdout, stderr };
21
+ } catch (error) {
22
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
23
+ }
24
+ }
25
+
26
+ async function resolveGitBase(cwd: string): Promise<string> {
27
+ for (const candidate of ["origin/dev", "dev", "origin/main", "main", "master"]) {
28
+ const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
29
+ if (exists.ok) return candidate;
30
+ }
31
+ throw new Error("unable to resolve an authoritative integration base");
32
+ }
33
+ export const REVIEW_SOURCE_COHORT_SCHEMA = "gjc.review_source_cohort.v1" as const;
34
+
35
+ export type ReviewSourceWorkflow = "ultragoal" | "ralplan";
36
+ export type ReviewSourceLane = "cleaner" | "architect" | "qa" | "critic";
37
+ export type ReviewDeliveryDisposition = "current" | "stale_review_delivery" | "invalid_provenance";
38
+
39
+ export interface ReviewSourceDispatch {
40
+ schema: "gjc.review_source_dispatch.v1";
41
+ cohortId: string;
42
+ generation: number;
43
+
44
+ dispatchId: string;
45
+ taskId: string;
46
+ lane: ReviewSourceLane;
47
+ snapshotId: string;
48
+ repositoryBindingDigest: string;
49
+ stateRevision: number;
50
+ rerunCommand: string;
51
+ createdAt: string;
52
+ }
53
+
54
+ export interface ReviewSourceDelivery {
55
+ deliveryId: string;
56
+ taskId: string;
57
+ lane: ReviewSourceLane;
58
+ snapshotId: string;
59
+ disposition: ReviewDeliveryDisposition;
60
+ receivedAt: string;
61
+ rerunCommand?: string;
62
+ cohortId: string;
63
+ dispatchId: string;
64
+ }
65
+
66
+ export interface ReviewSourceCohort {
67
+ schema: typeof REVIEW_SOURCE_COHORT_SCHEMA;
68
+ cohortId: string;
69
+ workflow: ReviewSourceWorkflow;
70
+ generation: number;
71
+ snapshotId: string;
72
+ repositoryBindingDigest: string;
73
+ stateRevision: number;
74
+ createdAt: string;
75
+ status: "active" | "superseded";
76
+ supersededBy?: string;
77
+ dispatches: ReviewSourceDispatch[];
78
+
79
+ deliveries: ReviewSourceDelivery[];
80
+ }
81
+
82
+ interface CapturedReviewSource {
83
+ snapshotId: string;
84
+ repositoryBindingDigest: string;
85
+ }
86
+
87
+ function hashBytes(value: string | Uint8Array): string {
88
+ return createHash("sha256").update(value).digest("hex");
89
+ }
90
+
91
+ function stableJson(value: unknown): string {
92
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
93
+ if (value && typeof value === "object") {
94
+ return `{${Object.entries(value as Record<string, unknown>)
95
+ .sort(([left], [right]) => left.localeCompare(right))
96
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
97
+ .join(",")}}`;
98
+ }
99
+ return JSON.stringify(value);
100
+ }
101
+
102
+ async function untrackedCommitments(
103
+ cwd: string,
104
+ ): Promise<Array<{ path: string; mode: number; kind: string; digest: string }>> {
105
+ const listed = await spawnText(["git", "ls-files", "--others", "--exclude-standard", "-z"], {
106
+ cwd,
107
+ timeoutMs: 5000,
108
+ });
109
+ if (!listed.ok) throw new Error(`review source capture failed to list untracked files: ${listed.stderr}`);
110
+ const rows: Array<{ path: string; mode: number; kind: string; digest: string }> = [];
111
+ for (const relativePath of listed.stdout.split("\0").filter(Boolean).sort()) {
112
+ const absolutePath = path.resolve(cwd, relativePath);
113
+ const stat = await fs.lstat(absolutePath);
114
+ if (stat.isSymbolicLink()) {
115
+ rows.push({
116
+ path: relativePath,
117
+ mode: stat.mode,
118
+ kind: "symlink",
119
+ digest: hashBytes(await fs.readlink(absolutePath)),
120
+ });
121
+ continue;
122
+ }
123
+ if (!stat.isFile())
124
+ throw new Error(`review source capture does not support untracked non-file path: ${relativePath}`);
125
+ rows.push({
126
+ path: relativePath,
127
+ mode: stat.mode,
128
+ kind: "file",
129
+ digest: hashBytes(await fs.readFile(absolutePath)),
130
+ });
131
+ }
132
+ return rows;
133
+ }
134
+
135
+ async function captureOnce(cwd: string, repositoryBinding: RepositoryBinding): Promise<CapturedReviewSource> {
136
+ const baseRef = await resolveGitBase(cwd);
137
+ const mergeBase = await spawnText(["git", "merge-base", "HEAD", baseRef], { cwd, timeoutMs: 5000 });
138
+ const head = await spawnText(["git", "rev-parse", "HEAD"], { cwd, timeoutMs: 5000 });
139
+ const indexTree = await spawnText(["git", "write-tree"], { cwd, timeoutMs: 5000 });
140
+ if (!mergeBase.ok || !head.ok || !indexTree.ok)
141
+ throw new Error("review source capture could not resolve git authority");
142
+ const diff = await spawnText(["git", "diff", "--binary", "--no-ext-diff", mergeBase.stdout.trim()], {
143
+ cwd,
144
+ timeoutMs: 15000,
145
+ });
146
+ if (!diff.ok) throw new Error(`review source capture failed to read canonical diff: ${diff.stderr}`);
147
+ const binding = publicRepositoryBinding(repositoryBinding);
148
+ const repositoryIdentity = {
149
+ schema: binding.schema,
150
+ worktreeRoot: binding.worktreeRoot,
151
+ commonDir: binding.commonDir,
152
+ relativeSubdir: binding.relativeSubdir,
153
+ };
154
+ const repositoryBindingDigest = `sha256:${hashBytes(stableJson(repositoryIdentity))}`;
155
+ const payload = {
156
+ schema: "gjc.review_source_snapshot.v1",
157
+ repositoryBindingDigest,
158
+ mergeBase: mergeBase.stdout.trim(),
159
+ head: head.stdout.trim(),
160
+ indexTree: indexTree.stdout.trim(),
161
+ binaryDiffSha256: hashBytes(diff.stdout),
162
+ untracked: await untrackedCommitments(cwd),
163
+ };
164
+ return { snapshotId: `sha256:${hashBytes(stableJson(payload))}`, repositoryBindingDigest };
165
+ }
166
+
167
+ export async function captureReviewSourceSnapshot(
168
+ cwd: string,
169
+ repositoryBinding: RepositoryBinding,
170
+ ): Promise<CapturedReviewSource> {
171
+ const first = await captureOnce(cwd, repositoryBinding);
172
+ const second = await captureOnce(cwd, repositoryBinding);
173
+ if (first.snapshotId !== second.snapshotId) {
174
+ throw new Error("review_source_capture_raced: repository source changed during canonical capture; rerun freeze");
175
+ }
176
+ return first;
177
+ }
178
+
179
+ export function createReviewSourceCohort(input: {
180
+ workflow: ReviewSourceWorkflow;
181
+ generation: number;
182
+ snapshotId: string;
183
+ repositoryBindingDigest: string;
184
+ stateRevision: number;
185
+ now?: string;
186
+ }): ReviewSourceCohort {
187
+ return {
188
+ schema: REVIEW_SOURCE_COHORT_SCHEMA,
189
+ cohortId: randomUUID(),
190
+ workflow: input.workflow,
191
+ generation: input.generation,
192
+ snapshotId: input.snapshotId,
193
+ repositoryBindingDigest: input.repositoryBindingDigest,
194
+ stateRevision: input.stateRevision,
195
+ createdAt: input.now ?? new Date().toISOString(),
196
+ status: "active",
197
+ dispatches: [],
198
+
199
+ deliveries: [],
200
+ };
201
+ }
202
+
203
+ export function createReviewSourceDispatch(input: {
204
+ cohort: ReviewSourceCohort;
205
+ taskId: string;
206
+ lane: ReviewSourceLane;
207
+ rerunCommand: string;
208
+ now?: string;
209
+ }): ReviewSourceDispatch {
210
+ if (input.cohort.status !== "active") throw new Error("cannot dispatch a superseded review source cohort");
211
+ if (input.cohort.dispatches.some(dispatch => dispatch.lane === input.lane)) {
212
+ throw new Error(`review source cohort already dispatched lane ${input.lane}`);
213
+ }
214
+ return {
215
+ schema: "gjc.review_source_dispatch.v1",
216
+ cohortId: input.cohort.cohortId,
217
+ generation: input.cohort.generation,
218
+ dispatchId: randomUUID(),
219
+ taskId: input.taskId,
220
+ lane: input.lane,
221
+ snapshotId: input.cohort.snapshotId,
222
+ repositoryBindingDigest: input.cohort.repositoryBindingDigest,
223
+ stateRevision: input.cohort.stateRevision,
224
+ rerunCommand: input.rerunCommand,
225
+ createdAt: input.now ?? new Date().toISOString(),
226
+ };
227
+ }
228
+
229
+ function isReviewSourceLane(value: unknown): value is ReviewSourceLane {
230
+ return value === "cleaner" || value === "architect" || value === "qa" || value === "critic";
231
+ }
232
+
233
+ function normalizeDispatch(value: unknown, cohort: Record<string, unknown>, index: number): ReviewSourceDispatch {
234
+ if (!value || typeof value !== "object" || Array.isArray(value))
235
+ throw new Error(`Invalid ultragoal plan: malformed dispatch ${index}`);
236
+ const record = value as Record<string, unknown>;
237
+ if (
238
+ record.schema !== "gjc.review_source_dispatch.v1" ||
239
+ typeof record.dispatchId !== "string" ||
240
+ typeof record.cohortId !== "string" ||
241
+ record.cohortId !== cohort.cohortId ||
242
+ record.generation !== cohort.generation ||
243
+ typeof record.taskId !== "string" ||
244
+ !isReviewSourceLane(record.lane) ||
245
+ record.snapshotId !== cohort.snapshotId ||
246
+ record.repositoryBindingDigest !== cohort.repositoryBindingDigest ||
247
+ record.stateRevision !== cohort.stateRevision ||
248
+ typeof record.rerunCommand !== "string" ||
249
+ typeof record.createdAt !== "string"
250
+ )
251
+ throw new Error(`Invalid ultragoal plan: malformed dispatch ${index}`);
252
+ return record as unknown as ReviewSourceDispatch;
253
+ }
254
+
255
+ function normalizeDelivery(
256
+ value: unknown,
257
+ dispatches: readonly ReviewSourceDispatch[],
258
+ index: number,
259
+ ): ReviewSourceDelivery {
260
+ if (!value || typeof value !== "object" || Array.isArray(value))
261
+ throw new Error(`Invalid ultragoal plan: malformed delivery ${index}`);
262
+ const record = value as Record<string, unknown>;
263
+ const dispatch = dispatches.find(item => item.dispatchId === record.dispatchId && item.cohortId === record.cohortId);
264
+
265
+ if (
266
+ typeof record.deliveryId !== "string" ||
267
+ !dispatch ||
268
+ !isReviewSourceLane(record.lane) ||
269
+ record.taskId !== dispatch.taskId ||
270
+ record.lane !== dispatch.lane ||
271
+ record.snapshotId !== dispatch.snapshotId ||
272
+ (record.disposition !== "current" &&
273
+ record.disposition !== "stale_review_delivery" &&
274
+ record.disposition !== "invalid_provenance") ||
275
+ typeof record.receivedAt !== "string" ||
276
+ (record.rerunCommand !== undefined && record.rerunCommand !== dispatch.rerunCommand)
277
+ )
278
+ throw new Error(`Invalid ultragoal plan: malformed delivery ${index}`);
279
+ return record as unknown as ReviewSourceDelivery;
280
+ }
281
+
282
+ export function normalizeReviewSourceCohorts(value: unknown): ReviewSourceCohort[] {
283
+ if (value === undefined) return [];
284
+ if (!Array.isArray(value)) throw new Error("Invalid ultragoal plan: reviewCohorts must be an array");
285
+ const cohorts = value.map((item, index) => {
286
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
287
+ throw new Error(`Invalid ultragoal plan: reviewCohorts[${index}] must be an object`);
288
+ }
289
+ const record = item as Record<string, unknown>;
290
+ if (
291
+ record.schema !== REVIEW_SOURCE_COHORT_SCHEMA ||
292
+ typeof record.cohortId !== "string" ||
293
+ (record.workflow !== "ultragoal" && record.workflow !== "ralplan") ||
294
+ typeof record.generation !== "number" ||
295
+ !Number.isInteger(record.generation) ||
296
+ record.generation < 1 ||
297
+ typeof record.snapshotId !== "string" ||
298
+ !record.snapshotId.startsWith("sha256:") ||
299
+ typeof record.repositoryBindingDigest !== "string" ||
300
+ !record.repositoryBindingDigest.startsWith("sha256:") ||
301
+ typeof record.stateRevision !== "number" ||
302
+ !Number.isInteger(record.stateRevision) ||
303
+ typeof record.createdAt !== "string" ||
304
+ (record.status !== "active" && record.status !== "superseded") ||
305
+ !Array.isArray(record.dispatches) ||
306
+ !Array.isArray(record.deliveries)
307
+ ) {
308
+ throw new Error(`Invalid ultragoal plan: malformed reviewCohorts[${index}]`);
309
+ }
310
+ const dispatches = record.dispatches.map((dispatch, dispatchIndex) =>
311
+ normalizeDispatch(dispatch, record, dispatchIndex),
312
+ );
313
+ const deliveries = record.deliveries.map((delivery, deliveryIndex) =>
314
+ normalizeDelivery(delivery, dispatches, deliveryIndex),
315
+ );
316
+ return { ...(record as unknown as ReviewSourceCohort), dispatches, deliveries };
317
+ });
318
+ const cohortIds = new Set<string>();
319
+ const dispatchIds = new Set<string>();
320
+ const deliveryIds = new Set<string>();
321
+ let activeCount = 0;
322
+ for (const cohort of cohorts) {
323
+ if (cohortIds.has(cohort.cohortId))
324
+ throw new Error(`Invalid ultragoal plan: duplicate cohortId ${cohort.cohortId}`);
325
+ cohortIds.add(cohort.cohortId);
326
+ if (cohort.status === "active") activeCount++;
327
+ const laneSet = new Set<ReviewSourceLane>();
328
+ const deliveredDispatches = new Set<string>();
329
+
330
+ for (const dispatch of cohort.dispatches) {
331
+ if (dispatchIds.has(dispatch.dispatchId))
332
+ throw new Error(`Invalid ultragoal plan: duplicate dispatchId ${dispatch.dispatchId}`);
333
+ dispatchIds.add(dispatch.dispatchId);
334
+ if (laneSet.has(dispatch.lane))
335
+ throw new Error(`Invalid ultragoal plan: duplicate lane dispatch ${dispatch.lane}`);
336
+ laneSet.add(dispatch.lane);
337
+ }
338
+ for (const delivery of cohort.deliveries) {
339
+ if (deliveryIds.has(delivery.deliveryId))
340
+ throw new Error(`Invalid ultragoal plan: duplicate deliveryId ${delivery.deliveryId}`);
341
+ deliveryIds.add(delivery.deliveryId);
342
+ if (deliveredDispatches.has(delivery.dispatchId)) {
343
+ throw new Error(`Invalid ultragoal plan: duplicate delivery for dispatch ${delivery.dispatchId}`);
344
+ }
345
+ deliveredDispatches.add(delivery.dispatchId);
346
+ }
347
+ }
348
+ if (activeCount > 1) throw new Error("Invalid ultragoal plan: multiple active review cohorts");
349
+ return cohorts;
350
+ }
351
+
352
+ export async function reconcileReviewSourceDelivery(input: {
353
+ cwd: string;
354
+ repositoryBinding: RepositoryBinding;
355
+ reviewSource: {
356
+ schema: "gjc.review_source_dispatch.v1";
357
+ cohortId: string;
358
+ generation: number;
359
+ lane: ReviewSourceLane;
360
+ snapshotId: string;
361
+ repositoryBindingDigest: string;
362
+ stateRevision: number;
363
+ dispatchId: string;
364
+ rerunCommand: string;
365
+ };
366
+ }): Promise<{
367
+ disposition: ReviewDeliveryDisposition;
368
+ currentSnapshotId?: string;
369
+ }> {
370
+ const current = await captureReviewSourceSnapshot(input.cwd, input.repositoryBinding);
371
+ if (current.repositoryBindingDigest !== input.reviewSource.repositoryBindingDigest) {
372
+ return { disposition: "invalid_provenance", currentSnapshotId: current.snapshotId };
373
+ }
374
+ if (current.snapshotId !== input.reviewSource.snapshotId) {
375
+ return { disposition: "stale_review_delivery", currentSnapshotId: current.snapshotId };
376
+ }
377
+ return { disposition: "current", currentSnapshotId: current.snapshotId };
378
+ }