@h-rig/task-sources-plugin 0.0.6-alpha.156 → 0.0.6-alpha.158
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/dist/src/control-plane/native/github-token-env.d.ts +3 -0
- package/dist/src/control-plane/native/github-token-env.js +26 -0
- package/dist/src/control-plane/native/native-git.d.ts +18 -0
- package/dist/src/control-plane/native/native-git.js +291 -0
- package/dist/src/control-plane/native/runtime-binary-build.d.ts +9 -0
- package/dist/src/control-plane/native/runtime-binary-build.js +107 -0
- package/dist/src/control-plane/native/task-ops.d.ts +52 -0
- package/dist/src/control-plane/native/task-ops.js +3192 -0
- package/dist/src/control-plane/native/task-state.d.ts +30 -0
- package/dist/src/control-plane/native/task-state.js +944 -0
- package/dist/src/control-plane/native/utils.d.ts +7 -0
- package/dist/src/control-plane/native/utils.js +110 -0
- package/dist/src/control-plane/native/validator-binaries.d.ts +3 -0
- package/dist/src/control-plane/native/validator-binaries.js +175 -0
- package/dist/src/control-plane/native/validator.d.ts +44 -0
- package/dist/src/control-plane/native/validator.js +979 -0
- package/dist/src/control-plane/state-sync/index.d.ts +4 -0
- package/dist/src/control-plane/state-sync/index.js +1205 -0
- package/dist/src/control-plane/state-sync/native-git.d.ts +1 -0
- package/dist/src/control-plane/state-sync/native-git.js +281 -0
- package/dist/src/control-plane/state-sync/read.d.ts +46 -0
- package/dist/src/control-plane/state-sync/read.js +564 -0
- package/dist/src/control-plane/state-sync/reconcile.d.ts +28 -0
- package/dist/src/control-plane/state-sync/reconcile.js +260 -0
- package/dist/src/control-plane/state-sync/repo.d.ts +1 -0
- package/dist/src/control-plane/state-sync/repo.js +42 -0
- package/dist/src/control-plane/state-sync/types.d.ts +28 -0
- package/dist/src/control-plane/state-sync/types.js +111 -0
- package/dist/src/control-plane/state-sync/write.d.ts +83 -0
- package/dist/src/control-plane/state-sync/write.js +1165 -0
- package/dist/src/control-plane/task-data-service.d.ts +17 -0
- package/dist/src/control-plane/task-data-service.js +3653 -0
- package/dist/src/control-plane/task-fields.d.ts +1 -0
- package/dist/src/control-plane/task-fields.js +6 -0
- package/dist/src/control-plane/task-io-service.d.ts +6 -0
- package/dist/src/control-plane/task-io-service.js +108 -0
- package/dist/src/control-plane/task-source-bootstrap.d.ts +1 -0
- package/dist/src/control-plane/task-source-bootstrap.js +6 -0
- package/dist/src/control-plane/task-source.d.ts +2 -0
- package/dist/src/control-plane/task-source.js +6 -0
- package/dist/src/control-plane/tasks/legacy-task-config-source.d.ts +19 -0
- package/dist/src/control-plane/tasks/legacy-task-config-source.js +124 -0
- package/dist/src/control-plane/tasks/plugin-task-source.d.ts +30 -0
- package/dist/src/control-plane/tasks/plugin-task-source.js +99 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.d.ts +28 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +642 -0
- package/dist/src/control-plane/tasks/source-lifecycle.d.ts +56 -0
- package/dist/src/control-plane/tasks/source-lifecycle.js +834 -0
- package/dist/src/plugin.d.ts +1 -1
- package/dist/src/plugin.js +3927 -64
- package/package.json +57 -4
|
@@ -0,0 +1,3192 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/task-sources-plugin/src/control-plane/native/task-ops.ts
|
|
3
|
+
import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
4
|
+
import { resolve as resolve10 } from "path";
|
|
5
|
+
import { assertPathInsideRoot as assertPathInsideRoot3, safePathSegment as safePathSegment3 } from "@rig/core/safe-identifiers";
|
|
6
|
+
import { readBuildConfig } from "@rig/core/build-time-config";
|
|
7
|
+
import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/core/runtime-context";
|
|
8
|
+
|
|
9
|
+
// packages/task-sources-plugin/src/control-plane/native/native-git.ts
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import { dirname, isAbsolute, resolve } from "path";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
var taskSourcesGitNativeOutputDir = resolve(tmpdir(), "rig-task-sources-native");
|
|
15
|
+
var taskSourcesGitNativeOutputPath = resolve(taskSourcesGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
16
|
+
var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
|
|
17
|
+
function nativePendingFiles(repoPath) {
|
|
18
|
+
const result = runGitNative("pending-files", [repoPath]);
|
|
19
|
+
if (!result.ok)
|
|
20
|
+
return null;
|
|
21
|
+
if ("files" in result && Array.isArray(result.files)) {
|
|
22
|
+
return result.files.map((file) => ({ path: file.path, status: file.status }));
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function nativeFetchRef(repoPath, remote, branch) {
|
|
27
|
+
return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
|
|
28
|
+
}
|
|
29
|
+
function nativeReadBlobAtRef(repoPath, ref, path) {
|
|
30
|
+
const requestDir = resolve(taskSourcesGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
31
|
+
mkdirSync(requestDir, { recursive: true });
|
|
32
|
+
const outputPath = resolve(requestDir, "blob.txt");
|
|
33
|
+
try {
|
|
34
|
+
requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
|
|
35
|
+
return readFileSync(outputPath, "utf8");
|
|
36
|
+
} finally {
|
|
37
|
+
rmSync(requestDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
|
|
41
|
+
const requestDir = resolve(taskSourcesGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
42
|
+
mkdirSync(requestDir, { recursive: true });
|
|
43
|
+
const messagePath = resolve(requestDir, "message.txt");
|
|
44
|
+
const updatesPath = resolve(requestDir, "updates.json");
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(messagePath, message, "utf8");
|
|
47
|
+
writeFileSync(updatesPath, `${JSON.stringify(serializeTreeCommitUpdates(updates), null, 2)}
|
|
48
|
+
`, "utf8");
|
|
49
|
+
return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(requestDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
|
|
55
|
+
return requireGitNativeString("push-ref-with-lease", [repoPath, localOid, remoteRef, expectedOldOid, remote]);
|
|
56
|
+
}
|
|
57
|
+
function runGitNative(command, args) {
|
|
58
|
+
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
59
|
+
return { ok: false, error: "rig-git native disabled" };
|
|
60
|
+
}
|
|
61
|
+
let binaryPath;
|
|
62
|
+
try {
|
|
63
|
+
binaryPath = resolveGitBinaryPath() ?? ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return { ok: false, error: message.includes("rig-git.zig source file not found") ? "rig-git binary not found" : message };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const proc = Bun.spawnSync([binaryPath, command, ...args], {
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
env: gitNativeEnv()
|
|
73
|
+
});
|
|
74
|
+
if (proc.exitCode !== 0) {
|
|
75
|
+
const stdoutText = proc.stdout.toString().trim();
|
|
76
|
+
if (stdoutText) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(stdoutText);
|
|
79
|
+
if (!parsed.ok) {
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
const error = proc.stderr.toString().trim() || `exit code ${proc.exitCode}`;
|
|
85
|
+
return { ok: false, error };
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(proc.stdout.toString().trim());
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { ok: false, error: String(error) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function requireGitNative(command, args) {
|
|
93
|
+
const result = runGitNative(command, args);
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
throw new Error(`rig-git ${command} failed: ${result.error}`);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function requireGitNativeString(command, args) {
|
|
100
|
+
const result = requireGitNative(command, args);
|
|
101
|
+
if ("value" in result && typeof result.value === "string") {
|
|
102
|
+
return result.value;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`rig-git ${command} returned an unexpected result payload`);
|
|
105
|
+
}
|
|
106
|
+
function serializeTreeCommitUpdates(updates) {
|
|
107
|
+
return updates.map((update) => {
|
|
108
|
+
if (typeof update.content === "string") {
|
|
109
|
+
return { path: update.path, kind: "text", content: update.content };
|
|
110
|
+
}
|
|
111
|
+
if (!isAbsolute(update.sourceFilePath)) {
|
|
112
|
+
throw new Error("tree commit binary updates require an absolute sourceFilePath");
|
|
113
|
+
}
|
|
114
|
+
return { path: update.path, kind: "file", sourceFilePath: update.sourceFilePath };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function gitNativeEnv() {
|
|
118
|
+
const env = { ...process.env };
|
|
119
|
+
const token = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim() || env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
120
|
+
if (token) {
|
|
121
|
+
env.RIG_GITHUB_TOKEN = env.RIG_GITHUB_TOKEN || token;
|
|
122
|
+
env.GITHUB_TOKEN = env.GITHUB_TOKEN || token;
|
|
123
|
+
env.GH_TOKEN = env.GH_TOKEN || token;
|
|
124
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
125
|
+
env.GIT_CONFIG_COUNT = "2";
|
|
126
|
+
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
127
|
+
env.GIT_CONFIG_VALUE_0 = "";
|
|
128
|
+
env.GIT_CONFIG_KEY_1 = "credential.helper";
|
|
129
|
+
env.GIT_CONFIG_VALUE_1 = '!f() { test "$1" = get || exit 0; token="${GITHUB_TOKEN:-${GH_TOKEN:-${RIG_GITHUB_TOKEN:-}}}"; test -n "$token" || exit 0; echo username=x-access-token; echo password="$token"; }; f';
|
|
130
|
+
}
|
|
131
|
+
return env;
|
|
132
|
+
}
|
|
133
|
+
function runtimeRigGitFileName() {
|
|
134
|
+
return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
|
|
135
|
+
}
|
|
136
|
+
function preferredGitBinaryOutputPath() {
|
|
137
|
+
const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
138
|
+
return explicit || taskSourcesGitNativeOutputPath;
|
|
139
|
+
}
|
|
140
|
+
function resolveGitBinaryPath() {
|
|
141
|
+
const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
142
|
+
if (explicit && existsSync(explicit)) {
|
|
143
|
+
return explicit;
|
|
144
|
+
}
|
|
145
|
+
if (explicit) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
for (const candidate of rigGitBinaryCandidates()) {
|
|
149
|
+
if (candidate && existsSync(candidate)) {
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function ensureRigGitBinaryPathSync(outputPath = taskSourcesGitNativeOutputPath) {
|
|
156
|
+
const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
157
|
+
if (explicit && existsSync(explicit)) {
|
|
158
|
+
return explicit;
|
|
159
|
+
}
|
|
160
|
+
const sourcePath = resolveGitSourcePath();
|
|
161
|
+
if (!sourcePath) {
|
|
162
|
+
const binaryPath = resolveGitBinaryPath();
|
|
163
|
+
if (binaryPath) {
|
|
164
|
+
return binaryPath;
|
|
165
|
+
}
|
|
166
|
+
throw new Error("rig-git.zig source file not found.");
|
|
167
|
+
}
|
|
168
|
+
const zigBinary = Bun.which("zig");
|
|
169
|
+
if (!zigBinary) {
|
|
170
|
+
throw new Error("zig is required to build native Rig git tools.");
|
|
171
|
+
}
|
|
172
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
173
|
+
const buildKey = JSON.stringify({
|
|
174
|
+
version: 1,
|
|
175
|
+
zigBinary,
|
|
176
|
+
platform: process.platform,
|
|
177
|
+
arch: process.arch,
|
|
178
|
+
sourcePath,
|
|
179
|
+
sourceDigest: createHash("sha256").update(readFileSync(sourcePath)).digest("hex")
|
|
180
|
+
});
|
|
181
|
+
const manifestPath = `${outputPath}.build-manifest.json`;
|
|
182
|
+
if (nativeBuildIsFresh(outputPath, manifestPath, buildKey)) {
|
|
183
|
+
chmodSync(outputPath, 493);
|
|
184
|
+
return outputPath;
|
|
185
|
+
}
|
|
186
|
+
const tempOutputPath = resolve(dirname(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${process.platform === "win32" ? ".exe" : ""}`);
|
|
187
|
+
const build = Bun.spawnSync([
|
|
188
|
+
zigBinary,
|
|
189
|
+
"build-exe",
|
|
190
|
+
sourcePath,
|
|
191
|
+
"-O",
|
|
192
|
+
"ReleaseFast",
|
|
193
|
+
`-femit-bin=${tempOutputPath}`
|
|
194
|
+
], {
|
|
195
|
+
cwd: dirname(sourcePath),
|
|
196
|
+
stdout: "pipe",
|
|
197
|
+
stderr: "pipe"
|
|
198
|
+
});
|
|
199
|
+
if (build.exitCode !== 0 || !existsSync(tempOutputPath)) {
|
|
200
|
+
const details = [build.stderr.toString().trim(), build.stdout.toString().trim()].filter(Boolean).join(`
|
|
201
|
+
`);
|
|
202
|
+
throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
|
|
203
|
+
}
|
|
204
|
+
chmodSync(tempOutputPath, 493);
|
|
205
|
+
renameSync(tempOutputPath, outputPath);
|
|
206
|
+
if (!binarySupportsTrackerCommands(outputPath)) {
|
|
207
|
+
rmSync(outputPath, { force: true });
|
|
208
|
+
throw new Error("Failed to build native Rig git tools: tracker command probe failed");
|
|
209
|
+
}
|
|
210
|
+
writeFileSync(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
|
|
211
|
+
`, "utf8");
|
|
212
|
+
return outputPath;
|
|
213
|
+
}
|
|
214
|
+
function nativeBuildIsFresh(outputPath, manifestPath, buildKey) {
|
|
215
|
+
if (!existsSync(outputPath) || !existsSync(manifestPath)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
220
|
+
return manifest.version === 1 && manifest.buildKey === buildKey && binarySupportsTrackerCommands(outputPath);
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function binarySupportsTrackerCommands(binaryPath) {
|
|
226
|
+
try {
|
|
227
|
+
const probe = Bun.spawnSync([binaryPath, "fetch-ref", "."], {
|
|
228
|
+
stdout: "pipe",
|
|
229
|
+
stderr: "pipe"
|
|
230
|
+
});
|
|
231
|
+
const stdout = probe.stdout.toString().trim();
|
|
232
|
+
const stderr = probe.stderr.toString().trim();
|
|
233
|
+
if (stdout.includes('"error":"unknown command"')) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return probe.exitCode === 2 && stderr.includes(trackerCommandUsageProbe);
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function resolveGitSourcePath() {
|
|
242
|
+
for (const candidate of rigGitSourceCandidates()) {
|
|
243
|
+
if (candidate && existsSync(candidate)) {
|
|
244
|
+
return candidate;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
function rigGitSourceCandidates() {
|
|
250
|
+
const execDir = process.execPath?.trim() ? dirname(process.execPath.trim()) : "";
|
|
251
|
+
const cwd = process.cwd()?.trim() || "";
|
|
252
|
+
const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
|
|
253
|
+
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
|
|
254
|
+
return [...new Set([
|
|
255
|
+
process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
|
|
256
|
+
resolve(import.meta.dir, "../../../native/rig-git.zig"),
|
|
257
|
+
projectRoot ? resolve(projectRoot, "packages/task-sources-plugin/native/rig-git.zig") : "",
|
|
258
|
+
projectRoot ? resolve(projectRoot, "packages/isolation-plugin/native/rig-git.zig") : "",
|
|
259
|
+
projectRoot ? resolve(projectRoot, "packages/shared/native/rig-git.zig") : "",
|
|
260
|
+
hostProjectRoot ? resolve(hostProjectRoot, "packages/task-sources-plugin/native/rig-git.zig") : "",
|
|
261
|
+
hostProjectRoot ? resolve(hostProjectRoot, "packages/isolation-plugin/native/rig-git.zig") : "",
|
|
262
|
+
hostProjectRoot ? resolve(hostProjectRoot, "packages/shared/native/rig-git.zig") : "",
|
|
263
|
+
cwd ? resolve(cwd, "packages/task-sources-plugin/native/rig-git.zig") : "",
|
|
264
|
+
cwd ? resolve(cwd, "packages/isolation-plugin/native/rig-git.zig") : "",
|
|
265
|
+
cwd ? resolve(cwd, "packages/shared/native/rig-git.zig") : "",
|
|
266
|
+
execDir ? resolve(execDir, "..", "native", "rig-git.zig") : ""
|
|
267
|
+
].filter(Boolean))];
|
|
268
|
+
}
|
|
269
|
+
function rigGitBinaryCandidates() {
|
|
270
|
+
const execDir = process.execPath?.trim() ? dirname(process.execPath.trim()) : "";
|
|
271
|
+
const fileName = runtimeRigGitFileName();
|
|
272
|
+
return [...new Set([
|
|
273
|
+
...nativePackageBinaryCandidates(import.meta.dir, fileName),
|
|
274
|
+
execDir ? resolve(execDir, fileName) : "",
|
|
275
|
+
execDir ? resolve(execDir, "..", fileName) : "",
|
|
276
|
+
execDir ? resolve(execDir, "..", "bin", fileName) : "",
|
|
277
|
+
taskSourcesGitNativeOutputPath
|
|
278
|
+
].filter(Boolean))];
|
|
279
|
+
}
|
|
280
|
+
function nativePackageBinaryCandidates(fromDir, fileName) {
|
|
281
|
+
const candidates = [];
|
|
282
|
+
let cursor = resolve(fromDir);
|
|
283
|
+
for (let index = 0;index < 8; index += 1) {
|
|
284
|
+
candidates.push(resolve(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve(cursor, "native", fileName), resolve(cursor, "native", "bin", fileName));
|
|
285
|
+
const parent = dirname(cursor);
|
|
286
|
+
if (parent === cursor)
|
|
287
|
+
break;
|
|
288
|
+
cursor = parent;
|
|
289
|
+
}
|
|
290
|
+
return candidates;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// packages/task-sources-plugin/src/control-plane/native/task-ops.ts
|
|
294
|
+
import {
|
|
295
|
+
BROWSER_CONTRACT_SERVICE_CAPABILITY,
|
|
296
|
+
RUNTIME_INSTRUCTION,
|
|
297
|
+
RUNTIME_FILE_TOOL_NAMES,
|
|
298
|
+
RUNTIME_SHELL_TOOL_NAMES
|
|
299
|
+
} from "@rig/contracts";
|
|
300
|
+
import { defineCapability as defineCapability2 } from "@rig/core/capability";
|
|
301
|
+
import { loadCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
302
|
+
|
|
303
|
+
// packages/task-sources-plugin/src/control-plane/tasks/source-lifecycle.ts
|
|
304
|
+
import { resolvePluginHost } from "@rig/core/project-plugins";
|
|
305
|
+
import { setScopeRules } from "@rig/core/scope-rules";
|
|
306
|
+
|
|
307
|
+
// packages/task-sources-plugin/src/control-plane/task-source-bootstrap.ts
|
|
308
|
+
import { buildTaskSourceRegistry } from "@rig/core/plugin-host-registries";
|
|
309
|
+
|
|
310
|
+
// packages/task-sources-plugin/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
311
|
+
import { spawnSync } from "child_process";
|
|
312
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
313
|
+
import { basename, join, resolve as resolve3 } from "path";
|
|
314
|
+
|
|
315
|
+
// packages/task-sources-plugin/src/control-plane/tasks/legacy-task-config-source.ts
|
|
316
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
317
|
+
import { resolve as resolve2 } from "path";
|
|
318
|
+
import { findTaskById } from "@rig/core/task-record-reader";
|
|
319
|
+
|
|
320
|
+
class LegacyTaskConfigReadError extends Error {
|
|
321
|
+
code = "LEGACY_TASK_CONFIG_READ_FAILED";
|
|
322
|
+
projectRoot;
|
|
323
|
+
configPath;
|
|
324
|
+
cause;
|
|
325
|
+
constructor(input) {
|
|
326
|
+
super(input.message, { cause: input.cause });
|
|
327
|
+
this.name = "LegacyTaskConfigReadError";
|
|
328
|
+
this.projectRoot = input.projectRoot;
|
|
329
|
+
this.configPath = input.configPath;
|
|
330
|
+
this.cause = input.cause;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
334
|
+
const configPath = options.configPath ?? resolve2(projectRoot, ".rig", "task-config.json");
|
|
335
|
+
const reader = {
|
|
336
|
+
async listTasks() {
|
|
337
|
+
return readLegacyTaskRecords(projectRoot, configPath);
|
|
338
|
+
},
|
|
339
|
+
async getTask(id) {
|
|
340
|
+
return findTaskById(reader, id);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
return reader;
|
|
344
|
+
}
|
|
345
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve2(projectRoot, ".rig", "task-config.json")) {
|
|
346
|
+
if (!existsSync2(configPath)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
|
|
350
|
+
return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
|
|
351
|
+
}
|
|
352
|
+
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
355
|
+
if (isPlainRecord(parsed)) {
|
|
356
|
+
return parsed;
|
|
357
|
+
}
|
|
358
|
+
throw new Error("task config root must be a JSON object");
|
|
359
|
+
} catch (cause) {
|
|
360
|
+
throw new LegacyTaskConfigReadError({
|
|
361
|
+
projectRoot,
|
|
362
|
+
configPath,
|
|
363
|
+
message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
364
|
+
cause
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function stripLegacyTaskConfigMetadata(raw) {
|
|
369
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
370
|
+
return tasks;
|
|
371
|
+
}
|
|
372
|
+
function legacyTaskConfigEntryToRecord(id, entry) {
|
|
373
|
+
if (!isPlainRecord(entry)) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
|
|
377
|
+
const validation = readStringList(entry.validation);
|
|
378
|
+
const validators = readStringList(entry.validators);
|
|
379
|
+
const scope = readStringList(entry.scope);
|
|
380
|
+
const status = typeof entry.status === "string" ? entry.status : "open";
|
|
381
|
+
const title = typeof entry.title === "string" ? entry.title : undefined;
|
|
382
|
+
const description = typeof entry.description === "string" ? entry.description : undefined;
|
|
383
|
+
const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
|
|
384
|
+
return {
|
|
385
|
+
id,
|
|
386
|
+
deps,
|
|
387
|
+
status,
|
|
388
|
+
source: "legacy-task-config",
|
|
389
|
+
...title ? { title } : {},
|
|
390
|
+
...description ? { description } : {},
|
|
391
|
+
...acceptanceCriteria ? { acceptanceCriteria } : {},
|
|
392
|
+
...scope.length > 0 ? { scope } : {},
|
|
393
|
+
...validation.length > 0 ? { validation } : {},
|
|
394
|
+
...validators.length > 0 ? { validators } : {},
|
|
395
|
+
...preservedLegacyFields(entry)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function preservedLegacyFields(entry) {
|
|
399
|
+
const preserved = {};
|
|
400
|
+
for (const key of [
|
|
401
|
+
"role",
|
|
402
|
+
"browser",
|
|
403
|
+
"repo_pins",
|
|
404
|
+
"criticality",
|
|
405
|
+
"queue_weight",
|
|
406
|
+
"creates_repo",
|
|
407
|
+
"auto_synced"
|
|
408
|
+
]) {
|
|
409
|
+
if (entry[key] !== undefined) {
|
|
410
|
+
preserved[key] = entry[key];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return preserved;
|
|
414
|
+
}
|
|
415
|
+
function firstStringList(...candidates) {
|
|
416
|
+
for (const candidate of candidates) {
|
|
417
|
+
const list = readStringList(candidate);
|
|
418
|
+
if (list.length > 0) {
|
|
419
|
+
return list;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
function readStringList(candidate) {
|
|
425
|
+
if (!Array.isArray(candidate)) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
return candidate.filter((value) => typeof value === "string");
|
|
429
|
+
}
|
|
430
|
+
function isPlainRecord(candidate) {
|
|
431
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// packages/task-sources-plugin/src/control-plane/native/github-token-env.ts
|
|
435
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
436
|
+
function cleanToken(value) {
|
|
437
|
+
const trimmed = value?.trim() ?? "";
|
|
438
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
439
|
+
}
|
|
440
|
+
function authStateToken(env = process.env) {
|
|
441
|
+
const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
|
|
442
|
+
if (!file || !existsSync3(file))
|
|
443
|
+
return null;
|
|
444
|
+
try {
|
|
445
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
446
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// packages/task-sources-plugin/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
453
|
+
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
454
|
+
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
455
|
+
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
456
|
+
const configPath = options.configPath ?? resolve3(projectRoot, ".rig", "task-config.json");
|
|
457
|
+
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
458
|
+
const spawnFn = options.spawn ?? spawnSync;
|
|
459
|
+
const ghBinary = options.ghBinary ?? "gh";
|
|
460
|
+
const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
|
|
461
|
+
return {
|
|
462
|
+
async listTasks() {
|
|
463
|
+
const rawConfig = readRawTaskConfig(configPath);
|
|
464
|
+
if (!rawConfig) {
|
|
465
|
+
const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
|
|
466
|
+
return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
|
|
467
|
+
}
|
|
468
|
+
const tasks = [];
|
|
469
|
+
const legacyTasks = await legacy.listTasks();
|
|
470
|
+
const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
|
|
471
|
+
for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
|
|
472
|
+
if (!isPlainRecord2(rawEntry)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const metadata = readMaterializedTaskMetadata(rawEntry);
|
|
476
|
+
if (metadata.taskSource?.kind === "github-issues") {
|
|
477
|
+
tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
|
|
481
|
+
const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
|
|
482
|
+
if (fileTask)
|
|
483
|
+
tasks.push(fileTask);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (!allowLocalFallback) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const legacyTask = legacyById.get(id);
|
|
490
|
+
if (legacyTask) {
|
|
491
|
+
tasks.push(legacyTask);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return tasks;
|
|
495
|
+
},
|
|
496
|
+
async getTask(id) {
|
|
497
|
+
const rawEntry = readRawTaskEntry(configPath, id);
|
|
498
|
+
if (!rawEntry) {
|
|
499
|
+
const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
|
|
500
|
+
return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
|
|
501
|
+
}
|
|
502
|
+
const metadata = readMaterializedTaskMetadata(rawEntry);
|
|
503
|
+
if (metadata.taskSource?.kind === "github-issues") {
|
|
504
|
+
return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
|
|
505
|
+
}
|
|
506
|
+
if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
|
|
507
|
+
return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
|
|
508
|
+
}
|
|
509
|
+
return allowLocalFallback ? legacy.getTask(id) : null;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function readMaterializedTaskMetadata(entry) {
|
|
514
|
+
const rawRig = entry._rig;
|
|
515
|
+
if (!isPlainRecord2(rawRig)) {
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
const rawSource = rawRig.taskSource;
|
|
519
|
+
const metadata = {};
|
|
520
|
+
if (isPlainRecord2(rawSource)) {
|
|
521
|
+
const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
|
|
522
|
+
if (kind.length > 0) {
|
|
523
|
+
metadata.taskSource = {
|
|
524
|
+
kind,
|
|
525
|
+
...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
|
|
526
|
+
...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
|
|
527
|
+
...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
|
|
528
|
+
...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
|
|
529
|
+
...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (typeof rawRig.sourceIssueId === "string") {
|
|
534
|
+
metadata.sourceIssueId = rawRig.sourceIssueId;
|
|
535
|
+
}
|
|
536
|
+
return metadata;
|
|
537
|
+
}
|
|
538
|
+
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
539
|
+
const jsonPath = resolve3(projectRoot, "rig.config.json");
|
|
540
|
+
if (existsSync4(jsonPath)) {
|
|
541
|
+
try {
|
|
542
|
+
const parsed = JSON.parse(readFileSync4(jsonPath, "utf8"));
|
|
543
|
+
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
544
|
+
const source = parsed.taskSource;
|
|
545
|
+
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const tsPath = resolve3(projectRoot, "rig.config.ts");
|
|
552
|
+
if (!existsSync4(tsPath)) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const source = readFileSync4(tsPath, "utf8");
|
|
557
|
+
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
558
|
+
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
559
|
+
if (kind !== "files") {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function readRawTaskEntry(configPath, taskId) {
|
|
568
|
+
const rawConfig = readRawTaskConfig(configPath);
|
|
569
|
+
if (!rawConfig) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
|
|
573
|
+
return isPlainRecord2(entry) ? entry : null;
|
|
574
|
+
}
|
|
575
|
+
function readRawTaskConfig(configPath) {
|
|
576
|
+
if (!existsSync4(configPath)) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf8"));
|
|
580
|
+
return isPlainRecord2(parsed) ? parsed : null;
|
|
581
|
+
}
|
|
582
|
+
function stripLegacyTaskConfigMetadata2(raw) {
|
|
583
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
584
|
+
return tasks;
|
|
585
|
+
}
|
|
586
|
+
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
587
|
+
const directory = resolve3(projectRoot, sourcePath);
|
|
588
|
+
if (!existsSync4(directory)) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
const tasks = [];
|
|
592
|
+
for (const name of readdirSync(directory)) {
|
|
593
|
+
if (!FILE_TASK_PATTERN.test(name))
|
|
594
|
+
continue;
|
|
595
|
+
const inferredId = basename(name).replace(FILE_TASK_PATTERN, "");
|
|
596
|
+
const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
|
|
597
|
+
if (task)
|
|
598
|
+
tasks.push(task);
|
|
599
|
+
}
|
|
600
|
+
return tasks;
|
|
601
|
+
}
|
|
602
|
+
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
603
|
+
const file = findFileBackedTaskFile(resolve3(projectRoot, sourcePath), taskId);
|
|
604
|
+
if (!file) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const raw = JSON.parse(readFileSync4(file, "utf8"));
|
|
608
|
+
if (!isPlainRecord2(raw)) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
id: typeof raw.id === "string" ? raw.id : taskId,
|
|
613
|
+
deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
|
|
614
|
+
status: typeof raw.status === "string" ? raw.status : "ready",
|
|
615
|
+
title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
|
|
616
|
+
...raw
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function findFileBackedTaskFile(directory, taskId) {
|
|
620
|
+
if (!existsSync4(directory)) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
for (const name of readdirSync(directory)) {
|
|
624
|
+
if (!FILE_TASK_PATTERN.test(name))
|
|
625
|
+
continue;
|
|
626
|
+
const file = join(directory, name);
|
|
627
|
+
try {
|
|
628
|
+
if (!statSync(file).isFile())
|
|
629
|
+
continue;
|
|
630
|
+
const raw = JSON.parse(readFileSync4(file, "utf8"));
|
|
631
|
+
const inferredId = basename(file).replace(FILE_TASK_PATTERN, "");
|
|
632
|
+
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
633
|
+
if (id === taskId) {
|
|
634
|
+
return file;
|
|
635
|
+
}
|
|
636
|
+
} catch {}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
|
|
641
|
+
const source = requireGithubIssueSource(metadata, id);
|
|
642
|
+
const issue = runGh(bin, [
|
|
643
|
+
"issue",
|
|
644
|
+
"view",
|
|
645
|
+
String(id),
|
|
646
|
+
"--repo",
|
|
647
|
+
`${source.owner}/${source.repo}`,
|
|
648
|
+
"--json",
|
|
649
|
+
"number,title,body,labels,state,url,assignees"
|
|
650
|
+
], spawnFn);
|
|
651
|
+
return githubIssueToTask(issue, source, rawEntry);
|
|
652
|
+
}
|
|
653
|
+
function requireGithubIssueSource(metadata, id) {
|
|
654
|
+
const source = metadata.taskSource;
|
|
655
|
+
if (source?.kind === "github-issues" && source.owner && source.repo) {
|
|
656
|
+
return { owner: source.owner, repo: source.repo };
|
|
657
|
+
}
|
|
658
|
+
const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
659
|
+
if (parsed && parsed[3] === id) {
|
|
660
|
+
return { owner: parsed[1], repo: parsed[2] };
|
|
661
|
+
}
|
|
662
|
+
throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
|
|
663
|
+
}
|
|
664
|
+
function githubIssueToTask(issue, source, rawEntry) {
|
|
665
|
+
const labelNames = (issue.labels ?? []).map((label) => label.name);
|
|
666
|
+
const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
667
|
+
const roleLabel = labelNames.find((label) => label.startsWith("role:"));
|
|
668
|
+
const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
669
|
+
const body = issue.body ?? "";
|
|
670
|
+
const repo = `${source.owner}/${source.repo}`;
|
|
671
|
+
return {
|
|
672
|
+
id: String(issue.number),
|
|
673
|
+
deps: parseDeps(body),
|
|
674
|
+
status: githubStatusFor(issue),
|
|
675
|
+
title: issue.title,
|
|
676
|
+
body,
|
|
677
|
+
...scope.length > 0 ? { scope } : {},
|
|
678
|
+
...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
|
|
679
|
+
...validators.length > 0 ? { validators } : {},
|
|
680
|
+
...issue.url ? { url: issue.url } : {},
|
|
681
|
+
issueType: issueTypeFor(labelNames),
|
|
682
|
+
sourceIssueId: `${repo}#${issue.number}`,
|
|
683
|
+
parentChildDeps: parseParents(body),
|
|
684
|
+
labels: labelNames,
|
|
685
|
+
raw: issue,
|
|
686
|
+
source: "github-issues",
|
|
687
|
+
_rig: {
|
|
688
|
+
taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
|
|
689
|
+
sourceIssueId: `${repo}#${issue.number}`
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function githubStatusFor(issue) {
|
|
694
|
+
const state = (issue.state ?? "").toUpperCase();
|
|
695
|
+
if (state === "CLOSED")
|
|
696
|
+
return "closed";
|
|
697
|
+
const labelNames = (issue.labels ?? []).map((label) => label.name);
|
|
698
|
+
if (labelNames.includes("in-progress"))
|
|
699
|
+
return "in_progress";
|
|
700
|
+
if (labelNames.includes("blocked"))
|
|
701
|
+
return "blocked";
|
|
702
|
+
if (labelNames.includes("ready"))
|
|
703
|
+
return "ready";
|
|
704
|
+
if (labelNames.includes("under-review"))
|
|
705
|
+
return "under_review";
|
|
706
|
+
if (labelNames.includes("failed"))
|
|
707
|
+
return "failed";
|
|
708
|
+
if (labelNames.includes("cancelled"))
|
|
709
|
+
return "cancelled";
|
|
710
|
+
return "open";
|
|
711
|
+
}
|
|
712
|
+
function selectedGitHubEnv() {
|
|
713
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
|
|
714
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
715
|
+
}
|
|
716
|
+
function ghSpawnOptions() {
|
|
717
|
+
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
718
|
+
}
|
|
719
|
+
function tokenDiagnostic(value) {
|
|
720
|
+
const clean = value?.trim() ?? "";
|
|
721
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
722
|
+
}
|
|
723
|
+
function runGh(bin, args, spawnFn) {
|
|
724
|
+
const options = ghSpawnOptions();
|
|
725
|
+
const res = spawnFn(bin, [...args], options);
|
|
726
|
+
assertGhSuccess(args, res, options.env);
|
|
727
|
+
if (!res.stdout || res.stdout.trim() === "") {
|
|
728
|
+
throw new Error(`gh ${args.join(" ")} returned empty stdout`);
|
|
729
|
+
}
|
|
730
|
+
return JSON.parse(res.stdout);
|
|
731
|
+
}
|
|
732
|
+
function assertGhSuccess(args, res, env) {
|
|
733
|
+
if (res.error) {
|
|
734
|
+
const msg = res.error.message ?? String(res.error);
|
|
735
|
+
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
736
|
+
}
|
|
737
|
+
if (res.status !== 0) {
|
|
738
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
739
|
+
[rig gh env:source-aware] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function parseDeps(body) {
|
|
743
|
+
return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
|
|
744
|
+
}
|
|
745
|
+
function parseParents(body) {
|
|
746
|
+
return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
|
|
747
|
+
}
|
|
748
|
+
function parseIssueRefs(body, pattern) {
|
|
749
|
+
const match = body.match(pattern);
|
|
750
|
+
if (!match)
|
|
751
|
+
return [];
|
|
752
|
+
return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
|
|
753
|
+
}
|
|
754
|
+
function issueTypeFor(labels) {
|
|
755
|
+
const typed = labels.find((label) => label.startsWith("type:"));
|
|
756
|
+
if (typed)
|
|
757
|
+
return typed.slice("type:".length);
|
|
758
|
+
if (labels.includes("epic"))
|
|
759
|
+
return "epic";
|
|
760
|
+
return "task";
|
|
761
|
+
}
|
|
762
|
+
function isPlainRecord2(candidate) {
|
|
763
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// packages/task-sources-plugin/src/control-plane/tasks/source-lifecycle.ts
|
|
767
|
+
async function resolveTaskSourceContext(projectRoot) {
|
|
768
|
+
let resolved;
|
|
769
|
+
try {
|
|
770
|
+
resolved = await resolvePluginHost(projectRoot);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
773
|
+
if (msg.includes("no rig.config"))
|
|
774
|
+
return null;
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
777
|
+
const { config, host } = resolved;
|
|
778
|
+
setScopeRules(config.workspace.scopeNormalization);
|
|
779
|
+
return {
|
|
780
|
+
taskSourceRegistry: buildTaskSourceRegistry(config, host, { projectRoot })
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function hasRunnableTaskSource(source) {
|
|
784
|
+
return Boolean(source && typeof source === "object" && !Array.isArray(source));
|
|
785
|
+
}
|
|
786
|
+
async function getPluginTask(projectRoot, taskId) {
|
|
787
|
+
const ctx = await resolveTaskSourceContext(projectRoot);
|
|
788
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
789
|
+
if (!hasRunnableTaskSource(source)) {
|
|
790
|
+
return ctx ? { configured: false, sourceKind: null, task: null } : null;
|
|
791
|
+
}
|
|
792
|
+
const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
|
|
793
|
+
return {
|
|
794
|
+
configured: true,
|
|
795
|
+
sourceKind: source.kind,
|
|
796
|
+
task
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async function readConfiguredTaskSourceTask(projectRoot, taskId) {
|
|
800
|
+
const pluginResult = await getPluginTask(projectRoot, taskId);
|
|
801
|
+
if (pluginResult)
|
|
802
|
+
return pluginResult;
|
|
803
|
+
const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
|
|
804
|
+
return {
|
|
805
|
+
configured: false,
|
|
806
|
+
sourceKind: null,
|
|
807
|
+
task
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// packages/task-sources-plugin/src/control-plane/native/task-state.ts
|
|
812
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
813
|
+
import { basename as basename2, resolve as resolve6 } from "path";
|
|
814
|
+
import { assertPathInsideRoot, safePathSegment } from "@rig/core/safe-identifiers";
|
|
815
|
+
import { loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
|
|
816
|
+
|
|
817
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/types.ts
|
|
818
|
+
var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
|
|
819
|
+
var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
820
|
+
"draft",
|
|
821
|
+
"open",
|
|
822
|
+
"ready",
|
|
823
|
+
"queued",
|
|
824
|
+
"in_progress",
|
|
825
|
+
"under_review",
|
|
826
|
+
"blocked",
|
|
827
|
+
"completed",
|
|
828
|
+
"cancelled"
|
|
829
|
+
]);
|
|
830
|
+
function normalizeTaskLifecycleStatus(status) {
|
|
831
|
+
switch (status) {
|
|
832
|
+
case "draft":
|
|
833
|
+
case "open":
|
|
834
|
+
case "ready":
|
|
835
|
+
case "queued":
|
|
836
|
+
case "in_progress":
|
|
837
|
+
case "under_review":
|
|
838
|
+
case "blocked":
|
|
839
|
+
case "completed":
|
|
840
|
+
case "cancelled":
|
|
841
|
+
return status;
|
|
842
|
+
case "closed":
|
|
843
|
+
return "completed";
|
|
844
|
+
case "running":
|
|
845
|
+
return "in_progress";
|
|
846
|
+
case "failed":
|
|
847
|
+
return "ready";
|
|
848
|
+
default:
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function normalizeTaskStateMetadataStatus(status) {
|
|
853
|
+
if (CANONICAL_TASK_LIFECYCLE_STATUSES.has(status)) {
|
|
854
|
+
return status;
|
|
855
|
+
}
|
|
856
|
+
switch (status) {
|
|
857
|
+
case "closed":
|
|
858
|
+
return "completed";
|
|
859
|
+
case "running":
|
|
860
|
+
return "in_progress";
|
|
861
|
+
case "failed":
|
|
862
|
+
return "ready";
|
|
863
|
+
default:
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function canonicalizeTaskStateMetadata(raw) {
|
|
868
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const metadata = raw;
|
|
872
|
+
const claimId = typeof metadata.claimId === "string" && metadata.claimId.length > 0 ? metadata.claimId : undefined;
|
|
873
|
+
const status = normalizeTaskStateMetadataStatus(metadata.status);
|
|
874
|
+
if (!status) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
...claimId ? { claimId } : {},
|
|
879
|
+
status,
|
|
880
|
+
...typeof metadata.ownerId === "string" && metadata.ownerId.length > 0 ? { ownerId: metadata.ownerId } : {},
|
|
881
|
+
...typeof metadata.claimedAt === "string" && metadata.claimedAt.length > 0 ? { claimedAt: metadata.claimedAt } : {},
|
|
882
|
+
...typeof metadata.lastEvidenceAt === "string" && metadata.lastEvidenceAt.length > 0 ? { lastEvidenceAt: metadata.lastEvidenceAt } : {},
|
|
883
|
+
...typeof metadata.runId === "string" && metadata.runId.length > 0 ? { runId: metadata.runId } : {},
|
|
884
|
+
...typeof metadata.branchName === "string" && metadata.branchName.length > 0 ? { branchName: metadata.branchName } : {},
|
|
885
|
+
...typeof metadata.prNumber === "number" ? { prNumber: metadata.prNumber } : {},
|
|
886
|
+
...typeof metadata.prUrl === "string" && metadata.prUrl.length > 0 ? { prUrl: metadata.prUrl } : {},
|
|
887
|
+
...typeof metadata.reviewState === "string" && metadata.reviewState.length > 0 ? { reviewState: metadata.reviewState } : {},
|
|
888
|
+
...typeof metadata.blockerReason === "string" && metadata.blockerReason.length > 0 ? { blockerReason: metadata.blockerReason } : {},
|
|
889
|
+
...typeof metadata.sourceCommit === "string" && metadata.sourceCommit.length > 0 ? { sourceCommit: metadata.sourceCommit } : {}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function discardMismatchedTaskStateMetadata(input) {
|
|
893
|
+
input.taskId;
|
|
894
|
+
const canonicalMetadata = canonicalizeTaskStateMetadata(input.metadata);
|
|
895
|
+
if (!canonicalMetadata || !input.lifecycleStatus) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
const metadataStatus = canonicalMetadata.status ?? null;
|
|
899
|
+
if (metadataStatus && metadataStatus !== input.lifecycleStatus) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
return canonicalMetadata;
|
|
903
|
+
}
|
|
904
|
+
function readTaskStateMetadataEnvelope(raw) {
|
|
905
|
+
if (!raw || typeof raw !== "object") {
|
|
906
|
+
return { schemaVersion: SUPPORTED_TASK_STATE_SCHEMA_VERSION, supported: true, baseTrackerCommit: null, tasks: {} };
|
|
907
|
+
}
|
|
908
|
+
const envelope = raw;
|
|
909
|
+
const schemaVersion = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : SUPPORTED_TASK_STATE_SCHEMA_VERSION;
|
|
910
|
+
if (schemaVersion !== SUPPORTED_TASK_STATE_SCHEMA_VERSION) {
|
|
911
|
+
return { schemaVersion, supported: false, baseTrackerCommit: null, tasks: {} };
|
|
912
|
+
}
|
|
913
|
+
const rawTasks = envelope.tasks && typeof envelope.tasks === "object" && !Array.isArray(envelope.tasks) ? envelope.tasks : {};
|
|
914
|
+
const tasks = Object.fromEntries(Object.entries(rawTasks).map(([taskId, metadata]) => [taskId, canonicalizeTaskStateMetadata(metadata)]).filter((entry) => entry[1] != null));
|
|
915
|
+
return {
|
|
916
|
+
schemaVersion,
|
|
917
|
+
supported: true,
|
|
918
|
+
baseTrackerCommit: typeof envelope.baseTrackerCommit === "string" && envelope.baseTrackerCommit.length > 0 ? envelope.baseTrackerCommit : null,
|
|
919
|
+
tasks
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/read.ts
|
|
923
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
924
|
+
import { resolve as resolve5 } from "path";
|
|
925
|
+
// packages/task-sources-plugin/src/control-plane/native/utils.ts
|
|
926
|
+
import { getScopeRules } from "@rig/core/scope-rules";
|
|
927
|
+
import { unique } from "@rig/core/exec";
|
|
928
|
+
import {
|
|
929
|
+
runCapture,
|
|
930
|
+
runCaptureAsync,
|
|
931
|
+
runInherit,
|
|
932
|
+
getValidationTimeoutMs,
|
|
933
|
+
readJsonFile,
|
|
934
|
+
nowIso,
|
|
935
|
+
unique as unique2,
|
|
936
|
+
fileLines
|
|
937
|
+
} from "@rig/core/exec";
|
|
938
|
+
import { resolveCheckoutRoot } from "@rig/core/checkout-root";
|
|
939
|
+
import { resolveHarnessPaths } from "@rig/core/harness-paths";
|
|
940
|
+
var scopeRegexCache = new Map;
|
|
941
|
+
function normalizeRelativeScopePath(inputPath) {
|
|
942
|
+
let normalized = inputPath.replace(/^\.\//, "");
|
|
943
|
+
const rules = getScopeRules();
|
|
944
|
+
if (rules?.stripPrefixes) {
|
|
945
|
+
for (const prefix of rules.stripPrefixes) {
|
|
946
|
+
if (normalized.startsWith(prefix)) {
|
|
947
|
+
normalized = normalized.slice(prefix.length);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return normalized;
|
|
952
|
+
}
|
|
953
|
+
function normalizePathToScope(projectRoot, monorepoRoot, inputPath) {
|
|
954
|
+
let normalized = inputPath.replace(/^\.\//, "");
|
|
955
|
+
if (normalized.startsWith(projectRoot + "/")) {
|
|
956
|
+
normalized = normalized.slice(projectRoot.length + 1);
|
|
957
|
+
}
|
|
958
|
+
if (normalized.startsWith(monorepoRoot + "/")) {
|
|
959
|
+
normalized = normalized.slice(monorepoRoot.length + 1);
|
|
960
|
+
}
|
|
961
|
+
return normalizeRelativeScopePath(normalized);
|
|
962
|
+
}
|
|
963
|
+
function monorepoSearchCandidates(inputPath) {
|
|
964
|
+
const normalized = inputPath.replace(/^\.\//, "");
|
|
965
|
+
const candidates = new Set;
|
|
966
|
+
const add = (value) => {
|
|
967
|
+
const trimmed = value.replace(/^\.\//, "");
|
|
968
|
+
if (trimmed) {
|
|
969
|
+
candidates.add(trimmed);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
add(normalized);
|
|
973
|
+
const stripped = normalizeRelativeScopePath(normalized);
|
|
974
|
+
add(stripped);
|
|
975
|
+
const rules = getScopeRules();
|
|
976
|
+
if (rules?.stripPrefixes) {
|
|
977
|
+
for (const prefix of rules.stripPrefixes) {
|
|
978
|
+
if (normalized.startsWith(prefix)) {
|
|
979
|
+
add(normalized.slice(prefix.length));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (rules?.searchPrefixes) {
|
|
984
|
+
for (const rule of rules.searchPrefixes) {
|
|
985
|
+
const matchesStart = rule.matchStartsWith?.some((prefix) => stripped.startsWith(prefix)) ?? false;
|
|
986
|
+
const matchesExact = rule.matchExact?.includes(stripped) ?? false;
|
|
987
|
+
if (matchesStart || matchesExact) {
|
|
988
|
+
add(`${rule.prefix}${stripped}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return [...candidates];
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/read.ts
|
|
996
|
+
import { RUNTIME_CONTEXT_ENV } from "@rig/core/runtime-context";
|
|
997
|
+
|
|
998
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/repo.ts
|
|
999
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1000
|
+
import { resolve as resolve4 } from "path";
|
|
1001
|
+
import { MANAGED_REPO_SERVICE_CAPABILITY } from "@rig/contracts";
|
|
1002
|
+
import { defineCapability } from "@rig/core/capability";
|
|
1003
|
+
import { getInstalledCapability } from "@rig/core/capability-loaders";
|
|
1004
|
+
function resolveTrackerRepoPath(projectRoot) {
|
|
1005
|
+
const monorepoRoot = resolveCheckoutRoot(projectRoot);
|
|
1006
|
+
const managedRepos = getInstalledCapability(defineCapability(MANAGED_REPO_SERVICE_CAPABILITY));
|
|
1007
|
+
if (managedRepos) {
|
|
1008
|
+
try {
|
|
1009
|
+
const layout = managedRepos.resolveMonorepoRepoLayout(projectRoot);
|
|
1010
|
+
if (existsSync5(resolve4(layout.mirrorRoot, "HEAD"))) {
|
|
1011
|
+
return layout.mirrorRoot;
|
|
1012
|
+
}
|
|
1013
|
+
} catch {}
|
|
1014
|
+
}
|
|
1015
|
+
return monorepoRoot;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/read.ts
|
|
1019
|
+
var DEFAULT_READ_DEPS = {
|
|
1020
|
+
fetchRef: nativeFetchRef,
|
|
1021
|
+
readBlobAtRef: nativeReadBlobAtRef,
|
|
1022
|
+
exists: existsSync6,
|
|
1023
|
+
readFile: (path) => readFileSync5(path, "utf8")
|
|
1024
|
+
};
|
|
1025
|
+
function parseIssueStatus(rawStatus) {
|
|
1026
|
+
const normalized = normalizeTaskLifecycleStatus(rawStatus);
|
|
1027
|
+
return normalized ?? "unknown";
|
|
1028
|
+
}
|
|
1029
|
+
function parseIssueDependencies(raw) {
|
|
1030
|
+
if (!Array.isArray(raw)) {
|
|
1031
|
+
return [];
|
|
1032
|
+
}
|
|
1033
|
+
return raw.filter((entry) => !!entry && typeof entry === "object" && !Array.isArray(entry)).map((entry) => ({
|
|
1034
|
+
issueId: typeof entry.issue_id === "string" && entry.issue_id.trim() ? entry.issue_id.trim() : null,
|
|
1035
|
+
dependsOnId: typeof entry.depends_on_id === "string" && entry.depends_on_id.trim() ? entry.depends_on_id.trim() : null,
|
|
1036
|
+
id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : null,
|
|
1037
|
+
type: typeof entry.type === "string" && entry.type.trim() ? entry.type.trim() : null
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
1040
|
+
function parseIssuesJsonl(raw) {
|
|
1041
|
+
const issues = [];
|
|
1042
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1043
|
+
const trimmed = line.trim();
|
|
1044
|
+
if (!trimmed) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
const record = JSON.parse(trimmed);
|
|
1049
|
+
const id = typeof record.id === "string" && record.id.trim() ? record.id.trim() : "";
|
|
1050
|
+
if (!id) {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const rawStatus = typeof record.status === "string" && record.status.trim() ? record.status.trim() : null;
|
|
1054
|
+
issues.push({
|
|
1055
|
+
id,
|
|
1056
|
+
title: typeof record.title === "string" && record.title.trim() ? record.title.trim() : null,
|
|
1057
|
+
description: typeof record.description === "string" && record.description.trim() ? record.description.trim() : null,
|
|
1058
|
+
acceptanceCriteria: typeof record.acceptance_criteria === "string" && record.acceptance_criteria.trim() ? record.acceptance_criteria.trim() : typeof record.acceptanceCriteria === "string" && record.acceptanceCriteria.trim() ? record.acceptanceCriteria.trim() : null,
|
|
1059
|
+
issueType: typeof record.issue_type === "string" && record.issue_type.trim() ? record.issue_type.trim() : null,
|
|
1060
|
+
status: parseIssueStatus(rawStatus),
|
|
1061
|
+
rawStatus,
|
|
1062
|
+
priority: typeof record.priority === "number" ? record.priority : null,
|
|
1063
|
+
dependencies: parseIssueDependencies(record.dependencies)
|
|
1064
|
+
});
|
|
1065
|
+
} catch {}
|
|
1066
|
+
}
|
|
1067
|
+
return issues;
|
|
1068
|
+
}
|
|
1069
|
+
function parseTaskStateEnvelope(raw) {
|
|
1070
|
+
if (!raw || !raw.trim()) {
|
|
1071
|
+
return readTaskStateMetadataEnvelope(null);
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
return readTaskStateMetadataEnvelope(JSON.parse(raw));
|
|
1075
|
+
} catch {
|
|
1076
|
+
return readTaskStateMetadataEnvelope(null);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function readRemoteBlobAtRef(deps, repoPath, ref, path, options) {
|
|
1080
|
+
try {
|
|
1081
|
+
return deps.readBlobAtRef(repoPath, ref, path);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (options.required) {
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function shouldPreferLocalTrackerState(options) {
|
|
1090
|
+
if (!options.allowLocalFallback) {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1094
|
+
if (!runtimeWorkspace) {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
if (process.env.RIG_TASK_RUNTIME_ID?.trim()) {
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
const runtimeContextPath = process.env[RUNTIME_CONTEXT_ENV]?.trim();
|
|
1101
|
+
if (runtimeContextPath) {
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
return existsSync6(resolve5(runtimeWorkspace, ".rig", "runtime-context.json"));
|
|
1105
|
+
}
|
|
1106
|
+
function readLocalTrackerState(projectRoot, deps) {
|
|
1107
|
+
const monorepoRoot = resolveCheckoutRoot(projectRoot);
|
|
1108
|
+
const issuesPath = resolve5(monorepoRoot, ".beads", "issues.jsonl");
|
|
1109
|
+
const taskStatePath = resolve5(monorepoRoot, ".beads", "task-state.json");
|
|
1110
|
+
return projectSyncedTrackerSnapshot({
|
|
1111
|
+
source: "local",
|
|
1112
|
+
issuesBaseOid: null,
|
|
1113
|
+
issuesText: deps.exists(issuesPath) ? deps.readFile(issuesPath) : "",
|
|
1114
|
+
taskStateBaseOid: null,
|
|
1115
|
+
taskStateText: deps.exists(taskStatePath) ? deps.readFile(taskStatePath) : null
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
function projectSyncedTrackerSnapshot(input) {
|
|
1119
|
+
if (input.source === "remote" && input.issuesBaseOid && input.taskStateBaseOid && input.issuesBaseOid !== input.taskStateBaseOid) {
|
|
1120
|
+
throw new Error("Remote tracker files must be read from the same fetched base.");
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
source: input.source,
|
|
1124
|
+
baseOid: input.issuesBaseOid ?? input.taskStateBaseOid ?? null,
|
|
1125
|
+
issues: parseIssuesJsonl(input.issuesText),
|
|
1126
|
+
taskState: parseTaskStateEnvelope(input.taskStateText)
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function readSyncedTrackerState(projectRoot, deps = {}, options = {}) {
|
|
1130
|
+
const readDeps = { ...DEFAULT_READ_DEPS, ...deps };
|
|
1131
|
+
const trackerRepoPath = resolveTrackerRepoPath(projectRoot);
|
|
1132
|
+
if (shouldPreferLocalTrackerState(options)) {
|
|
1133
|
+
return readLocalTrackerState(projectRoot, readDeps);
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
const baseOid = readDeps.fetchRef(trackerRepoPath, "origin", "main");
|
|
1137
|
+
return projectSyncedTrackerSnapshot({
|
|
1138
|
+
source: "remote",
|
|
1139
|
+
issuesBaseOid: baseOid,
|
|
1140
|
+
issuesText: readRemoteBlobAtRef(readDeps, trackerRepoPath, baseOid, ".beads/issues.jsonl", {
|
|
1141
|
+
required: true
|
|
1142
|
+
}) ?? "",
|
|
1143
|
+
taskStateBaseOid: baseOid,
|
|
1144
|
+
taskStateText: readRemoteBlobAtRef(readDeps, trackerRepoPath, baseOid, ".beads/task-state.json", {
|
|
1145
|
+
required: false
|
|
1146
|
+
})
|
|
1147
|
+
});
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
if (!options.allowLocalFallback) {
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
return readLocalTrackerState(projectRoot, readDeps);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/reconcile.ts
|
|
1156
|
+
var STALE_CLAIM_MS = 24 * 60 * 60 * 1000;
|
|
1157
|
+
// packages/task-sources-plugin/src/control-plane/state-sync/write.ts
|
|
1158
|
+
import { isDeepStrictEqual } from "util";
|
|
1159
|
+
var DEFAULT_WRITER_DEPS = {
|
|
1160
|
+
fetchRef: nativeFetchRef,
|
|
1161
|
+
readBlobAtRef: nativeReadBlobAtRef,
|
|
1162
|
+
writeTreeCommit: nativeWriteTreeCommit,
|
|
1163
|
+
pushRefWithLease: nativePushRefWithLease,
|
|
1164
|
+
readSnapshot: readSyncedTrackerState,
|
|
1165
|
+
createClaimId: () => `claim-${crypto.randomUUID()}`,
|
|
1166
|
+
now: () => new Date().toISOString()
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
class TrackerStateMutationError extends Error {
|
|
1170
|
+
code;
|
|
1171
|
+
constructor(message, code) {
|
|
1172
|
+
super(message);
|
|
1173
|
+
this.code = code;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function parseIssuesJsonl2(raw) {
|
|
1177
|
+
const rows = [];
|
|
1178
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1179
|
+
const trimmed = line.trim();
|
|
1180
|
+
if (!trimmed) {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const parsed = JSON.parse(trimmed);
|
|
1185
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1186
|
+
rows.push(parsed);
|
|
1187
|
+
}
|
|
1188
|
+
} catch {}
|
|
1189
|
+
}
|
|
1190
|
+
return rows;
|
|
1191
|
+
}
|
|
1192
|
+
function parseRawTaskStateFile(raw) {
|
|
1193
|
+
if (!raw || !raw.trim()) {
|
|
1194
|
+
return {
|
|
1195
|
+
schemaVersion: 1,
|
|
1196
|
+
tasks: {}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = JSON.parse(raw);
|
|
1201
|
+
return {
|
|
1202
|
+
schemaVersion: typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1,
|
|
1203
|
+
tasks: parsed.tasks && typeof parsed.tasks === "object" && !Array.isArray(parsed.tasks) ? parsed.tasks : {}
|
|
1204
|
+
};
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function parseTaskStateEnvelope2(raw, baseOid) {
|
|
1210
|
+
if (!raw || !raw.trim()) {
|
|
1211
|
+
return {
|
|
1212
|
+
schemaVersion: 1,
|
|
1213
|
+
supported: true,
|
|
1214
|
+
baseTrackerCommit: baseOid,
|
|
1215
|
+
tasks: {}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
const parsed = readTaskStateMetadataEnvelope(JSON.parse(raw));
|
|
1220
|
+
return {
|
|
1221
|
+
schemaVersion: parsed.supported ? parsed.schemaVersion : 1,
|
|
1222
|
+
supported: true,
|
|
1223
|
+
baseTrackerCommit: parsed.baseTrackerCommit ?? baseOid,
|
|
1224
|
+
tasks: parsed.supported ? parsed.tasks : {}
|
|
1225
|
+
};
|
|
1226
|
+
} catch {
|
|
1227
|
+
return {
|
|
1228
|
+
schemaVersion: 1,
|
|
1229
|
+
supported: true,
|
|
1230
|
+
baseTrackerCommit: baseOid,
|
|
1231
|
+
tasks: {}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function sanitizeTaskStateEnvelope(issueRows, envelope) {
|
|
1236
|
+
const lifecycleByTaskId = new Map;
|
|
1237
|
+
for (const row of issueRows) {
|
|
1238
|
+
const taskId = typeof row.id === "string" ? row.id : null;
|
|
1239
|
+
if (!taskId) {
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
lifecycleByTaskId.set(taskId, normalizeTaskLifecycleStatus(row.status));
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
...envelope,
|
|
1246
|
+
tasks: Object.fromEntries(Object.entries(envelope.tasks).map(([taskId, metadata]) => [
|
|
1247
|
+
taskId,
|
|
1248
|
+
discardMismatchedTaskStateMetadata({
|
|
1249
|
+
taskId,
|
|
1250
|
+
lifecycleStatus: lifecycleByTaskId.get(taskId) ?? null,
|
|
1251
|
+
metadata
|
|
1252
|
+
})
|
|
1253
|
+
]).filter((entry) => entry[1] != null))
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function serializeIssuesJsonl(rows) {
|
|
1257
|
+
return `${rows.map((row) => JSON.stringify(row)).join(`
|
|
1258
|
+
`)}
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
function serializeTaskStateEnvelope(envelope) {
|
|
1262
|
+
return `${JSON.stringify({
|
|
1263
|
+
schemaVersion: envelope.schemaVersion,
|
|
1264
|
+
baseTrackerCommit: envelope.baseTrackerCommit,
|
|
1265
|
+
tasks: envelope.tasks
|
|
1266
|
+
}, null, 2)}
|
|
1267
|
+
`;
|
|
1268
|
+
}
|
|
1269
|
+
function findIssueRowIndex(rows, taskId) {
|
|
1270
|
+
return rows.findIndex((row) => typeof row.id === "string" && row.id === taskId);
|
|
1271
|
+
}
|
|
1272
|
+
function assertWritableTrackerStateBases(state) {
|
|
1273
|
+
if (state.issuesBaseOid !== state.taskStateBaseOid) {
|
|
1274
|
+
throw new TrackerStateMutationError("Tracker writes require issues.jsonl and task-state.json from the same remote base.", "same-base-required");
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function readWritableTrackerState(projectRoot, deps) {
|
|
1278
|
+
const repoPath = resolveTrackerRepoPath(projectRoot);
|
|
1279
|
+
const baseOid = deps.fetchRef(repoPath, "origin", "main");
|
|
1280
|
+
const issuesText = deps.readBlobAtRef(repoPath, baseOid, ".beads/issues.jsonl");
|
|
1281
|
+
const taskStateText = (() => {
|
|
1282
|
+
try {
|
|
1283
|
+
return deps.readBlobAtRef(repoPath, baseOid, ".beads/task-state.json");
|
|
1284
|
+
} catch {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
})();
|
|
1288
|
+
return {
|
|
1289
|
+
repoPath,
|
|
1290
|
+
baseOid,
|
|
1291
|
+
issuesBaseOid: baseOid,
|
|
1292
|
+
taskStateBaseOid: baseOid,
|
|
1293
|
+
issueRows: parseIssuesJsonl2(issuesText),
|
|
1294
|
+
taskStateEnvelope: sanitizeTaskStateEnvelope(parseIssuesJsonl2(issuesText), parseTaskStateEnvelope2(taskStateText, baseOid))
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function cloneWritableState(state) {
|
|
1298
|
+
return {
|
|
1299
|
+
...state,
|
|
1300
|
+
issueRows: state.issueRows.map((row) => ({ ...row })),
|
|
1301
|
+
taskStateEnvelope: {
|
|
1302
|
+
...state.taskStateEnvelope,
|
|
1303
|
+
tasks: Object.fromEntries(Object.entries(state.taskStateEnvelope.tasks).map(([taskId, metadata]) => [taskId, { ...metadata }]))
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function currentLifecycleStatus(state, taskId) {
|
|
1308
|
+
const issueIndex = findIssueRowIndex(state.issueRows, taskId);
|
|
1309
|
+
if (issueIndex === -1) {
|
|
1310
|
+
throw new TrackerStateMutationError(`Task ${taskId} was not found in issues.jsonl.`, "task-not-found");
|
|
1311
|
+
}
|
|
1312
|
+
const status = normalizeTaskLifecycleStatus(state.issueRows[issueIndex]?.status);
|
|
1313
|
+
if (!status) {
|
|
1314
|
+
throw new TrackerStateMutationError(`Task ${taskId} has an invalid lifecycle status.`, "invalid-task-status");
|
|
1315
|
+
}
|
|
1316
|
+
return status;
|
|
1317
|
+
}
|
|
1318
|
+
function setLifecycleStatus(state, taskId, status) {
|
|
1319
|
+
const issueIndex = findIssueRowIndex(state.issueRows, taskId);
|
|
1320
|
+
if (issueIndex === -1) {
|
|
1321
|
+
throw new TrackerStateMutationError(`Task ${taskId} was not found in issues.jsonl.`, "task-not-found");
|
|
1322
|
+
}
|
|
1323
|
+
state.issueRows[issueIndex] = {
|
|
1324
|
+
...state.issueRows[issueIndex],
|
|
1325
|
+
status
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function prepareLifecycleTaskMutation(state, input) {
|
|
1329
|
+
assertWritableTrackerStateBases(state);
|
|
1330
|
+
const next = cloneWritableState(state);
|
|
1331
|
+
const lifecycleStatus = currentLifecycleStatus(next, input.taskId);
|
|
1332
|
+
if (input.allowedFrom && !input.allowedFrom.includes(lifecycleStatus)) {
|
|
1333
|
+
throw new TrackerStateMutationError(`Task ${input.taskId} is ${lifecycleStatus}, expected one of ${input.allowedFrom.join(", ")}.`, "status-conflict");
|
|
1334
|
+
}
|
|
1335
|
+
setLifecycleStatus(next, input.taskId, input.status);
|
|
1336
|
+
next.taskStateEnvelope.baseTrackerCommit = next.baseOid;
|
|
1337
|
+
if (input.clearMetadata || input.metadata == null) {
|
|
1338
|
+
delete next.taskStateEnvelope.tasks[input.taskId];
|
|
1339
|
+
} else {
|
|
1340
|
+
next.taskStateEnvelope.tasks[input.taskId] = input.metadata;
|
|
1341
|
+
}
|
|
1342
|
+
return next;
|
|
1343
|
+
}
|
|
1344
|
+
function desiredTaskStateSatisfied(raw, desired) {
|
|
1345
|
+
const issue = parseIssuesJsonl2(raw.issuesText).find((entry) => entry.id === desired.taskId);
|
|
1346
|
+
if (!issue || issue.status !== desired.status) {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
const rawTaskState = parseRawTaskStateFile(raw.taskStateText);
|
|
1350
|
+
if (!rawTaskState || rawTaskState.schemaVersion !== 1) {
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
const rawMetadata = rawTaskState.tasks[desired.taskId] ?? null;
|
|
1354
|
+
const snapshotMetadata = discardMismatchedTaskStateMetadata({
|
|
1355
|
+
taskId: desired.taskId,
|
|
1356
|
+
lifecycleStatus: desired.status,
|
|
1357
|
+
metadata: rawMetadata
|
|
1358
|
+
});
|
|
1359
|
+
if (desired.metadata == null) {
|
|
1360
|
+
return rawMetadata == null && snapshotMetadata == null;
|
|
1361
|
+
}
|
|
1362
|
+
if (rawMetadata == null || snapshotMetadata == null) {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
return isDeepStrictEqual(snapshotMetadata ?? null, desired.metadata);
|
|
1366
|
+
}
|
|
1367
|
+
function persistTrackerState(projectRoot, state, desired, message, deps) {
|
|
1368
|
+
const updates = [
|
|
1369
|
+
{ path: ".beads/issues.jsonl", content: serializeIssuesJsonl(state.issueRows) },
|
|
1370
|
+
{ path: ".beads/task-state.json", content: serializeTaskStateEnvelope(state.taskStateEnvelope) }
|
|
1371
|
+
];
|
|
1372
|
+
const commitOid = deps.writeTreeCommit(state.repoPath, state.baseOid, updates, message);
|
|
1373
|
+
try {
|
|
1374
|
+
deps.pushRefWithLease(state.repoPath, commitOid, "refs/heads/main", state.baseOid);
|
|
1375
|
+
return {
|
|
1376
|
+
outcome: "applied",
|
|
1377
|
+
snapshot: deps.readSnapshot(projectRoot),
|
|
1378
|
+
commitOid
|
|
1379
|
+
};
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
const latestBaseOid = deps.fetchRef(state.repoPath, "origin", "main");
|
|
1382
|
+
const latestIssuesText = deps.readBlobAtRef(state.repoPath, latestBaseOid, ".beads/issues.jsonl");
|
|
1383
|
+
const latestTaskStateText = (() => {
|
|
1384
|
+
try {
|
|
1385
|
+
return deps.readBlobAtRef(state.repoPath, latestBaseOid, ".beads/task-state.json");
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
})();
|
|
1390
|
+
const latestSnapshot = deps.readSnapshot(projectRoot);
|
|
1391
|
+
if (desiredTaskStateSatisfied({
|
|
1392
|
+
issuesText: latestIssuesText,
|
|
1393
|
+
taskStateText: latestTaskStateText
|
|
1394
|
+
}, desired)) {
|
|
1395
|
+
return {
|
|
1396
|
+
outcome: "noop",
|
|
1397
|
+
snapshot: latestSnapshot,
|
|
1398
|
+
commitOid: null
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
throw new TrackerStateMutationError(error instanceof Error ? error.message : String(error), "push-conflict");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function updateRemoteTrackerTaskLifecycle(projectRoot, input, deps = {}) {
|
|
1405
|
+
const writerDeps = { ...DEFAULT_WRITER_DEPS, ...deps };
|
|
1406
|
+
const nextState = prepareLifecycleTaskMutation(readWritableTrackerState(projectRoot, writerDeps), input);
|
|
1407
|
+
return persistTrackerState(projectRoot, nextState, { taskId: input.taskId, status: input.status, metadata: input.clearMetadata ? null : nextState.taskStateEnvelope.tasks[input.taskId] ?? null }, `chore(tracker): ${input.reason ?? "update"} ${input.taskId} -> ${input.status}`, writerDeps);
|
|
1408
|
+
}
|
|
1409
|
+
// packages/task-sources-plugin/src/control-plane/native/task-state.ts
|
|
1410
|
+
function readTaskConfig(projectRoot) {
|
|
1411
|
+
const raw = readJsonFile(resolveTaskConfigPath(projectRoot), {});
|
|
1412
|
+
return stripTaskConfigMetadata(raw);
|
|
1413
|
+
}
|
|
1414
|
+
function readSourceTaskConfig(projectRoot) {
|
|
1415
|
+
const raw = readAndSyncSourceTaskConfig(projectRoot);
|
|
1416
|
+
return stripTaskConfigMetadata(raw);
|
|
1417
|
+
}
|
|
1418
|
+
function readValidationDescriptions(projectRoot) {
|
|
1419
|
+
const raw = readJsonFile(resolveTaskConfigPath(projectRoot), {});
|
|
1420
|
+
return readValidationDescriptionMap(raw);
|
|
1421
|
+
}
|
|
1422
|
+
function readSourceValidationDescriptions(projectRoot) {
|
|
1423
|
+
const rootRaw = readJsonFile(resolve6(projectRoot, "rig", "task-config.json"), {});
|
|
1424
|
+
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
1425
|
+
const sourceRaw = sourcePath ? readJsonFile(sourcePath, {}) : {};
|
|
1426
|
+
const rootDescriptions = readValidationDescriptionMap(rootRaw);
|
|
1427
|
+
const sourceDescriptions = readValidationDescriptionMap(sourceRaw);
|
|
1428
|
+
return {
|
|
1429
|
+
...rootDescriptions,
|
|
1430
|
+
...sourceDescriptions
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function currentTaskId(projectRoot) {
|
|
1434
|
+
const fromEnv = (process.env.RIG_TASK_ID || "").trim();
|
|
1435
|
+
if (fromEnv) {
|
|
1436
|
+
return fromEnv;
|
|
1437
|
+
}
|
|
1438
|
+
const runtimeId = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
|
|
1439
|
+
if (runtimeId.startsWith("task-") && runtimeId.length > "task-".length) {
|
|
1440
|
+
return runtimeId.slice("task-".length);
|
|
1441
|
+
}
|
|
1442
|
+
const workspace = (process.env.RIG_TASK_WORKSPACE || "").trim();
|
|
1443
|
+
const inferredFromWorkspace = inferTaskIdFromRuntimePath(workspace);
|
|
1444
|
+
if (inferredFromWorkspace) {
|
|
1445
|
+
return inferredFromWorkspace;
|
|
1446
|
+
}
|
|
1447
|
+
const inferredFromCwd = inferTaskIdFromRuntimePath(process.cwd());
|
|
1448
|
+
if (inferredFromCwd) {
|
|
1449
|
+
return inferredFromCwd;
|
|
1450
|
+
}
|
|
1451
|
+
try {
|
|
1452
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1453
|
+
const session = readJsonFile(paths.sessionPath, {});
|
|
1454
|
+
return session.activeTaskIds?.[0] || "";
|
|
1455
|
+
} catch {
|
|
1456
|
+
return "";
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function readSourceTaskStateMetadata(projectRoot, taskId, lifecycleStatus, snapshot) {
|
|
1460
|
+
const syncedSnapshot = snapshot ?? readSyncedTrackerState(projectRoot);
|
|
1461
|
+
const syncedIssue = syncedSnapshot.issues.find((issue) => issue.id === taskId) ?? null;
|
|
1462
|
+
const syncedLifecycleStatus = syncedIssue && syncedIssue.status !== "unknown" ? syncedIssue.status : readLocalSourceTaskLifecycleStatus(projectRoot, taskId);
|
|
1463
|
+
const metadata = syncedSnapshot.taskState.tasks[taskId] ?? readLocalSourceTaskStateEnvelope(projectRoot).tasks[taskId] ?? null;
|
|
1464
|
+
return discardMismatchedTaskStateMetadata({
|
|
1465
|
+
taskId,
|
|
1466
|
+
lifecycleStatus: syncedLifecycleStatus ?? normalizeTaskLifecycleStatus(lifecycleStatus),
|
|
1467
|
+
metadata
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
function readValidationDescriptionMap(raw) {
|
|
1471
|
+
return {
|
|
1472
|
+
...coerceValidationDescriptions(raw.validation_descriptions),
|
|
1473
|
+
...coerceValidationDescriptions(readValidationDescriptionsFromMeta(raw._meta))
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function stripTaskConfigMetadata(raw) {
|
|
1477
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
1478
|
+
return tasks;
|
|
1479
|
+
}
|
|
1480
|
+
function coerceValidationDescriptions(candidate) {
|
|
1481
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
1482
|
+
return {};
|
|
1483
|
+
}
|
|
1484
|
+
const descriptions = {};
|
|
1485
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
1486
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1487
|
+
descriptions[key] = value;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return descriptions;
|
|
1491
|
+
}
|
|
1492
|
+
function readValidationDescriptionsFromMeta(meta) {
|
|
1493
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
return meta.validation_descriptions;
|
|
1497
|
+
}
|
|
1498
|
+
function readLocalSourceTaskStateEnvelope(projectRoot) {
|
|
1499
|
+
const taskStatePath = resolve6(resolveCheckoutRoot(projectRoot), ".beads", "task-state.json");
|
|
1500
|
+
return readTaskStateMetadataEnvelope(readJsonFile(taskStatePath, {}));
|
|
1501
|
+
}
|
|
1502
|
+
function readLocalSourceTaskLifecycleStatus(projectRoot, taskId) {
|
|
1503
|
+
const issuesPath = resolve6(resolveCheckoutRoot(projectRoot), ".beads", "issues.jsonl");
|
|
1504
|
+
if (!existsSync7(issuesPath)) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
for (const line of readFileSync6(issuesPath, "utf8").split(/\r?\n/)) {
|
|
1508
|
+
const trimmed = line.trim();
|
|
1509
|
+
if (!trimmed) {
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
try {
|
|
1513
|
+
const parsed = JSON.parse(trimmed);
|
|
1514
|
+
if (parsed.id === taskId && parsed.issue_type === "task") {
|
|
1515
|
+
return normalizeTaskLifecycleStatus(parsed.status);
|
|
1516
|
+
}
|
|
1517
|
+
} catch {}
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
function inferTaskIdFromRuntimePath(path) {
|
|
1522
|
+
if (!path) {
|
|
1523
|
+
return "";
|
|
1524
|
+
}
|
|
1525
|
+
const match = path.match(/\/\.rig\/runtime\/agents\/task-([^/]+)\/worktree(?:\/|$)/) || path.match(/\/\.worktrees\/([^/]+)(?:\/|$)/);
|
|
1526
|
+
const candidate = match?.[1] || "";
|
|
1527
|
+
return candidate.startsWith("bd-") ? candidate : "";
|
|
1528
|
+
}
|
|
1529
|
+
function lookupTask(projectRoot, input) {
|
|
1530
|
+
if (!input) {
|
|
1531
|
+
return "";
|
|
1532
|
+
}
|
|
1533
|
+
if (!input.startsWith("bd-")) {
|
|
1534
|
+
return input;
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const taskConfig2 = readTaskConfig(projectRoot);
|
|
1538
|
+
if (taskConfig2[input]) {
|
|
1539
|
+
return input;
|
|
1540
|
+
}
|
|
1541
|
+
} catch {}
|
|
1542
|
+
const taskConfig = readSourceTaskConfig(projectRoot);
|
|
1543
|
+
return taskConfig[input] ? input : "";
|
|
1544
|
+
}
|
|
1545
|
+
function artifactDirForId(projectRoot, id) {
|
|
1546
|
+
const safeId = safePathSegment(id, { fallback: "task", maxLength: 96 });
|
|
1547
|
+
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1548
|
+
if (workspaceDir) {
|
|
1549
|
+
const worktreeArtifactsRoot = resolve6(workspaceDir, "artifacts");
|
|
1550
|
+
const worktreeArtifacts = assertPathInsideRoot(worktreeArtifactsRoot, resolve6(worktreeArtifactsRoot, safeId), "artifact directory");
|
|
1551
|
+
if (existsSync7(worktreeArtifacts) || existsSync7(worktreeArtifactsRoot)) {
|
|
1552
|
+
return worktreeArtifacts;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1557
|
+
return assertPathInsideRoot(paths.artifactsDir, resolve6(paths.artifactsDir, safeId), "artifact directory");
|
|
1558
|
+
} catch {
|
|
1559
|
+
const artifactsRoot = resolve6(resolveCheckoutRoot(projectRoot), "artifacts");
|
|
1560
|
+
return assertPathInsideRoot(artifactsRoot, resolve6(artifactsRoot, safeId), "artifact directory");
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function resolveTaskConfigPath(projectRoot) {
|
|
1564
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1565
|
+
if (existsSync7(paths.taskConfigPath)) {
|
|
1566
|
+
return paths.taskConfigPath;
|
|
1567
|
+
}
|
|
1568
|
+
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1569
|
+
if (existsSync7(candidate)) {
|
|
1570
|
+
return candidate;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
throw new Error(`Task config missing at ${paths.taskConfigPath}.`);
|
|
1574
|
+
}
|
|
1575
|
+
function findSourceTaskConfigPath(projectRoot) {
|
|
1576
|
+
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1577
|
+
if (existsSync7(candidate)) {
|
|
1578
|
+
return candidate;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
var FILE_TASK_PATTERN2 = /\.(task\.)?json$/;
|
|
1584
|
+
function readAndSyncSourceTaskConfig(projectRoot) {
|
|
1585
|
+
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
1586
|
+
const raw = sourcePath ? readJsonFile(sourcePath, {}) : readConfiguredFileTaskConfig(projectRoot);
|
|
1587
|
+
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
1588
|
+
if (sourcePath && synced.updated) {
|
|
1589
|
+
try {
|
|
1590
|
+
writeFileSync3(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
1591
|
+
`, "utf-8");
|
|
1592
|
+
} catch {}
|
|
1593
|
+
}
|
|
1594
|
+
return synced.config;
|
|
1595
|
+
}
|
|
1596
|
+
function synchronizeTaskConfigWithTracker(projectRoot, rawConfig) {
|
|
1597
|
+
const issues = readSourceIssueRecords(projectRoot);
|
|
1598
|
+
if (issues.length === 0) {
|
|
1599
|
+
return { config: rawConfig, updated: false };
|
|
1600
|
+
}
|
|
1601
|
+
const taskConfig = stripTaskConfigMetadata(rawConfig);
|
|
1602
|
+
const mergedConfig = { ...taskConfig };
|
|
1603
|
+
const validationDescriptions = coerceValidationDescriptions(rawConfig.validation_descriptions);
|
|
1604
|
+
const metaValidationDescriptions = coerceValidationDescriptions(readValidationDescriptionsFromMeta(rawConfig._meta));
|
|
1605
|
+
let updated = false;
|
|
1606
|
+
for (const issue of issues) {
|
|
1607
|
+
if (issue.issueType !== "task") {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (mergedConfig[issue.id] && !shouldRefreshAutoSyncedTaskConfigEntry(mergedConfig[issue.id])) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
mergedConfig[issue.id] = buildAutoSyncedTaskConfigEntry(issue);
|
|
1614
|
+
updated = true;
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
config: {
|
|
1618
|
+
...mergedConfig,
|
|
1619
|
+
...Object.keys(validationDescriptions).length > 0 ? { validation_descriptions: validationDescriptions } : {},
|
|
1620
|
+
...Object.keys(metaValidationDescriptions).length > 0 ? { _meta: { validation_descriptions: metaValidationDescriptions } } : {}
|
|
1621
|
+
},
|
|
1622
|
+
updated
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
1626
|
+
if (!entry || typeof entry !== "object") {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
const candidate = entry;
|
|
1630
|
+
if (!candidate.auto_synced) {
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
if (!Array.isArray(candidate.scope) || candidate.scope.length === 0) {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
if (candidate.scope.some((glob) => typeof glob !== "string" || glob.trim().length === 0)) {
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
return !candidate.role;
|
|
1640
|
+
}
|
|
1641
|
+
function readSourceIssueRecords(projectRoot) {
|
|
1642
|
+
const issuesPath = resolve6(resolveCheckoutRoot(projectRoot), ".beads", "issues.jsonl");
|
|
1643
|
+
if (!existsSync7(issuesPath)) {
|
|
1644
|
+
return [];
|
|
1645
|
+
}
|
|
1646
|
+
const records = [];
|
|
1647
|
+
for (const line of readFileSync6(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
1648
|
+
const trimmed = line.trim();
|
|
1649
|
+
if (!trimmed) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
try {
|
|
1653
|
+
const parsed = JSON.parse(trimmed);
|
|
1654
|
+
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
1655
|
+
if (!id) {
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
records.push({
|
|
1659
|
+
id,
|
|
1660
|
+
title: typeof parsed.title === "string" ? parsed.title.trim() : "",
|
|
1661
|
+
issueType: typeof parsed.issue_type === "string" ? parsed.issue_type.trim() : null,
|
|
1662
|
+
labels: Array.isArray(parsed.labels) ? parsed.labels.filter((label) => typeof label === "string") : []
|
|
1663
|
+
});
|
|
1664
|
+
} catch {}
|
|
1665
|
+
}
|
|
1666
|
+
return records;
|
|
1667
|
+
}
|
|
1668
|
+
function buildAutoSyncedTaskConfigEntry(issue) {
|
|
1669
|
+
return {
|
|
1670
|
+
auto_synced: true,
|
|
1671
|
+
role: inferAutoSyncedTaskRole(issue),
|
|
1672
|
+
scope: inferAutoSyncedTaskScope(issue),
|
|
1673
|
+
validation: []
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function inferAutoSyncedTaskRole(issue) {
|
|
1677
|
+
for (const label of issue.labels) {
|
|
1678
|
+
if (label === "role:architect")
|
|
1679
|
+
return "architect";
|
|
1680
|
+
if (label === "role:extractor")
|
|
1681
|
+
return "extractor";
|
|
1682
|
+
if (label === "role:mechanic")
|
|
1683
|
+
return "mechanic";
|
|
1684
|
+
if (label === "role:verifier")
|
|
1685
|
+
return "verifier";
|
|
1686
|
+
}
|
|
1687
|
+
if (/\bDESIGN\b/i.test(issue.title)) {
|
|
1688
|
+
return "architect";
|
|
1689
|
+
}
|
|
1690
|
+
if (/\bInitialize\b/i.test(issue.title)) {
|
|
1691
|
+
return "mechanic";
|
|
1692
|
+
}
|
|
1693
|
+
return "extractor";
|
|
1694
|
+
}
|
|
1695
|
+
function inferAutoSyncedTaskScope(issue) {
|
|
1696
|
+
return [`artifacts/${issue.id}/**`];
|
|
1697
|
+
}
|
|
1698
|
+
function readConfiguredFileTaskConfig(projectRoot) {
|
|
1699
|
+
const sourcePath = readConfiguredFilesTaskSourcePath2(projectRoot);
|
|
1700
|
+
if (!sourcePath) {
|
|
1701
|
+
return {};
|
|
1702
|
+
}
|
|
1703
|
+
const directory = resolve6(projectRoot, sourcePath);
|
|
1704
|
+
if (!existsSync7(directory)) {
|
|
1705
|
+
return {};
|
|
1706
|
+
}
|
|
1707
|
+
const config = {};
|
|
1708
|
+
for (const name of readdirSync2(directory)) {
|
|
1709
|
+
if (!FILE_TASK_PATTERN2.test(name))
|
|
1710
|
+
continue;
|
|
1711
|
+
const file = resolve6(directory, name);
|
|
1712
|
+
try {
|
|
1713
|
+
if (!statSync2(file).isFile())
|
|
1714
|
+
continue;
|
|
1715
|
+
const raw = JSON.parse(readFileSync6(file, "utf8"));
|
|
1716
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1717
|
+
continue;
|
|
1718
|
+
const record = raw;
|
|
1719
|
+
const inferredId = basename2(name).replace(FILE_TASK_PATTERN2, "");
|
|
1720
|
+
const id = typeof record.id === "string" && record.id.trim().length > 0 ? record.id.trim() : inferredId;
|
|
1721
|
+
config[id] = fileTaskToConfigEntry(record, { kind: "files", path: sourcePath });
|
|
1722
|
+
} catch {}
|
|
1723
|
+
}
|
|
1724
|
+
return config;
|
|
1725
|
+
}
|
|
1726
|
+
function fileTaskToConfigEntry(task, source) {
|
|
1727
|
+
const labels = Array.isArray(task.labels) ? task.labels.filter((label) => typeof label === "string") : [];
|
|
1728
|
+
const scope = firstStringList2(task.scope, labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length)));
|
|
1729
|
+
const validation = firstStringList2(task.validation, task.validators, labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length)));
|
|
1730
|
+
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
1731
|
+
const role = typeof task.role === "string" && task.role.trim().length > 0 ? task.role.trim() : roleLabel ? roleLabel.slice("role:".length) : undefined;
|
|
1732
|
+
return {
|
|
1733
|
+
auto_synced: true,
|
|
1734
|
+
...typeof task.title === "string" ? { title: task.title } : {},
|
|
1735
|
+
...typeof task.status === "string" ? { status: task.status } : {},
|
|
1736
|
+
...typeof task.description === "string" ? { description: task.description } : {},
|
|
1737
|
+
...typeof task.acceptance_criteria === "string" ? { acceptance_criteria: task.acceptance_criteria } : typeof task.acceptanceCriteria === "string" ? { acceptance_criteria: task.acceptanceCriteria } : {},
|
|
1738
|
+
...role ? { role } : {},
|
|
1739
|
+
...scope.length > 0 ? { scope } : {},
|
|
1740
|
+
...validation.length > 0 ? { validation } : {},
|
|
1741
|
+
_rig: { taskSource: source }
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function firstStringList2(...candidates) {
|
|
1745
|
+
for (const candidate of candidates) {
|
|
1746
|
+
if (!Array.isArray(candidate)) {
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
const list = candidate.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
1750
|
+
if (list.length > 0) {
|
|
1751
|
+
return list;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return [];
|
|
1755
|
+
}
|
|
1756
|
+
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
1757
|
+
const jsonPath = resolve6(projectRoot, "rig.config.json");
|
|
1758
|
+
if (existsSync7(jsonPath)) {
|
|
1759
|
+
try {
|
|
1760
|
+
const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
|
|
1761
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1762
|
+
const taskSource = parsed.taskSource;
|
|
1763
|
+
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
1764
|
+
const record = taskSource;
|
|
1765
|
+
return record.kind === "files" && typeof record.path === "string" ? record.path : null;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const tsPath = resolve6(projectRoot, "rig.config.ts");
|
|
1773
|
+
if (!existsSync7(tsPath)) {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
const source = readFileSync6(tsPath, "utf8");
|
|
1778
|
+
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
1779
|
+
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
1780
|
+
if (kind !== "files") {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
1784
|
+
} catch {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function sourceTaskConfigCandidates(projectRoot) {
|
|
1789
|
+
const runtimeContext = loadRuntimeContextFromEnv();
|
|
1790
|
+
return [
|
|
1791
|
+
runtimeContext?.monorepoMainRoot ? resolve6(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
1792
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve6(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
1793
|
+
resolve6(resolveCheckoutRoot(projectRoot), ".rig", "task-config.json")
|
|
1794
|
+
].filter(Boolean);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// packages/task-sources-plugin/src/control-plane/native/validator.ts
|
|
1798
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1799
|
+
import { resolve as resolve9 } from "path";
|
|
1800
|
+
import { assertPathInsideRoot as assertPathInsideRoot2, safePathSegment as safePathSegment2 } from "@rig/core/safe-identifiers";
|
|
1801
|
+
import { resolveMonorepoRoot } from "@rig/core/layout";
|
|
1802
|
+
|
|
1803
|
+
// packages/task-sources-plugin/src/control-plane/native/validator-binaries.ts
|
|
1804
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync3 } from "fs";
|
|
1805
|
+
import { dirname as dirname3, resolve as resolve8 } from "path";
|
|
1806
|
+
import { runtimeProvisioningEnv } from "@rig/core/runtime-provisioning-env";
|
|
1807
|
+
import { resolveBunBinaryPath } from "@rig/core/runtime-paths";
|
|
1808
|
+
|
|
1809
|
+
// packages/task-sources-plugin/src/control-plane/native/runtime-binary-build.ts
|
|
1810
|
+
import { chmodSync as chmodSync2, existsSync as existsSync8, mkdirSync as mkdirSync2, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
|
|
1811
|
+
import { basename as basename3, dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve7 } from "path";
|
|
1812
|
+
var runtimeBinaryBuildQueue = Promise.resolve();
|
|
1813
|
+
async function buildRuntimeBinary(options) {
|
|
1814
|
+
await runSerializedRuntimeBinaryBuild(async () => {
|
|
1815
|
+
const entrypoint = isAbsolute2(options.sourcePath) ? options.sourcePath : resolve7(options.cwd, options.sourcePath);
|
|
1816
|
+
const outputPath = resolve7(options.outputPath);
|
|
1817
|
+
const tempBuildDir = resolve7(dirname2(outputPath), `.bun-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
1818
|
+
const tempOutputPath = resolve7(tempBuildDir, basename3(outputPath));
|
|
1819
|
+
mkdirSync2(tempBuildDir, { recursive: true });
|
|
1820
|
+
await withTemporaryEnv({
|
|
1821
|
+
...options.env,
|
|
1822
|
+
...options.define ? { RIG_BUILD_CONFIG_JSON: JSON.stringify(options.define) } : {}
|
|
1823
|
+
}, async () => withTemporaryCwd(tempBuildDir, async () => {
|
|
1824
|
+
const buildResult = await Bun.build({
|
|
1825
|
+
entrypoints: [entrypoint],
|
|
1826
|
+
compile: {
|
|
1827
|
+
target: currentCompileTarget(),
|
|
1828
|
+
outfile: tempOutputPath
|
|
1829
|
+
},
|
|
1830
|
+
target: "bun",
|
|
1831
|
+
format: "esm",
|
|
1832
|
+
minify: true,
|
|
1833
|
+
bytecode: true,
|
|
1834
|
+
...options.external ? { external: options.external } : {},
|
|
1835
|
+
...options.define ? { define: { __RIG_BUILD_CONFIG__: JSON.stringify(options.define) } } : {}
|
|
1836
|
+
});
|
|
1837
|
+
if (!buildResult.success) {
|
|
1838
|
+
const details = buildResult.logs.map((log) => [
|
|
1839
|
+
log.message,
|
|
1840
|
+
log.position?.file ? `${log.position.file}:${log.position.line}:${log.position.column}` : ""
|
|
1841
|
+
].filter(Boolean).join(" ")).filter(Boolean).join(`
|
|
1842
|
+
`);
|
|
1843
|
+
throw new Error(`Failed to build ${entrypoint}: ${details || "Bun.build() returned errors"}`);
|
|
1844
|
+
}
|
|
1845
|
+
if (!existsSync8(tempOutputPath)) {
|
|
1846
|
+
const emitted = buildResult.outputs.map((output) => output.path).join(", ") || "(none)";
|
|
1847
|
+
throw new Error(`Failed to build ${entrypoint}: Bun.build() did not emit ${tempOutputPath}. Emitted: ${emitted}`);
|
|
1848
|
+
}
|
|
1849
|
+
renameSync2(tempOutputPath, outputPath);
|
|
1850
|
+
chmodSync2(outputPath, 493);
|
|
1851
|
+
})).finally(() => {
|
|
1852
|
+
rmSync2(tempBuildDir, { recursive: true, force: true });
|
|
1853
|
+
});
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function currentCompileTarget() {
|
|
1857
|
+
if (process.platform === "darwin") {
|
|
1858
|
+
return process.arch === "arm64" ? "bun-darwin-arm64" : "bun-darwin-x64";
|
|
1859
|
+
}
|
|
1860
|
+
if (process.platform === "linux") {
|
|
1861
|
+
return process.arch === "arm64" ? "bun-linux-arm64" : "bun-linux-x64";
|
|
1862
|
+
}
|
|
1863
|
+
return "bun-windows-x64";
|
|
1864
|
+
}
|
|
1865
|
+
async function runSerializedRuntimeBinaryBuild(action) {
|
|
1866
|
+
const previous = runtimeBinaryBuildQueue;
|
|
1867
|
+
let release;
|
|
1868
|
+
runtimeBinaryBuildQueue = new Promise((resolveRelease) => {
|
|
1869
|
+
release = resolveRelease;
|
|
1870
|
+
});
|
|
1871
|
+
await previous;
|
|
1872
|
+
try {
|
|
1873
|
+
return await action();
|
|
1874
|
+
} finally {
|
|
1875
|
+
release();
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function withTemporaryEnv(env, action) {
|
|
1879
|
+
if (!env) {
|
|
1880
|
+
return action();
|
|
1881
|
+
}
|
|
1882
|
+
const previousValues = new Map;
|
|
1883
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1884
|
+
previousValues.set(key, process.env[key]);
|
|
1885
|
+
if (value === undefined) {
|
|
1886
|
+
delete process.env[key];
|
|
1887
|
+
} else {
|
|
1888
|
+
process.env[key] = value;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
return await action();
|
|
1893
|
+
} finally {
|
|
1894
|
+
for (const [key, value] of previousValues.entries()) {
|
|
1895
|
+
if (value === undefined) {
|
|
1896
|
+
delete process.env[key];
|
|
1897
|
+
} else {
|
|
1898
|
+
process.env[key] = value;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async function withTemporaryCwd(cwd, action) {
|
|
1904
|
+
const previousCwd = process.cwd();
|
|
1905
|
+
process.chdir(cwd);
|
|
1906
|
+
try {
|
|
1907
|
+
return await action();
|
|
1908
|
+
} finally {
|
|
1909
|
+
process.chdir(previousCwd);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// packages/task-sources-plugin/src/control-plane/native/validator-binaries.ts
|
|
1914
|
+
function resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext) {
|
|
1915
|
+
if (runtimeContext) {
|
|
1916
|
+
return resolve8(runtimeContext.binDir, "validators", binaryName);
|
|
1917
|
+
}
|
|
1918
|
+
return resolve8(resolveHarnessPaths(projectRoot).binDir, "validators", binaryName);
|
|
1919
|
+
}
|
|
1920
|
+
async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
1921
|
+
const match = checkId.match(/^([a-z][\w-]*):([a-z][\w-]*)$/);
|
|
1922
|
+
if (!match) {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
const category = match[1];
|
|
1926
|
+
const check = match[2];
|
|
1927
|
+
if (!category || !check) {
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
const binaryName = `${category}-${check}`;
|
|
1931
|
+
const binaryPath = resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
1932
|
+
const hostProjectRoot = runtimeContext?.hostProjectRoot?.trim() || projectRoot;
|
|
1933
|
+
const sourcePath = resolve8(hostProjectRoot, "packages/validator-kit/src/validators", category, `${check}.ts`);
|
|
1934
|
+
if (!existsSync9(sourcePath)) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
const sourceMtime = statSync3(sourcePath).mtimeMs;
|
|
1938
|
+
const binaryExists = existsSync9(binaryPath);
|
|
1939
|
+
const binaryMtime = binaryExists ? statSync3(binaryPath).mtimeMs : 0;
|
|
1940
|
+
if (!binaryExists || sourceMtime > binaryMtime) {
|
|
1941
|
+
if (binaryExists) {
|
|
1942
|
+
rmSync3(binaryPath, { force: true });
|
|
1943
|
+
rmSync3(`${binaryPath}.build-manifest.json`, { force: true });
|
|
1944
|
+
}
|
|
1945
|
+
mkdirSync3(dirname3(binaryPath), { recursive: true });
|
|
1946
|
+
await buildRuntimeBinary({
|
|
1947
|
+
sourcePath: `packages/validator-kit/src/validators/${category}/${check}.ts`,
|
|
1948
|
+
outputPath: binaryPath,
|
|
1949
|
+
cwd: hostProjectRoot,
|
|
1950
|
+
define: { AGENT_BUN_PATH: resolveBunBinaryPath() },
|
|
1951
|
+
env: runtimeProvisioningEnv()
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
return existsSync9(binaryPath) ? binaryPath : null;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// packages/task-sources-plugin/src/control-plane/native/validator.ts
|
|
1958
|
+
import { createValidatorRegistry } from "@rig/validator-kit";
|
|
1959
|
+
function isCheckId(entry) {
|
|
1960
|
+
return /^[a-z][\w-]*:[a-z][\w-]*$/.test(entry);
|
|
1961
|
+
}
|
|
1962
|
+
function stringArray(candidate) {
|
|
1963
|
+
return Array.isArray(candidate) ? candidate.filter((entry) => typeof entry === "string") : [];
|
|
1964
|
+
}
|
|
1965
|
+
function safeReadTaskConfig(projectRoot) {
|
|
1966
|
+
try {
|
|
1967
|
+
return readTaskConfig(projectRoot);
|
|
1968
|
+
} catch {
|
|
1969
|
+
return {};
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
async function readTaskSourceValidation(projectRoot, taskId) {
|
|
1973
|
+
const sourceTask = await readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task).catch(() => null);
|
|
1974
|
+
if (!sourceTask) {
|
|
1975
|
+
return { validation: [], scope: [], taskConfig: undefined };
|
|
1976
|
+
}
|
|
1977
|
+
const record = sourceTask;
|
|
1978
|
+
const validation = stringArray(record.validation).length > 0 ? stringArray(record.validation) : stringArray(record.validators);
|
|
1979
|
+
return {
|
|
1980
|
+
validation,
|
|
1981
|
+
scope: stringArray(record.scope),
|
|
1982
|
+
taskConfig: {
|
|
1983
|
+
...typeof record.role === "string" ? { role: record.role } : {},
|
|
1984
|
+
...stringArray(record.scope).length > 0 ? { scope: stringArray(record.scope) } : {},
|
|
1985
|
+
...validation.length > 0 ? { validation } : {}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function resolveValidationPaths(projectRoot, taskId, runtimeContext) {
|
|
1990
|
+
const taskSegment = safePathSegment2(taskId, { fallback: "task", maxLength: 96 });
|
|
1991
|
+
if (runtimeContext) {
|
|
1992
|
+
const runtimeArtifactsRoot = resolve9(runtimeContext.workspaceDir, "artifacts");
|
|
1993
|
+
return {
|
|
1994
|
+
taskLogDir: assertPathInsideRoot2(runtimeContext.logsDir, resolve9(runtimeContext.logsDir, taskSegment), "validation log directory"),
|
|
1995
|
+
artifactDir: assertPathInsideRoot2(runtimeArtifactsRoot, resolve9(runtimeArtifactsRoot, taskSegment), "validation artifact directory")
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1999
|
+
return {
|
|
2000
|
+
taskLogDir: assertPathInsideRoot2(paths.logsDir, resolve9(paths.logsDir, taskSegment), "validation log directory"),
|
|
2001
|
+
artifactDir: assertPathInsideRoot2(paths.artifactsDir, resolve9(paths.artifactsDir, taskSegment), "validation artifact directory")
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext) {
|
|
2005
|
+
const binaryName = checkId.replace(":", "-");
|
|
2006
|
+
const binaryPath = await ensureValidatorBinary(projectRoot, checkId, runtimeContext) ?? resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
2007
|
+
if (!existsSync10(binaryPath)) {
|
|
2008
|
+
return {
|
|
2009
|
+
result: {
|
|
2010
|
+
id: checkId,
|
|
2011
|
+
passed: false,
|
|
2012
|
+
summary: `Validator binary not found: ${binaryPath}`
|
|
2013
|
+
},
|
|
2014
|
+
exitCode: 2
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
const validatorCwd = runtimeContext?.workspaceDir || resolveMonorepoRoot(projectRoot);
|
|
2018
|
+
const runtimeShellPath = runtimeContext ? resolve9(runtimeContext.binDir, "rig-shell") : "";
|
|
2019
|
+
const monorepoMainRoot = runtimeContext?.monorepoMainRoot || process.env.MONOREPO_MAIN_ROOT?.trim() || resolveMonorepoRoot(projectRoot);
|
|
2020
|
+
const validatorEnv = {
|
|
2021
|
+
PROJECT_RIG_ROOT: runtimeContext?.hostProjectRoot || projectRoot,
|
|
2022
|
+
RIG_HOST_PROJECT_ROOT: projectRoot,
|
|
2023
|
+
RIG_TASK_WORKSPACE: validatorCwd,
|
|
2024
|
+
MONOREPO_ROOT: validatorCwd,
|
|
2025
|
+
MONOREPO_MAIN_ROOT: monorepoMainRoot,
|
|
2026
|
+
RIG_TASK_ID: taskId
|
|
2027
|
+
};
|
|
2028
|
+
if (runtimeContext) {
|
|
2029
|
+
validatorEnv.RIG_TASK_RUNTIME_ID = runtimeContext.runtimeId;
|
|
2030
|
+
validatorEnv.RIG_LOGS_DIR = runtimeContext.logsDir;
|
|
2031
|
+
validatorEnv.RIG_RUNTIME_BIN_DIR = runtimeContext.binDir;
|
|
2032
|
+
}
|
|
2033
|
+
const { exitCode, stdout, stderr } = await runCaptureAsync(runtimeShellPath && existsSync10(runtimeShellPath) ? [runtimeShellPath, "run-binary", binaryPath] : [binaryPath], validatorCwd, validatorEnv);
|
|
2034
|
+
try {
|
|
2035
|
+
const result = JSON.parse(stdout.trim());
|
|
2036
|
+
return { result, exitCode };
|
|
2037
|
+
} catch {
|
|
2038
|
+
return {
|
|
2039
|
+
result: {
|
|
2040
|
+
id: checkId,
|
|
2041
|
+
passed: false,
|
|
2042
|
+
summary: `Failed to parse validator output: ${stderr || stdout}`.slice(0, 200)
|
|
2043
|
+
},
|
|
2044
|
+
exitCode: exitCode || 2
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
async function dispatchValidator(checkId, registry, ctx, subprocessFallback) {
|
|
2049
|
+
let registered;
|
|
2050
|
+
try {
|
|
2051
|
+
registered = registry.resolve(checkId);
|
|
2052
|
+
} catch {
|
|
2053
|
+
return subprocessFallback(checkId);
|
|
2054
|
+
}
|
|
2055
|
+
const validatorResult = await registered.run(ctx);
|
|
2056
|
+
return {
|
|
2057
|
+
result: {
|
|
2058
|
+
id: validatorResult.id,
|
|
2059
|
+
passed: validatorResult.passed,
|
|
2060
|
+
summary: validatorResult.summary,
|
|
2061
|
+
...validatorResult.details !== undefined ? { details: validatorResult.details } : {}
|
|
2062
|
+
},
|
|
2063
|
+
exitCode: validatorResult.passed ? 0 : 1
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
async function validateTask(projectRoot, taskId, runtimeContext, registry, options = {}) {
|
|
2067
|
+
const resolvedContext = runtimeContext ?? null;
|
|
2068
|
+
const taskConfig = resolvedContext ? {} : options.taskConfig ?? safeReadTaskConfig(projectRoot);
|
|
2069
|
+
const sourceValidation = !resolvedContext ? await readTaskSourceValidation(projectRoot, taskId) : { validation: [], scope: [], taskConfig: undefined };
|
|
2070
|
+
const configuredValidation = stringArray(taskConfig[taskId]?.validation);
|
|
2071
|
+
const commands = resolvedContext?.validation?.length ? resolvedContext.validation : configuredValidation.length > 0 ? configuredValidation : sourceValidation.validation;
|
|
2072
|
+
const { taskLogDir, artifactDir } = resolveValidationPaths(projectRoot, taskId, resolvedContext);
|
|
2073
|
+
mkdirSync4(taskLogDir, { recursive: true });
|
|
2074
|
+
mkdirSync4(artifactDir, { recursive: true });
|
|
2075
|
+
if (commands.length === 0) {
|
|
2076
|
+
const skipped = {
|
|
2077
|
+
status: "skipped",
|
|
2078
|
+
total: 0,
|
|
2079
|
+
passed: 0,
|
|
2080
|
+
failed: 0,
|
|
2081
|
+
categories: []
|
|
2082
|
+
};
|
|
2083
|
+
writeFileSync4(assertPathInsideRoot2(artifactDir, resolve9(artifactDir, "validation-summary.json"), "validation summary file"), `${JSON.stringify(skipped, null, 2)}
|
|
2084
|
+
`, "utf-8");
|
|
2085
|
+
return skipped;
|
|
2086
|
+
}
|
|
2087
|
+
const effectiveRegistry = registry ?? createValidatorRegistry();
|
|
2088
|
+
const workspaceRoot = resolvedContext?.workspaceDir ?? resolveMonorepoRoot(projectRoot);
|
|
2089
|
+
const monorepoRoot = resolvedContext?.monorepoMainRoot ?? process.env.MONOREPO_MAIN_ROOT?.trim() ?? resolveMonorepoRoot(projectRoot);
|
|
2090
|
+
const validatorCtx = {
|
|
2091
|
+
taskId,
|
|
2092
|
+
workspaceRoot,
|
|
2093
|
+
scope: resolvedContext?.scopes ?? (stringArray(taskConfig[taskId]?.scope).length > 0 ? stringArray(taskConfig[taskId]?.scope) : sourceValidation.scope),
|
|
2094
|
+
monorepoRoot,
|
|
2095
|
+
artifactsDir: artifactDir,
|
|
2096
|
+
taskConfig: sourceValidation.taskConfig ?? taskConfig[taskId] ?? undefined
|
|
2097
|
+
};
|
|
2098
|
+
const valDescriptions = resolvedContext ? {} : options.validationDescriptions ?? (() => {
|
|
2099
|
+
try {
|
|
2100
|
+
return readValidationDescriptions(projectRoot);
|
|
2101
|
+
} catch {
|
|
2102
|
+
return {};
|
|
2103
|
+
}
|
|
2104
|
+
})();
|
|
2105
|
+
const categories = [];
|
|
2106
|
+
let passed = 0;
|
|
2107
|
+
let failed = 0;
|
|
2108
|
+
for (const cmd of commands) {
|
|
2109
|
+
const startedAt = Date.now();
|
|
2110
|
+
if (!isCheckId(cmd)) {
|
|
2111
|
+
failed += 1;
|
|
2112
|
+
categories.push({
|
|
2113
|
+
category: cmd,
|
|
2114
|
+
status: "fail",
|
|
2115
|
+
exit_code: 2,
|
|
2116
|
+
duration_seconds: 0
|
|
2117
|
+
});
|
|
2118
|
+
const logFile2 = assertPathInsideRoot2(taskLogDir, resolve9(taskLogDir, `invalid-entry-validation.log`), "validation log file");
|
|
2119
|
+
mkdirSync4(taskLogDir, { recursive: true });
|
|
2120
|
+
writeFileSync4(logFile2, `=== ${nowIso()} :: ${cmd} ===
|
|
2121
|
+
Invalid validation entry: not a check-ID. All entries must use format "category:check-name".
|
|
2122
|
+
`, "utf-8");
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
const { result, exitCode } = await dispatchValidator(cmd, effectiveRegistry, validatorCtx, (id) => runValidatorBinary(projectRoot, taskId, id, resolvedContext));
|
|
2126
|
+
const durationSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
2127
|
+
const logFile = assertPathInsideRoot2(taskLogDir, resolve9(taskLogDir, `${cmd.replace(":", "-")}-validation.log`), "validation log file");
|
|
2128
|
+
mkdirSync4(taskLogDir, { recursive: true });
|
|
2129
|
+
writeFileSync4(logFile, `=== ${nowIso()} :: ${cmd} ===
|
|
2130
|
+
${JSON.stringify(result, null, 2)}
|
|
2131
|
+
`, "utf-8");
|
|
2132
|
+
if (result.passed) {
|
|
2133
|
+
passed += 1;
|
|
2134
|
+
categories.push({ category: cmd, status: "pass", duration_seconds: durationSeconds });
|
|
2135
|
+
} else {
|
|
2136
|
+
failed += 1;
|
|
2137
|
+
categories.push({ category: cmd, status: "fail", exit_code: exitCode, duration_seconds: durationSeconds });
|
|
2138
|
+
const desc = valDescriptions[cmd];
|
|
2139
|
+
if (desc) {
|
|
2140
|
+
console.log(` What this checks (${cmd}): ${desc}`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const summary = {
|
|
2145
|
+
status: failed === 0 ? "pass" : "fail",
|
|
2146
|
+
total: commands.length,
|
|
2147
|
+
passed,
|
|
2148
|
+
failed,
|
|
2149
|
+
categories
|
|
2150
|
+
};
|
|
2151
|
+
mkdirSync4(artifactDir, { recursive: true });
|
|
2152
|
+
writeFileSync4(assertPathInsideRoot2(artifactDir, resolve9(artifactDir, "validation-summary.json"), "validation summary file"), `${JSON.stringify(summary, null, 2)}
|
|
2153
|
+
`, "utf-8");
|
|
2154
|
+
return summary;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// packages/task-sources-plugin/src/control-plane/native/task-ops.ts
|
|
2158
|
+
var BUILD_CONFIG = readBuildConfig();
|
|
2159
|
+
var BAKED_INFO_OUTPUT = BUILD_CONFIG.AGENT_INFO_OUTPUT ?? "";
|
|
2160
|
+
var BAKED_DEPS_OUTPUT = BUILD_CONFIG.AGENT_DEPS_OUTPUT ?? "";
|
|
2161
|
+
var BAKED_STATUS_OUTPUT = BUILD_CONFIG.AGENT_STATUS_OUTPUT ?? "";
|
|
2162
|
+
var REOPENABLE_TASK_STATUSES = new Set(["completed", "cancelled", "blocked"]);
|
|
2163
|
+
function hasRuntimeWorkspace() {
|
|
2164
|
+
return Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
2165
|
+
}
|
|
2166
|
+
function readTaskConfigForInvocation(projectRoot) {
|
|
2167
|
+
return hasRuntimeWorkspace() ? readTaskConfig(projectRoot) : readSourceTaskConfig(projectRoot);
|
|
2168
|
+
}
|
|
2169
|
+
function readValidationDescriptionsForInvocation(projectRoot) {
|
|
2170
|
+
return hasRuntimeWorkspace() ? readValidationDescriptions(projectRoot) : readSourceValidationDescriptions(projectRoot);
|
|
2171
|
+
}
|
|
2172
|
+
function readStringList2(candidate) {
|
|
2173
|
+
return Array.isArray(candidate) ? candidate.filter((value) => typeof value === "string") : [];
|
|
2174
|
+
}
|
|
2175
|
+
function taskConfigEntryFromSourceTask(task) {
|
|
2176
|
+
if (!task)
|
|
2177
|
+
return {};
|
|
2178
|
+
const record = task;
|
|
2179
|
+
const description = firstNonEmpty(typeof record.description === "string" ? record.description : undefined, typeof record.body === "string" ? record.body : undefined);
|
|
2180
|
+
const acceptance = firstNonEmpty(typeof record.acceptanceCriteria === "string" ? record.acceptanceCriteria : undefined, typeof record.acceptance_criteria === "string" ? record.acceptance_criteria : undefined);
|
|
2181
|
+
const validation = readStringList2(record.validation).length > 0 ? readStringList2(record.validation) : readStringList2(record.validators);
|
|
2182
|
+
const scope = readStringList2(record.scope);
|
|
2183
|
+
const browser = record.browser && typeof record.browser === "object" && !Array.isArray(record.browser) ? record.browser : undefined;
|
|
2184
|
+
return {
|
|
2185
|
+
...typeof record.role === "string" ? { role: record.role } : {},
|
|
2186
|
+
...description ? { description } : {},
|
|
2187
|
+
...acceptance ? { acceptance_criteria: acceptance } : {},
|
|
2188
|
+
...scope.length > 0 ? { scope } : {},
|
|
2189
|
+
...validation.length > 0 ? { validation } : {},
|
|
2190
|
+
...browser ? { browser } : {}
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
function taskMetadataFromSourceTask(task) {
|
|
2194
|
+
if (!task)
|
|
2195
|
+
return null;
|
|
2196
|
+
const record = task;
|
|
2197
|
+
const description = firstNonEmpty(typeof record.description === "string" ? record.description : undefined, typeof record.body === "string" ? record.body : undefined);
|
|
2198
|
+
const acceptanceCriteria = firstNonEmpty(typeof record.acceptanceCriteria === "string" ? record.acceptanceCriteria : undefined, typeof record.acceptance_criteria === "string" ? record.acceptance_criteria : undefined);
|
|
2199
|
+
return {
|
|
2200
|
+
title: typeof record.title === "string" && record.title.trim() ? record.title : task.id,
|
|
2201
|
+
...typeof record.status === "string" ? { status: record.status } : {},
|
|
2202
|
+
...typeof record.priority === "number" ? { priority: record.priority } : {},
|
|
2203
|
+
...description ? { description } : {},
|
|
2204
|
+
...acceptanceCriteria ? { acceptanceCriteria } : {}
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
function sourceTaskDependencyIds(task) {
|
|
2208
|
+
if (!task)
|
|
2209
|
+
return null;
|
|
2210
|
+
const record = task;
|
|
2211
|
+
return unique2([
|
|
2212
|
+
...readStringList2(record.deps),
|
|
2213
|
+
...readStringList2(record.dependencies)
|
|
2214
|
+
]).filter((id) => id !== task.id);
|
|
2215
|
+
}
|
|
2216
|
+
async function readTaskSourceRecordForInvocation(projectRoot, taskId) {
|
|
2217
|
+
if (hasRuntimeWorkspace()) {
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
try {
|
|
2221
|
+
const result = await readConfiguredTaskSourceTask(projectRoot, taskId);
|
|
2222
|
+
return result.task;
|
|
2223
|
+
} catch {
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function runtimeToolSummary() {
|
|
2228
|
+
const available = new Set(RUNTIME_SHELL_TOOL_NAMES);
|
|
2229
|
+
const shell = ["bash", "sh", "python3", "tee", "cp", "mv", "rm", "mkdir", "touch"].filter((tool) => available.has(tool));
|
|
2230
|
+
return { shell, file: [...RUNTIME_FILE_TOOL_NAMES] };
|
|
2231
|
+
}
|
|
2232
|
+
function providerToolReferenceLine(_provider) {
|
|
2233
|
+
return "Pi tool names: `read`, `write`, `edit`, `bash`.";
|
|
2234
|
+
}
|
|
2235
|
+
async function taskInfo(projectRoot, taskId, runtimeProviderOverride) {
|
|
2236
|
+
if (BAKED_INFO_OUTPUT) {
|
|
2237
|
+
process.stdout.write(BAKED_INFO_OUTPUT);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2241
|
+
if (!activeTask) {
|
|
2242
|
+
throw new Error("No active task. Start one with: rig run start-serial");
|
|
2243
|
+
}
|
|
2244
|
+
const runtimeContext = loadRuntimeContextFromEnv2();
|
|
2245
|
+
const sourceTask = runtimeContext?.sourceTask ?? await readTaskSourceRecordForInvocation(projectRoot, activeTask);
|
|
2246
|
+
const tracker = loadReadonlyTaskTrackerContext(projectRoot);
|
|
2247
|
+
const taskConfig = (() => {
|
|
2248
|
+
try {
|
|
2249
|
+
return readTaskConfigForInvocation(projectRoot);
|
|
2250
|
+
} catch {
|
|
2251
|
+
return {};
|
|
2252
|
+
}
|
|
2253
|
+
})();
|
|
2254
|
+
const entry = { ...taskConfig[activeTask] || {}, ...taskConfigEntryFromSourceTask(sourceTask) };
|
|
2255
|
+
const taskMeta = taskMetadataFromSourceTask(sourceTask) ?? await readTaskMetadata(projectRoot, activeTask, tracker);
|
|
2256
|
+
const instructionService = await loadCapabilityForRoot(projectRoot, defineCapability2(RUNTIME_INSTRUCTION));
|
|
2257
|
+
const runtimeProvider = runtimeProviderOverride ?? instructionService?.normalizeProvider(process.env.RIG_RUNTIME_ADAPTER) ?? "pi";
|
|
2258
|
+
const description = firstNonEmpty(taskMeta?.description, entry.description);
|
|
2259
|
+
const acceptanceCriteria = firstNonEmpty(taskMeta?.acceptanceCriteria, entry.acceptance_criteria);
|
|
2260
|
+
const browserContractService = await loadCapabilityForRoot(projectRoot, defineCapability2(BROWSER_CONTRACT_SERVICE_CAPABILITY));
|
|
2261
|
+
const browserContext = runtimeContext?.browser ?? browserContractService?.resolveTaskBrowserContext(entry.browser, {
|
|
2262
|
+
hostProjectRoot: projectRoot
|
|
2263
|
+
});
|
|
2264
|
+
console.log(`=== Task: ${activeTask} ===`);
|
|
2265
|
+
console.log("");
|
|
2266
|
+
console.log(`Role: ${entry.role || "unassigned"}`);
|
|
2267
|
+
if (taskMeta) {
|
|
2268
|
+
console.log(`Title: ${taskMeta.title}`);
|
|
2269
|
+
if (taskMeta.status) {
|
|
2270
|
+
console.log(`Status: ${taskMeta.status.toUpperCase()}`);
|
|
2271
|
+
}
|
|
2272
|
+
if (taskMeta.claimId) {
|
|
2273
|
+
console.log(`Claim: ${taskMeta.claimId}`);
|
|
2274
|
+
}
|
|
2275
|
+
if (typeof taskMeta.priority === "number") {
|
|
2276
|
+
console.log(`Priority: P${taskMeta.priority}`);
|
|
2277
|
+
}
|
|
2278
|
+
} else {
|
|
2279
|
+
console.log("Title: (task metadata unavailable)");
|
|
2280
|
+
}
|
|
2281
|
+
if (description) {
|
|
2282
|
+
console.log(`
|
|
2283
|
+
Description:`);
|
|
2284
|
+
printIndented(description);
|
|
2285
|
+
} else {
|
|
2286
|
+
console.log('\nDescription: (not set; add one with `br update <task-id> --description "..."`)');
|
|
2287
|
+
}
|
|
2288
|
+
if (acceptanceCriteria) {
|
|
2289
|
+
console.log(`
|
|
2290
|
+
Acceptance Criteria:`);
|
|
2291
|
+
printIndented(acceptanceCriteria);
|
|
2292
|
+
}
|
|
2293
|
+
console.log(`
|
|
2294
|
+
Scope globs:`);
|
|
2295
|
+
for (const scope of entry.scope || []) {
|
|
2296
|
+
console.log(` - ${scope}`);
|
|
2297
|
+
}
|
|
2298
|
+
if (entry.creates_repo) {
|
|
2299
|
+
console.log(`
|
|
2300
|
+
NOTE: This task creates a new repository. The scope directory does not exist yet \u2014 you are scaffolding it from scratch.`);
|
|
2301
|
+
}
|
|
2302
|
+
const valDescriptions = (() => {
|
|
2303
|
+
try {
|
|
2304
|
+
return readValidationDescriptionsForInvocation(projectRoot);
|
|
2305
|
+
} catch {
|
|
2306
|
+
return {};
|
|
2307
|
+
}
|
|
2308
|
+
})();
|
|
2309
|
+
console.log(`
|
|
2310
|
+
Validation:`);
|
|
2311
|
+
for (const cmd of entry.validation || []) {
|
|
2312
|
+
const desc = valDescriptions[cmd];
|
|
2313
|
+
console.log(desc ? ` $ ${cmd} \u2014 ${desc}` : ` $ ${cmd}`);
|
|
2314
|
+
}
|
|
2315
|
+
if (browserContext?.required) {
|
|
2316
|
+
console.log(`
|
|
2317
|
+
Browser:`);
|
|
2318
|
+
for (const line of browserContractService?.buildBrowserGuidanceLines(browserContext) ?? []) {
|
|
2319
|
+
console.log(` - ${line}`);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
const runtimeTools = runtimeToolSummary();
|
|
2323
|
+
console.log(`
|
|
2324
|
+
Runtime Tools:`);
|
|
2325
|
+
console.log(" - This task runtime provides audited shell tooling and Rig-owned file tools.");
|
|
2326
|
+
console.log(" - Use the Rig-owned router tools for all read, search, edit, and write work inside the task workspace.");
|
|
2327
|
+
for (const line of instructionService?.buildRuntimeContextLines(runtimeProvider) ?? []) {
|
|
2328
|
+
console.log(` - ${line}`);
|
|
2329
|
+
}
|
|
2330
|
+
console.log(" - `write` and `edit` remain restricted to the scoped task workspace, plus the task artifact subtree at `artifacts/<taskId>/` for closeout files.");
|
|
2331
|
+
console.log(` - ${providerToolReferenceLine(runtimeProvider)}`);
|
|
2332
|
+
console.log(" - Runtime tool location: `$RIG_TASK_WORKSPACE/.rig/bin`.");
|
|
2333
|
+
if (runtimeTools.file.length > 0) {
|
|
2334
|
+
console.log(` - Runtime file binaries: ${runtimeTools.file.join(", ")}`);
|
|
2335
|
+
}
|
|
2336
|
+
if (runtimeTools.shell.length > 0) {
|
|
2337
|
+
console.log(` - Shell helpers: ${runtimeTools.shell.join(", ")}`);
|
|
2338
|
+
}
|
|
2339
|
+
console.log(" - Shell commands route through `rig-shell`; file-tool binaries enforce runtime workspace and scope boundaries directly.");
|
|
2340
|
+
console.log(`
|
|
2341
|
+
Dependencies:`);
|
|
2342
|
+
const deps = sourceTaskDependencyIds(sourceTask) ?? taskDependencies(projectRoot, activeTask, tracker);
|
|
2343
|
+
if (deps.length === 0) {
|
|
2344
|
+
console.log(" (none - root task)");
|
|
2345
|
+
} else {
|
|
2346
|
+
for (const dep of deps) {
|
|
2347
|
+
const depMeta = readTaskMetadataFromTracker(projectRoot, dep, tracker);
|
|
2348
|
+
const depStatus = depMeta?.status ? `status=${depMeta.status}` : "";
|
|
2349
|
+
console.log(` - ${dep} ${depStatus}`.trim());
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
async function taskReady(projectRoot) {
|
|
2354
|
+
const readyTaskIds = listReadyTaskIds(loadReadonlyTaskTrackerContext(projectRoot));
|
|
2355
|
+
if (readyTaskIds.length > 0) {
|
|
2356
|
+
process.stdout.write(`${readyTaskIds.join(`
|
|
2357
|
+
`)}
|
|
2358
|
+
`);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
async function taskScope(projectRoot, expandFiles, taskId) {
|
|
2362
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2363
|
+
if (!activeTask) {
|
|
2364
|
+
throw new Error("No active task.");
|
|
2365
|
+
}
|
|
2366
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2367
|
+
const sourceTask = loadRuntimeContextFromEnv2()?.sourceTask ?? await readTaskSourceRecordForInvocation(projectRoot, activeTask);
|
|
2368
|
+
const taskConfig = (() => {
|
|
2369
|
+
try {
|
|
2370
|
+
return readTaskConfigForInvocation(projectRoot);
|
|
2371
|
+
} catch {
|
|
2372
|
+
return {};
|
|
2373
|
+
}
|
|
2374
|
+
})();
|
|
2375
|
+
const entry = { ...taskConfig[activeTask] || {}, ...taskConfigEntryFromSourceTask(sourceTask) };
|
|
2376
|
+
const scopes = entry.scope || [];
|
|
2377
|
+
if (scopes.length === 0) {
|
|
2378
|
+
throw new Error(`No scope defined for ${activeTask}.`);
|
|
2379
|
+
}
|
|
2380
|
+
if (!expandFiles) {
|
|
2381
|
+
console.log(`Scope globs for ${activeTask}:`);
|
|
2382
|
+
for (const scope of scopes) {
|
|
2383
|
+
console.log(` ${scope}`);
|
|
2384
|
+
}
|
|
2385
|
+
console.log(`
|
|
2386
|
+
Use --files to expand globs to actual file paths.`);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
console.log(`Files matching scope for ${activeTask}:
|
|
2390
|
+
`);
|
|
2391
|
+
const files = [];
|
|
2392
|
+
for (const scope of scopes) {
|
|
2393
|
+
const normalizedScope = normalizePathToScope(projectRoot, paths.monorepoRoot, scope);
|
|
2394
|
+
const inMonorepo = /^(humanity|moongate|packages|shared-migrations|microservices|TSAPITests|hp-next)\//.test(normalizedScope);
|
|
2395
|
+
if (inMonorepo) {
|
|
2396
|
+
for (const candidate of monorepoSearchCandidates(scope)) {
|
|
2397
|
+
const result = runCapture(["git", "-C", paths.monorepoRoot, "ls-files", candidate], projectRoot);
|
|
2398
|
+
if (result.exitCode === 0) {
|
|
2399
|
+
files.push(...result.stdout.split(/\r?\n/).filter(Boolean));
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
} else {
|
|
2403
|
+
const result = runCapture(["git", "-C", projectRoot, "ls-files", scope], projectRoot);
|
|
2404
|
+
if (result.exitCode === 0) {
|
|
2405
|
+
files.push(...result.stdout.split(/\r?\n/).filter(Boolean));
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (files.length === 0) {
|
|
2410
|
+
const taskEntry = entry;
|
|
2411
|
+
if (taskEntry.creates_repo) {
|
|
2412
|
+
console.log("(No files yet \u2014 this is a creates_repo task. You will create these directories.)");
|
|
2413
|
+
} else {
|
|
2414
|
+
console.log("(No files match scope globs.)");
|
|
2415
|
+
}
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
for (const file of unique2(files).sort()) {
|
|
2419
|
+
console.log(file);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
async function taskDeps(projectRoot, taskId) {
|
|
2423
|
+
if (BAKED_DEPS_OUTPUT) {
|
|
2424
|
+
process.stdout.write(BAKED_DEPS_OUTPUT);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2428
|
+
if (!activeTask) {
|
|
2429
|
+
throw new Error("No active task.");
|
|
2430
|
+
}
|
|
2431
|
+
const sourceTask = loadRuntimeContextFromEnv2()?.sourceTask ?? await readTaskSourceRecordForInvocation(projectRoot, activeTask);
|
|
2432
|
+
const tracker = loadReadonlyTaskTrackerContext(projectRoot);
|
|
2433
|
+
const deps = sourceTaskDependencyIds(sourceTask) ?? taskDependencies(projectRoot, activeTask, tracker);
|
|
2434
|
+
if (deps.length === 0) {
|
|
2435
|
+
console.log(`No dependencies for ${activeTask}.`);
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
for (const dep of deps) {
|
|
2439
|
+
const artifactDir = artifactDirForId(projectRoot, dep);
|
|
2440
|
+
console.log(`=== ${dep} ===`);
|
|
2441
|
+
if (!existsSync11(artifactDir)) {
|
|
2442
|
+
console.log(` (no artifacts yet)
|
|
2443
|
+
`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
printArtifactSection(resolve10(artifactDir, "decision-log.md"), "--- Decisions ---");
|
|
2447
|
+
printArtifactSection(resolve10(artifactDir, "next-actions.md"), "--- Next Actions (for you) ---");
|
|
2448
|
+
const changedFiles = resolve10(artifactDir, "changed-files.txt");
|
|
2449
|
+
if (existsSync11(changedFiles)) {
|
|
2450
|
+
const lines = readFileSync7(changedFiles, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
2451
|
+
console.log(`--- Changed Files (${lines.length}) ---`);
|
|
2452
|
+
for (const line of lines) {
|
|
2453
|
+
console.log(line);
|
|
2454
|
+
}
|
|
2455
|
+
console.log("");
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function taskStatus(projectRoot) {
|
|
2460
|
+
if (BAKED_STATUS_OUTPUT) {
|
|
2461
|
+
process.stdout.write(BAKED_STATUS_OUTPUT);
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
console.log(`=== Project Rig Progress ===
|
|
2465
|
+
`);
|
|
2466
|
+
const tasks = readBeadsTasks(projectRoot, undefined, true);
|
|
2467
|
+
if (tasks.length === 0) {
|
|
2468
|
+
console.log("(no task tracker rows found)");
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const counts = tasks.reduce((acc, task) => {
|
|
2472
|
+
const key = task.status || "open";
|
|
2473
|
+
acc[key] = (acc[key] || 0) + 1;
|
|
2474
|
+
return acc;
|
|
2475
|
+
}, {});
|
|
2476
|
+
const total = tasks.length;
|
|
2477
|
+
console.log(`Total tasks: ${total}`);
|
|
2478
|
+
for (const status of Object.keys(counts).sort()) {
|
|
2479
|
+
console.log(` ${status}: ${counts[status]}`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function taskLookup(projectRoot, id) {
|
|
2483
|
+
const result = lookupTask(projectRoot, id);
|
|
2484
|
+
if (!result) {
|
|
2485
|
+
throw new Error(`Not found: ${id}`);
|
|
2486
|
+
}
|
|
2487
|
+
return result;
|
|
2488
|
+
}
|
|
2489
|
+
function taskRecord(projectRoot, type, text, taskId) {
|
|
2490
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2491
|
+
if (!activeTask) {
|
|
2492
|
+
throw new Error("No active task.");
|
|
2493
|
+
}
|
|
2494
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2495
|
+
mkdirSync5(paths.stateDir, { recursive: true });
|
|
2496
|
+
if (type === "decision") {
|
|
2497
|
+
const artifactDir = taskArtifactDir(projectRoot, activeTask);
|
|
2498
|
+
mkdirSync5(artifactDir, { recursive: true });
|
|
2499
|
+
const timestamp = nowIso();
|
|
2500
|
+
appendFileSync(resolve10(artifactDir, "decision-log.md"), `
|
|
2501
|
+
### ${timestamp}
|
|
2502
|
+
|
|
2503
|
+
${text}
|
|
2504
|
+
|
|
2505
|
+
`, "utf-8");
|
|
2506
|
+
console.log(`Decision recorded for ${activeTask}.`);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
const failedPath = paths.failedApproachesPath;
|
|
2510
|
+
if (!existsSync11(failedPath)) {
|
|
2511
|
+
writeFileSync5(failedPath, `# Failed Approaches Log
|
|
2512
|
+
|
|
2513
|
+
This file records approaches that did not work.
|
|
2514
|
+
|
|
2515
|
+
`, "utf-8");
|
|
2516
|
+
}
|
|
2517
|
+
const content = readFileSync7(failedPath, "utf-8");
|
|
2518
|
+
const attempts = (content.match(new RegExp(`^## ${escapeRegExp(activeTask)}\\b`, "gm")) || []).length + 1;
|
|
2519
|
+
appendFileSync(failedPath, `
|
|
2520
|
+
## ${activeTask} - Attempt ${attempts} (${nowIso()})
|
|
2521
|
+
|
|
2522
|
+
**Reason:** ${text}
|
|
2523
|
+
|
|
2524
|
+
---
|
|
2525
|
+
`, "utf-8");
|
|
2526
|
+
console.log(`Failed approach #${attempts} recorded for ${activeTask}. Note: visible to future sessions, not re-read in this session.`);
|
|
2527
|
+
}
|
|
2528
|
+
function taskArtifacts(projectRoot, taskId) {
|
|
2529
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2530
|
+
if (!activeTask) {
|
|
2531
|
+
throw new Error("No active task.");
|
|
2532
|
+
}
|
|
2533
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2534
|
+
const artifactDir = taskArtifactDir(projectRoot, activeTask);
|
|
2535
|
+
mkdirSync5(artifactDir, { recursive: true });
|
|
2536
|
+
const changed = changedFilesForTask(projectRoot, activeTask, true);
|
|
2537
|
+
writeFileSync5(resolve10(artifactDir, "changed-files.txt"), `${changed.join(`
|
|
2538
|
+
`)}
|
|
2539
|
+
`, "utf-8");
|
|
2540
|
+
console.log(`changed-files.txt: ${changed.length} files`);
|
|
2541
|
+
const taskResultPath = resolve10(artifactDir, "task-result.json");
|
|
2542
|
+
if (!existsSync11(taskResultPath)) {
|
|
2543
|
+
const template = {
|
|
2544
|
+
task_id: activeTask,
|
|
2545
|
+
status: "completed",
|
|
2546
|
+
summary: "TODO: Write a one-line summary of what you did",
|
|
2547
|
+
completed_at: nowIso()
|
|
2548
|
+
};
|
|
2549
|
+
writeFileSync5(taskResultPath, `${JSON.stringify(template, null, 2)}
|
|
2550
|
+
`, "utf-8");
|
|
2551
|
+
console.log("task-result.json: created (update the summary!)");
|
|
2552
|
+
} else {
|
|
2553
|
+
console.log("task-result.json: already exists");
|
|
2554
|
+
}
|
|
2555
|
+
const decisionLogPath = resolve10(artifactDir, "decision-log.md");
|
|
2556
|
+
if (!existsSync11(decisionLogPath)) {
|
|
2557
|
+
const content = `# Decision Log: ${activeTask}
|
|
2558
|
+
|
|
2559
|
+
Record key decisions here using: rig-agent record decision "..."
|
|
2560
|
+
`;
|
|
2561
|
+
writeFileSync5(decisionLogPath, content, "utf-8");
|
|
2562
|
+
console.log("decision-log.md: created (record your decisions!)");
|
|
2563
|
+
} else {
|
|
2564
|
+
console.log("decision-log.md: already exists");
|
|
2565
|
+
}
|
|
2566
|
+
const nextActionsPath = resolve10(artifactDir, "next-actions.md");
|
|
2567
|
+
if (!existsSync11(nextActionsPath)) {
|
|
2568
|
+
const content = [
|
|
2569
|
+
`# Next Actions: ${activeTask}`,
|
|
2570
|
+
"",
|
|
2571
|
+
"## For downstream tasks",
|
|
2572
|
+
"",
|
|
2573
|
+
"### bd-<downstream-task-id>",
|
|
2574
|
+
"- **Decision impact:** What you decided that affects this downstream task",
|
|
2575
|
+
"- **Recommended approach:** What they should do differently because of your work",
|
|
2576
|
+
"- **Key files:** What files they should look at first",
|
|
2577
|
+
"",
|
|
2578
|
+
"## General notes",
|
|
2579
|
+
"",
|
|
2580
|
+
"- TODO: Replace this scaffold with real content before completion",
|
|
2581
|
+
""
|
|
2582
|
+
].join(`
|
|
2583
|
+
`);
|
|
2584
|
+
writeFileSync5(nextActionsPath, content, "utf-8");
|
|
2585
|
+
console.log("next-actions.md: created (add recommendations for downstream tasks!)");
|
|
2586
|
+
} else {
|
|
2587
|
+
console.log("next-actions.md: already exists");
|
|
2588
|
+
}
|
|
2589
|
+
const validationSummaryPath = resolve10(artifactDir, "validation-summary.json");
|
|
2590
|
+
if (existsSync11(validationSummaryPath)) {
|
|
2591
|
+
console.log("validation-summary.json: already exists");
|
|
2592
|
+
} else {
|
|
2593
|
+
console.log("validation-summary.json: not yet created (run: rig-agent validate)");
|
|
2594
|
+
}
|
|
2595
|
+
console.log(`
|
|
2596
|
+
Artifacts at: ${artifactDir}/`);
|
|
2597
|
+
}
|
|
2598
|
+
function taskArtifactDir(projectRoot, taskId) {
|
|
2599
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2600
|
+
if (!activeTask) {
|
|
2601
|
+
throw new Error("No active task.");
|
|
2602
|
+
}
|
|
2603
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2604
|
+
const artifactDir = resolve10(paths.artifactsDir, safePathSegment3(activeTask, { fallback: "task", maxLength: 96 }));
|
|
2605
|
+
assertPathInsideRoot3(paths.artifactsDir, artifactDir, "artifact directory");
|
|
2606
|
+
mkdirSync5(artifactDir, { recursive: true });
|
|
2607
|
+
return artifactDir;
|
|
2608
|
+
}
|
|
2609
|
+
function taskArtifactWrite(projectRoot, filename, content, taskId) {
|
|
2610
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2611
|
+
if (!activeTask) {
|
|
2612
|
+
throw new Error("No active task.");
|
|
2613
|
+
}
|
|
2614
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2615
|
+
const artifactDir = resolve10(paths.artifactsDir, safePathSegment3(activeTask, { fallback: "task", maxLength: 96 }));
|
|
2616
|
+
assertPathInsideRoot3(paths.artifactsDir, artifactDir, "artifact directory");
|
|
2617
|
+
mkdirSync5(artifactDir, { recursive: true });
|
|
2618
|
+
const targetPath = assertPathInsideRoot3(artifactDir, resolve10(artifactDir, filename), "artifact file");
|
|
2619
|
+
writeFileSync5(targetPath, content, "utf-8");
|
|
2620
|
+
console.log(`Wrote: ${targetPath}`);
|
|
2621
|
+
}
|
|
2622
|
+
function taskArtifactRead(projectRoot, filename, options) {
|
|
2623
|
+
const activeTask = options?.taskId || currentTaskId(projectRoot);
|
|
2624
|
+
if (!activeTask) {
|
|
2625
|
+
throw new Error("No active task.");
|
|
2626
|
+
}
|
|
2627
|
+
const maxBytes = options?.maxBytes ?? 64 * 1024;
|
|
2628
|
+
const artifactDir = taskArtifactDir(projectRoot, activeTask);
|
|
2629
|
+
const targetPath = assertPathInsideRoot3(artifactDir, resolve10(artifactDir, filename), "artifact file");
|
|
2630
|
+
if (!existsSync11(targetPath)) {
|
|
2631
|
+
throw new Error(`Artifact not found: ${targetPath}`);
|
|
2632
|
+
}
|
|
2633
|
+
const buffer = readFileSync7(targetPath);
|
|
2634
|
+
const preview = buffer.subarray(0, maxBytes);
|
|
2635
|
+
return {
|
|
2636
|
+
path: targetPath,
|
|
2637
|
+
sizeBytes: buffer.length,
|
|
2638
|
+
contents: preview.includes(0) ? "[binary content omitted]" : preview.toString("utf-8"),
|
|
2639
|
+
truncated: buffer.length > maxBytes,
|
|
2640
|
+
maxBytes
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
async function taskValidate(projectRoot, taskId, validatorRegistry) {
|
|
2644
|
+
const activeTask = taskId || currentTaskId(projectRoot);
|
|
2645
|
+
if (!activeTask) {
|
|
2646
|
+
throw new Error("No active task.");
|
|
2647
|
+
}
|
|
2648
|
+
console.log(`Running validation for ${activeTask}...
|
|
2649
|
+
`);
|
|
2650
|
+
const runtimeContext = loadRuntimeContextFromEnv2();
|
|
2651
|
+
const summary = await validateTask(projectRoot, activeTask, runtimeContext, validatorRegistry, runtimeContext ? {} : {
|
|
2652
|
+
taskConfig: (() => {
|
|
2653
|
+
try {
|
|
2654
|
+
return readTaskConfigForInvocation(projectRoot);
|
|
2655
|
+
} catch {
|
|
2656
|
+
return {};
|
|
2657
|
+
}
|
|
2658
|
+
})(),
|
|
2659
|
+
validationDescriptions: (() => {
|
|
2660
|
+
try {
|
|
2661
|
+
return readValidationDescriptionsForInvocation(projectRoot);
|
|
2662
|
+
} catch {
|
|
2663
|
+
return {};
|
|
2664
|
+
}
|
|
2665
|
+
})()
|
|
2666
|
+
});
|
|
2667
|
+
if (summary.status === "skipped") {
|
|
2668
|
+
console.log(`Validation skipped: no validation commands defined`);
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2671
|
+
if (summary.status !== "pass") {
|
|
2672
|
+
console.log(`Validation failed: ${summary.failed}/${summary.total} failed`);
|
|
2673
|
+
return false;
|
|
2674
|
+
}
|
|
2675
|
+
console.log(`Validation passed: ${summary.passed}/${summary.total}`);
|
|
2676
|
+
return true;
|
|
2677
|
+
}
|
|
2678
|
+
function taskReopen(projectRoot, options) {
|
|
2679
|
+
const dryRun = options.dryRun === true;
|
|
2680
|
+
const tasks = readBeadsTasks(projectRoot);
|
|
2681
|
+
const summary = {
|
|
2682
|
+
mode: options.all ? "all" : "single",
|
|
2683
|
+
requestedTaskId: options.taskId || null,
|
|
2684
|
+
dryRun,
|
|
2685
|
+
closedFound: 0,
|
|
2686
|
+
reopened: [],
|
|
2687
|
+
failed: [],
|
|
2688
|
+
skipped: []
|
|
2689
|
+
};
|
|
2690
|
+
if (options.all) {
|
|
2691
|
+
const reopenable = tasks.filter((task2) => {
|
|
2692
|
+
const normalizedStatus2 = normalizeTaskLifecycleStatus(task2.status);
|
|
2693
|
+
return normalizedStatus2 != null && REOPENABLE_TASK_STATUSES.has(normalizedStatus2);
|
|
2694
|
+
}).map((task2) => task2.id);
|
|
2695
|
+
summary.closedFound = reopenable.length;
|
|
2696
|
+
if (reopenable.length === 0) {
|
|
2697
|
+
console.log("No reopenable tasks found.");
|
|
2698
|
+
return summary;
|
|
2699
|
+
}
|
|
2700
|
+
if (dryRun) {
|
|
2701
|
+
console.log(`Dry-run: would reopen ${reopenable.length} task(s):`);
|
|
2702
|
+
for (const id of reopenable) {
|
|
2703
|
+
console.log(`- ${id}`);
|
|
2704
|
+
}
|
|
2705
|
+
return summary;
|
|
2706
|
+
}
|
|
2707
|
+
for (const id of reopenable) {
|
|
2708
|
+
reopenSingleTask(projectRoot, id, summary);
|
|
2709
|
+
}
|
|
2710
|
+
printTaskReopenSummary(summary);
|
|
2711
|
+
return summary;
|
|
2712
|
+
}
|
|
2713
|
+
if (!options.taskId) {
|
|
2714
|
+
throw new Error("Missing task id. Use --task <beads-id> or --all.");
|
|
2715
|
+
}
|
|
2716
|
+
const task = tasks.find((entry) => entry.id === options.taskId);
|
|
2717
|
+
if (!task) {
|
|
2718
|
+
throw new Error(`Task not found in .beads/issues.jsonl: ${options.taskId}`);
|
|
2719
|
+
}
|
|
2720
|
+
const normalizedStatus = normalizeTaskLifecycleStatus(task.status);
|
|
2721
|
+
if (!normalizedStatus || !REOPENABLE_TASK_STATUSES.has(normalizedStatus)) {
|
|
2722
|
+
summary.skipped.push(options.taskId);
|
|
2723
|
+
console.log(`Task ${options.taskId} is already open.`);
|
|
2724
|
+
return summary;
|
|
2725
|
+
}
|
|
2726
|
+
summary.closedFound = 1;
|
|
2727
|
+
if (dryRun) {
|
|
2728
|
+
console.log(`Dry-run: would reopen task ${options.taskId}.`);
|
|
2729
|
+
return summary;
|
|
2730
|
+
}
|
|
2731
|
+
reopenSingleTask(projectRoot, options.taskId, summary);
|
|
2732
|
+
printTaskReopenSummary(summary);
|
|
2733
|
+
return summary;
|
|
2734
|
+
}
|
|
2735
|
+
var GENERATED_TASK_ARTIFACT_FILES = new Set([
|
|
2736
|
+
"changed-files.txt",
|
|
2737
|
+
"decision-log.md",
|
|
2738
|
+
"next-actions.md",
|
|
2739
|
+
"task-result.json",
|
|
2740
|
+
"validation-summary.json",
|
|
2741
|
+
"review-feedback.md",
|
|
2742
|
+
"review-state.json",
|
|
2743
|
+
"review-status.txt",
|
|
2744
|
+
"review-greptile-raw.json",
|
|
2745
|
+
"pr-state.json",
|
|
2746
|
+
"git-state.txt"
|
|
2747
|
+
]);
|
|
2748
|
+
function collectTaskChangedFiles(projectRoot, taskId, includeCommitted) {
|
|
2749
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2750
|
+
const monorepoRepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
2751
|
+
const files = [];
|
|
2752
|
+
const projectBaseline = resolveRuntimeDirtyBaseline(projectRoot, projectRoot);
|
|
2753
|
+
const monorepoBaseline = resolveRuntimeDirtyBaseline(projectRoot, monorepoRepoRoot);
|
|
2754
|
+
for (const [repo, prefix] of [
|
|
2755
|
+
[projectRoot, ""],
|
|
2756
|
+
[monorepoRepoRoot, ""]
|
|
2757
|
+
]) {
|
|
2758
|
+
if (!existsSync11(resolve10(repo, ".git"))) {
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
if (includeCommitted && repo === monorepoRepoRoot) {
|
|
2762
|
+
for (const line of collectCommittedMonorepoFiles(projectRoot, repo)) {
|
|
2763
|
+
files.push(`${prefix}${line}`);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
const baseline = repo === paths.monorepoRoot ? monorepoBaseline : projectBaseline;
|
|
2767
|
+
for (const line of collectWorkingTreeFiles(projectRoot, repo, baseline)) {
|
|
2768
|
+
files.push(`${prefix}${line}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
const uniqueFiles = unique2(files).map((file) => normalizeChangedFilePath(file)).filter(Boolean);
|
|
2772
|
+
return filterTaskChangedFiles(projectRoot, taskId, uniqueFiles, false);
|
|
2773
|
+
}
|
|
2774
|
+
function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
|
|
2775
|
+
const uniqueFiles = unique2(files).map((file) => normalizeChangedFilePath(file)).filter(Boolean);
|
|
2776
|
+
if (!taskId) {
|
|
2777
|
+
return unique2(uniqueFiles).sort();
|
|
2778
|
+
}
|
|
2779
|
+
return unique2(uniqueFiles).filter((file) => !isGeneratedTaskChangePath(taskId, file)).sort();
|
|
2780
|
+
}
|
|
2781
|
+
function resolveTaskMonorepoRoot(projectRoot) {
|
|
2782
|
+
const runtimeWorkspace = loadRuntimeContextFromEnv2()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2783
|
+
if (runtimeWorkspace && existsSync11(resolve10(runtimeWorkspace, ".git"))) {
|
|
2784
|
+
return resolve10(runtimeWorkspace);
|
|
2785
|
+
}
|
|
2786
|
+
return resolveHarnessPaths(projectRoot).monorepoRoot;
|
|
2787
|
+
}
|
|
2788
|
+
function collectCommittedMonorepoFiles(projectRoot, repo) {
|
|
2789
|
+
const baseRefCandidates = [];
|
|
2790
|
+
const originHead = runCapture(["git", "-C", repo, "rev-parse", "--abbrev-ref", "origin/HEAD"], projectRoot);
|
|
2791
|
+
if (originHead.exitCode === 0 && originHead.stdout.trim()) {
|
|
2792
|
+
baseRefCandidates.push(originHead.stdout.trim());
|
|
2793
|
+
}
|
|
2794
|
+
baseRefCandidates.push("origin/main", "origin/dev", "origin/master", "main", "dev", "master");
|
|
2795
|
+
for (const baseRef of baseRefCandidates) {
|
|
2796
|
+
const mergeBase = runCapture(["git", "-C", repo, "merge-base", "HEAD", baseRef], projectRoot);
|
|
2797
|
+
if (mergeBase.exitCode !== 0 || !mergeBase.stdout.trim())
|
|
2798
|
+
continue;
|
|
2799
|
+
const result = runCapture(["git", "-C", repo, "diff", "--name-only", `${mergeBase.stdout.trim()}..HEAD`], projectRoot);
|
|
2800
|
+
if (result.exitCode === 0) {
|
|
2801
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const initialHeadCommit = resolveRuntimeInitialHeadCommit(projectRoot, repo);
|
|
2805
|
+
if (initialHeadCommit) {
|
|
2806
|
+
const result = runCapture(["git", "-C", repo, "diff", "--name-only", `${initialHeadCommit}..HEAD`], projectRoot);
|
|
2807
|
+
if (result.exitCode === 0) {
|
|
2808
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
const baseCommit = resolveMonorepoBaseCommit(projectRoot, repo);
|
|
2812
|
+
if (!baseCommit) {
|
|
2813
|
+
return [];
|
|
2814
|
+
}
|
|
2815
|
+
for (const revision of [`${baseCommit}...HEAD`, `${baseCommit}..HEAD`]) {
|
|
2816
|
+
const result = runCapture(["git", "-C", repo, "diff", "--name-only", revision], projectRoot);
|
|
2817
|
+
if (result.exitCode === 0) {
|
|
2818
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return [];
|
|
2822
|
+
}
|
|
2823
|
+
function resolveRuntimeInitialHeadCommit(projectRoot, repo) {
|
|
2824
|
+
const runtimeContext = loadRuntimeContextFromEnv2();
|
|
2825
|
+
if (runtimeContext?.initialHeadCommits?.monorepo?.trim()) {
|
|
2826
|
+
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
2827
|
+
if (resolve10(monorepoRoot) === resolve10(repo)) {
|
|
2828
|
+
return runtimeContext.initialHeadCommits.monorepo.trim();
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return "";
|
|
2832
|
+
}
|
|
2833
|
+
function resolveMonorepoBaseCommit(projectRoot, repo) {
|
|
2834
|
+
const runtimeContext = loadRuntimeContextFromEnv2();
|
|
2835
|
+
if (runtimeContext?.monorepoBaseCommit?.trim()) {
|
|
2836
|
+
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
2837
|
+
if (resolve10(monorepoRoot) === resolve10(repo)) {
|
|
2838
|
+
return runtimeContext.monorepoBaseCommit.trim();
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
return "";
|
|
2842
|
+
}
|
|
2843
|
+
function collectWorkingTreeFiles(projectRoot, repo, baseline) {
|
|
2844
|
+
const files = new Set;
|
|
2845
|
+
const nativeFiles = nativePendingFiles(repo);
|
|
2846
|
+
if (nativeFiles !== null) {
|
|
2847
|
+
for (const entry of nativeFiles) {
|
|
2848
|
+
const normalized = normalizeChangedFilePath(entry.path);
|
|
2849
|
+
if (!normalized || baseline.has(normalized)) {
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
files.add(normalized);
|
|
2853
|
+
}
|
|
2854
|
+
return [...files].sort();
|
|
2855
|
+
}
|
|
2856
|
+
for (const args of [
|
|
2857
|
+
["diff", "--name-only"],
|
|
2858
|
+
["diff", "--cached", "--name-only"],
|
|
2859
|
+
["ls-files", "--others", "--exclude-standard"]
|
|
2860
|
+
]) {
|
|
2861
|
+
const result = runCapture(["git", "-C", repo, ...args], projectRoot);
|
|
2862
|
+
if (result.exitCode !== 0) {
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
2866
|
+
const normalized = normalizeChangedFilePath(line);
|
|
2867
|
+
if (!normalized || baseline.has(normalized)) {
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
files.add(normalized);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return [...files].sort();
|
|
2874
|
+
}
|
|
2875
|
+
function resolveRuntimeDirtyBaseline(projectRoot, repo) {
|
|
2876
|
+
const runtimeContext = loadRuntimeContextFromEnv2();
|
|
2877
|
+
const dirtyFiles = runtimeContext?.initialDirtyFiles;
|
|
2878
|
+
if (!dirtyFiles) {
|
|
2879
|
+
return new Set;
|
|
2880
|
+
}
|
|
2881
|
+
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
2882
|
+
const selected = resolve10(repo) === resolve10(monorepoRoot) ? dirtyFiles.monorepo : resolve10(repo) === resolve10(projectRoot) ? dirtyFiles.project : undefined;
|
|
2883
|
+
return new Set((selected || []).map((file) => normalizeChangedFilePath(file)).filter(Boolean));
|
|
2884
|
+
}
|
|
2885
|
+
function normalizeChangedFilePath(file) {
|
|
2886
|
+
return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2887
|
+
}
|
|
2888
|
+
function isGeneratedTaskChangePath(taskId, file) {
|
|
2889
|
+
const normalized = normalizeChangedFilePath(file);
|
|
2890
|
+
if (!normalized) {
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
2893
|
+
if (normalized.startsWith(".rig/") || normalized.startsWith(".beads/") || normalized.startsWith(".worktrees/")) {
|
|
2894
|
+
return true;
|
|
2895
|
+
}
|
|
2896
|
+
const safeTaskId = safePathSegment3(taskId, { fallback: "task", maxLength: 96 });
|
|
2897
|
+
const taskArtifactPrefix = `artifacts/${safeTaskId}/`;
|
|
2898
|
+
if (!normalized.startsWith(taskArtifactPrefix)) {
|
|
2899
|
+
return false;
|
|
2900
|
+
}
|
|
2901
|
+
const artifactRelativePath = normalized.slice(taskArtifactPrefix.length);
|
|
2902
|
+
return GENERATED_TASK_ARTIFACT_FILES.has(artifactRelativePath) || artifactRelativePath.startsWith("runtime-snapshots/");
|
|
2903
|
+
}
|
|
2904
|
+
async function readTaskMetadata(projectRoot, taskId, tracker) {
|
|
2905
|
+
const trackerMetadata = readTaskMetadataFromTracker(projectRoot, taskId, tracker);
|
|
2906
|
+
if (trackerMetadata) {
|
|
2907
|
+
return trackerMetadata;
|
|
2908
|
+
}
|
|
2909
|
+
try {
|
|
2910
|
+
const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
|
|
2911
|
+
if (!task) {
|
|
2912
|
+
return null;
|
|
2913
|
+
}
|
|
2914
|
+
const record = task;
|
|
2915
|
+
const title = asNonEmptyString(record.title) || taskId;
|
|
2916
|
+
const metadata = { title };
|
|
2917
|
+
const status = asNonEmptyString(record.status);
|
|
2918
|
+
if (status) {
|
|
2919
|
+
metadata.status = normalizeTaskLifecycleStatus(status) ?? status;
|
|
2920
|
+
}
|
|
2921
|
+
const description = firstNonEmpty(asNonEmptyString(record.description), asNonEmptyString(record.body));
|
|
2922
|
+
if (description)
|
|
2923
|
+
metadata.description = description;
|
|
2924
|
+
const acceptance = firstNonEmpty(asNonEmptyString(record.acceptance_criteria), asNonEmptyString(record.acceptanceCriteria));
|
|
2925
|
+
if (acceptance)
|
|
2926
|
+
metadata.acceptanceCriteria = acceptance;
|
|
2927
|
+
const priority = record.priority;
|
|
2928
|
+
if (typeof priority === "number")
|
|
2929
|
+
metadata.priority = priority;
|
|
2930
|
+
return metadata;
|
|
2931
|
+
} catch {
|
|
2932
|
+
return null;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
function readTaskMetadataFromTracker(projectRoot, taskId, tracker) {
|
|
2936
|
+
const context = tracker ?? loadTaskTrackerContext(projectRoot);
|
|
2937
|
+
const record = context.tasksById.get(taskId);
|
|
2938
|
+
if (!record?.title) {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
const metadata = { title: record.title };
|
|
2942
|
+
const status = asNonEmptyString(record.status);
|
|
2943
|
+
if (status) {
|
|
2944
|
+
const normalizedStatus = normalizeTaskLifecycleStatus(status);
|
|
2945
|
+
metadata.status = normalizedStatus ?? status;
|
|
2946
|
+
if (normalizedStatus) {
|
|
2947
|
+
const taskState = readSourceTaskStateMetadata(projectRoot, taskId, normalizedStatus, context.snapshot);
|
|
2948
|
+
if (taskState?.claimId) {
|
|
2949
|
+
metadata.claimId = taskState.claimId;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
if (typeof record.priority === "number")
|
|
2954
|
+
metadata.priority = record.priority;
|
|
2955
|
+
const description = asNonEmptyString(record.description);
|
|
2956
|
+
if (description)
|
|
2957
|
+
metadata.description = description;
|
|
2958
|
+
const acceptance = firstNonEmpty(asNonEmptyString(record.acceptance_criteria), asNonEmptyString(record.acceptanceCriteria));
|
|
2959
|
+
if (acceptance)
|
|
2960
|
+
metadata.acceptanceCriteria = acceptance;
|
|
2961
|
+
return metadata;
|
|
2962
|
+
}
|
|
2963
|
+
function asNonEmptyString(value) {
|
|
2964
|
+
if (typeof value !== "string") {
|
|
2965
|
+
return "";
|
|
2966
|
+
}
|
|
2967
|
+
return value.trim();
|
|
2968
|
+
}
|
|
2969
|
+
function firstNonEmpty(...values) {
|
|
2970
|
+
for (const value of values) {
|
|
2971
|
+
if (typeof value === "string" && value.trim()) {
|
|
2972
|
+
return value.trim();
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
return "";
|
|
2976
|
+
}
|
|
2977
|
+
function printIndented(text) {
|
|
2978
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2979
|
+
console.log(` ${line}`);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
function readLocalBeadsTasks(projectRoot) {
|
|
2983
|
+
const issuesPath = resolve10(resolveCheckoutRoot(projectRoot), ".beads/issues.jsonl");
|
|
2984
|
+
if (!existsSync11(issuesPath)) {
|
|
2985
|
+
return [];
|
|
2986
|
+
}
|
|
2987
|
+
const tasks = [];
|
|
2988
|
+
for (const line of readFileSync7(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
2989
|
+
const trimmed = line.trim();
|
|
2990
|
+
if (!trimmed) {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
const parsed = JSON.parse(trimmed);
|
|
2995
|
+
if (parsed.issue_type === "task" && parsed.id) {
|
|
2996
|
+
tasks.push({
|
|
2997
|
+
id: parsed.id,
|
|
2998
|
+
status: parsed.status || "open",
|
|
2999
|
+
issue_type: parsed.issue_type,
|
|
3000
|
+
title: parsed.title,
|
|
3001
|
+
priority: parsed.priority,
|
|
3002
|
+
description: parsed.description,
|
|
3003
|
+
acceptance_criteria: parsed.acceptance_criteria,
|
|
3004
|
+
acceptanceCriteria: parsed.acceptanceCriteria,
|
|
3005
|
+
dependencies: Array.isArray(parsed.dependencies) ? parsed.dependencies : []
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
} catch {}
|
|
3009
|
+
}
|
|
3010
|
+
return tasks;
|
|
3011
|
+
}
|
|
3012
|
+
function readBeadsTasks(projectRoot, tracker, allowLocalFallback = false) {
|
|
3013
|
+
return (tracker ?? loadTaskTrackerContext(projectRoot, undefined, allowLocalFallback)).tasks;
|
|
3014
|
+
}
|
|
3015
|
+
function loadTaskTrackerContext(projectRoot, snapshot, allowLocalFallback = false) {
|
|
3016
|
+
const resolvedSnapshot = snapshot ?? readSyncedTrackerState(projectRoot, {}, allowLocalFallback ? { allowLocalFallback: true } : {});
|
|
3017
|
+
const tasks = resolvedSnapshot.issues.filter((issue) => issue.issueType === "task").map(projectTaskRecordFromSyncedIssue);
|
|
3018
|
+
const seenTaskIds = new Set(tasks.map((task) => task.id));
|
|
3019
|
+
if (allowLocalFallback) {
|
|
3020
|
+
for (const localTask of readLocalBeadsTasks(projectRoot)) {
|
|
3021
|
+
if (seenTaskIds.has(localTask.id)) {
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
tasks.push(localTask);
|
|
3025
|
+
seenTaskIds.add(localTask.id);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
snapshot: resolvedSnapshot,
|
|
3030
|
+
tasks,
|
|
3031
|
+
tasksById: new Map(tasks.map((task) => [task.id, task]))
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function loadReadonlyTaskTrackerContext(projectRoot) {
|
|
3035
|
+
return loadTaskTrackerContext(projectRoot, undefined, true);
|
|
3036
|
+
}
|
|
3037
|
+
function listReadyTaskIds(context) {
|
|
3038
|
+
const readyTaskIds = [];
|
|
3039
|
+
for (const issue of context.tasks) {
|
|
3040
|
+
const normalizedStatus = normalizeTaskLifecycleStatus(issue.status) ?? issue.status;
|
|
3041
|
+
if (normalizedStatus === "ready") {
|
|
3042
|
+
readyTaskIds.push(issue.id);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
if (normalizedStatus !== "open") {
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
const hasOpenBlocker = (issue.dependencies ?? []).some((dependency) => {
|
|
3049
|
+
if (dependency.type === "parent-child") {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
const targetTaskId = dependency.depends_on_id || dependency.issue_id || dependency.id || "";
|
|
3053
|
+
if (!targetTaskId) {
|
|
3054
|
+
return false;
|
|
3055
|
+
}
|
|
3056
|
+
const dependencyStatus = normalizeTaskLifecycleStatus(context.tasksById.get(targetTaskId)?.status) ?? "unknown";
|
|
3057
|
+
return dependencyStatus !== "completed" && dependencyStatus !== "cancelled";
|
|
3058
|
+
});
|
|
3059
|
+
if (!hasOpenBlocker) {
|
|
3060
|
+
readyTaskIds.push(issue.id);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
return readyTaskIds;
|
|
3064
|
+
}
|
|
3065
|
+
function projectTaskRecordFromSyncedIssue(issue) {
|
|
3066
|
+
const dependencies = issue.dependencies ?? [];
|
|
3067
|
+
return {
|
|
3068
|
+
id: issue.id,
|
|
3069
|
+
status: issue.status === "unknown" ? issue.rawStatus || "open" : issue.status,
|
|
3070
|
+
issue_type: "task",
|
|
3071
|
+
title: issue.title ?? undefined,
|
|
3072
|
+
priority: issue.priority ?? undefined,
|
|
3073
|
+
description: issue.description ?? undefined,
|
|
3074
|
+
acceptance_criteria: issue.acceptanceCriteria ?? undefined,
|
|
3075
|
+
acceptanceCriteria: issue.acceptanceCriteria ?? undefined,
|
|
3076
|
+
dependencies: dependencies.map((dependency) => ({
|
|
3077
|
+
type: dependency.type ?? undefined,
|
|
3078
|
+
issue_id: dependency.issueId ?? undefined,
|
|
3079
|
+
depends_on_id: dependency.dependsOnId ?? undefined,
|
|
3080
|
+
id: dependency.id ?? dependency.issueId ?? dependency.dependsOnId ?? undefined
|
|
3081
|
+
}))
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
function reopenSingleTask(projectRoot, taskId, summary) {
|
|
3085
|
+
try {
|
|
3086
|
+
updateRemoteTrackerTaskLifecycle(projectRoot, {
|
|
3087
|
+
taskId,
|
|
3088
|
+
status: "open",
|
|
3089
|
+
allowedFrom: ["completed", "cancelled", "blocked"],
|
|
3090
|
+
clearMetadata: true,
|
|
3091
|
+
reason: "reopen"
|
|
3092
|
+
});
|
|
3093
|
+
summary.reopened.push(taskId);
|
|
3094
|
+
return;
|
|
3095
|
+
} catch (error) {
|
|
3096
|
+
summary.failed.push(taskId);
|
|
3097
|
+
const details = error instanceof Error ? error.message.trim() : String(error).trim();
|
|
3098
|
+
if (details) {
|
|
3099
|
+
console.log(`WARN: failed to reopen ${taskId}: ${details}`);
|
|
3100
|
+
} else {
|
|
3101
|
+
console.log(`WARN: failed to reopen ${taskId}`);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
function printTaskReopenSummary(summary) {
|
|
3106
|
+
console.log(`Reopened: ${summary.reopened.length}`);
|
|
3107
|
+
if (summary.failed.length > 0) {
|
|
3108
|
+
console.log(`Failed: ${summary.failed.length}`);
|
|
3109
|
+
}
|
|
3110
|
+
if (summary.skipped.length > 0) {
|
|
3111
|
+
console.log(`Skipped: ${summary.skipped.length}`);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
function taskDependencyIds(projectRoot, taskId) {
|
|
3115
|
+
const tracker = loadReadonlyTaskTrackerContext(projectRoot);
|
|
3116
|
+
const canonicalTaskIds = new Set(tracker.tasks.map((task) => task.id));
|
|
3117
|
+
const record = readBeadsTasks(projectRoot, tracker).find((entry) => entry.id === taskId);
|
|
3118
|
+
if (!record?.dependencies?.length) {
|
|
3119
|
+
return [];
|
|
3120
|
+
}
|
|
3121
|
+
const ids = new Set;
|
|
3122
|
+
for (const edge of record.dependencies) {
|
|
3123
|
+
if (!edge || edge.type === "parent-child") {
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
for (const candidate of [edge.depends_on_id, edge.id, edge.issue_id]) {
|
|
3127
|
+
if (typeof candidate !== "string" || candidate === taskId || !canonicalTaskIds.has(candidate)) {
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
ids.add(candidate);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return [...ids].sort();
|
|
3134
|
+
}
|
|
3135
|
+
function taskDependencies(projectRoot, taskId, tracker) {
|
|
3136
|
+
if (!tracker) {
|
|
3137
|
+
return taskDependencyIds(projectRoot, taskId);
|
|
3138
|
+
}
|
|
3139
|
+
const canonicalTaskIds = new Set(tracker.tasks.map((task) => task.id));
|
|
3140
|
+
const record = readBeadsTasks(projectRoot, tracker).find((entry) => entry.id === taskId);
|
|
3141
|
+
if (!record?.dependencies?.length) {
|
|
3142
|
+
return [];
|
|
3143
|
+
}
|
|
3144
|
+
const ids = new Set;
|
|
3145
|
+
for (const edge of record.dependencies) {
|
|
3146
|
+
if (!edge || edge.type === "parent-child") {
|
|
3147
|
+
continue;
|
|
3148
|
+
}
|
|
3149
|
+
for (const candidate of [edge.depends_on_id, edge.id, edge.issue_id]) {
|
|
3150
|
+
if (typeof candidate !== "string" || candidate === taskId || !canonicalTaskIds.has(candidate)) {
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
ids.add(candidate);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
return [...ids].sort();
|
|
3157
|
+
}
|
|
3158
|
+
function printArtifactSection(path, header) {
|
|
3159
|
+
if (!existsSync11(path)) {
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
console.log(header);
|
|
3163
|
+
process.stdout.write(readFileSync7(path, "utf-8"));
|
|
3164
|
+
console.log("");
|
|
3165
|
+
}
|
|
3166
|
+
function escapeRegExp(value) {
|
|
3167
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3168
|
+
}
|
|
3169
|
+
function changedFilesForTask(projectRoot, taskId, scoped) {
|
|
3170
|
+
return filterTaskChangedFiles(projectRoot, taskId, collectTaskChangedFiles(projectRoot, taskId, true), scoped);
|
|
3171
|
+
}
|
|
3172
|
+
function pendingFilesForTask(projectRoot, taskId, scoped) {
|
|
3173
|
+
return filterTaskChangedFiles(projectRoot, taskId, collectTaskChangedFiles(projectRoot, taskId, false), scoped);
|
|
3174
|
+
}
|
|
3175
|
+
export {
|
|
3176
|
+
taskValidate,
|
|
3177
|
+
taskStatus,
|
|
3178
|
+
taskScope,
|
|
3179
|
+
taskReopen,
|
|
3180
|
+
taskRecord,
|
|
3181
|
+
taskReady,
|
|
3182
|
+
taskLookup,
|
|
3183
|
+
taskInfo,
|
|
3184
|
+
taskDeps,
|
|
3185
|
+
taskDependencyIds,
|
|
3186
|
+
taskArtifacts,
|
|
3187
|
+
taskArtifactWrite,
|
|
3188
|
+
taskArtifactRead,
|
|
3189
|
+
taskArtifactDir,
|
|
3190
|
+
pendingFilesForTask,
|
|
3191
|
+
changedFilesForTask
|
|
3192
|
+
};
|