@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.
- package/CHANGELOG.md +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
categorizeComputerChangePath,
|
|
3
|
-
|
|
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 [
|
|
19
|
-
new Response(proc.stdout).
|
|
20
|
-
new Response(proc.stderr).
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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:
|
|
81
|
-
oldPath: oldPath ?
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
.
|
|
104
|
-
.filter(Boolean)
|
|
127
|
+
.filter(row => row.length > 0)
|
|
105
128
|
.map(pathValue => ({
|
|
106
|
-
path:
|
|
129
|
+
path: normalizeChangeSetPath(pathValue),
|
|
107
130
|
status: "unknown" as UltragoalChangeStatus,
|
|
108
|
-
category:
|
|
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)
|
|
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
|
|
138
|
-
|
|
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:
|
|
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 =
|
|
163
|
-
const newPath =
|
|
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
|
+
}
|