@brainervirus/workit-codex 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/dist/launch-mcp.js +2377 -1254
- package/dist/workit-hook.js +2326 -1181
- package/package.json +3 -3
package/dist/launch-mcp.js
CHANGED
|
@@ -6622,12 +6622,12 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
6622
6622
|
|
|
6623
6623
|
// packages/workit-codex/scripts/launch-mcp.ts
|
|
6624
6624
|
import { existsSync as existsSync8, realpathSync as realpathSync9, statSync as statSync3 } from "node:fs";
|
|
6625
|
-
import
|
|
6625
|
+
import path20 from "node:path";
|
|
6626
6626
|
|
|
6627
6627
|
// packages/workit-core/src/core.ts
|
|
6628
|
-
import { spawnSync as
|
|
6628
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
6629
6629
|
import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
|
|
6630
|
-
import
|
|
6630
|
+
import path15 from "node:path";
|
|
6631
6631
|
|
|
6632
6632
|
// packages/workit-core/src/core/task-contract.ts
|
|
6633
6633
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -15249,16 +15249,6 @@ var stateOperations = {
|
|
|
15249
15249
|
expectedWorkspaceRevision: revision.nullable().optional(),
|
|
15250
15250
|
bundle: exportBundleSchema,
|
|
15251
15251
|
authorityRefs: array(refSchema)
|
|
15252
|
-
}),
|
|
15253
|
-
recover: operation({
|
|
15254
|
-
action: literal("recover"),
|
|
15255
|
-
taskId: id.optional(),
|
|
15256
|
-
expectedWorkspaceRevision: revision.optional(),
|
|
15257
|
-
target: _enum(["task", "workspace"]),
|
|
15258
|
-
expectedBytes: digest,
|
|
15259
|
-
snapshotDigest: digest,
|
|
15260
|
-
reason: text,
|
|
15261
|
-
authorityRefs: array(refSchema)
|
|
15262
15252
|
})
|
|
15263
15253
|
};
|
|
15264
15254
|
var operationSchemas = {
|
|
@@ -15271,10 +15261,17 @@ var operationSchemas = {
|
|
|
15271
15261
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
15272
15262
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
15273
15263
|
};
|
|
15274
|
-
var {
|
|
15264
|
+
var withImplicitTask = (options) => Object.values(options).map((option) => ("taskId" in option.shape) ? option.extend({ taskId: id.optional() }) : option);
|
|
15265
|
+
var advertised = (options) => discriminatedUnion("action", withImplicitTask(options));
|
|
15275
15266
|
var advertisedOperationSchemas = {
|
|
15276
|
-
|
|
15277
|
-
|
|
15267
|
+
task: advertised(taskOperations),
|
|
15268
|
+
policy: advertised(policyOperations),
|
|
15269
|
+
evidence: advertised(evidenceOperations),
|
|
15270
|
+
finding: advertised(findingOperations),
|
|
15271
|
+
decision: advertised(decisionOperations),
|
|
15272
|
+
worker: advertised(workerOperations),
|
|
15273
|
+
writer: advertised(writerOperations),
|
|
15274
|
+
state: advertised(stateOperations)
|
|
15278
15275
|
};
|
|
15279
15276
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
15280
15277
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
@@ -15489,29 +15486,10 @@ function requirementId(input) {
|
|
|
15489
15486
|
});
|
|
15490
15487
|
}
|
|
15491
15488
|
// packages/workit-core/src/core/task-store.ts
|
|
15492
|
-
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
15493
15489
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15494
|
-
import
|
|
15495
|
-
import
|
|
15496
|
-
|
|
15497
|
-
// packages/workit-core/src/core/package-root.ts
|
|
15498
|
-
import { existsSync } from "node:fs";
|
|
15499
|
-
import path from "node:path";
|
|
15500
|
-
import { fileURLToPath } from "node:url";
|
|
15501
|
-
var packageRoot = () => {
|
|
15502
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
15503
|
-
while (true) {
|
|
15504
|
-
const parent = path.dirname(dir);
|
|
15505
|
-
if (existsSync(path.join(dir, "package.json")) || parent === dir)
|
|
15506
|
-
return dir;
|
|
15507
|
-
dir = parent;
|
|
15508
|
-
}
|
|
15509
|
-
};
|
|
15510
|
-
var assetRoot = () => {
|
|
15511
|
-
const root = packageRoot();
|
|
15512
|
-
const assets = path.join(root, "assets");
|
|
15513
|
-
return existsSync(assets) ? assets : root;
|
|
15514
|
-
};
|
|
15490
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "node:crypto";
|
|
15491
|
+
import * as fs7 from "node:fs";
|
|
15492
|
+
import path6 from "node:path";
|
|
15515
15493
|
|
|
15516
15494
|
// node_modules/@openclaw/fs-safe/dist/errors.js
|
|
15517
15495
|
var OPERATIONAL_CODES = new Set([
|
|
@@ -15562,7 +15540,7 @@ function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
15562
15540
|
// node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
15563
15541
|
import { randomBytes } from "node:crypto";
|
|
15564
15542
|
import fsSync from "node:fs";
|
|
15565
|
-
import
|
|
15543
|
+
import path from "node:path";
|
|
15566
15544
|
|
|
15567
15545
|
// node_modules/@openclaw/fs-safe/dist/bounded-read.js
|
|
15568
15546
|
import fs from "node:fs";
|
|
@@ -15652,13 +15630,13 @@ ${ownershipToken}
|
|
|
15652
15630
|
return { raw, ownershipToken };
|
|
15653
15631
|
}
|
|
15654
15632
|
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
15655
|
-
const resolved =
|
|
15656
|
-
const lexicalRelative =
|
|
15657
|
-
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${
|
|
15658
|
-
if (!relative || relative === ".." || relative.startsWith(`..${
|
|
15633
|
+
const resolved = path.resolve(lockPath);
|
|
15634
|
+
const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
|
|
15635
|
+
const relative = lexicalRelative !== ".." && !lexicalRelative.startsWith(`..${path.sep}`) && !path.isAbsolute(lexicalRelative) ? lexicalRelative : path.relative(lockRoot.rootReal, resolved);
|
|
15636
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
15659
15637
|
throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
15660
15638
|
}
|
|
15661
|
-
return relative.split(
|
|
15639
|
+
return relative.split(path.sep).join(path.posix.sep);
|
|
15662
15640
|
}
|
|
15663
15641
|
function parseSidecarLockPayload(raw, parser) {
|
|
15664
15642
|
if (parser) {
|
|
@@ -15805,7 +15783,7 @@ function getFsSafeLockConfig() {
|
|
|
15805
15783
|
|
|
15806
15784
|
// node_modules/@openclaw/fs-safe/dist/file-lock-sync.js
|
|
15807
15785
|
import fs3 from "node:fs";
|
|
15808
|
-
import
|
|
15786
|
+
import path2 from "node:path";
|
|
15809
15787
|
|
|
15810
15788
|
// node_modules/@openclaw/fs-safe/dist/timing.js
|
|
15811
15789
|
function sleepSync(ms) {
|
|
@@ -15896,26 +15874,26 @@ function canonicalLockParentSync(parent) {
|
|
|
15896
15874
|
return process.platform === "win32" ? fs3.realpathSync.native(parent) : fs3.realpathSync(parent);
|
|
15897
15875
|
}
|
|
15898
15876
|
function normalizeTargetPath(targetPath) {
|
|
15899
|
-
const resolved =
|
|
15900
|
-
fs3.mkdirSync(
|
|
15877
|
+
const resolved = path2.resolve(targetPath);
|
|
15878
|
+
fs3.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
15901
15879
|
try {
|
|
15902
|
-
return
|
|
15880
|
+
return path2.join(canonicalLockParentSync(path2.dirname(resolved)), path2.basename(resolved));
|
|
15903
15881
|
} catch {
|
|
15904
15882
|
return resolved;
|
|
15905
15883
|
}
|
|
15906
15884
|
}
|
|
15907
15885
|
function boundedLockPath(lockPath, lockRoot) {
|
|
15908
|
-
const resolved =
|
|
15886
|
+
const resolved = path2.resolve(lockPath);
|
|
15909
15887
|
if (!lockRoot)
|
|
15910
15888
|
return resolved;
|
|
15911
15889
|
relativeSidecarLockPath(lockRoot, resolved);
|
|
15912
|
-
const parent =
|
|
15890
|
+
const parent = path2.dirname(resolved);
|
|
15913
15891
|
const parentReal = canonicalLockParentSync(parent);
|
|
15914
|
-
const parentRelative =
|
|
15915
|
-
if (parentRelative === ".." || parentRelative.startsWith(`..${
|
|
15892
|
+
const parentRelative = path2.relative(lockRoot.rootReal, parentReal);
|
|
15893
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path2.sep}`) || path2.isAbsolute(parentRelative)) {
|
|
15916
15894
|
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
15917
15895
|
}
|
|
15918
|
-
return
|
|
15896
|
+
return path2.join(parentReal, path2.basename(resolved));
|
|
15919
15897
|
}
|
|
15920
15898
|
function defaultShouldReclaim(snapshot, staleMs, nowMs) {
|
|
15921
15899
|
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
@@ -16114,10 +16092,311 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
16114
16092
|
}
|
|
16115
16093
|
}
|
|
16116
16094
|
}
|
|
16095
|
+
// packages/workit-core/src/core/package-root.ts
|
|
16096
|
+
import { existsSync } from "node:fs";
|
|
16097
|
+
import path3 from "node:path";
|
|
16098
|
+
import { fileURLToPath } from "node:url";
|
|
16099
|
+
var packageRoot = () => {
|
|
16100
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
16101
|
+
while (true) {
|
|
16102
|
+
const parent = path3.dirname(dir);
|
|
16103
|
+
if (existsSync(path3.join(dir, "package.json")) || parent === dir)
|
|
16104
|
+
return dir;
|
|
16105
|
+
dir = parent;
|
|
16106
|
+
}
|
|
16107
|
+
};
|
|
16108
|
+
var assetRoot = () => {
|
|
16109
|
+
const root = packageRoot();
|
|
16110
|
+
const assets = path3.join(root, "assets");
|
|
16111
|
+
return existsSync(assets) ? assets : root;
|
|
16112
|
+
};
|
|
16113
|
+
|
|
16117
16114
|
// packages/workit-core/src/core/store-lock.ts
|
|
16115
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
16116
|
+
import fs5 from "node:fs";
|
|
16117
|
+
import { hostname } from "node:os";
|
|
16118
|
+
|
|
16119
|
+
// packages/workit-core/src/store/paths.ts
|
|
16118
16120
|
import { spawnSync } from "node:child_process";
|
|
16121
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
16119
16122
|
import fs4 from "node:fs";
|
|
16120
|
-
import
|
|
16123
|
+
import path4 from "node:path";
|
|
16124
|
+
var NOT_A_REPO = /not a git repository \(or any (of the parent directories|parent up to)/u;
|
|
16125
|
+
var gitRun = (cwd, args) => {
|
|
16126
|
+
const run = spawnSync("git", args, {
|
|
16127
|
+
cwd,
|
|
16128
|
+
encoding: "utf8",
|
|
16129
|
+
timeout: 20000,
|
|
16130
|
+
killSignal: "SIGKILL",
|
|
16131
|
+
windowsHide: true,
|
|
16132
|
+
env: {
|
|
16133
|
+
...process.env,
|
|
16134
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
16135
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
16136
|
+
LC_ALL: "C",
|
|
16137
|
+
LANGUAGE: "C"
|
|
16138
|
+
}
|
|
16139
|
+
});
|
|
16140
|
+
const error = run.error;
|
|
16141
|
+
return {
|
|
16142
|
+
ok: run.status === 0,
|
|
16143
|
+
stdout: run.stdout ?? "",
|
|
16144
|
+
stderr: error ? String(error.message) : run.stderr ?? "",
|
|
16145
|
+
missing: error?.code === "ENOENT"
|
|
16146
|
+
};
|
|
16147
|
+
};
|
|
16148
|
+
var shortHash = (value) => createHash2("sha256").update(value).digest("hex").slice(0, 12);
|
|
16149
|
+
var real = (file) => {
|
|
16150
|
+
try {
|
|
16151
|
+
return fs4.realpathSync.native(file);
|
|
16152
|
+
} catch {
|
|
16153
|
+
return file;
|
|
16154
|
+
}
|
|
16155
|
+
};
|
|
16156
|
+
var DISCOVERY_ENV = [
|
|
16157
|
+
"GIT_DIR",
|
|
16158
|
+
"GIT_COMMON_DIR",
|
|
16159
|
+
"GIT_WORK_TREE",
|
|
16160
|
+
"GIT_CEILING_DIRECTORIES",
|
|
16161
|
+
"GIT_DISCOVERY_ACROSS_FILESYSTEM"
|
|
16162
|
+
];
|
|
16163
|
+
var isGitDir = (dir) => {
|
|
16164
|
+
try {
|
|
16165
|
+
return fs4.statSync(path4.join(dir, "HEAD")).isFile() && fs4.statSync(path4.join(dir, "objects")).isDirectory() && fs4.statSync(path4.join(dir, "refs")).isDirectory();
|
|
16166
|
+
} catch {
|
|
16167
|
+
return false;
|
|
16168
|
+
}
|
|
16169
|
+
};
|
|
16170
|
+
var reftableRefs = (common) => {
|
|
16171
|
+
try {
|
|
16172
|
+
return /^\s*refstorage\s*=\s*reftable\s*$/imu.test(fs4.readFileSync(path4.join(common, "config"), "utf8"));
|
|
16173
|
+
} catch {
|
|
16174
|
+
return false;
|
|
16175
|
+
}
|
|
16176
|
+
};
|
|
16177
|
+
function fastGit(root) {
|
|
16178
|
+
if (DISCOVERY_ENV.some((name) => process.env[name]))
|
|
16179
|
+
return null;
|
|
16180
|
+
let dir = real(root);
|
|
16181
|
+
let device = null;
|
|
16182
|
+
for (;; ) {
|
|
16183
|
+
let stat;
|
|
16184
|
+
try {
|
|
16185
|
+
stat = fs4.statSync(dir);
|
|
16186
|
+
} catch {
|
|
16187
|
+
return null;
|
|
16188
|
+
}
|
|
16189
|
+
if (device !== null && stat.dev !== device)
|
|
16190
|
+
return { kind: "none" };
|
|
16191
|
+
device = stat.dev;
|
|
16192
|
+
const dotGit = path4.join(dir, ".git");
|
|
16193
|
+
let entry = null;
|
|
16194
|
+
try {
|
|
16195
|
+
entry = fs4.statSync(dotGit);
|
|
16196
|
+
} catch {}
|
|
16197
|
+
let gitDir = null;
|
|
16198
|
+
let top = null;
|
|
16199
|
+
if (entry) {
|
|
16200
|
+
top = dir;
|
|
16201
|
+
if (entry.isDirectory())
|
|
16202
|
+
gitDir = dotGit;
|
|
16203
|
+
else if (entry.isFile()) {
|
|
16204
|
+
let text = "";
|
|
16205
|
+
try {
|
|
16206
|
+
text = fs4.readFileSync(dotGit, "utf8");
|
|
16207
|
+
} catch {
|
|
16208
|
+
return null;
|
|
16209
|
+
}
|
|
16210
|
+
const match = /^gitdir: (.+)$/mu.exec(text);
|
|
16211
|
+
if (!match)
|
|
16212
|
+
return null;
|
|
16213
|
+
gitDir = path4.resolve(dir, match[1].trim());
|
|
16214
|
+
} else
|
|
16215
|
+
return null;
|
|
16216
|
+
if (!fs4.existsSync(path4.join(gitDir, "HEAD")))
|
|
16217
|
+
return null;
|
|
16218
|
+
} else if (isGitDir(dir))
|
|
16219
|
+
gitDir = dir;
|
|
16220
|
+
if (gitDir) {
|
|
16221
|
+
let common = gitDir;
|
|
16222
|
+
try {
|
|
16223
|
+
common = path4.resolve(gitDir, fs4.readFileSync(path4.join(gitDir, "commondir"), "utf8").trim());
|
|
16224
|
+
} catch {}
|
|
16225
|
+
if (!fs4.existsSync(path4.join(common, "objects")))
|
|
16226
|
+
return null;
|
|
16227
|
+
return {
|
|
16228
|
+
kind: "repo",
|
|
16229
|
+
common: real(common),
|
|
16230
|
+
gitDir: real(gitDir),
|
|
16231
|
+
top: top === null ? null : real(top),
|
|
16232
|
+
reftable: reftableRefs(common)
|
|
16233
|
+
};
|
|
16234
|
+
}
|
|
16235
|
+
const parent = path4.dirname(dir);
|
|
16236
|
+
if (parent === dir)
|
|
16237
|
+
return { kind: "none" };
|
|
16238
|
+
dir = parent;
|
|
16239
|
+
}
|
|
16240
|
+
}
|
|
16241
|
+
function resolveStore(root) {
|
|
16242
|
+
root = path4.resolve(root);
|
|
16243
|
+
let directory = false;
|
|
16244
|
+
try {
|
|
16245
|
+
directory = fs4.statSync(root).isDirectory();
|
|
16246
|
+
} catch {}
|
|
16247
|
+
if (!directory)
|
|
16248
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
16249
|
+
const fast = fastGit(root);
|
|
16250
|
+
if (fast?.kind === "none")
|
|
16251
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
16252
|
+
if (fast?.kind === "repo")
|
|
16253
|
+
return {
|
|
16254
|
+
dir: path4.join(fast.common, "workit"),
|
|
16255
|
+
shared: true,
|
|
16256
|
+
top: fast.top,
|
|
16257
|
+
git: { gitDir: fast.gitDir, top: fast.top, reftable: fast.reftable }
|
|
16258
|
+
};
|
|
16259
|
+
const run = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
16260
|
+
const common = run.stdout.trim();
|
|
16261
|
+
if (run.ok && common) {
|
|
16262
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
16263
|
+
return {
|
|
16264
|
+
dir: path4.join(real(path4.resolve(root, common)), "workit"),
|
|
16265
|
+
shared: true,
|
|
16266
|
+
top: top.ok && top.stdout.trim() ? real(top.stdout.trim()) : null
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
if (run.missing || !run.ok && NOT_A_REPO.test(run.stderr))
|
|
16270
|
+
return { dir: path4.join(root, ".workit"), shared: false, top: null };
|
|
16271
|
+
return new Error(`cannot locate the workit store: git rev-parse failed (${run.stderr.trim().split(`
|
|
16272
|
+
`)[0] || "no output"}); fix the repository (git status should work here)`);
|
|
16273
|
+
}
|
|
16274
|
+
var rebasingBranch = (gitDir) => {
|
|
16275
|
+
for (const state of ["rebase-merge", "rebase-apply"]) {
|
|
16276
|
+
try {
|
|
16277
|
+
const ref = fs4.readFileSync(path4.join(gitDir, state, "head-name"), "utf8").trim();
|
|
16278
|
+
const match = /^refs\/heads\/(.+)$/u.exec(ref);
|
|
16279
|
+
if (match)
|
|
16280
|
+
return match[1];
|
|
16281
|
+
} catch {}
|
|
16282
|
+
}
|
|
16283
|
+
return null;
|
|
16284
|
+
};
|
|
16285
|
+
function resolveTaskKey(root, location) {
|
|
16286
|
+
if (!location.shared)
|
|
16287
|
+
return { key: `dir-${shortHash(root)}`, kind: "dir", branch: null };
|
|
16288
|
+
const branchKey = (name) => ({ key: name, kind: "branch", branch: name });
|
|
16289
|
+
if (location.git && !location.git.reftable) {
|
|
16290
|
+
let head = "";
|
|
16291
|
+
try {
|
|
16292
|
+
head = fs4.readFileSync(path4.join(location.git.gitDir, "HEAD"), "utf8").trim();
|
|
16293
|
+
} catch {}
|
|
16294
|
+
const ref = /^ref: refs\/heads\/(.+)$/u.exec(head);
|
|
16295
|
+
if (ref && ref[1] !== ".invalid")
|
|
16296
|
+
return branchKey(ref[1]);
|
|
16297
|
+
if (/^[0-9a-f]{40,64}$/u.test(head)) {
|
|
16298
|
+
const rebasing = rebasingBranch(location.git.gitDir);
|
|
16299
|
+
if (rebasing)
|
|
16300
|
+
return branchKey(rebasing);
|
|
16301
|
+
return {
|
|
16302
|
+
key: `detached-${shortHash(location.git.top ?? location.git.gitDir)}`,
|
|
16303
|
+
kind: "detached",
|
|
16304
|
+
branch: null
|
|
16305
|
+
};
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
const branch = gitRun(root, ["symbolic-ref", "-q", "--short", "HEAD"]);
|
|
16309
|
+
const name = branch.ok ? branch.stdout.trim() : "";
|
|
16310
|
+
if (name && name !== ".invalid")
|
|
16311
|
+
return branchKey(name);
|
|
16312
|
+
const gitDir = gitRun(root, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
16313
|
+
if (gitDir.ok && gitDir.stdout.trim()) {
|
|
16314
|
+
const rebasing = rebasingBranch(gitDir.stdout.trim());
|
|
16315
|
+
if (rebasing)
|
|
16316
|
+
return branchKey(rebasing);
|
|
16317
|
+
}
|
|
16318
|
+
const top = gitRun(root, ["rev-parse", "--path-format=absolute", "--show-toplevel"]);
|
|
16319
|
+
const worktree = top.ok && top.stdout.trim() ? top.stdout.trim() : root;
|
|
16320
|
+
return { key: `detached-${shortHash(real(worktree))}`, kind: "detached", branch: null };
|
|
16321
|
+
}
|
|
16322
|
+
function checkoutRootOf(root, location) {
|
|
16323
|
+
const resolved = path4.resolve(root);
|
|
16324
|
+
let canonical = resolved;
|
|
16325
|
+
try {
|
|
16326
|
+
canonical = fs4.realpathSync(resolved);
|
|
16327
|
+
} catch {
|
|
16328
|
+
return resolved;
|
|
16329
|
+
}
|
|
16330
|
+
const found = location ?? resolveStore(canonical);
|
|
16331
|
+
if (found instanceof Error || !found.shared || !found.top)
|
|
16332
|
+
return canonical;
|
|
16333
|
+
if (real(canonical) === found.top)
|
|
16334
|
+
return canonical;
|
|
16335
|
+
try {
|
|
16336
|
+
return fs4.realpathSync(found.top);
|
|
16337
|
+
} catch {
|
|
16338
|
+
return canonical;
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
function branchRenames(location) {
|
|
16342
|
+
if (!location.shared)
|
|
16343
|
+
return [];
|
|
16344
|
+
const files = [];
|
|
16345
|
+
const walk = (dir) => {
|
|
16346
|
+
let entries;
|
|
16347
|
+
try {
|
|
16348
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
16349
|
+
} catch {
|
|
16350
|
+
return;
|
|
16351
|
+
}
|
|
16352
|
+
for (const entry of entries)
|
|
16353
|
+
if (entry.isDirectory())
|
|
16354
|
+
walk(path4.join(dir, entry.name));
|
|
16355
|
+
else
|
|
16356
|
+
files.push(path4.join(dir, entry.name));
|
|
16357
|
+
};
|
|
16358
|
+
walk(path4.join(path4.dirname(location.dir), "logs", "refs", "heads"));
|
|
16359
|
+
if (location.git)
|
|
16360
|
+
files.push(path4.join(location.git.gitDir, "logs", "HEAD"));
|
|
16361
|
+
const seen = new Set;
|
|
16362
|
+
const renames = [];
|
|
16363
|
+
for (const file of files) {
|
|
16364
|
+
let text = "";
|
|
16365
|
+
try {
|
|
16366
|
+
text = fs4.readFileSync(file, "utf8");
|
|
16367
|
+
} catch {
|
|
16368
|
+
continue;
|
|
16369
|
+
}
|
|
16370
|
+
for (const match of text.matchAll(/ (\d+) [+-]\d{4}\tBranch: renamed refs\/heads\/(\S+) to refs\/heads\/(\S+)/gu)) {
|
|
16371
|
+
const rename = { from: match[2], to: match[3], at: Number(match[1]) };
|
|
16372
|
+
const id = `${rename.at}\x00${rename.from}\x00${rename.to}`;
|
|
16373
|
+
if (seen.has(id))
|
|
16374
|
+
continue;
|
|
16375
|
+
seen.add(id);
|
|
16376
|
+
renames.push(rename);
|
|
16377
|
+
}
|
|
16378
|
+
}
|
|
16379
|
+
return renames.toSorted((left, right) => left.at - right.at);
|
|
16380
|
+
}
|
|
16381
|
+
function followRenames(key, boundAt, renames) {
|
|
16382
|
+
let current = key;
|
|
16383
|
+
let since = Math.floor(Date.parse(boundAt) / 1000);
|
|
16384
|
+
for (const rename of renames)
|
|
16385
|
+
if (rename.from === current && rename.at >= since) {
|
|
16386
|
+
current = rename.to;
|
|
16387
|
+
since = rename.at;
|
|
16388
|
+
}
|
|
16389
|
+
return current;
|
|
16390
|
+
}
|
|
16391
|
+
var checkoutSlug = (root) => {
|
|
16392
|
+
let canonical = real(root);
|
|
16393
|
+
if (process.platform === "win32")
|
|
16394
|
+
canonical = canonical.toLowerCase();
|
|
16395
|
+
const base = path4.basename(canonical).replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40) || "root";
|
|
16396
|
+
return `${base}-${shortHash(canonical)}`;
|
|
16397
|
+
};
|
|
16398
|
+
|
|
16399
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
16121
16400
|
var metadataLockSchema = object({
|
|
16122
16401
|
pid: number2().int().nonnegative().safe(),
|
|
16123
16402
|
processStart: string2().nullable(),
|
|
@@ -16149,10 +16428,6 @@ var parseMetadataLockOrNull = (raw) => {
|
|
|
16149
16428
|
return null;
|
|
16150
16429
|
}
|
|
16151
16430
|
};
|
|
16152
|
-
var sameMetadataLock = (left, right) => {
|
|
16153
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
16154
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
16155
|
-
};
|
|
16156
16431
|
var parseProcStatStart = (stat) => {
|
|
16157
16432
|
const close = stat.lastIndexOf(")");
|
|
16158
16433
|
if (close < 0)
|
|
@@ -16162,14 +16437,14 @@ var parseProcStatStart = (stat) => {
|
|
|
16162
16437
|
var processStartOf = (pid) => {
|
|
16163
16438
|
if (process.platform === "linux") {
|
|
16164
16439
|
try {
|
|
16165
|
-
return parseProcStatStart(
|
|
16440
|
+
return parseProcStatStart(fs5.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
16166
16441
|
} catch {
|
|
16167
16442
|
return null;
|
|
16168
16443
|
}
|
|
16169
16444
|
}
|
|
16170
16445
|
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
16171
16446
|
try {
|
|
16172
|
-
const run =
|
|
16447
|
+
const run = spawnSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
16173
16448
|
encoding: "utf8",
|
|
16174
16449
|
timeout: 1000
|
|
16175
16450
|
});
|
|
@@ -16183,7 +16458,7 @@ var processStartOf = (pid) => {
|
|
|
16183
16458
|
};
|
|
16184
16459
|
var readTrimmed = (file) => {
|
|
16185
16460
|
try {
|
|
16186
|
-
return
|
|
16461
|
+
return fs5.readFileSync(file, "utf8").trim() || null;
|
|
16187
16462
|
} catch {
|
|
16188
16463
|
return null;
|
|
16189
16464
|
}
|
|
@@ -16194,7 +16469,7 @@ var localLockHost = () => {
|
|
|
16194
16469
|
return cachedLockHost;
|
|
16195
16470
|
let pidns = null;
|
|
16196
16471
|
try {
|
|
16197
|
-
pidns = /\[(\d+)\]/.exec(
|
|
16472
|
+
pidns = /\[(\d+)\]/.exec(fs5.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
16198
16473
|
} catch {}
|
|
16199
16474
|
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
16200
16475
|
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
@@ -16234,7 +16509,7 @@ var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
|
16234
16509
|
};
|
|
16235
16510
|
var ageOf = (file, nowMs) => {
|
|
16236
16511
|
try {
|
|
16237
|
-
return nowMs -
|
|
16512
|
+
return nowMs - fs5.lstatSync(file).mtimeMs;
|
|
16238
16513
|
} catch {
|
|
16239
16514
|
return null;
|
|
16240
16515
|
}
|
|
@@ -16245,117 +16520,151 @@ var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
|
16245
16520
|
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
16246
16521
|
return false;
|
|
16247
16522
|
try {
|
|
16248
|
-
|
|
16523
|
+
fs5.rmdirSync(guard);
|
|
16249
16524
|
return true;
|
|
16250
16525
|
} catch {
|
|
16251
16526
|
return false;
|
|
16252
16527
|
}
|
|
16253
16528
|
};
|
|
16254
16529
|
|
|
16255
|
-
// packages/workit-core/src/
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
var
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
16267
|
-
var indexEntry = (task, file) => {
|
|
16268
|
-
const sessions = [];
|
|
16269
|
-
const intent = hostSession(task.intent.provenance.session);
|
|
16270
|
-
if (intent)
|
|
16271
|
-
sessions.push({ ...intent, workerId: null });
|
|
16272
|
-
for (const worker of task.workers) {
|
|
16273
|
-
const session = hostSession(worker.data.session);
|
|
16274
|
-
if (session)
|
|
16275
|
-
sessions.push({ ...session, workerId: worker.id });
|
|
16530
|
+
// packages/workit-core/src/store/event-log.ts
|
|
16531
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16532
|
+
import fs6 from "node:fs";
|
|
16533
|
+
import path5 from "node:path";
|
|
16534
|
+
var EVENT_VERSION = 1;
|
|
16535
|
+
|
|
16536
|
+
class LogDamage extends Error {
|
|
16537
|
+
upgrade;
|
|
16538
|
+
constructor(message, upgrade = false) {
|
|
16539
|
+
super(message);
|
|
16540
|
+
this.upgrade = upgrade;
|
|
16276
16541
|
}
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
createdAt: task.createdAt,
|
|
16282
|
-
updatedAt: task.updatedAt,
|
|
16283
|
-
objective: task.intent.data.objective,
|
|
16284
|
-
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
16285
|
-
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
16286
|
-
sessions,
|
|
16287
|
-
file
|
|
16288
|
-
};
|
|
16289
|
-
};
|
|
16290
|
-
var fileSignature = (file) => {
|
|
16542
|
+
}
|
|
16543
|
+
var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16544
|
+
var parseEvent = (line, file, offset) => {
|
|
16545
|
+
let value;
|
|
16291
16546
|
try {
|
|
16292
|
-
|
|
16293
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
16547
|
+
value = JSON.parse(line);
|
|
16294
16548
|
} catch {
|
|
16295
|
-
|
|
16296
|
-
}
|
|
16297
|
-
|
|
16298
|
-
|
|
16299
|
-
|
|
16300
|
-
|
|
16301
|
-
|
|
16302
|
-
|
|
16549
|
+
throw new LogDamage(`${file}: unreadable event at byte ${offset}`);
|
|
16550
|
+
}
|
|
16551
|
+
if (!isObject2(value) || typeof value.v !== "number")
|
|
16552
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
16553
|
+
if (value.v > EVENT_VERSION)
|
|
16554
|
+
throw new LogDamage(`${file}: event format v${value.v} was written by a newer Workit; upgrade Workit to read this task`, true);
|
|
16555
|
+
if (typeof value.seq !== "number" || !Number.isInteger(value.seq) || typeof value.type !== "string" || typeof value.id !== "string" || typeof value.task !== "string" || !isObject2(value.data))
|
|
16556
|
+
throw new LogDamage(`${file}: malformed event at byte ${offset}`);
|
|
16557
|
+
return value;
|
|
16303
16558
|
};
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
|
|
16307
|
-
|
|
16559
|
+
function readLog(file, from = 0, expectSeq = null) {
|
|
16560
|
+
let fd;
|
|
16561
|
+
try {
|
|
16562
|
+
fd = fs6.openSync(file, "r");
|
|
16563
|
+
} catch (error) {
|
|
16564
|
+
if (error.code === "ENOENT")
|
|
16565
|
+
return null;
|
|
16566
|
+
throw error;
|
|
16567
|
+
}
|
|
16568
|
+
let bytes;
|
|
16569
|
+
let size;
|
|
16570
|
+
try {
|
|
16571
|
+
size = fs6.fstatSync(fd).size;
|
|
16572
|
+
if (from > size)
|
|
16573
|
+
throw new LogDamage(`${file}: shorter than its snapshot`);
|
|
16574
|
+
bytes = Buffer.alloc(size - from);
|
|
16575
|
+
let read = 0;
|
|
16576
|
+
while (read < bytes.length) {
|
|
16577
|
+
const got = fs6.readSync(fd, bytes, read, bytes.length - read, from + read);
|
|
16578
|
+
if (got === 0)
|
|
16579
|
+
break;
|
|
16580
|
+
read += got;
|
|
16581
|
+
}
|
|
16582
|
+
bytes = bytes.subarray(0, read);
|
|
16583
|
+
size = from + read;
|
|
16584
|
+
} finally {
|
|
16585
|
+
fs6.closeSync(fd);
|
|
16586
|
+
}
|
|
16587
|
+
const events = [];
|
|
16588
|
+
let start = 0;
|
|
16589
|
+
let lastStart = from;
|
|
16590
|
+
let seq = expectSeq;
|
|
16591
|
+
for (;; ) {
|
|
16592
|
+
const newline = bytes.indexOf(10, start);
|
|
16593
|
+
if (newline < 0)
|
|
16594
|
+
break;
|
|
16595
|
+
const line = bytes.subarray(start, newline).toString("utf8");
|
|
16596
|
+
if (line.trim() !== "") {
|
|
16597
|
+
const event = parseEvent(line, file, from + start);
|
|
16598
|
+
if (seq !== null && event.seq !== seq)
|
|
16599
|
+
throw new LogDamage(`${file}: event seq ${event.seq} where ${seq} was expected`);
|
|
16600
|
+
seq = event.seq + 1;
|
|
16601
|
+
events.push(event);
|
|
16602
|
+
lastStart = from + start;
|
|
16603
|
+
}
|
|
16604
|
+
start = newline + 1;
|
|
16605
|
+
}
|
|
16606
|
+
return { events, end: from + start, lastStart, size };
|
|
16607
|
+
}
|
|
16608
|
+
var fsyncDirectory = (dir) => {
|
|
16609
|
+
try {
|
|
16610
|
+
const fd = fs6.openSync(dir, "r");
|
|
16308
16611
|
try {
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16312
|
-
cachedRuntimeVersion = "0.0.0";
|
|
16612
|
+
fs6.fsyncSync(fd);
|
|
16613
|
+
} finally {
|
|
16614
|
+
fs6.closeSync(fd);
|
|
16313
16615
|
}
|
|
16616
|
+
} catch (error) {
|
|
16617
|
+
if (process.platform !== "win32")
|
|
16618
|
+
throw error;
|
|
16314
16619
|
}
|
|
16315
|
-
return cachedRuntimeVersion;
|
|
16316
16620
|
};
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
|
|
16320
|
-
|
|
16321
|
-
|
|
16322
|
-
|
|
16323
|
-
|
|
16324
|
-
|
|
16325
|
-
|
|
16326
|
-
|
|
16327
|
-
|
|
16328
|
-
|
|
16621
|
+
function appendEvent(file, event, validEnd) {
|
|
16622
|
+
const line = Buffer.from(`${JSON.stringify(event)}
|
|
16623
|
+
`, "utf8");
|
|
16624
|
+
const created = !fs6.existsSync(file);
|
|
16625
|
+
if (!created && fs6.statSync(file).size !== validEnd) {
|
|
16626
|
+
const cut = fs6.openSync(file, "r+");
|
|
16627
|
+
try {
|
|
16628
|
+
fs6.ftruncateSync(cut, validEnd);
|
|
16629
|
+
fs6.fsyncSync(cut);
|
|
16630
|
+
} finally {
|
|
16631
|
+
fs6.closeSync(cut);
|
|
16632
|
+
}
|
|
16329
16633
|
}
|
|
16330
|
-
|
|
16331
|
-
};
|
|
16332
|
-
var jsonBytes = (value) => `${canonicalJson(value)}
|
|
16333
|
-
`;
|
|
16334
|
-
var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
16335
|
-
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
16336
|
-
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
16337
|
-
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
16338
|
-
var sameDirectoryIdentity = (left, right) => {
|
|
16339
|
-
if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
|
|
16340
|
-
return false;
|
|
16341
|
-
const normalizedLeft = path4.resolve(left);
|
|
16342
|
-
const normalizedRight = path4.resolve(right);
|
|
16634
|
+
const fd = fs6.openSync(file, "a", 384);
|
|
16343
16635
|
try {
|
|
16344
|
-
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16350
|
-
const canonicalLeft = fs5.realpathSync(normalizedLeft);
|
|
16351
|
-
const canonicalRight = fs5.realpathSync(normalizedRight);
|
|
16352
|
-
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
16353
|
-
return true;
|
|
16354
|
-
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
16355
|
-
} catch {
|
|
16356
|
-
return false;
|
|
16636
|
+
let written = 0;
|
|
16637
|
+
while (written < line.length)
|
|
16638
|
+
written += fs6.writeSync(fd, line, written, line.length - written);
|
|
16639
|
+
fs6.fsyncSync(fd);
|
|
16640
|
+
} finally {
|
|
16641
|
+
fs6.closeSync(fd);
|
|
16357
16642
|
}
|
|
16358
|
-
|
|
16643
|
+
if (created)
|
|
16644
|
+
fsyncDirectory(path5.dirname(file));
|
|
16645
|
+
return validEnd;
|
|
16646
|
+
}
|
|
16647
|
+
function replaceFile(file, bytes, durable) {
|
|
16648
|
+
const temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
16649
|
+
try {
|
|
16650
|
+
const fd = fs6.openSync(temporary, "wx", 384);
|
|
16651
|
+
try {
|
|
16652
|
+
fs6.writeSync(fd, bytes);
|
|
16653
|
+
if (durable)
|
|
16654
|
+
fs6.fsyncSync(fd);
|
|
16655
|
+
} finally {
|
|
16656
|
+
fs6.closeSync(fd);
|
|
16657
|
+
}
|
|
16658
|
+
renameWithRetry(temporary, file);
|
|
16659
|
+
if (durable)
|
|
16660
|
+
fsyncDirectory(path5.dirname(file));
|
|
16661
|
+
} catch (error) {
|
|
16662
|
+
try {
|
|
16663
|
+
fs6.unlinkSync(temporary);
|
|
16664
|
+
} catch {}
|
|
16665
|
+
throw error;
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16359
16668
|
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
16360
16669
|
var isTransientWindowsError = (error) => {
|
|
16361
16670
|
if (process.platform !== "win32")
|
|
@@ -16376,138 +16685,596 @@ var retryTransient = (run) => {
|
|
|
16376
16685
|
}
|
|
16377
16686
|
}
|
|
16378
16687
|
};
|
|
16688
|
+
var renameWithRetry = (from, to) => retryTransient(() => fs6.renameSync(from, to));
|
|
16689
|
+
|
|
16690
|
+
// packages/workit-core/src/store/patch.ts
|
|
16691
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16692
|
+
var equal = (left, right) => {
|
|
16693
|
+
if (left === right)
|
|
16694
|
+
return true;
|
|
16695
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null)
|
|
16696
|
+
return false;
|
|
16697
|
+
if (Array.isArray(left) !== Array.isArray(right))
|
|
16698
|
+
return false;
|
|
16699
|
+
if (Array.isArray(left)) {
|
|
16700
|
+
const other = right;
|
|
16701
|
+
return left.length === other.length && left.every((item, index) => equal(item, other[index]));
|
|
16702
|
+
}
|
|
16703
|
+
const leftKeys = Object.keys(left).filter((key) => left[key] !== undefined);
|
|
16704
|
+
const rightKeys = Object.keys(right).filter((key) => right[key] !== undefined);
|
|
16705
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => equal(left[key], right[key]));
|
|
16706
|
+
};
|
|
16707
|
+
function diff(before, after, path = []) {
|
|
16708
|
+
if (equal(before, after))
|
|
16709
|
+
return [];
|
|
16710
|
+
if (isPlainObject2(before) && isPlainObject2(after)) {
|
|
16711
|
+
const ops = [];
|
|
16712
|
+
for (const key of Object.keys(before))
|
|
16713
|
+
if (before[key] !== undefined && after[key] === undefined)
|
|
16714
|
+
ops.push({ op: "del", path: [...path, key] });
|
|
16715
|
+
for (const key of Object.keys(after)) {
|
|
16716
|
+
if (after[key] === undefined)
|
|
16717
|
+
continue;
|
|
16718
|
+
if (before[key] === undefined)
|
|
16719
|
+
ops.push({ op: "set", path: [...path, key], value: after[key] });
|
|
16720
|
+
else
|
|
16721
|
+
ops.push(...diff(before[key], after[key], [...path, key]));
|
|
16722
|
+
}
|
|
16723
|
+
return ops;
|
|
16724
|
+
}
|
|
16725
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
16726
|
+
const ops = [];
|
|
16727
|
+
if (after.length < before.length)
|
|
16728
|
+
ops.push({ op: "cut", path, length: after.length });
|
|
16729
|
+
const shared = Math.min(before.length, after.length);
|
|
16730
|
+
let changed = 0;
|
|
16731
|
+
for (let index = 0;index < shared; index += 1)
|
|
16732
|
+
if (!equal(before[index], after[index]))
|
|
16733
|
+
changed += 1;
|
|
16734
|
+
if (shared > 0 && changed > shared / 2 && changed > 4)
|
|
16735
|
+
return [{ op: "set", path, value: after }];
|
|
16736
|
+
for (let index = 0;index < shared; index += 1)
|
|
16737
|
+
ops.push(...diff(before[index], after[index], [...path, index]));
|
|
16738
|
+
if (after.length > before.length)
|
|
16739
|
+
ops.push({ op: "push", path, values: after.slice(before.length) });
|
|
16740
|
+
return ops;
|
|
16741
|
+
}
|
|
16742
|
+
return [{ op: "set", path, value: after }];
|
|
16743
|
+
}
|
|
16744
|
+
var clone2 = (value) => structuredClone(value);
|
|
16745
|
+
function apply(target, ops) {
|
|
16746
|
+
let root = target;
|
|
16747
|
+
for (const op of ops) {
|
|
16748
|
+
if (op.path.length === 0) {
|
|
16749
|
+
if (op.op === "set")
|
|
16750
|
+
root = clone2(op.value);
|
|
16751
|
+
else if (op.op === "push" && Array.isArray(root))
|
|
16752
|
+
root.push(...clone2(op.values));
|
|
16753
|
+
else if (op.op === "cut" && Array.isArray(root))
|
|
16754
|
+
root.length = op.length;
|
|
16755
|
+
else
|
|
16756
|
+
throw new Error(`patch: ${op.op} at the root is invalid`);
|
|
16757
|
+
continue;
|
|
16758
|
+
}
|
|
16759
|
+
let parent = root;
|
|
16760
|
+
for (const key of op.path.slice(0, -1)) {
|
|
16761
|
+
if (typeof parent !== "object" || parent === null)
|
|
16762
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
16763
|
+
parent = parent[key];
|
|
16764
|
+
}
|
|
16765
|
+
if (typeof parent !== "object" || parent === null)
|
|
16766
|
+
throw new Error(`patch: missing path ${op.path.join(".")}`);
|
|
16767
|
+
const key = op.path.at(-1);
|
|
16768
|
+
const container = parent;
|
|
16769
|
+
if (op.op === "set")
|
|
16770
|
+
container[key] = clone2(op.value);
|
|
16771
|
+
else if (op.op === "del")
|
|
16772
|
+
delete container[key];
|
|
16773
|
+
else {
|
|
16774
|
+
const array = container[key];
|
|
16775
|
+
if (!Array.isArray(array))
|
|
16776
|
+
throw new Error(`patch: ${op.path.join(".")} is not an array`);
|
|
16777
|
+
if (op.op === "push")
|
|
16778
|
+
array.push(...clone2(op.values));
|
|
16779
|
+
else
|
|
16780
|
+
array.length = op.length;
|
|
16781
|
+
}
|
|
16782
|
+
}
|
|
16783
|
+
return root;
|
|
16784
|
+
}
|
|
16785
|
+
var isPath = (value) => Array.isArray(value) && value.every((key) => typeof key === "string" || typeof key === "number" && Number.isInteger(key) && key >= 0);
|
|
16786
|
+
var isPatch = (value) => Array.isArray(value) && value.every((op) => {
|
|
16787
|
+
if (!isPlainObject2(op) || !isPath(op.path))
|
|
16788
|
+
return false;
|
|
16789
|
+
if (op.op === "set")
|
|
16790
|
+
return "value" in op;
|
|
16791
|
+
if (op.op === "del")
|
|
16792
|
+
return op.path.length > 0;
|
|
16793
|
+
if (op.op === "push")
|
|
16794
|
+
return Array.isArray(op.values);
|
|
16795
|
+
if (op.op === "cut")
|
|
16796
|
+
return typeof op.length === "number" && Number.isInteger(op.length) && op.length >= 0;
|
|
16797
|
+
return false;
|
|
16798
|
+
});
|
|
16799
|
+
|
|
16800
|
+
// packages/workit-core/src/store/reduce.ts
|
|
16801
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
16802
|
+
var OPENING_TYPES = new Set([
|
|
16803
|
+
"task.opened",
|
|
16804
|
+
"task.imported",
|
|
16805
|
+
"migrated.from_v2",
|
|
16806
|
+
"task.checkpoint"
|
|
16807
|
+
]);
|
|
16808
|
+
var KNOWN_TYPES = new Set([
|
|
16809
|
+
...OPENING_TYPES,
|
|
16810
|
+
"task.bound",
|
|
16811
|
+
"task.noted",
|
|
16812
|
+
"task.revised",
|
|
16813
|
+
"task.judged",
|
|
16814
|
+
"task.paused",
|
|
16815
|
+
"task.resumed",
|
|
16816
|
+
"task.closed",
|
|
16817
|
+
"task.updated",
|
|
16818
|
+
"check.ran",
|
|
16819
|
+
"evidence.recorded",
|
|
16820
|
+
"finding.recorded",
|
|
16821
|
+
"finding.updated",
|
|
16822
|
+
"decision.recorded",
|
|
16823
|
+
"decision.updated",
|
|
16824
|
+
"worker.updated"
|
|
16825
|
+
]);
|
|
16826
|
+
var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16827
|
+
var isKey = (value) => isObject3(value) && typeof value.key === "string" && (value.kind === "branch" || value.kind === "detached" || value.kind === "dir") && (value.branch === null || typeof value.branch === "string");
|
|
16828
|
+
var BLOB_REF = "$blob";
|
|
16829
|
+
var blobRef = (value) => isObject3(value) && Object.keys(value).length === 1 && typeof value[BLOB_REF] === "string" && /^[0-9a-f]{64}$/.test(value[BLOB_REF]) ? value[BLOB_REF] : null;
|
|
16830
|
+
var blobDigest = (value) => createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16831
|
+
var toBlob = (value, blobs) => {
|
|
16832
|
+
if (!isObject3(value))
|
|
16833
|
+
return value;
|
|
16834
|
+
const digest = blobDigest(value);
|
|
16835
|
+
blobs.write(digest, value);
|
|
16836
|
+
return { [BLOB_REF]: digest };
|
|
16837
|
+
};
|
|
16838
|
+
var encodeRecord = (record, blobs) => Array.isArray(record.candidates) ? { ...record, candidates: record.candidates.map((item) => toBlob(item, blobs)) } : record;
|
|
16839
|
+
var encodeOps = (ops, blobs) => ops.map((op) => {
|
|
16840
|
+
if (op.path[0] !== "candidates")
|
|
16841
|
+
return op;
|
|
16842
|
+
if (op.op === "push" && op.path.length === 1)
|
|
16843
|
+
return { ...op, values: op.values.map((item) => toBlob(item, blobs)) };
|
|
16844
|
+
if (op.op === "set" && op.path.length === 1 && Array.isArray(op.value))
|
|
16845
|
+
return { ...op, value: op.value.map((item) => toBlob(item, blobs)) };
|
|
16846
|
+
if (op.op === "set" && op.path.length === 2)
|
|
16847
|
+
return { ...op, value: toBlob(op.value, blobs) };
|
|
16848
|
+
return op;
|
|
16849
|
+
});
|
|
16850
|
+
var decode2 = (value, blobs) => {
|
|
16851
|
+
const digest = blobRef(value);
|
|
16852
|
+
if (digest)
|
|
16853
|
+
return blobs.read(digest);
|
|
16854
|
+
if (Array.isArray(value))
|
|
16855
|
+
return value.map((item) => decode2(item, blobs));
|
|
16856
|
+
if (isObject3(value)) {
|
|
16857
|
+
const out = {};
|
|
16858
|
+
for (const [key, item] of Object.entries(value))
|
|
16859
|
+
out[key] = decode2(item, blobs);
|
|
16860
|
+
return out;
|
|
16861
|
+
}
|
|
16862
|
+
return value;
|
|
16863
|
+
};
|
|
16864
|
+
function reduce(state, event, blobs) {
|
|
16865
|
+
const data = event.data;
|
|
16866
|
+
const known = KNOWN_TYPES.has(event.type);
|
|
16867
|
+
const carries = "record" in data || "ops" in data || "key" in data;
|
|
16868
|
+
if (!known && !carries) {
|
|
16869
|
+
if (event.critical === true)
|
|
16870
|
+
throw new LogDamage(`event ${event.type} was written by a newer Workit and must be understood; upgrade Workit to read this task`, true);
|
|
16871
|
+
return state;
|
|
16872
|
+
}
|
|
16873
|
+
let next = state;
|
|
16874
|
+
if ("record" in data) {
|
|
16875
|
+
if (!isObject3(data.record))
|
|
16876
|
+
throw new LogDamage(`event ${event.seq} carries a malformed record`);
|
|
16877
|
+
next = {
|
|
16878
|
+
record: decode2(data.record, blobs),
|
|
16879
|
+
key: null,
|
|
16880
|
+
legacy: null
|
|
16881
|
+
};
|
|
16882
|
+
} else if (next === null) {
|
|
16883
|
+
throw new LogDamage(`the log does not start with an opening event (found ${event.type})`);
|
|
16884
|
+
} else
|
|
16885
|
+
next = { ...next };
|
|
16886
|
+
if ("ops" in data) {
|
|
16887
|
+
if (!isPatch(data.ops))
|
|
16888
|
+
throw new LogDamage(`event ${event.seq} carries a malformed patch`);
|
|
16889
|
+
try {
|
|
16890
|
+
next.record = apply(next.record, decode2(data.ops, blobs));
|
|
16891
|
+
} catch (error) {
|
|
16892
|
+
throw new LogDamage(`event ${event.seq} does not apply: ${String(error)}`);
|
|
16893
|
+
}
|
|
16894
|
+
}
|
|
16895
|
+
if ("key" in data)
|
|
16896
|
+
next.key = isKey(data.key) ? data.key : null;
|
|
16897
|
+
if ("legacy" in data && isObject3(data.legacy))
|
|
16898
|
+
next.legacy = {
|
|
16899
|
+
path: String(data.legacy.path ?? ""),
|
|
16900
|
+
digest: String(data.legacy.digest ?? "")
|
|
16901
|
+
};
|
|
16902
|
+
return next;
|
|
16903
|
+
}
|
|
16904
|
+
function eventType(before, after) {
|
|
16905
|
+
const count = (value) => Array.isArray(value) ? value.length : 0;
|
|
16906
|
+
if (before.status !== after.status)
|
|
16907
|
+
return after.status === "closed" ? "task.closed" : after.status === "paused" ? "task.paused" : "task.resumed";
|
|
16908
|
+
if (count(after.evidence) > count(before.evidence)) {
|
|
16909
|
+
const last = after.evidence.at(-1);
|
|
16910
|
+
return last?.data?.observation ? "check.ran" : "evidence.recorded";
|
|
16911
|
+
}
|
|
16912
|
+
if (count(after.findings) > count(before.findings))
|
|
16913
|
+
return "finding.recorded";
|
|
16914
|
+
if (count(after.decisions) > count(before.decisions))
|
|
16915
|
+
return "decision.recorded";
|
|
16916
|
+
if (JSON.stringify(before.findings) !== JSON.stringify(after.findings))
|
|
16917
|
+
return "finding.updated";
|
|
16918
|
+
if (JSON.stringify(before.decisions) !== JSON.stringify(after.decisions))
|
|
16919
|
+
return "decision.updated";
|
|
16920
|
+
if (JSON.stringify(before.workers) !== JSON.stringify(after.workers))
|
|
16921
|
+
return "worker.updated";
|
|
16922
|
+
if (JSON.stringify(before.intent) !== JSON.stringify(after.intent))
|
|
16923
|
+
return "task.revised";
|
|
16924
|
+
if (JSON.stringify(before.assessments) !== JSON.stringify(after.assessments) || JSON.stringify(before.policy) !== JSON.stringify(after.policy))
|
|
16925
|
+
return "task.judged";
|
|
16926
|
+
if (JSON.stringify(before.progress) !== JSON.stringify(after.progress))
|
|
16927
|
+
return "task.noted";
|
|
16928
|
+
return "task.updated";
|
|
16929
|
+
}
|
|
16930
|
+
|
|
16931
|
+
// packages/workit-core/src/store/notes.ts
|
|
16932
|
+
var SLOT = Symbol.for("workit.migrationReporter");
|
|
16933
|
+
var reportMigration = (report) => {
|
|
16934
|
+
try {
|
|
16935
|
+
const reporter = globalThis[SLOT];
|
|
16936
|
+
if (typeof reporter === "function")
|
|
16937
|
+
reporter(report);
|
|
16938
|
+
} catch {}
|
|
16939
|
+
};
|
|
16940
|
+
var migrationReporterInstalled = () => typeof globalThis[SLOT] === "function";
|
|
16941
|
+
|
|
16942
|
+
// packages/workit-core/src/core/task-store.ts
|
|
16943
|
+
var MIGRATION_PENDING = "workit migration pending";
|
|
16944
|
+
var STORE_FORMAT = 3;
|
|
16945
|
+
var STORE_MIN_RUNTIME = "3.0.0";
|
|
16946
|
+
var SNAPSHOT_EVERY = 32;
|
|
16947
|
+
var COMPACT_ABOVE = 200;
|
|
16948
|
+
var COMPACT_KEEP = 50;
|
|
16949
|
+
var AUTO_COMPACT_BYTES = 2 * 1024 * 1024;
|
|
16950
|
+
var TOMBSTONE = Symbol("tombstone");
|
|
16951
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
16952
|
+
var INDEX_VERSION = 3;
|
|
16953
|
+
var isObject4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16954
|
+
var hostSession = (value) => isObject4(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
16955
|
+
var indexEntry = (task, key, legacy, file) => {
|
|
16956
|
+
const sessions = [];
|
|
16957
|
+
const intent = hostSession(task.intent.provenance.session);
|
|
16958
|
+
if (intent)
|
|
16959
|
+
sessions.push({ ...intent, workerId: null });
|
|
16960
|
+
for (const worker of task.workers) {
|
|
16961
|
+
const session = hostSession(worker.data.session);
|
|
16962
|
+
if (session)
|
|
16963
|
+
sessions.push({ ...session, workerId: worker.id });
|
|
16964
|
+
}
|
|
16965
|
+
return {
|
|
16966
|
+
id: task.id,
|
|
16967
|
+
revision: task.revision,
|
|
16968
|
+
status: task.status,
|
|
16969
|
+
createdAt: task.createdAt,
|
|
16970
|
+
updatedAt: task.updatedAt,
|
|
16971
|
+
objective: task.intent.data.objective,
|
|
16972
|
+
source: { host: task.intent.provenance.host, kind: task.intent.provenance.kind },
|
|
16973
|
+
progress: { summary: task.progress.summary, nextAction: task.progress.nextAction },
|
|
16974
|
+
sessions,
|
|
16975
|
+
workspaceId: task.workspaceId,
|
|
16976
|
+
key: key?.key ?? null,
|
|
16977
|
+
branch: key?.branch ?? null,
|
|
16978
|
+
boundAt: key?.boundAt ?? null,
|
|
16979
|
+
legacy,
|
|
16980
|
+
file
|
|
16981
|
+
};
|
|
16982
|
+
};
|
|
16983
|
+
var fileSignature = (file) => {
|
|
16984
|
+
try {
|
|
16985
|
+
const stat = fs7.statSync(file, { bigint: true });
|
|
16986
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
16987
|
+
} catch {
|
|
16988
|
+
return null;
|
|
16989
|
+
}
|
|
16990
|
+
};
|
|
16991
|
+
var RACY_WINDOW_NS = 2000000000n;
|
|
16992
|
+
var racySignature = (signature) => {
|
|
16993
|
+
const [mtime = 0n, ctime = 0n] = signature.split(":").slice(3, 5).map(BigInt);
|
|
16994
|
+
const changed = mtime > ctime ? mtime : ctime;
|
|
16995
|
+
return BigInt(Date.now()) * 1000000n - changed < RACY_WINDOW_NS;
|
|
16996
|
+
};
|
|
16997
|
+
var now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
16998
|
+
var oldestFirst = (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id);
|
|
16999
|
+
var cachedRuntimeVersion = null;
|
|
17000
|
+
var runtimeVersion = () => {
|
|
17001
|
+
if (cachedRuntimeVersion === null) {
|
|
17002
|
+
try {
|
|
17003
|
+
const pkg = JSON.parse(fs7.readFileSync(path6.join(packageRoot(), "package.json"), "utf8"));
|
|
17004
|
+
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
17005
|
+
} catch {
|
|
17006
|
+
cachedRuntimeVersion = "0.0.0";
|
|
17007
|
+
}
|
|
17008
|
+
}
|
|
17009
|
+
return cachedRuntimeVersion;
|
|
17010
|
+
};
|
|
17011
|
+
var stampNew = (record) => ({
|
|
17012
|
+
createdWith: record.runtime?.createdWith ?? null,
|
|
17013
|
+
updatedWith: runtimeVersion()
|
|
17014
|
+
});
|
|
17015
|
+
var isNewerVersion = (candidate, current) => {
|
|
17016
|
+
const parts = (value) => value.split("-")[0].split(".").map((item) => Number.parseInt(item, 10) || 0);
|
|
17017
|
+
const [candidateParts, currentParts] = [parts(candidate), parts(current)];
|
|
17018
|
+
for (let index = 0;index < 3; index += 1) {
|
|
17019
|
+
const left = candidateParts[index] ?? 0;
|
|
17020
|
+
const right = currentParts[index] ?? 0;
|
|
17021
|
+
if (left !== right)
|
|
17022
|
+
return left > right;
|
|
17023
|
+
}
|
|
17024
|
+
return false;
|
|
17025
|
+
};
|
|
17026
|
+
var validId = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
17027
|
+
var sameDirectoryIdentity = (left, right) => {
|
|
17028
|
+
if (!path6.isAbsolute(left) || !path6.isAbsolute(right))
|
|
17029
|
+
return false;
|
|
17030
|
+
const normalizedLeft = path6.resolve(left);
|
|
17031
|
+
const normalizedRight = path6.resolve(right);
|
|
17032
|
+
try {
|
|
17033
|
+
const a = fs7.statSync(normalizedLeft, { bigint: true });
|
|
17034
|
+
const b = fs7.statSync(normalizedRight, { bigint: true });
|
|
17035
|
+
if (!a.isDirectory() || !b.isDirectory())
|
|
17036
|
+
return false;
|
|
17037
|
+
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
17038
|
+
return true;
|
|
17039
|
+
const canonicalLeft = fs7.realpathSync(normalizedLeft);
|
|
17040
|
+
const canonicalRight = fs7.realpathSync(normalizedRight);
|
|
17041
|
+
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
17042
|
+
return true;
|
|
17043
|
+
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
17044
|
+
} catch {
|
|
17045
|
+
return false;
|
|
17046
|
+
}
|
|
17047
|
+
};
|
|
16379
17048
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
16380
17049
|
var heldInProcess = new Map;
|
|
16381
|
-
var
|
|
16382
|
-
var
|
|
16383
|
-
const count = (heldInProcess.get(
|
|
17050
|
+
var hold = (lock) => heldInProcess.set(lock, (heldInProcess.get(lock) ?? 0) + 1);
|
|
17051
|
+
var drop = (lock) => {
|
|
17052
|
+
const count = (heldInProcess.get(lock) ?? 1) - 1;
|
|
16384
17053
|
if (count > 0)
|
|
16385
|
-
heldInProcess.set(
|
|
17054
|
+
heldInProcess.set(lock, count);
|
|
16386
17055
|
else
|
|
16387
|
-
heldInProcess.delete(
|
|
17056
|
+
heldInProcess.delete(lock);
|
|
16388
17057
|
};
|
|
16389
|
-
var
|
|
16390
|
-
const
|
|
16391
|
-
return
|
|
17058
|
+
var envActor = () => {
|
|
17059
|
+
const value = (key) => process.env[key]?.trim() || null;
|
|
17060
|
+
return {
|
|
17061
|
+
host: value("WORKIT_HOST") ?? "runtime",
|
|
17062
|
+
session: value("WORKIT_SESSION_ID"),
|
|
17063
|
+
agentId: value("WORKIT_AGENT_ID")
|
|
17064
|
+
};
|
|
16392
17065
|
};
|
|
16393
17066
|
|
|
16394
17067
|
class TaskStore {
|
|
16395
17068
|
root;
|
|
17069
|
+
origin;
|
|
16396
17070
|
lockTimeoutMs;
|
|
17071
|
+
actor;
|
|
17072
|
+
migrateOnRead;
|
|
17073
|
+
located;
|
|
17074
|
+
migrationChecked = false;
|
|
17075
|
+
compactDue = new Set;
|
|
16397
17076
|
constructor(root, options = {}) {
|
|
16398
|
-
|
|
17077
|
+
const resolved = path6.resolve(root);
|
|
17078
|
+
this.origin = fs7.existsSync(resolved) ? fs7.realpathSync(resolved) : resolved;
|
|
17079
|
+
this.located = resolveStore(this.origin);
|
|
17080
|
+
this.root = checkoutRootOf(this.origin, this.located);
|
|
16399
17081
|
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
17082
|
+
this.actor = options.actor ?? envActor();
|
|
17083
|
+
this.migrateOnRead = options.migrateOnRead;
|
|
17084
|
+
}
|
|
17085
|
+
location() {
|
|
17086
|
+
if (this.located !== undefined && !(this.located instanceof Error) && !this.located.shared && fs7.existsSync(path6.join(this.root, ".git"))) {
|
|
17087
|
+
this.located = undefined;
|
|
17088
|
+
this.migrationChecked = false;
|
|
17089
|
+
}
|
|
17090
|
+
if (this.located === undefined)
|
|
17091
|
+
this.located = resolveStore(this.root);
|
|
17092
|
+
return this.located instanceof Error ? failure2("storage_error", this.located.message, { path: this.root }) : success(null, null, this.located);
|
|
17093
|
+
}
|
|
17094
|
+
currentKey() {
|
|
17095
|
+
const location = this.location();
|
|
17096
|
+
if (!location.ok)
|
|
17097
|
+
return location;
|
|
17098
|
+
return success(null, null, resolveTaskKey(this.root, location.data));
|
|
16400
17099
|
}
|
|
16401
17100
|
readTask(taskId) {
|
|
16402
17101
|
if (!validId(taskId))
|
|
16403
17102
|
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
16404
|
-
const
|
|
16405
|
-
if (!
|
|
17103
|
+
const ready = this.ready();
|
|
17104
|
+
if (!ready.ok)
|
|
17105
|
+
return ready;
|
|
17106
|
+
const loaded = this.load(taskId);
|
|
17107
|
+
if (!loaded.ok)
|
|
17108
|
+
return loaded;
|
|
17109
|
+
if (!loaded.data)
|
|
16406
17110
|
return failure2("not_found", "task not found", { taskId });
|
|
16407
|
-
if (result.result.ok && result.result.data.id !== taskId)
|
|
16408
|
-
return failure2("recovery_required", "task filename and record ID differ", { taskId });
|
|
16409
|
-
if (result.result.ok) {
|
|
16410
|
-
const workspace = this.readWorkspace();
|
|
16411
|
-
if (!workspace.ok)
|
|
16412
|
-
return workspace;
|
|
16413
|
-
if (!workspace.data || result.result.data.workspaceId !== workspace.data.id)
|
|
16414
|
-
return failure2("recovery_required", "task workspace binding is invalid", { taskId });
|
|
16415
|
-
}
|
|
16416
|
-
return result.result;
|
|
16417
|
-
}
|
|
16418
|
-
listTasks() {
|
|
16419
|
-
if (!fs5.existsSync(this.tasksDir))
|
|
16420
|
-
return success(null, null, []);
|
|
16421
|
-
let names;
|
|
16422
|
-
try {
|
|
16423
|
-
names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
16424
|
-
} catch (error) {
|
|
16425
|
-
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
16426
|
-
path: this.tasksDir
|
|
16427
|
-
});
|
|
16428
|
-
}
|
|
16429
17111
|
const workspace = this.readWorkspace();
|
|
16430
17112
|
if (!workspace.ok)
|
|
16431
17113
|
return workspace;
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
17114
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
17115
|
+
return bound ?? success(null, null, loaded.data.record);
|
|
17116
|
+
}
|
|
17117
|
+
listTasks() {
|
|
17118
|
+
const listed = this.listTaskIndex();
|
|
17119
|
+
if (!listed.ok)
|
|
17120
|
+
return listed;
|
|
16436
17121
|
const tasks = [];
|
|
16437
|
-
for (const
|
|
16438
|
-
const
|
|
16439
|
-
if (!
|
|
16440
|
-
|
|
16441
|
-
if (
|
|
16442
|
-
|
|
16443
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
16444
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
16445
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
16446
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
16447
|
-
tasks.push(item.result.data);
|
|
17122
|
+
for (const entry of listed.data.toSorted((left, right) => left.id.localeCompare(right.id))) {
|
|
17123
|
+
const loaded = this.load(entry.id);
|
|
17124
|
+
if (!loaded.ok)
|
|
17125
|
+
return loaded;
|
|
17126
|
+
if (loaded.data)
|
|
17127
|
+
tasks.push(loaded.data.record);
|
|
16448
17128
|
}
|
|
16449
17129
|
return success(null, null, tasks);
|
|
16450
17130
|
}
|
|
16451
17131
|
listTaskIndex() {
|
|
16452
|
-
|
|
17132
|
+
const all = this.listStoreIndex();
|
|
17133
|
+
if (!all.ok)
|
|
17134
|
+
return all;
|
|
17135
|
+
const workspace = this.readWorkspace();
|
|
17136
|
+
if (!workspace.ok)
|
|
17137
|
+
return workspace;
|
|
17138
|
+
if (!workspace.data)
|
|
16453
17139
|
return success(null, null, []);
|
|
17140
|
+
const id = workspace.data.id;
|
|
17141
|
+
return success(null, null, all.data.filter((entry) => entry.workspaceId === id));
|
|
17142
|
+
}
|
|
17143
|
+
listStoreIndex() {
|
|
17144
|
+
const ready = this.ready();
|
|
17145
|
+
if (!ready.ok)
|
|
17146
|
+
return ready;
|
|
17147
|
+
const tasksDir = this.paths().tasks;
|
|
16454
17148
|
let names;
|
|
16455
17149
|
try {
|
|
16456
|
-
names =
|
|
17150
|
+
names = fs7.existsSync(tasksDir) ? fs7.readdirSync(tasksDir).filter((name) => validId(name)) : [];
|
|
16457
17151
|
} catch (error) {
|
|
16458
17152
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
16459
|
-
path:
|
|
17153
|
+
path: tasksDir
|
|
16460
17154
|
});
|
|
16461
17155
|
}
|
|
16462
|
-
const
|
|
16463
|
-
if (!workspace.ok)
|
|
16464
|
-
return workspace;
|
|
16465
|
-
if (!workspace.data)
|
|
16466
|
-
return failure2("recovery_required", "task workspace binding is invalid", {
|
|
16467
|
-
path: this.workspacePath
|
|
16468
|
-
});
|
|
16469
|
-
const stored = this.readIndex(workspace.data.id);
|
|
17156
|
+
const stored = this.readIndex();
|
|
16470
17157
|
const entries = [];
|
|
16471
17158
|
let changed = Object.keys(stored).length !== names.length;
|
|
16472
17159
|
for (const name of names.toSorted()) {
|
|
16473
|
-
const
|
|
16474
|
-
const signature = fileSignature(file);
|
|
17160
|
+
const signature = fileSignature(this.eventsPath(name));
|
|
16475
17161
|
if (signature === null) {
|
|
16476
17162
|
changed = true;
|
|
16477
17163
|
continue;
|
|
16478
17164
|
}
|
|
16479
|
-
const cached = stored[name
|
|
17165
|
+
const cached = stored[name];
|
|
16480
17166
|
if (cached && cached.file === signature && !racySignature(signature)) {
|
|
16481
17167
|
entries.push(cached);
|
|
16482
17168
|
continue;
|
|
16483
17169
|
}
|
|
16484
|
-
const
|
|
16485
|
-
if (!
|
|
17170
|
+
const loaded = this.load(name);
|
|
17171
|
+
if (!loaded.ok)
|
|
17172
|
+
return loaded;
|
|
17173
|
+
if (!loaded.data)
|
|
16486
17174
|
continue;
|
|
16487
|
-
|
|
16488
|
-
return item.result;
|
|
16489
|
-
if (!validId(name.slice(0, -5)) || item.result.data.id !== name.slice(0, -5))
|
|
16490
|
-
return failure2("recovery_required", "task filename and record ID differ", { path: name });
|
|
16491
|
-
if (item.result.data.workspaceId !== workspace.data.id)
|
|
16492
|
-
return failure2("recovery_required", "task workspace binding is invalid", { path: name });
|
|
16493
|
-
const entry = indexEntry(item.result.data, signature);
|
|
17175
|
+
const entry = indexEntry(loaded.data.record, loaded.data.key, loaded.data.legacy !== null, signature);
|
|
16494
17176
|
if (!cached || canonicalJson(cached) !== canonicalJson(entry))
|
|
16495
17177
|
changed = true;
|
|
16496
17178
|
entries.push(entry);
|
|
16497
17179
|
}
|
|
16498
17180
|
if (changed)
|
|
16499
|
-
this.writeIndex(
|
|
17181
|
+
this.writeIndex(entries);
|
|
16500
17182
|
return success(null, null, entries);
|
|
16501
17183
|
}
|
|
16502
17184
|
readWorkspace() {
|
|
16503
|
-
const
|
|
16504
|
-
if (!
|
|
16505
|
-
return
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
17185
|
+
const ready = this.ready();
|
|
17186
|
+
if (!ready.ok)
|
|
17187
|
+
return ready;
|
|
17188
|
+
const file = this.paths().workspace;
|
|
17189
|
+
let bytes;
|
|
17190
|
+
try {
|
|
17191
|
+
bytes = retryTransient(() => fs7.readFileSync(file, "utf8"));
|
|
17192
|
+
} catch (error) {
|
|
17193
|
+
if (error.code === "ENOENT")
|
|
17194
|
+
return success(null, null, null);
|
|
17195
|
+
return failure2("recovery_required", `workspace record cannot be read: ${String(error)}`, {
|
|
17196
|
+
path: file
|
|
16509
17197
|
});
|
|
16510
|
-
|
|
17198
|
+
}
|
|
17199
|
+
const parsed = this.parseRecord(bytes, workspaceRecordSchema, "workspace");
|
|
17200
|
+
if (!parsed.ok)
|
|
17201
|
+
return parsed;
|
|
17202
|
+
if (parsed.data.root !== this.root && !sameDirectoryIdentity(parsed.data.root, this.root))
|
|
17203
|
+
return failure2("recovery_required", "workspace root binding is invalid", { path: file });
|
|
17204
|
+
return parsed;
|
|
17205
|
+
}
|
|
17206
|
+
implicitTask(input) {
|
|
17207
|
+
const ready = this.ready(input.create !== false);
|
|
17208
|
+
if (!ready.ok)
|
|
17209
|
+
return ready;
|
|
17210
|
+
const key = this.currentKey();
|
|
17211
|
+
if (!key.ok)
|
|
17212
|
+
return key;
|
|
17213
|
+
const found = this.openTaskForKey(key.data.key);
|
|
17214
|
+
if (!found.ok)
|
|
17215
|
+
return found;
|
|
17216
|
+
const workspace = this.readWorkspace();
|
|
17217
|
+
if (!workspace.ok)
|
|
17218
|
+
return workspace;
|
|
17219
|
+
if (found.data && workspace.data && found.data.workspaceId === workspace.data.id && (found.data.key === key.data.key || input.create === false)) {
|
|
17220
|
+
const task = this.readTask(found.data.id);
|
|
17221
|
+
return task.ok ? success(task.revision, null, { task: task.data, created: false, key: key.data }) : task;
|
|
17222
|
+
}
|
|
17223
|
+
if (input.create === false) {
|
|
17224
|
+
if (!found.data)
|
|
17225
|
+
return success(null, null, null);
|
|
17226
|
+
const loaded = this.load(found.data.id);
|
|
17227
|
+
if (!loaded.ok)
|
|
17228
|
+
return loaded;
|
|
17229
|
+
return loaded.data ? success(null, null, { task: loaded.data.record, created: false, key: key.data }) : success(null, null, null);
|
|
17230
|
+
}
|
|
17231
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => this.implicitLocked(input, key.data)));
|
|
17232
|
+
}
|
|
17233
|
+
implicitLocked(input, key) {
|
|
17234
|
+
{
|
|
17235
|
+
const again = this.openTaskForKey(key.key);
|
|
17236
|
+
if (!again.ok)
|
|
17237
|
+
return again;
|
|
17238
|
+
if (again.data) {
|
|
17239
|
+
const moved = this.bindLocked(again.data.id, key, input.now);
|
|
17240
|
+
return moved.ok ? success(moved.data.revision, null, { task: moved.data, created: false, key }) : moved;
|
|
17241
|
+
}
|
|
17242
|
+
const label = key.kind === "branch" ? `branch ${key.branch}` : key.kind === "detached" ? "a detached HEAD" : path6.basename(this.root) || this.root;
|
|
17243
|
+
const intent = {
|
|
17244
|
+
objective: input.objective?.trim() || `Work on ${label}`,
|
|
17245
|
+
scope: { description: `the checkout (${label})`, paths: ["."], exclusions: [] },
|
|
17246
|
+
authorityRefs: []
|
|
17247
|
+
};
|
|
17248
|
+
const created = this.createLocked({ intent, provenance: input.provenance, expectedWorkspaceRevision: null, now: input.now }, false, key, false);
|
|
17249
|
+
return created.ok ? success(created.data.revision, null, { task: created.data, created: true, key }) : created;
|
|
17250
|
+
}
|
|
17251
|
+
}
|
|
17252
|
+
adoptTask(taskId) {
|
|
17253
|
+
if (!validId(taskId))
|
|
17254
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
17255
|
+
const ready = this.ready(true);
|
|
17256
|
+
if (!ready.ok)
|
|
17257
|
+
return ready;
|
|
17258
|
+
const key = this.currentKey();
|
|
17259
|
+
if (!key.ok)
|
|
17260
|
+
return key;
|
|
17261
|
+
return this.withLock(this.paths().checkoutLock, () => this.withKeyLock(key.data.key, () => {
|
|
17262
|
+
const holder = this.openTaskForKey(key.data.key);
|
|
17263
|
+
if (!holder.ok)
|
|
17264
|
+
return holder;
|
|
17265
|
+
if (holder.data && holder.data.id !== taskId)
|
|
17266
|
+
return failure2("invalid_transition", `${key.data.key} already has open task ${holder.data.id}; close it (\`workit task close\`) before adopting another`, { taskId: holder.data.id });
|
|
17267
|
+
return this.bindLocked(taskId, key.data);
|
|
17268
|
+
}));
|
|
17269
|
+
}
|
|
17270
|
+
keyReport() {
|
|
17271
|
+
const key = this.currentKey();
|
|
17272
|
+
if (!key.ok)
|
|
17273
|
+
return key;
|
|
17274
|
+
const bound = this.openTasksForKey(key.data.key);
|
|
17275
|
+
if (!bound.ok)
|
|
17276
|
+
return bound;
|
|
17277
|
+
return success(null, null, { key: key.data, bound: bound.data });
|
|
16511
17278
|
}
|
|
16512
17279
|
create(input, provenance, expectedWorkspaceRevision) {
|
|
16513
17280
|
const value = "intent" in input ? input : {
|
|
@@ -16515,82 +17282,16 @@ class TaskStore {
|
|
|
16515
17282
|
provenance,
|
|
16516
17283
|
expectedWorkspaceRevision: expectedWorkspaceRevision ?? null
|
|
16517
17284
|
};
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
if (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null) {
|
|
16523
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
16524
|
-
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
16525
|
-
actualWorkspaceRevision: current.data?.revision ?? null
|
|
16526
|
-
});
|
|
16527
|
-
}
|
|
16528
|
-
if (!value.provenance)
|
|
16529
|
-
return failure2("invalid_input", "provenance is required");
|
|
16530
|
-
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
16531
|
-
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
16532
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
16533
|
-
if (current.data && !previousWorkspaceBytes)
|
|
16534
|
-
return failure2("storage_error", "workspace snapshot disappeared during creation");
|
|
16535
|
-
const workspace = current.data ? {
|
|
16536
|
-
...current.data,
|
|
16537
|
-
revision: newRevision(),
|
|
16538
|
-
root: this.root,
|
|
16539
|
-
runtime: stampNew(current.data)
|
|
16540
|
-
} : {
|
|
16541
|
-
schemaVersion: SCHEMA_VERSION,
|
|
16542
|
-
id: newId(),
|
|
16543
|
-
revision: newRevision(),
|
|
16544
|
-
root: this.root,
|
|
16545
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
16546
|
-
writer: null
|
|
16547
|
-
};
|
|
16548
|
-
const timestamp = value.now ?? now();
|
|
16549
|
-
const task = {
|
|
16550
|
-
schemaVersion: SCHEMA_VERSION,
|
|
16551
|
-
id: newId(),
|
|
16552
|
-
workspaceId: workspace.id,
|
|
16553
|
-
revision: newRevision(),
|
|
16554
|
-
createdAt: timestamp,
|
|
16555
|
-
updatedAt: timestamp,
|
|
16556
|
-
origin: null,
|
|
16557
|
-
intent: {
|
|
16558
|
-
id: newId(),
|
|
16559
|
-
recordedAt: timestamp,
|
|
16560
|
-
provenance: value.provenance,
|
|
16561
|
-
data: value.intent
|
|
16562
|
-
},
|
|
16563
|
-
constraints: [],
|
|
16564
|
-
status: "active",
|
|
16565
|
-
closure: null,
|
|
16566
|
-
progress: { summary: "", nextAction: null, blockers: [] },
|
|
16567
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
16568
|
-
assessments: [],
|
|
16569
|
-
policy: null,
|
|
16570
|
-
policyChanges: [],
|
|
16571
|
-
candidates: [],
|
|
16572
|
-
evidence: [],
|
|
16573
|
-
decisions: [],
|
|
16574
|
-
findings: [],
|
|
16575
|
-
workers: []
|
|
16576
|
-
};
|
|
16577
|
-
const writtenWorkspace = this.replaceSnapshot(this.workspacePath, workspace, previousWorkspaceBytes);
|
|
16578
|
-
if (!writtenWorkspace.ok)
|
|
16579
|
-
return writtenWorkspace;
|
|
16580
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), task, null);
|
|
16581
|
-
if (!writtenTask.ok) {
|
|
16582
|
-
if (current.data && previousWorkspaceBytes) {
|
|
16583
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
16584
|
-
if (!restored.ok)
|
|
16585
|
-
return failure2("external_outcome_unknown", "task creation failed and workspace restoration is uncertain", { operation: "create", outcome: "unknown" });
|
|
16586
|
-
}
|
|
16587
|
-
return failure2("external_outcome_unknown", "workspace created but task write is uncertain", { operation: "create", outcome: "unknown" });
|
|
16588
|
-
}
|
|
16589
|
-
return success(task.revision, workspace.revision, task);
|
|
16590
|
-
});
|
|
17285
|
+
const ready = this.ready(true);
|
|
17286
|
+
if (!ready.ok)
|
|
17287
|
+
return ready;
|
|
17288
|
+
return this.withLock(this.paths().checkoutLock, () => this.createLocked(value, true, null));
|
|
16591
17289
|
}
|
|
16592
17290
|
importTask(input) {
|
|
16593
|
-
|
|
17291
|
+
const ready = this.ready(true);
|
|
17292
|
+
if (!ready.ok)
|
|
17293
|
+
return ready;
|
|
17294
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
16594
17295
|
const current = this.readWorkspace();
|
|
16595
17296
|
if (!current.ok)
|
|
16596
17297
|
return current;
|
|
@@ -16622,23 +17323,8 @@ class TaskStore {
|
|
|
16622
17323
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
16623
17324
|
actualWorkspaceRevision: current.data?.revision ?? null
|
|
16624
17325
|
});
|
|
16625
|
-
const previousWorkspaceBytes = current.data ? this.snapshotBytes(this.workspacePath) : null;
|
|
16626
|
-
if (current.data && !previousWorkspaceBytes)
|
|
16627
|
-
return failure2("storage_error", "workspace snapshot disappeared during import");
|
|
16628
17326
|
const timestamp = input.now ?? now();
|
|
16629
|
-
const workspace = current.data
|
|
16630
|
-
...current.data,
|
|
16631
|
-
revision: newRevision(),
|
|
16632
|
-
root: this.root,
|
|
16633
|
-
runtime: stampNew(current.data)
|
|
16634
|
-
} : {
|
|
16635
|
-
schemaVersion: SCHEMA_VERSION,
|
|
16636
|
-
id: input.workspaceId ?? newId(),
|
|
16637
|
-
revision: newRevision(),
|
|
16638
|
-
root: this.root,
|
|
16639
|
-
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
16640
|
-
writer: null
|
|
16641
|
-
};
|
|
17327
|
+
const workspace = this.nextWorkspace(current.data, input.workspaceId);
|
|
16642
17328
|
const task = {
|
|
16643
17329
|
...input.task,
|
|
16644
17330
|
workspaceId: workspace.id,
|
|
@@ -16653,62 +17339,46 @@ class TaskStore {
|
|
|
16653
17339
|
const validTask = taskRecordSchema.safeParse(task);
|
|
16654
17340
|
if (!validTask.success)
|
|
16655
17341
|
return failure2("invalid_input", "imported task does not satisfy its schema");
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
const writtenTask = this.replaceSnapshot(this.taskPath(task.id), validTask.data, null);
|
|
16660
|
-
if (!writtenTask.ok) {
|
|
16661
|
-
if (current.data && previousWorkspaceBytes) {
|
|
16662
|
-
const restored = this.replaceSnapshot(this.workspacePath, current.data, this.snapshotBytes(this.workspacePath));
|
|
16663
|
-
if (!restored.ok)
|
|
16664
|
-
return failure2("external_outcome_unknown", "import failed and workspace restoration is uncertain", { operation: "import", outcome: "unknown" });
|
|
16665
|
-
}
|
|
16666
|
-
return failure2("external_outcome_unknown", `workspace created but task import is uncertain: ${writtenTask.error}`, {
|
|
16667
|
-
operation: "import",
|
|
16668
|
-
outcome: "unknown",
|
|
16669
|
-
path: writtenTask.details.path
|
|
16670
|
-
});
|
|
16671
|
-
}
|
|
16672
|
-
return success(validTask.data.revision, workspace.revision, validTask.data);
|
|
17342
|
+
return this.writeNewTask(current.data, workspace, validTask.data, "task.imported", null, {
|
|
17343
|
+
operation: "import"
|
|
17344
|
+
});
|
|
16673
17345
|
});
|
|
16674
17346
|
}
|
|
16675
17347
|
mutateTask(taskId, expected, update, timestamp) {
|
|
16676
|
-
|
|
16677
|
-
|
|
16678
|
-
|
|
16679
|
-
|
|
16680
|
-
|
|
16681
|
-
|
|
16682
|
-
const
|
|
16683
|
-
if (!
|
|
16684
|
-
return
|
|
17348
|
+
if (!validId(taskId))
|
|
17349
|
+
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
17350
|
+
const ready = this.ready(true);
|
|
17351
|
+
if (!ready.ok)
|
|
17352
|
+
return ready;
|
|
17353
|
+
const result = this.withLock(this.taskLockPath(taskId), () => {
|
|
17354
|
+
const loaded = this.loadBound(taskId);
|
|
17355
|
+
if (!loaded.ok)
|
|
17356
|
+
return loaded;
|
|
17357
|
+
if (loaded.data.record.revision !== expected)
|
|
17358
|
+
return this.conflict(expected, loaded.data.record.revision);
|
|
16685
17359
|
const context = { now: timestamp ?? now(), revision: newRevision() };
|
|
16686
17360
|
let changed;
|
|
16687
17361
|
try {
|
|
16688
|
-
changed = update(
|
|
17362
|
+
changed = update(structuredClone(loaded.data.record), Object.freeze({ ...context }));
|
|
16689
17363
|
} catch (error) {
|
|
16690
17364
|
return failure2("storage_error", `task mutation failed: ${String(error)}`);
|
|
16691
17365
|
}
|
|
16692
17366
|
if (!changed.ok)
|
|
16693
17367
|
return changed;
|
|
16694
|
-
const
|
|
16695
|
-
|
|
16696
|
-
id: taskId,
|
|
16697
|
-
workspaceId: current.data.workspaceId,
|
|
16698
|
-
createdAt: current.data.createdAt,
|
|
16699
|
-
revision: context.revision,
|
|
16700
|
-
updatedAt: context.now,
|
|
16701
|
-
runtime: stampNew(current.data)
|
|
16702
|
-
};
|
|
16703
|
-
const valid = taskRecordSchema.safeParse(record);
|
|
16704
|
-
if (!valid.success)
|
|
17368
|
+
const valid = this.nextTask(loaded.data.record, changed.data, context);
|
|
17369
|
+
if (!valid)
|
|
16705
17370
|
return failure2("invalid_input", "task mutation produced an invalid record");
|
|
16706
|
-
const written = this.
|
|
16707
|
-
return written.ok ? success(valid.
|
|
17371
|
+
const written = this.appendPatch(loaded.data, valid);
|
|
17372
|
+
return written.ok ? success(valid.revision, null, valid) : written;
|
|
16708
17373
|
});
|
|
17374
|
+
this.compactPending();
|
|
17375
|
+
return result;
|
|
16709
17376
|
}
|
|
16710
17377
|
mutateWorkspace(expected, update) {
|
|
16711
|
-
|
|
17378
|
+
const ready = this.ready(true);
|
|
17379
|
+
if (!ready.ok)
|
|
17380
|
+
return ready;
|
|
17381
|
+
return this.withLock(this.paths().checkoutLock, () => {
|
|
16712
17382
|
const current = this.readWorkspace();
|
|
16713
17383
|
if (!current.ok)
|
|
16714
17384
|
return current;
|
|
@@ -16716,9 +17386,6 @@ class TaskStore {
|
|
|
16716
17386
|
return failure2("not_found", "workspace not found");
|
|
16717
17387
|
if (current.data.revision !== expected)
|
|
16718
17388
|
return this.workspaceConflict(expected, current.data.revision);
|
|
16719
|
-
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
16720
|
-
if (!previousBytes)
|
|
16721
|
-
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
16722
17389
|
const context = { now: now(), revision: newRevision() };
|
|
16723
17390
|
let changed;
|
|
16724
17391
|
try {
|
|
@@ -16728,23 +17395,27 @@ class TaskStore {
|
|
|
16728
17395
|
}
|
|
16729
17396
|
if (!changed.ok)
|
|
16730
17397
|
return changed;
|
|
16731
|
-
const
|
|
17398
|
+
const valid = workspaceRecordSchema.safeParse({
|
|
16732
17399
|
...changed.data,
|
|
16733
17400
|
id: current.data.id,
|
|
16734
17401
|
root: this.root,
|
|
16735
17402
|
revision: context.revision,
|
|
16736
17403
|
runtime: stampNew(current.data)
|
|
16737
|
-
};
|
|
16738
|
-
const valid = workspaceRecordSchema.safeParse(record);
|
|
17404
|
+
});
|
|
16739
17405
|
if (!valid.success)
|
|
16740
17406
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
16741
|
-
const written = this.
|
|
17407
|
+
const written = this.writeWorkspace(valid.data);
|
|
16742
17408
|
return written.ok ? success(valid.data.revision, valid.data.revision, valid.data) : written;
|
|
16743
17409
|
});
|
|
16744
17410
|
}
|
|
16745
17411
|
mutateTaskAndWorkspace(input) {
|
|
16746
|
-
|
|
16747
|
-
|
|
17412
|
+
if (!validId(input.taskId))
|
|
17413
|
+
return failure2("invalid_input", "task ID is invalid", { taskId: input.taskId });
|
|
17414
|
+
const ready = this.ready(true);
|
|
17415
|
+
if (!ready.ok)
|
|
17416
|
+
return ready;
|
|
17417
|
+
const result = this.withLock(this.paths().checkoutLock, () => this.withLock(this.taskLockPath(input.taskId), () => {
|
|
17418
|
+
const task = this.loadBound(input.taskId);
|
|
16748
17419
|
if (!task.ok)
|
|
16749
17420
|
return task;
|
|
16750
17421
|
const workspace = this.readWorkspace();
|
|
@@ -16755,14 +17426,10 @@ class TaskStore {
|
|
|
16755
17426
|
const expected = input.expectedRevision;
|
|
16756
17427
|
if (!expected)
|
|
16757
17428
|
return failure2("invalid_input", "task revision is required");
|
|
16758
|
-
if (task.data.revision !== expected)
|
|
16759
|
-
return this.conflict(expected, task.data.revision);
|
|
17429
|
+
if (task.data.record.revision !== expected)
|
|
17430
|
+
return this.conflict(expected, task.data.record.revision);
|
|
16760
17431
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
16761
17432
|
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
16762
|
-
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
16763
|
-
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
16764
|
-
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
16765
|
-
return failure2("storage_error", "snapshot disappeared during coupled mutation");
|
|
16766
17433
|
const workspaceContext = { now: input.now ?? now(), revision: newRevision() };
|
|
16767
17434
|
let changedWorkspace;
|
|
16768
17435
|
try {
|
|
@@ -16781,286 +17448,69 @@ class TaskStore {
|
|
|
16781
17448
|
});
|
|
16782
17449
|
if (!nextWorkspace.success)
|
|
16783
17450
|
return failure2("invalid_input", "workspace mutation produced an invalid record");
|
|
16784
|
-
const reserved = this.
|
|
17451
|
+
const reserved = this.writeWorkspace(nextWorkspace.data);
|
|
16785
17452
|
if (!reserved.ok)
|
|
16786
17453
|
return reserved;
|
|
17454
|
+
const uncertain = (message) => {
|
|
17455
|
+
this.markUncertain(nextWorkspace.data);
|
|
17456
|
+
return failure2("external_outcome_unknown", message, {
|
|
17457
|
+
operation: "coupled_mutation",
|
|
17458
|
+
outcome: "unknown"
|
|
17459
|
+
});
|
|
17460
|
+
};
|
|
16787
17461
|
const taskContext = { now: input.now ?? now(), revision: newRevision() };
|
|
16788
17462
|
let changedTask;
|
|
16789
17463
|
try {
|
|
16790
|
-
changedTask = input.task(task.data, Object.freeze({ ...taskContext }));
|
|
17464
|
+
changedTask = input.task(structuredClone(task.data.record), Object.freeze({ ...taskContext }));
|
|
16791
17465
|
} catch (error) {
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
const
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
revision: taskContext.revision,
|
|
16805
|
-
updatedAt: taskContext.now,
|
|
16806
|
-
runtime: stampNew(task.data)
|
|
16807
|
-
});
|
|
16808
|
-
if (!nextTask.success) {
|
|
16809
|
-
this.markUncertain(nextWorkspace.data);
|
|
16810
|
-
return failure2("external_outcome_unknown", "workspace reserved but task update is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
16811
|
-
}
|
|
16812
|
-
const written = this.replaceSnapshot(this.taskPath(input.taskId), nextTask.data, previousTaskBytes);
|
|
16813
|
-
if (!written.ok) {
|
|
16814
|
-
this.markUncertain(nextWorkspace.data);
|
|
16815
|
-
return failure2("external_outcome_unknown", "workspace reserved but task replacement is uncertain", { operation: "coupled_mutation", outcome: "unknown" });
|
|
16816
|
-
}
|
|
16817
|
-
return success(nextTask.data.revision, nextWorkspace.data.revision, {
|
|
16818
|
-
task: nextTask.data,
|
|
17466
|
+
return uncertain(`workspace reserved but task mutation threw: ${String(error)}`);
|
|
17467
|
+
}
|
|
17468
|
+
if (!changedTask.ok)
|
|
17469
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
17470
|
+
const nextTask = this.nextTask(task.data.record, changedTask.data, taskContext);
|
|
17471
|
+
if (!nextTask)
|
|
17472
|
+
return uncertain("workspace reserved but task update is uncertain");
|
|
17473
|
+
const written = this.appendPatch(task.data, nextTask);
|
|
17474
|
+
if (!written.ok)
|
|
17475
|
+
return uncertain("workspace reserved but task append is uncertain");
|
|
17476
|
+
return success(nextTask.revision, nextWorkspace.data.revision, {
|
|
17477
|
+
task: nextTask,
|
|
16819
17478
|
workspace: nextWorkspace.data
|
|
16820
17479
|
});
|
|
16821
|
-
});
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
if (!fs5.existsSync(this.recoveryDir))
|
|
16825
|
-
return success(null, null, []);
|
|
16826
|
-
try {
|
|
16827
|
-
const candidates = [];
|
|
16828
|
-
for (const name of fs5.readdirSync(this.recoveryDir)) {
|
|
16829
|
-
const match = RECOVERY_NAME.exec(name);
|
|
16830
|
-
if (match)
|
|
16831
|
-
candidates.push({
|
|
16832
|
-
target: match[1],
|
|
16833
|
-
path: path4.join(this.recoveryDir, name),
|
|
16834
|
-
digest: match[3]
|
|
16835
|
-
});
|
|
16836
|
-
}
|
|
16837
|
-
return success(null, null, candidates);
|
|
16838
|
-
} catch (error) {
|
|
16839
|
-
return failure2("storage_error", `unable to list recovery: ${String(error)}`, {
|
|
16840
|
-
path: this.recoveryDir
|
|
16841
|
-
});
|
|
16842
|
-
}
|
|
16843
|
-
}
|
|
16844
|
-
recoverTask(taskId, input) {
|
|
16845
|
-
return this.recover("task", taskId, input);
|
|
16846
|
-
}
|
|
16847
|
-
recoverWorkspace(input) {
|
|
16848
|
-
return this.recover("workspace", null, input);
|
|
16849
|
-
}
|
|
16850
|
-
recover(target, taskId, input) {
|
|
16851
|
-
if (taskId !== null && !validId(taskId))
|
|
16852
|
-
return failure2("invalid_input", "task ID is invalid", { taskId });
|
|
16853
|
-
if (!input.reason || !Array.isArray(input.authorityRefs) || typeof input.processEvidence !== "function" || !validDigest(input.expectedBytes) || !validDigest(input.snapshotDigest))
|
|
16854
|
-
return failure2("invalid_input", "recovery authority is invalid");
|
|
16855
|
-
if (input.authorityRefs.some((ref) => !refSchema.safeParse(ref).success))
|
|
16856
|
-
return failure2("invalid_input", "recovery references are invalid");
|
|
16857
|
-
try {
|
|
16858
|
-
const workspace = this.readWorkspace();
|
|
16859
|
-
if (!workspace.ok && target === "task")
|
|
16860
|
-
return workspace;
|
|
16861
|
-
const workspaceValue = workspace.ok ? workspace.data : null;
|
|
16862
|
-
if (workspaceValue && workspaceValue.revision !== input.expectedWorkspaceRevision)
|
|
16863
|
-
return this.conflict(input.expectedWorkspaceRevision, workspaceValue.revision);
|
|
16864
|
-
const file = target === "workspace" ? this.workspacePath : this.taskPath(taskId);
|
|
16865
|
-
const currentBytes = this.snapshotBytes(file);
|
|
16866
|
-
if (!currentBytes)
|
|
16867
|
-
return failure2("not_found", "snapshot not found");
|
|
16868
|
-
if (digestBytes(currentBytes) !== input.expectedBytes)
|
|
16869
|
-
return failure2("revision_conflict", "snapshot bytes do not match expected bytes");
|
|
16870
|
-
let selectedBytes = currentBytes;
|
|
16871
|
-
if (digestBytes(currentBytes) !== input.snapshotDigest) {
|
|
16872
|
-
const candidate = this.findRecovery(target, taskId, input.snapshotDigest, workspaceValue);
|
|
16873
|
-
if (!candidate)
|
|
16874
|
-
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
16875
|
-
selectedBytes = candidate;
|
|
16876
|
-
}
|
|
16877
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
16878
|
-
if (!selectedRecord.ok)
|
|
16879
|
-
return selectedRecord;
|
|
16880
|
-
if (target === "workspace") {
|
|
16881
|
-
const parsed = selectedRecord;
|
|
16882
|
-
if (!parsed.ok || parsed.data.root !== this.root)
|
|
16883
|
-
return failure2("recovery_required", "workspace recovery binding is invalid");
|
|
16884
|
-
} else {
|
|
16885
|
-
const parsed = selectedRecord;
|
|
16886
|
-
if (!parsed.ok || parsed.data.id !== taskId || !workspaceValue || parsed.data.workspaceId !== workspaceValue.id)
|
|
16887
|
-
return failure2("recovery_required", "task recovery binding is invalid");
|
|
16888
|
-
}
|
|
16889
|
-
const lock = this.readLockSnapshot();
|
|
16890
|
-
if (!lock.ok)
|
|
16891
|
-
return lock;
|
|
16892
|
-
const writer = target === "workspace" ? selectedRecord.data.writer : workspaceValue?.writer ?? null;
|
|
16893
|
-
const evidence = this.processEvidence(input, lock.data?.data ?? null, writer);
|
|
16894
|
-
if (!evidence.ok)
|
|
16895
|
-
return evidence;
|
|
16896
|
-
if (lock.data && (evidence.data.pid !== lock.data.data.pid || evidence.data.processStart !== lock.data.data.processStart))
|
|
16897
|
-
return failure2("recovery_required", "process evidence does not match metadata lock");
|
|
16898
|
-
if (writer && (evidence.data.state !== "accounted_for" || evidence.data.ownerDigest !== sha256(canonicalJson(writer))))
|
|
16899
|
-
return failure2("recovery_required", "workspace writer is not accounted for");
|
|
16900
|
-
const recoveryGate = { reclaimed: false };
|
|
16901
|
-
const result = this.withLock((handle) => {
|
|
16902
|
-
if (lock.data && !recoveryGate.reclaimed)
|
|
16903
|
-
return failure2("recovery_required", "metadata lock changed during recovery", {
|
|
16904
|
-
path: this.lockPath
|
|
16905
|
-
});
|
|
16906
|
-
if (!handle.verifyStillHeld())
|
|
16907
|
-
return failure2("recovery_required", "metadata lock was compromised", {
|
|
16908
|
-
path: this.lockPath
|
|
16909
|
-
});
|
|
16910
|
-
const reacquiredBytes = this.snapshotBytes(file);
|
|
16911
|
-
if (!reacquiredBytes)
|
|
16912
|
-
return failure2("recovery_required", "snapshot disappeared during recovery");
|
|
16913
|
-
if (digestBytes(reacquiredBytes) !== input.expectedBytes)
|
|
16914
|
-
return failure2("revision_conflict", "snapshot changed during recovery");
|
|
16915
|
-
if (target === "task") {
|
|
16916
|
-
const currentWorkspace = this.readWorkspace();
|
|
16917
|
-
if (!currentWorkspace.ok || !currentWorkspace.data)
|
|
16918
|
-
return failure2("recovery_required", "workspace changed during recovery");
|
|
16919
|
-
if (currentWorkspace.data.revision !== input.expectedWorkspaceRevision)
|
|
16920
|
-
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
16921
|
-
}
|
|
16922
|
-
if (target === "workspace") {
|
|
16923
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
16924
|
-
if (!parsed.ok)
|
|
16925
|
-
return parsed;
|
|
16926
|
-
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
16927
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
16928
|
-
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
16929
|
-
}
|
|
16930
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
16931
|
-
if (!parsed.ok)
|
|
16932
|
-
return parsed;
|
|
16933
|
-
const value = {
|
|
16934
|
-
...parsed.data,
|
|
16935
|
-
revision: newRevision(),
|
|
16936
|
-
updatedAt: now(),
|
|
16937
|
-
status: parsed.data.status === "active" ? "paused" : parsed.data.status
|
|
16938
|
-
};
|
|
16939
|
-
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
16940
|
-
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
16941
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
16942
|
-
return result;
|
|
16943
|
-
} catch (error) {
|
|
16944
|
-
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
16945
|
-
}
|
|
16946
|
-
}
|
|
16947
|
-
processEvidence(input, lock, writer) {
|
|
16948
|
-
const lockCopy = lock ? this.deepFreeze(structuredClone(lock)) : null;
|
|
16949
|
-
const writerCopy = writer ? this.deepFreeze(structuredClone(writer)) : null;
|
|
16950
|
-
const lockDigest = lockCopy ? sha256(canonicalJson(lockCopy)) : null;
|
|
16951
|
-
const writerDigest = writerCopy ? sha256(canonicalJson(writerCopy)) : null;
|
|
16952
|
-
try {
|
|
16953
|
-
const result = input.processEvidence(lockCopy, writerCopy);
|
|
16954
|
-
if (lockCopy && sha256(canonicalJson(lockCopy)) !== lockDigest)
|
|
16955
|
-
return failure2("recovery_required", "process evidence mutated lock identity");
|
|
16956
|
-
if (writerCopy && sha256(canonicalJson(writerCopy)) !== writerDigest)
|
|
16957
|
-
return failure2("recovery_required", "process evidence mutated writer identity");
|
|
16958
|
-
if (!result.ok)
|
|
16959
|
-
return result;
|
|
16960
|
-
const parsed = processEvidenceSchema.safeParse(result.data);
|
|
16961
|
-
return parsed.success ? success(null, null, parsed.data) : failure2("recovery_required", "process evidence is invalid");
|
|
16962
|
-
} catch (error) {
|
|
16963
|
-
return failure2("recovery_required", `process evidence failed: ${String(error)}`);
|
|
16964
|
-
}
|
|
16965
|
-
}
|
|
16966
|
-
deepFreeze(value) {
|
|
16967
|
-
if (value && typeof value === "object") {
|
|
16968
|
-
for (const child of Object.values(value))
|
|
16969
|
-
this.deepFreeze(child);
|
|
16970
|
-
Object.freeze(value);
|
|
16971
|
-
}
|
|
16972
|
-
return value;
|
|
16973
|
-
}
|
|
16974
|
-
readLockSnapshot() {
|
|
16975
|
-
try {
|
|
16976
|
-
const stat = fs5.lstatSync(this.lockPath);
|
|
16977
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
16978
|
-
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
16979
|
-
path: this.lockPath
|
|
16980
|
-
});
|
|
16981
|
-
const raw = fs5.readFileSync(this.lockPath, "utf8");
|
|
16982
|
-
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
16983
|
-
} catch (error) {
|
|
16984
|
-
if (error?.code === "ENOENT")
|
|
16985
|
-
return success(null, null, null);
|
|
16986
|
-
return failure2("recovery_required", `metadata lock is invalid: ${String(error)}`, {
|
|
16987
|
-
path: this.lockPath
|
|
16988
|
-
});
|
|
16989
|
-
}
|
|
16990
|
-
}
|
|
16991
|
-
metadataLockOptions() {
|
|
16992
|
-
const inProcess = heldInProcess.has(this.root);
|
|
16993
|
-
return {
|
|
16994
|
-
lockPath: this.lockPath,
|
|
16995
|
-
staleMs: Number.MAX_SAFE_INTEGER,
|
|
16996
|
-
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
16997
|
-
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
16998
|
-
staleRecovery: "remove-if-unchanged",
|
|
16999
|
-
shouldReclaim: ({ payload, nowMs }) => {
|
|
17000
|
-
let ageMs = null;
|
|
17001
|
-
try {
|
|
17002
|
-
ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
|
|
17003
|
-
} catch {}
|
|
17004
|
-
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
17005
|
-
},
|
|
17006
|
-
shouldRemoveStaleLock: () => true,
|
|
17007
|
-
parsePayload: parseMetadataLockOrNull,
|
|
17008
|
-
payload: () => ({
|
|
17009
|
-
pid: process.pid,
|
|
17010
|
-
processStart: processStartOf(process.pid),
|
|
17011
|
-
host: localLockHost(),
|
|
17012
|
-
nonce: randomUUID2()
|
|
17013
|
-
})
|
|
17014
|
-
};
|
|
17015
|
-
}
|
|
17016
|
-
acquireMetadataLock(options) {
|
|
17017
|
-
clearAbandonedReclaimGuard(this.lockPath);
|
|
17018
|
-
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
17019
|
-
while (true) {
|
|
17020
|
-
try {
|
|
17021
|
-
return acquireFileLockSync(this.workspacePath, {
|
|
17022
|
-
...options,
|
|
17023
|
-
timeoutMs: Math.max(0, deadline - Date.now())
|
|
17024
|
-
});
|
|
17025
|
-
} catch (error) {
|
|
17026
|
-
const code = error?.code;
|
|
17027
|
-
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
17028
|
-
throw error;
|
|
17029
|
-
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
17030
|
-
throw error;
|
|
17031
|
-
if (code !== "file_lock_stale")
|
|
17032
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
17033
|
-
}
|
|
17034
|
-
}
|
|
17035
|
-
}
|
|
17036
|
-
externalActionLockOptions() {
|
|
17037
|
-
const options = this.metadataLockOptions();
|
|
17038
|
-
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
17480
|
+
}));
|
|
17481
|
+
this.compactPending();
|
|
17482
|
+
return result;
|
|
17039
17483
|
}
|
|
17040
17484
|
async withExternalActionLock(operation, reentrant = false) {
|
|
17041
17485
|
const activeRoots = externalActionLockRoots.getStore();
|
|
17042
17486
|
if (activeRoots && [...activeRoots].some((root) => root === this.root || sameDirectoryIdentity(root, this.root)))
|
|
17043
17487
|
return operation();
|
|
17488
|
+
const ready = this.ready(true);
|
|
17489
|
+
if (!ready.ok)
|
|
17490
|
+
return ready;
|
|
17491
|
+
const lockPath = this.paths().checkoutLock;
|
|
17044
17492
|
try {
|
|
17045
|
-
this.
|
|
17493
|
+
this.initializeStorage();
|
|
17046
17494
|
} catch (error) {
|
|
17047
17495
|
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
17048
|
-
path: this.
|
|
17496
|
+
path: this.paths().dir
|
|
17049
17497
|
});
|
|
17050
17498
|
}
|
|
17051
17499
|
let handle;
|
|
17052
17500
|
try {
|
|
17053
|
-
|
|
17501
|
+
const options = this.lockOptions(lockPath);
|
|
17502
|
+
handle = this.acquire(lockPath, {
|
|
17503
|
+
...options,
|
|
17504
|
+
payload: () => ({ ...options.payload(), externalAction: true })
|
|
17505
|
+
});
|
|
17054
17506
|
} catch (error) {
|
|
17055
|
-
return this.lockFailure(error);
|
|
17507
|
+
return this.lockFailure(lockPath, error);
|
|
17056
17508
|
}
|
|
17057
|
-
|
|
17509
|
+
hold(lockPath);
|
|
17058
17510
|
let result;
|
|
17059
17511
|
try {
|
|
17060
17512
|
if (!handle.verifyStillHeld())
|
|
17061
|
-
result = failure2("recovery_required", "metadata lock was compromised", {
|
|
17062
|
-
path: this.lockPath
|
|
17063
|
-
});
|
|
17513
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
17064
17514
|
else {
|
|
17065
17515
|
try {
|
|
17066
17516
|
const run = () => operation();
|
|
@@ -17074,526 +17524,1144 @@ class TaskStore {
|
|
|
17074
17524
|
if (!handle.verifyStillHeld())
|
|
17075
17525
|
result = failure2("external_outcome_unknown", "metadata lock was compromised", {
|
|
17076
17526
|
outcome: "unknown",
|
|
17077
|
-
path:
|
|
17527
|
+
path: lockPath
|
|
17078
17528
|
});
|
|
17079
17529
|
} catch {
|
|
17080
17530
|
result = failure2("external_outcome_unknown", "metadata lock could not be verified", {
|
|
17081
17531
|
outcome: "unknown",
|
|
17082
|
-
path:
|
|
17532
|
+
path: lockPath
|
|
17083
17533
|
});
|
|
17084
17534
|
}
|
|
17085
17535
|
}
|
|
17086
17536
|
} catch {
|
|
17087
17537
|
result = failure2("external_outcome_unknown", "external action lock operation failed", {
|
|
17088
17538
|
outcome: "unknown",
|
|
17089
|
-
path:
|
|
17539
|
+
path: lockPath
|
|
17090
17540
|
});
|
|
17091
17541
|
}
|
|
17092
|
-
|
|
17542
|
+
drop(lockPath);
|
|
17093
17543
|
try {
|
|
17094
17544
|
retryTransient(() => handle.release());
|
|
17095
17545
|
} catch (error) {
|
|
17096
|
-
const released = this.lockFailure(error);
|
|
17546
|
+
const released = this.lockFailure(lockPath, error);
|
|
17097
17547
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
17098
17548
|
return result.ok ? failure2("external_outcome_unknown", "external action metadata lock release is uncertain", {
|
|
17099
17549
|
outcome: "unknown",
|
|
17100
|
-
path:
|
|
17550
|
+
path: lockPath
|
|
17101
17551
|
}) : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
17102
17552
|
}
|
|
17103
17553
|
return result;
|
|
17104
17554
|
}
|
|
17105
|
-
|
|
17106
|
-
const
|
|
17107
|
-
options.
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17555
|
+
collectGarbage(options = {}) {
|
|
17556
|
+
const dryRun = options.dryRun === true;
|
|
17557
|
+
const compactAbove = options.compactAbove ?? COMPACT_ABOVE;
|
|
17558
|
+
const report = {
|
|
17559
|
+
dryRun,
|
|
17560
|
+
compacted: { tasks: [], eventsFolded: 0, bytesBefore: 0, bytesAfter: 0 },
|
|
17561
|
+
blobs: { removed: 0, removedBytes: 0, kept: 0 },
|
|
17562
|
+
temporary: { removed: 0 },
|
|
17563
|
+
legacyRecovery: null,
|
|
17564
|
+
retried: [],
|
|
17565
|
+
failed: []
|
|
17566
|
+
};
|
|
17567
|
+
const ready = this.ready(!dryRun);
|
|
17568
|
+
if (!ready.ok)
|
|
17569
|
+
return ready;
|
|
17570
|
+
const dirs = this.paths();
|
|
17571
|
+
const recovery = path6.join(this.root, ".workit", "recovery");
|
|
17572
|
+
if (fs7.existsSync(recovery)) {
|
|
17573
|
+
let files = 0;
|
|
17574
|
+
let bytes = 0;
|
|
17575
|
+
try {
|
|
17576
|
+
for (const name of fs7.readdirSync(recovery)) {
|
|
17577
|
+
const stat = fs7.lstatSync(path6.join(recovery, name));
|
|
17578
|
+
if (!stat.isFile())
|
|
17579
|
+
continue;
|
|
17580
|
+
files += 1;
|
|
17581
|
+
bytes += stat.size;
|
|
17582
|
+
}
|
|
17583
|
+
} catch {}
|
|
17584
|
+
const remove = options.pruneRecovery === true && !dryRun;
|
|
17585
|
+
if (remove)
|
|
17586
|
+
fs7.rmSync(recovery, { recursive: true, force: true });
|
|
17587
|
+
report.legacyRecovery = { path: recovery, files, bytes, removed: remove };
|
|
17588
|
+
}
|
|
17589
|
+
if (!fs7.existsSync(dirs.tasks))
|
|
17590
|
+
return success(null, null, report);
|
|
17591
|
+
const names = fs7.readdirSync(dirs.tasks).filter((name) => validId(name));
|
|
17592
|
+
for (const name of names) {
|
|
17593
|
+
const file = this.eventsPath(name);
|
|
17594
|
+
let size = 0;
|
|
17595
|
+
try {
|
|
17596
|
+
size = fs7.statSync(file).size;
|
|
17597
|
+
} catch {
|
|
17598
|
+
continue;
|
|
17599
|
+
}
|
|
17600
|
+
const lines = this.countEvents(file);
|
|
17601
|
+
if (lines > compactAbove) {
|
|
17602
|
+
if (dryRun) {
|
|
17603
|
+
report.compacted.tasks.push(name);
|
|
17604
|
+
report.compacted.eventsFolded += lines - COMPACT_KEEP;
|
|
17605
|
+
report.compacted.bytesBefore += size;
|
|
17606
|
+
} else {
|
|
17607
|
+
const compacted = this.compact(name);
|
|
17608
|
+
if (!compacted.ok)
|
|
17609
|
+
(compacted.code === "busy" ? report.retried : report.failed).push(name);
|
|
17610
|
+
else {
|
|
17611
|
+
report.compacted.tasks.push(name);
|
|
17612
|
+
report.compacted.eventsFolded += compacted.data.folded;
|
|
17613
|
+
report.compacted.bytesBefore += size;
|
|
17614
|
+
report.compacted.bytesAfter += compacted.data.bytes;
|
|
17615
|
+
}
|
|
17616
|
+
}
|
|
17617
|
+
}
|
|
17618
|
+
}
|
|
17619
|
+
const nowMs = Date.now();
|
|
17620
|
+
const sweep = () => {
|
|
17621
|
+
const references = new Set;
|
|
17622
|
+
for (const name of names)
|
|
17623
|
+
try {
|
|
17624
|
+
for (const match of fs7.readFileSync(this.eventsPath(name), "utf8").matchAll(/"\$blob":"([0-9a-f]{64})"/g))
|
|
17625
|
+
references.add(match[1]);
|
|
17626
|
+
} catch {}
|
|
17627
|
+
if (fs7.existsSync(dirs.blobs))
|
|
17628
|
+
for (const name of fs7.readdirSync(dirs.blobs)) {
|
|
17629
|
+
const digest = name.replace(/\.json$/, "");
|
|
17630
|
+
const file = path6.join(dirs.blobs, name);
|
|
17631
|
+
let stat;
|
|
17632
|
+
try {
|
|
17633
|
+
stat = fs7.lstatSync(file);
|
|
17634
|
+
} catch {
|
|
17635
|
+
continue;
|
|
17636
|
+
}
|
|
17637
|
+
if (references.has(digest) || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS) {
|
|
17638
|
+
report.blobs.kept += 1;
|
|
17639
|
+
continue;
|
|
17640
|
+
}
|
|
17641
|
+
report.blobs.removed += 1;
|
|
17642
|
+
report.blobs.removedBytes += stat.size;
|
|
17643
|
+
if (!dryRun)
|
|
17644
|
+
fs7.rmSync(file, { force: true });
|
|
17645
|
+
}
|
|
17646
|
+
return success(null, null, null);
|
|
17117
17647
|
};
|
|
17118
|
-
|
|
17648
|
+
const swept = dryRun ? sweep() : this.withLock(path6.join(dirs.dir, "gc.lock"), sweep);
|
|
17649
|
+
if (!swept.ok)
|
|
17650
|
+
return swept;
|
|
17651
|
+
const temporaryDirs = [
|
|
17652
|
+
dirs.dir,
|
|
17653
|
+
dirs.tasks,
|
|
17654
|
+
dirs.blobs,
|
|
17655
|
+
dirs.checkout,
|
|
17656
|
+
...names.map((name) => path6.join(dirs.tasks, name))
|
|
17657
|
+
];
|
|
17658
|
+
for (const directory of temporaryDirs) {
|
|
17659
|
+
if (!fs7.existsSync(directory))
|
|
17660
|
+
continue;
|
|
17661
|
+
for (const name of fs7.readdirSync(directory)) {
|
|
17662
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
17663
|
+
continue;
|
|
17664
|
+
const file = path6.join(directory, name);
|
|
17665
|
+
try {
|
|
17666
|
+
const stat = fs7.lstatSync(file);
|
|
17667
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
17668
|
+
continue;
|
|
17669
|
+
} catch {
|
|
17670
|
+
continue;
|
|
17671
|
+
}
|
|
17672
|
+
report.temporary.removed += 1;
|
|
17673
|
+
if (!dryRun)
|
|
17674
|
+
fs7.rmSync(file, { force: true });
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
return success(null, null, report);
|
|
17119
17678
|
}
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
const
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17679
|
+
paths() {
|
|
17680
|
+
const location = this.location();
|
|
17681
|
+
const dir = location.ok ? location.data.dir : path6.join(this.root, ".workit");
|
|
17682
|
+
const checkout = path6.join(dir, "checkouts", checkoutSlug(this.root));
|
|
17683
|
+
return {
|
|
17684
|
+
dir,
|
|
17685
|
+
shared: location.ok ? location.data.shared : false,
|
|
17686
|
+
marker: path6.join(dir, "store.json"),
|
|
17687
|
+
checkout,
|
|
17688
|
+
workspace: path6.join(checkout, "workspace.json"),
|
|
17689
|
+
checkoutLock: path6.join(checkout, "metadata.lock"),
|
|
17690
|
+
tasks: path6.join(dir, "tasks"),
|
|
17691
|
+
index: path6.join(dir, "task-index.json"),
|
|
17692
|
+
blobs: path6.join(dir, "blobs", "candidates"),
|
|
17693
|
+
legacy: path6.join(dir, "legacy", checkoutSlug(this.root), "v2"),
|
|
17694
|
+
v2: path6.join(this.root, ".workit")
|
|
17128
17695
|
};
|
|
17129
|
-
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
17130
17696
|
}
|
|
17131
|
-
|
|
17697
|
+
eventsPath(taskId) {
|
|
17698
|
+
return path6.join(this.paths().tasks, taskId, "events.jsonl");
|
|
17699
|
+
}
|
|
17700
|
+
snapshotPath(taskId) {
|
|
17701
|
+
return path6.join(this.paths().tasks, taskId, "snapshot.json");
|
|
17702
|
+
}
|
|
17703
|
+
taskLockPath(taskId) {
|
|
17704
|
+
return path6.join(this.paths().tasks, taskId, "lock");
|
|
17705
|
+
}
|
|
17706
|
+
ready(write = false) {
|
|
17707
|
+
const location = this.location();
|
|
17708
|
+
if (!location.ok)
|
|
17709
|
+
return location;
|
|
17710
|
+
if (this.migrationChecked)
|
|
17711
|
+
return success(null, null, null);
|
|
17712
|
+
const dirs = this.paths();
|
|
17132
17713
|
try {
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17714
|
+
const marker = JSON.parse(fs7.readFileSync(dirs.marker, "utf8"));
|
|
17715
|
+
if (typeof marker.version === "number" && marker.version > STORE_FORMAT)
|
|
17716
|
+
return failure2("recovery_required", `the workit store at ${dirs.dir} has format ${marker.version}; upgrade Workit to use it`, { path: dirs.marker });
|
|
17717
|
+
} catch {}
|
|
17718
|
+
this.migrationChecked = true;
|
|
17719
|
+
const settled = this.settleMigrations(write);
|
|
17720
|
+
if (!settled.ok)
|
|
17721
|
+
this.migrationChecked = false;
|
|
17722
|
+
return settled;
|
|
17723
|
+
}
|
|
17724
|
+
settleMigrations(write) {
|
|
17725
|
+
const moved = this.adoptLocalStore();
|
|
17726
|
+
if (!moved.ok)
|
|
17727
|
+
return moved;
|
|
17728
|
+
const pending = this.pendingMigrations();
|
|
17729
|
+
if (pending.length > 0 && !write && !(this.migrateOnRead ?? migrationReporterInstalled()))
|
|
17730
|
+
return failure2("needs_input", `${MIGRATION_PENDING}: the task store at ${pending[0]} has not been migrated yet; run \`workit task status\` to migrate it`, { path: pending[0], guidance: "workit task status" });
|
|
17731
|
+
for (const v2 of this.v2Dirs()) {
|
|
17732
|
+
const migrated = this.migrateV2(v2);
|
|
17733
|
+
if (!migrated.ok)
|
|
17734
|
+
return migrated;
|
|
17138
17735
|
}
|
|
17139
|
-
|
|
17140
|
-
|
|
17736
|
+
return success(null, null, null);
|
|
17737
|
+
}
|
|
17738
|
+
blobStore() {
|
|
17739
|
+
const dir = this.paths().blobs;
|
|
17740
|
+
return {
|
|
17741
|
+
read: (digest) => {
|
|
17742
|
+
try {
|
|
17743
|
+
return JSON.parse(fs7.readFileSync(path6.join(dir, `${digest}.json`), "utf8"));
|
|
17744
|
+
} catch (error) {
|
|
17745
|
+
throw new LogDamage(`stored candidate ${digest} is missing or unreadable: ${String(error)}`);
|
|
17746
|
+
}
|
|
17747
|
+
},
|
|
17748
|
+
write: (digest, value) => {
|
|
17749
|
+
const file = path6.join(dir, `${digest}.json`);
|
|
17750
|
+
if (fs7.existsSync(file)) {
|
|
17751
|
+
const stamp = new Date;
|
|
17752
|
+
try {
|
|
17753
|
+
fs7.utimesSync(file, stamp, stamp);
|
|
17754
|
+
return;
|
|
17755
|
+
} catch {}
|
|
17756
|
+
}
|
|
17757
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
17758
|
+
replaceFile(file, JSON.stringify(value), true);
|
|
17759
|
+
}
|
|
17760
|
+
};
|
|
17761
|
+
}
|
|
17762
|
+
load(taskId) {
|
|
17141
17763
|
try {
|
|
17142
|
-
|
|
17764
|
+
const loaded = this.loadOrThrow(taskId);
|
|
17765
|
+
if (!loaded)
|
|
17766
|
+
return success(null, null, null);
|
|
17767
|
+
const parsed = this.parseTask(loaded.state.record, taskId);
|
|
17768
|
+
if (!parsed.ok)
|
|
17769
|
+
return parsed;
|
|
17770
|
+
return success(null, null, {
|
|
17771
|
+
record: parsed.data,
|
|
17772
|
+
key: loaded.state.key,
|
|
17773
|
+
legacy: loaded.state.legacy,
|
|
17774
|
+
seq: loaded.seq,
|
|
17775
|
+
lastId: loaded.lastId,
|
|
17776
|
+
lastStart: loaded.lastStart,
|
|
17777
|
+
end: loaded.end,
|
|
17778
|
+
size: loaded.size,
|
|
17779
|
+
snapshotSeq: loaded.snapshotSeq
|
|
17780
|
+
});
|
|
17143
17781
|
} catch (error) {
|
|
17144
|
-
|
|
17782
|
+
if (error instanceof LogDamage)
|
|
17783
|
+
return failure2("recovery_required", error.upgrade ? error.message : `task ${taskId} log is damaged: ${error.message}`, { taskId, path: this.eventsPath(taskId) });
|
|
17784
|
+
return failure2("recovery_required", `task ${taskId} cannot be read: ${String(error)}`, {
|
|
17785
|
+
taskId
|
|
17786
|
+
});
|
|
17145
17787
|
}
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17788
|
+
}
|
|
17789
|
+
loadOrThrow(taskId) {
|
|
17790
|
+
const blobs = this.blobStore();
|
|
17791
|
+
const file = this.eventsPath(taskId);
|
|
17792
|
+
const snapshot = this.readSnapshot(taskId);
|
|
17793
|
+
if (snapshot) {
|
|
17149
17794
|
try {
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
}
|
|
17158
|
-
|
|
17159
|
-
|
|
17795
|
+
const tail = readLog(file, snapshot.lastStart, snapshot.seq);
|
|
17796
|
+
const first = tail?.events[0];
|
|
17797
|
+
if (tail && first && first.id === snapshot.lastId) {
|
|
17798
|
+
let state = {
|
|
17799
|
+
record: snapshot.record,
|
|
17800
|
+
key: snapshot.key,
|
|
17801
|
+
legacy: snapshot.legacy
|
|
17802
|
+
};
|
|
17803
|
+
for (const event of tail.events.slice(1))
|
|
17804
|
+
state = reduce(state, event, blobs);
|
|
17805
|
+
return this.loadedFrom(state, tail, snapshot.seq);
|
|
17160
17806
|
}
|
|
17161
17807
|
} catch (error) {
|
|
17162
|
-
|
|
17808
|
+
if (error instanceof LogDamage && error.upgrade)
|
|
17809
|
+
throw error;
|
|
17163
17810
|
}
|
|
17164
17811
|
}
|
|
17165
|
-
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
const releaseFailure = this.lockFailure(error);
|
|
17171
|
-
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
17172
|
-
result = result.ok ? releaseFailure : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
17173
|
-
}
|
|
17812
|
+
const log = readLog(file, 0, null);
|
|
17813
|
+
if (!log || log.events.length === 0) {
|
|
17814
|
+
if (log && log.size > 0)
|
|
17815
|
+
throw new LogDamage("the log holds no complete event");
|
|
17816
|
+
return null;
|
|
17174
17817
|
}
|
|
17175
|
-
|
|
17818
|
+
if (!OPENING_TYPES.has(log.events[0].type))
|
|
17819
|
+
throw new LogDamage(`the log starts with ${log.events[0].type}, not an opening event`);
|
|
17820
|
+
let state = null;
|
|
17821
|
+
for (const event of log.events)
|
|
17822
|
+
state = reduce(state, event, blobs);
|
|
17823
|
+
return this.loadedFrom(state, log, 0);
|
|
17176
17824
|
}
|
|
17177
|
-
|
|
17178
|
-
const
|
|
17825
|
+
loadedFrom(state, log, snapshotSeq) {
|
|
17826
|
+
const last = log.events.at(-1);
|
|
17827
|
+
return {
|
|
17828
|
+
state,
|
|
17829
|
+
seq: last.seq,
|
|
17830
|
+
lastId: last.id,
|
|
17831
|
+
lastStart: log.lastStart,
|
|
17832
|
+
end: log.end,
|
|
17833
|
+
size: log.size,
|
|
17834
|
+
snapshotSeq
|
|
17835
|
+
};
|
|
17836
|
+
}
|
|
17837
|
+
readSnapshot(taskId) {
|
|
17179
17838
|
try {
|
|
17180
|
-
|
|
17839
|
+
const value = JSON.parse(fs7.readFileSync(this.snapshotPath(taskId), "utf8"));
|
|
17840
|
+
if (!isObject4(value) || value.v !== 1 || value.task !== taskId || typeof value.seq !== "number" || typeof value.lastId !== "string" || typeof value.lastStart !== "number" || typeof value.end !== "number" || !isObject4(value.record))
|
|
17841
|
+
return null;
|
|
17842
|
+
return value;
|
|
17181
17843
|
} catch {
|
|
17182
|
-
return
|
|
17844
|
+
return null;
|
|
17183
17845
|
}
|
|
17846
|
+
}
|
|
17847
|
+
writeSnapshot(taskId, loaded) {
|
|
17848
|
+
const snapshot = {
|
|
17849
|
+
v: 1,
|
|
17850
|
+
task: taskId,
|
|
17851
|
+
seq: loaded.seq,
|
|
17852
|
+
lastId: loaded.lastId,
|
|
17853
|
+
lastStart: loaded.lastStart,
|
|
17854
|
+
end: loaded.end,
|
|
17855
|
+
key: loaded.key,
|
|
17856
|
+
legacy: loaded.legacy,
|
|
17857
|
+
record: loaded.record
|
|
17858
|
+
};
|
|
17184
17859
|
try {
|
|
17185
|
-
|
|
17860
|
+
replaceFile(this.snapshotPath(taskId), JSON.stringify(snapshot), false);
|
|
17186
17861
|
} catch {}
|
|
17187
|
-
return true;
|
|
17188
17862
|
}
|
|
17189
|
-
|
|
17863
|
+
loadBound(taskId) {
|
|
17864
|
+
const loaded = this.load(taskId);
|
|
17865
|
+
if (!loaded.ok)
|
|
17866
|
+
return loaded;
|
|
17867
|
+
if (!loaded.data)
|
|
17868
|
+
return failure2("not_found", "task not found", { taskId });
|
|
17869
|
+
const workspace = this.readWorkspace();
|
|
17870
|
+
if (!workspace.ok)
|
|
17871
|
+
return workspace;
|
|
17872
|
+
const bound = this.bindingError(loaded.data.record, workspace.data);
|
|
17873
|
+
return bound ?? success(null, null, loaded.data);
|
|
17874
|
+
}
|
|
17875
|
+
bindingError(task, workspace) {
|
|
17876
|
+
if (workspace && task.workspaceId === workspace.id)
|
|
17877
|
+
return null;
|
|
17878
|
+
return failure2("not_found", `task ${task.id} belongs to another checkout of this repository; run there, or move it here with \`workit task adopt ${task.id}\``, { taskId: task.id });
|
|
17879
|
+
}
|
|
17880
|
+
nextTask(current, changed, context) {
|
|
17881
|
+
const valid = taskRecordSchema.safeParse({
|
|
17882
|
+
...changed,
|
|
17883
|
+
id: current.id,
|
|
17884
|
+
workspaceId: current.workspaceId,
|
|
17885
|
+
createdAt: current.createdAt,
|
|
17886
|
+
revision: context.revision,
|
|
17887
|
+
updatedAt: context.now,
|
|
17888
|
+
runtime: stampNew(current)
|
|
17889
|
+
});
|
|
17890
|
+
return valid.success ? valid.data : null;
|
|
17891
|
+
}
|
|
17892
|
+
event(taskId, seq, type, data) {
|
|
17893
|
+
return {
|
|
17894
|
+
v: EVENT_VERSION,
|
|
17895
|
+
seq,
|
|
17896
|
+
at: new Date().toISOString(),
|
|
17897
|
+
id: newId(),
|
|
17898
|
+
task: taskId,
|
|
17899
|
+
actor: this.actor,
|
|
17900
|
+
type,
|
|
17901
|
+
data
|
|
17902
|
+
};
|
|
17903
|
+
}
|
|
17904
|
+
appendPatch(loaded, next, extra = {}) {
|
|
17905
|
+
const taskId = next.id;
|
|
17190
17906
|
try {
|
|
17191
|
-
|
|
17192
|
-
|
|
17907
|
+
const blobs = this.blobStore();
|
|
17908
|
+
const before = loaded.record;
|
|
17909
|
+
const after = next;
|
|
17910
|
+
const ops = encodeOps(diff(before, after), blobs);
|
|
17911
|
+
const { type, ...data } = extra;
|
|
17912
|
+
if (data.key)
|
|
17913
|
+
data.key = { ...data.key, boundAt: data.key.boundAt ?? new Date().toISOString() };
|
|
17914
|
+
const event = this.event(taskId, loaded.seq + 1, type ?? ("key" in extra ? "task.bound" : eventType(before, after)), { ops, ...data });
|
|
17915
|
+
const start = appendEvent(this.eventsPath(taskId), event, loaded.end);
|
|
17916
|
+
const end = start + Buffer.byteLength(`${JSON.stringify(event)}
|
|
17917
|
+
`);
|
|
17918
|
+
const key = "key" in data ? data.key ?? null : loaded.key;
|
|
17919
|
+
const state = {
|
|
17920
|
+
record: next,
|
|
17921
|
+
key,
|
|
17922
|
+
legacy: extra.legacy ?? loaded.legacy,
|
|
17923
|
+
seq: event.seq,
|
|
17924
|
+
lastId: event.id,
|
|
17925
|
+
lastStart: start,
|
|
17926
|
+
end
|
|
17927
|
+
};
|
|
17928
|
+
if (event.seq - loaded.snapshotSeq >= SNAPSHOT_EVERY || next.status !== loaded.record.status || loaded.snapshotSeq === 0)
|
|
17929
|
+
this.writeSnapshot(taskId, state);
|
|
17930
|
+
if (end > AUTO_COMPACT_BYTES)
|
|
17931
|
+
this.compactDue.add(taskId);
|
|
17932
|
+
this.indexTask(next, key, (extra.legacy ?? loaded.legacy) !== null);
|
|
17933
|
+
return success(null, null, null);
|
|
17193
17934
|
} catch (error) {
|
|
17194
|
-
return error
|
|
17935
|
+
return failure2("storage_error", `task append failed: ${String(error)}`, {
|
|
17936
|
+
path: this.eventsPath(taskId)
|
|
17937
|
+
});
|
|
17195
17938
|
}
|
|
17196
17939
|
}
|
|
17197
|
-
|
|
17198
|
-
const
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17940
|
+
openTask(record, type, key, legacy = null) {
|
|
17941
|
+
const dir = path6.join(this.paths().tasks, record.id);
|
|
17942
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
17943
|
+
const blobs = this.blobStore();
|
|
17944
|
+
const bound = key ? { ...key, boundAt: key.boundAt ?? new Date().toISOString() } : null;
|
|
17945
|
+
const event = this.event(record.id, 1, type, {
|
|
17946
|
+
record: encodeRecord(record, blobs),
|
|
17947
|
+
key: bound,
|
|
17948
|
+
...legacy ? { legacy } : {}
|
|
17949
|
+
});
|
|
17950
|
+
const file = this.eventsPath(record.id);
|
|
17951
|
+
const existing = readLog(file, 0, null);
|
|
17952
|
+
if (existing && existing.events.length > 0)
|
|
17953
|
+
throw new Error(`task ${record.id} already has a log`);
|
|
17954
|
+
appendEvent(file, event, 0);
|
|
17955
|
+
this.indexTask(record, bound, legacy !== null);
|
|
17956
|
+
}
|
|
17957
|
+
compactPending() {
|
|
17958
|
+
for (const taskId of this.compactDue) {
|
|
17959
|
+
this.compactDue.delete(taskId);
|
|
17960
|
+
this.compact(taskId);
|
|
17961
|
+
}
|
|
17962
|
+
}
|
|
17963
|
+
compact(taskId) {
|
|
17964
|
+
try {
|
|
17965
|
+
const file = this.eventsPath(taskId);
|
|
17966
|
+
const log = readLog(file, 0, null);
|
|
17967
|
+
if (!log || log.events.length <= COMPACT_KEEP + 1)
|
|
17968
|
+
return success(null, null, { folded: 0, bytes: log?.size ?? 0 });
|
|
17969
|
+
const blobs = this.blobStore();
|
|
17970
|
+
const cut = log.events.length - COMPACT_KEEP;
|
|
17971
|
+
let state = null;
|
|
17972
|
+
for (const event of log.events.slice(0, cut))
|
|
17973
|
+
state = reduce(state, event, blobs);
|
|
17974
|
+
const folded = log.events[cut - 1];
|
|
17975
|
+
const checkpoint = {
|
|
17976
|
+
...this.event(taskId, folded.seq, "task.checkpoint", {
|
|
17977
|
+
record: encodeRecord(state.record, blobs),
|
|
17978
|
+
key: state.key,
|
|
17979
|
+
...state.legacy ? { legacy: state.legacy } : {},
|
|
17980
|
+
folded: { from: log.events[0].seq, to: folded.seq }
|
|
17981
|
+
})
|
|
17982
|
+
};
|
|
17983
|
+
const prefix = `${[checkpoint, ...log.events.slice(cut)].map((event) => JSON.stringify(event)).join(`
|
|
17984
|
+
`)}
|
|
17985
|
+
`;
|
|
17986
|
+
const last = log.events.at(-1);
|
|
17987
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
17988
|
+
let latest = null;
|
|
17989
|
+
try {
|
|
17990
|
+
latest = readLog(file, log.lastStart, last.seq);
|
|
17991
|
+
} catch {}
|
|
17992
|
+
if (!latest || latest.events[0]?.id !== last.id)
|
|
17993
|
+
return failure2("busy", "the task log changed during compaction; retry", { taskId });
|
|
17994
|
+
const appended = fs7.readFileSync(file).subarray(log.end, latest.end);
|
|
17995
|
+
const bytes = Buffer.concat([Buffer.from(prefix, "utf8"), appended]);
|
|
17996
|
+
replaceFile(file, bytes.toString("utf8"), true);
|
|
17997
|
+
try {
|
|
17998
|
+
fs7.rmSync(this.snapshotPath(taskId), { force: true });
|
|
17999
|
+
} catch {}
|
|
18000
|
+
const reloaded = this.load(taskId);
|
|
18001
|
+
if (reloaded.ok && reloaded.data)
|
|
18002
|
+
this.writeSnapshot(taskId, reloaded.data);
|
|
18003
|
+
return success(null, null, { folded: cut, bytes: bytes.length });
|
|
17204
18004
|
});
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
|
|
18005
|
+
} catch (error) {
|
|
18006
|
+
return failure2("storage_error", `compaction failed: ${String(error)}`, { taskId });
|
|
18007
|
+
}
|
|
18008
|
+
}
|
|
18009
|
+
countEvents(file) {
|
|
18010
|
+
try {
|
|
18011
|
+
const bytes = fs7.readFileSync(file);
|
|
18012
|
+
let count = 0;
|
|
18013
|
+
for (let index = bytes.indexOf(10);index >= 0; index = bytes.indexOf(10, index + 1))
|
|
18014
|
+
count += 1;
|
|
18015
|
+
return count;
|
|
18016
|
+
} catch {
|
|
18017
|
+
return 0;
|
|
18018
|
+
}
|
|
18019
|
+
}
|
|
18020
|
+
openTasksForKey(key) {
|
|
18021
|
+
const all = this.listStoreIndex();
|
|
18022
|
+
if (!all.ok)
|
|
18023
|
+
return all;
|
|
18024
|
+
const location = this.location();
|
|
18025
|
+
const renames = location.ok ? branchRenames(location.data) : [];
|
|
18026
|
+
return success(null, null, all.data.filter((entry) => entry.key !== null && entry.status !== "closed").filter((entry) => (renames.length ? followRenames(entry.key, entry.boundAt ?? entry.createdAt, renames) : entry.key) === key).toSorted(oldestFirst));
|
|
18027
|
+
}
|
|
18028
|
+
openTaskForKey(key) {
|
|
18029
|
+
const bound = this.openTasksForKey(key);
|
|
18030
|
+
return bound.ok ? success(null, null, bound.data[0] ?? null) : bound;
|
|
18031
|
+
}
|
|
18032
|
+
withKeyLock(key, operation) {
|
|
18033
|
+
const digest = createHash4("sha256").update(key).digest("hex").slice(0, 32);
|
|
18034
|
+
return this.withLock(path6.join(this.paths().dir, "keys", `${digest}.lock`), operation);
|
|
18035
|
+
}
|
|
18036
|
+
bindLocked(taskId, key, timestamp) {
|
|
18037
|
+
const current = this.readWorkspace();
|
|
18038
|
+
if (!current.ok)
|
|
18039
|
+
return current;
|
|
18040
|
+
let workspace = current.data;
|
|
18041
|
+
if (!workspace) {
|
|
18042
|
+
workspace = this.nextWorkspace(null);
|
|
18043
|
+
const written = this.writeWorkspace(workspace);
|
|
18044
|
+
if (!written.ok)
|
|
18045
|
+
return written;
|
|
18046
|
+
}
|
|
18047
|
+
const target = workspace;
|
|
18048
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
18049
|
+
const loaded = this.load(taskId);
|
|
18050
|
+
if (!loaded.ok)
|
|
18051
|
+
return loaded;
|
|
18052
|
+
if (!loaded.data)
|
|
18053
|
+
return failure2("not_found", "task not found", { taskId });
|
|
18054
|
+
if (loaded.data.record.status === "closed")
|
|
18055
|
+
return failure2("invalid_transition", "a closed task cannot be adopted", { taskId });
|
|
18056
|
+
if (loaded.data.record.workspaceId === target.id && loaded.data.key?.key === key.key)
|
|
18057
|
+
return success(loaded.data.record.revision, null, loaded.data.record);
|
|
18058
|
+
const next = taskRecordSchema.safeParse({
|
|
18059
|
+
...loaded.data.record,
|
|
18060
|
+
workspaceId: target.id,
|
|
18061
|
+
revision: newRevision(),
|
|
18062
|
+
updatedAt: timestamp ?? now(),
|
|
18063
|
+
runtime: stampNew(loaded.data.record)
|
|
18064
|
+
});
|
|
18065
|
+
if (!next.success)
|
|
18066
|
+
return failure2("invalid_input", "rebound task is invalid");
|
|
18067
|
+
const written = this.appendPatch(loaded.data, next.data, { key });
|
|
18068
|
+
return written.ok ? success(next.data.revision, null, next.data) : written;
|
|
18069
|
+
});
|
|
18070
|
+
}
|
|
18071
|
+
unbindLocked(taskId) {
|
|
18072
|
+
return this.withLock(this.taskLockPath(taskId), () => {
|
|
18073
|
+
const loaded = this.load(taskId);
|
|
18074
|
+
if (!loaded.ok)
|
|
18075
|
+
return loaded;
|
|
18076
|
+
if (!loaded.data || loaded.data.key === null)
|
|
18077
|
+
return success(null, null, null);
|
|
18078
|
+
return this.appendPatch(loaded.data, loaded.data.record, { key: null });
|
|
18079
|
+
});
|
|
18080
|
+
}
|
|
18081
|
+
createLocked(value, bindCurrent, key, checkRevision = true) {
|
|
18082
|
+
const current = this.readWorkspace();
|
|
18083
|
+
if (!current.ok)
|
|
18084
|
+
return current;
|
|
18085
|
+
if (checkRevision && (current.data ? value.expectedWorkspaceRevision !== current.data.revision : value.expectedWorkspaceRevision !== null))
|
|
18086
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", {
|
|
18087
|
+
expectedWorkspaceRevision: value.expectedWorkspaceRevision,
|
|
18088
|
+
actualWorkspaceRevision: current.data?.revision ?? null
|
|
18089
|
+
});
|
|
18090
|
+
if (!value.provenance)
|
|
18091
|
+
return failure2("invalid_input", "provenance is required");
|
|
18092
|
+
if (!intentSchema.safeParse(value.intent).success || !provenanceSchema.safeParse(value.provenance).success)
|
|
18093
|
+
return failure2("invalid_input", "task intent or provenance is invalid");
|
|
18094
|
+
if (bindCurrent) {
|
|
18095
|
+
const currentKey = this.currentKey();
|
|
18096
|
+
if (currentKey.ok)
|
|
18097
|
+
return this.withKeyLock(currentKey.data.key, () => {
|
|
18098
|
+
const holder = this.openTaskForKey(currentKey.data.key);
|
|
18099
|
+
if (!holder.ok)
|
|
18100
|
+
return holder;
|
|
18101
|
+
if (holder.data) {
|
|
18102
|
+
const released = this.unbindLocked(holder.data.id);
|
|
18103
|
+
if (!released.ok)
|
|
18104
|
+
return released;
|
|
18105
|
+
}
|
|
18106
|
+
return this.newTask(value, current.data, currentKey.data);
|
|
17220
18107
|
});
|
|
17221
18108
|
}
|
|
17222
|
-
|
|
17223
|
-
|
|
18109
|
+
return this.newTask(value, current.data, key);
|
|
18110
|
+
}
|
|
18111
|
+
newTask(value, previous, binding) {
|
|
18112
|
+
const current = { data: previous };
|
|
18113
|
+
const workspace = this.nextWorkspace(current.data);
|
|
18114
|
+
const timestamp = value.now ?? now();
|
|
18115
|
+
const task = {
|
|
18116
|
+
schemaVersion: SCHEMA_VERSION,
|
|
18117
|
+
id: newId(),
|
|
18118
|
+
workspaceId: workspace.id,
|
|
18119
|
+
revision: newRevision(),
|
|
18120
|
+
createdAt: timestamp,
|
|
18121
|
+
updatedAt: timestamp,
|
|
18122
|
+
origin: null,
|
|
18123
|
+
intent: {
|
|
18124
|
+
id: newId(),
|
|
18125
|
+
recordedAt: timestamp,
|
|
18126
|
+
provenance: value.provenance,
|
|
18127
|
+
data: value.intent
|
|
18128
|
+
},
|
|
18129
|
+
constraints: [],
|
|
18130
|
+
status: "active",
|
|
18131
|
+
closure: null,
|
|
18132
|
+
progress: { summary: "", nextAction: null, blockers: [] },
|
|
18133
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
18134
|
+
assessments: [],
|
|
18135
|
+
policy: null,
|
|
18136
|
+
policyChanges: [],
|
|
18137
|
+
candidates: [],
|
|
18138
|
+
evidence: [],
|
|
18139
|
+
decisions: [],
|
|
18140
|
+
findings: [],
|
|
18141
|
+
workers: []
|
|
18142
|
+
};
|
|
18143
|
+
return this.writeNewTask(current.data, workspace, task, "task.opened", binding, {
|
|
18144
|
+
operation: "create"
|
|
18145
|
+
});
|
|
17224
18146
|
}
|
|
17225
|
-
|
|
17226
|
-
|
|
17227
|
-
|
|
17228
|
-
|
|
18147
|
+
nextWorkspace(current, id) {
|
|
18148
|
+
return current ? { ...current, revision: newRevision(), root: this.root, runtime: stampNew(current) } : {
|
|
18149
|
+
schemaVersion: SCHEMA_VERSION,
|
|
18150
|
+
id: id ?? newId(),
|
|
18151
|
+
revision: newRevision(),
|
|
18152
|
+
root: this.root,
|
|
18153
|
+
runtime: { createdWith: runtimeVersion(), updatedWith: runtimeVersion() },
|
|
18154
|
+
writer: null
|
|
18155
|
+
};
|
|
18156
|
+
}
|
|
18157
|
+
writeNewTask(previous, workspace, task, type, key, details) {
|
|
18158
|
+
const written = this.writeWorkspace(workspace);
|
|
18159
|
+
if (!written.ok)
|
|
18160
|
+
return written;
|
|
17229
18161
|
try {
|
|
17230
|
-
|
|
17231
|
-
} catch {
|
|
17232
|
-
|
|
17233
|
-
|
|
17234
|
-
|
|
17235
|
-
`)
|
|
18162
|
+
this.openTask(task, type, key);
|
|
18163
|
+
} catch (error) {
|
|
18164
|
+
if (previous) {
|
|
18165
|
+
const restored = this.writeWorkspace(previous);
|
|
18166
|
+
if (!restored.ok)
|
|
18167
|
+
return failure2("external_outcome_unknown", `task ${details.operation} failed and workspace restoration is uncertain`, { ...details, outcome: "unknown" });
|
|
18168
|
+
}
|
|
18169
|
+
return failure2("external_outcome_unknown", `workspace written but the task log is uncertain: ${String(error)}`, { ...details, outcome: "unknown" });
|
|
18170
|
+
}
|
|
18171
|
+
return success(task.revision, workspace.revision, task);
|
|
17236
18172
|
}
|
|
17237
|
-
|
|
17238
|
-
|
|
18173
|
+
writeWorkspace(record) {
|
|
18174
|
+
const file = this.paths().workspace;
|
|
17239
18175
|
try {
|
|
17240
|
-
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
17244
|
-
else if (typeof previous === "string")
|
|
17245
|
-
this.saveRecovery(file, previous);
|
|
17246
|
-
temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
17247
|
-
const fd = fs5.openSync(temporary, "wx", 384);
|
|
17248
|
-
try {
|
|
17249
|
-
fs5.writeSync(fd, jsonBytes(value));
|
|
17250
|
-
fs5.fsyncSync(fd);
|
|
17251
|
-
} finally {
|
|
17252
|
-
fs5.closeSync(fd);
|
|
17253
|
-
}
|
|
17254
|
-
retryTransient(() => fs5.renameSync(temporary, file));
|
|
17255
|
-
temporary = undefined;
|
|
17256
|
-
this.fsyncDirectory(path4.dirname(file));
|
|
17257
|
-
if (path4.dirname(file) === this.tasksDir)
|
|
17258
|
-
this.indexTaskWrite(file, value);
|
|
17259
|
-
return success(null, null, value);
|
|
18176
|
+
fs7.mkdirSync(path6.dirname(file), { recursive: true });
|
|
18177
|
+
replaceFile(file, `${canonicalJson(record)}
|
|
18178
|
+
`, true);
|
|
18179
|
+
return success(null, null, null);
|
|
17260
18180
|
} catch (error) {
|
|
17261
|
-
return failure2("storage_error", `
|
|
17262
|
-
path: file
|
|
17263
|
-
});
|
|
17264
|
-
} finally {
|
|
17265
|
-
if (temporary)
|
|
17266
|
-
try {
|
|
17267
|
-
fs5.unlinkSync(temporary);
|
|
17268
|
-
} catch {}
|
|
18181
|
+
return failure2("storage_error", `workspace write failed: ${String(error)}`, { path: file });
|
|
17269
18182
|
}
|
|
17270
18183
|
}
|
|
17271
|
-
|
|
18184
|
+
markUncertain(workspace) {
|
|
18185
|
+
if (!workspace.writer || workspace.writer.state === "uncertain")
|
|
18186
|
+
return;
|
|
18187
|
+
this.writeWorkspace({
|
|
18188
|
+
...workspace,
|
|
18189
|
+
writer: { ...workspace.writer, state: "uncertain" },
|
|
18190
|
+
revision: newRevision(),
|
|
18191
|
+
runtime: stampNew(workspace)
|
|
18192
|
+
});
|
|
18193
|
+
}
|
|
18194
|
+
readIndex() {
|
|
17272
18195
|
try {
|
|
17273
|
-
const value = JSON.parse(
|
|
17274
|
-
if (!
|
|
18196
|
+
const value = JSON.parse(fs7.readFileSync(this.paths().index, "utf8"));
|
|
18197
|
+
if (!isObject4(value) || value.version !== INDEX_VERSION || !isObject4(value.tasks))
|
|
17275
18198
|
return {};
|
|
17276
18199
|
const tasks = {};
|
|
17277
18200
|
for (const [id, entry] of Object.entries(value.tasks))
|
|
17278
|
-
if (
|
|
18201
|
+
if (isObject4(entry) && entry.id === id && typeof entry.file === "string" && typeof entry.status === "string" && typeof entry.updatedAt === "string" && typeof entry.workspaceId === "string" && Array.isArray(entry.sessions) && isObject4(entry.progress) && isObject4(entry.source))
|
|
17279
18202
|
tasks[id] = entry;
|
|
17280
18203
|
return tasks;
|
|
17281
18204
|
} catch {
|
|
17282
18205
|
return {};
|
|
17283
18206
|
}
|
|
17284
18207
|
}
|
|
17285
|
-
writeIndex(
|
|
17286
|
-
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
18208
|
+
writeIndex(entries) {
|
|
17287
18209
|
try {
|
|
17288
|
-
|
|
18210
|
+
fs7.mkdirSync(this.paths().tasks, { recursive: true });
|
|
18211
|
+
replaceFile(this.paths().index, JSON.stringify({
|
|
17289
18212
|
version: INDEX_VERSION,
|
|
17290
|
-
workspaceId,
|
|
17291
18213
|
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
17292
|
-
}),
|
|
17293
|
-
|
|
18214
|
+
}), false);
|
|
18215
|
+
} catch {}
|
|
18216
|
+
}
|
|
18217
|
+
indexTask(task, key, legacy) {
|
|
18218
|
+
try {
|
|
18219
|
+
const signature = fileSignature(this.eventsPath(task.id));
|
|
18220
|
+
if (signature === null)
|
|
18221
|
+
return;
|
|
18222
|
+
const tasks = this.readIndex();
|
|
18223
|
+
tasks[task.id] = indexEntry(task, key, legacy, signature);
|
|
18224
|
+
this.writeIndex(Object.values(tasks));
|
|
18225
|
+
} catch {}
|
|
18226
|
+
}
|
|
18227
|
+
parseTask(record, taskId) {
|
|
18228
|
+
const parsed = this.parseValue(record, taskRecordSchema, "task");
|
|
18229
|
+
if (!parsed.ok)
|
|
18230
|
+
return { ...parsed, details: { ...parsed.details, taskId } };
|
|
18231
|
+
if (parsed.data.id !== taskId)
|
|
18232
|
+
return failure2("recovery_required", "task log and record ID differ", { taskId });
|
|
18233
|
+
return parsed;
|
|
18234
|
+
}
|
|
18235
|
+
parseRecord(bytes, schema, label) {
|
|
18236
|
+
let value;
|
|
18237
|
+
try {
|
|
18238
|
+
value = JSON.parse(bytes);
|
|
18239
|
+
} catch {
|
|
18240
|
+
return failure2("recovery_required", `${label} record JSON is corrupt`);
|
|
18241
|
+
}
|
|
18242
|
+
return this.parseValue(value, schema, label);
|
|
18243
|
+
}
|
|
18244
|
+
parseValue(value, schema, label) {
|
|
18245
|
+
if (isObject4(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
18246
|
+
return failure2("unsupported_version", `unsupported ${label} schema version`);
|
|
18247
|
+
const parsed = parseStoredRecord(schema, value);
|
|
18248
|
+
if (parsed.success)
|
|
18249
|
+
return success(null, null, parsed.data);
|
|
18250
|
+
const writerVersion = isObject4(value) && isObject4(value.runtime) ? value.runtime.updatedWith : null;
|
|
18251
|
+
if (parsed.critical.length > 0)
|
|
18252
|
+
return failure2("recovery_required", `${label} record requires fields this Workit cannot read (${parsed.critical.join(", ")}); upgrade Workit before mutating this checkout`);
|
|
18253
|
+
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
18254
|
+
return failure2("recovery_required", `${label} record was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
18255
|
+
return failure2("recovery_required", `${label} record does not satisfy its schema`);
|
|
18256
|
+
}
|
|
18257
|
+
conflict(expected, actual) {
|
|
18258
|
+
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", { expectedRevision: expected, actualRevision: actual });
|
|
18259
|
+
}
|
|
18260
|
+
workspaceConflict(expected, actual) {
|
|
18261
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
18262
|
+
}
|
|
18263
|
+
v2Dirs() {
|
|
18264
|
+
return [...new Set([this.root, this.origin])].map((dir) => path6.join(dir, ".workit"));
|
|
18265
|
+
}
|
|
18266
|
+
pendingMigrations() {
|
|
18267
|
+
return this.v2Dirs().filter((dir) => {
|
|
18268
|
+
const raw = this.readV2Workspace(path6.join(dir, "workspace.json"));
|
|
18269
|
+
return raw !== null && raw !== TOMBSTONE;
|
|
18270
|
+
});
|
|
18271
|
+
}
|
|
18272
|
+
migrateV2(v2) {
|
|
18273
|
+
const dirs = this.paths();
|
|
18274
|
+
const v2Workspace = path6.join(v2, "workspace.json");
|
|
18275
|
+
const v2Tasks = path6.join(v2, "tasks");
|
|
18276
|
+
const legacy = path6.join(dirs.dir, "legacy", checkoutSlug(path6.dirname(v2)), "v2");
|
|
18277
|
+
const raw = this.readV2Workspace(v2Workspace);
|
|
18278
|
+
if (raw === null || raw === TOMBSTONE)
|
|
18279
|
+
return success(null, null, null);
|
|
18280
|
+
return this.withLock(dirs.checkoutLock, () => this.withLock(path6.join(v2, "metadata.lock"), () => {
|
|
18281
|
+
const workspace = this.readV2Workspace(v2Workspace);
|
|
18282
|
+
if (workspace === null || workspace === TOMBSTONE)
|
|
18283
|
+
return success(null, null, null);
|
|
18284
|
+
const parsedWorkspace = this.parseRecord(workspace.bytes, workspaceRecordSchema, "2.x workspace");
|
|
18285
|
+
if (!parsedWorkspace.ok)
|
|
18286
|
+
return failure2("recovery_required", `cannot migrate the 2.x store at ${v2}: ${parsedWorkspace.error}; move it aside to start fresh`, { path: v2Workspace });
|
|
18287
|
+
let files = [];
|
|
18288
|
+
try {
|
|
18289
|
+
files = fs7.readdirSync(v2Tasks).filter((name) => name.endsWith(".json") && validId(name.slice(0, -5))).filter((name) => !this.isStub(path6.join(v2Tasks, name)));
|
|
18290
|
+
} catch {}
|
|
18291
|
+
const current = this.readWorkspace();
|
|
18292
|
+
if (!current.ok)
|
|
18293
|
+
return current;
|
|
18294
|
+
let workspaceMoved = false;
|
|
18295
|
+
let target = current.data;
|
|
18296
|
+
if (!target) {
|
|
18297
|
+
target = { ...parsedWorkspace.data, root: this.root };
|
|
18298
|
+
const written = this.writeWorkspace(target);
|
|
18299
|
+
if (!written.ok)
|
|
18300
|
+
return written;
|
|
18301
|
+
workspaceMoved = true;
|
|
18302
|
+
}
|
|
18303
|
+
let migrated = 0;
|
|
18304
|
+
let skipped = 0;
|
|
18305
|
+
const done = new Map;
|
|
18306
|
+
try {
|
|
18307
|
+
fs7.mkdirSync(path6.join(legacy, "tasks"), { recursive: true });
|
|
18308
|
+
for (const name of ["workspace.json", "index.json"])
|
|
18309
|
+
if (fs7.existsSync(path6.join(v2, name)))
|
|
18310
|
+
fs7.copyFileSync(path6.join(v2, name), path6.join(legacy, name));
|
|
18311
|
+
for (const name of files) {
|
|
18312
|
+
const id = name.slice(0, -5);
|
|
18313
|
+
const file = path6.join(v2Tasks, name);
|
|
18314
|
+
const bytes = fs7.readFileSync(file, "utf8");
|
|
18315
|
+
const digest = sha256(bytes);
|
|
18316
|
+
fs7.writeFileSync(path6.join(legacy, "tasks", name), bytes);
|
|
18317
|
+
const parsed = this.parseRecord(bytes, taskRecordSchema, "2.x task");
|
|
18318
|
+
if (!parsed.ok || parsed.data.id !== id)
|
|
18319
|
+
return failure2("recovery_required", `cannot migrate 2.x task ${id}: ${parsed.ok ? "file name and record ID differ" : parsed.error}; move ${file} aside to continue`, { path: file });
|
|
18320
|
+
const record = { ...parsed.data, workspaceId: target.id };
|
|
18321
|
+
const origin = { path: path6.join(legacy, "tasks", name), digest };
|
|
18322
|
+
if (!fs7.existsSync(this.eventsPath(id))) {
|
|
18323
|
+
this.openTask(record, "migrated.from_v2", null, origin);
|
|
18324
|
+
migrated += 1;
|
|
18325
|
+
} else {
|
|
18326
|
+
const resynced = this.withLock(this.taskLockPath(id), () => {
|
|
18327
|
+
const loaded = this.load(id);
|
|
18328
|
+
if (!loaded.ok)
|
|
18329
|
+
return loaded;
|
|
18330
|
+
if (!loaded.data)
|
|
18331
|
+
return failure2("not_found", "task not found", { taskId: id });
|
|
18332
|
+
if (loaded.data.legacy?.digest === digest)
|
|
18333
|
+
return success(null, null, false);
|
|
18334
|
+
const appended = this.appendPatch(loaded.data, record, {
|
|
18335
|
+
type: "migrated.from_v2",
|
|
18336
|
+
legacy: origin
|
|
18337
|
+
});
|
|
18338
|
+
return appended.ok ? success(null, null, true) : appended;
|
|
18339
|
+
});
|
|
18340
|
+
if (!resynced.ok)
|
|
18341
|
+
return resynced;
|
|
18342
|
+
if (resynced.data)
|
|
18343
|
+
migrated += 1;
|
|
18344
|
+
else
|
|
18345
|
+
skipped += 1;
|
|
18346
|
+
}
|
|
18347
|
+
done.set(name, digest);
|
|
18348
|
+
}
|
|
18349
|
+
} catch (error) {
|
|
18350
|
+
return failure2("storage_error", `cannot migrate the 2.x store at ${v2}: ${String(error)}`, {
|
|
18351
|
+
path: v2
|
|
18352
|
+
});
|
|
18353
|
+
}
|
|
18354
|
+
try {
|
|
18355
|
+
let complete = true;
|
|
18356
|
+
for (const [name, digest] of done) {
|
|
18357
|
+
const file = path6.join(v2Tasks, name);
|
|
18358
|
+
if (sha256(fs7.readFileSync(file, "utf8")) !== digest) {
|
|
18359
|
+
complete = false;
|
|
18360
|
+
continue;
|
|
18361
|
+
}
|
|
18362
|
+
replaceFile(file, this.stubFor(name.slice(0, -5)), false);
|
|
18363
|
+
}
|
|
18364
|
+
if (!complete)
|
|
18365
|
+
return failure2("busy", `the 2.x store at ${v2} changed during migration; retry`, {
|
|
18366
|
+
path: v2
|
|
18367
|
+
});
|
|
18368
|
+
fs7.rmSync(path6.join(v2, "index.json"), { force: true });
|
|
18369
|
+
this.writeTombstone(v2, target.id);
|
|
18370
|
+
} catch (error) {
|
|
18371
|
+
return failure2("storage_error", `cannot finish the 2.x migration: ${String(error)}`, {
|
|
18372
|
+
path: v2
|
|
18373
|
+
});
|
|
18374
|
+
}
|
|
18375
|
+
reportMigration({
|
|
18376
|
+
from: v2,
|
|
18377
|
+
to: dirs.dir,
|
|
18378
|
+
backup: legacy,
|
|
18379
|
+
tasks: migrated,
|
|
18380
|
+
skipped,
|
|
18381
|
+
workspace: workspaceMoved
|
|
18382
|
+
});
|
|
18383
|
+
return success(null, null, null);
|
|
18384
|
+
}));
|
|
18385
|
+
}
|
|
18386
|
+
adoptLocalStore() {
|
|
18387
|
+
const dirs = this.paths();
|
|
18388
|
+
const marker = path6.join(dirs.v2, "store.json");
|
|
18389
|
+
if (!dirs.shared || !fs7.existsSync(marker))
|
|
18390
|
+
return success(null, null, null);
|
|
18391
|
+
return this.withLock(dirs.checkoutLock, () => {
|
|
18392
|
+
if (!fs7.existsSync(marker))
|
|
18393
|
+
return success(null, null, null);
|
|
18394
|
+
let tasks = 0;
|
|
18395
|
+
try {
|
|
18396
|
+
const merge = (from, to) => {
|
|
18397
|
+
if (!fs7.existsSync(to)) {
|
|
18398
|
+
fs7.mkdirSync(path6.dirname(to), { recursive: true });
|
|
18399
|
+
try {
|
|
18400
|
+
fs7.renameSync(from, to);
|
|
18401
|
+
} catch {
|
|
18402
|
+
fs7.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
18403
|
+
fs7.rmSync(from, { recursive: true, force: true });
|
|
18404
|
+
}
|
|
18405
|
+
return;
|
|
18406
|
+
}
|
|
18407
|
+
if (fs7.lstatSync(from).isDirectory() && fs7.lstatSync(to).isDirectory())
|
|
18408
|
+
for (const name of fs7.readdirSync(from))
|
|
18409
|
+
merge(path6.join(from, name), path6.join(to, name));
|
|
18410
|
+
};
|
|
18411
|
+
for (const sub of ["tasks", "checkouts", "blobs", "legacy"]) {
|
|
18412
|
+
const from = path6.join(dirs.v2, sub);
|
|
18413
|
+
if (!fs7.existsSync(from))
|
|
18414
|
+
continue;
|
|
18415
|
+
if (sub === "tasks") {
|
|
18416
|
+
for (const name of fs7.readdirSync(from))
|
|
18417
|
+
if (validId(name) && !fs7.existsSync(path6.join(dirs.dir, sub, name)))
|
|
18418
|
+
tasks += 1;
|
|
18419
|
+
}
|
|
18420
|
+
merge(from, path6.join(dirs.dir, sub));
|
|
18421
|
+
}
|
|
18422
|
+
const ledger = path6.join(dirs.v2, "ledger");
|
|
18423
|
+
if (fs7.existsSync(ledger) && !fs7.existsSync(path6.join(dirs.dir, "ledger")))
|
|
18424
|
+
merge(ledger, path6.join(dirs.dir, "ledger"));
|
|
18425
|
+
fs7.rmSync(path6.join(dirs.v2, "task-index.json"), { force: true });
|
|
18426
|
+
fs7.renameSync(marker, path6.join(dirs.v2, "store.moved.json"));
|
|
18427
|
+
} catch (error) {
|
|
18428
|
+
return failure2("storage_error", `cannot move ${dirs.v2} into ${dirs.dir}: ${String(error)}`, {
|
|
18429
|
+
path: dirs.v2
|
|
18430
|
+
});
|
|
18431
|
+
}
|
|
18432
|
+
reportMigration({
|
|
18433
|
+
from: dirs.v2,
|
|
18434
|
+
to: dirs.dir,
|
|
18435
|
+
backup: dirs.v2,
|
|
18436
|
+
tasks,
|
|
18437
|
+
skipped: 0,
|
|
18438
|
+
workspace: false
|
|
18439
|
+
});
|
|
18440
|
+
return success(null, null, null);
|
|
18441
|
+
});
|
|
18442
|
+
}
|
|
18443
|
+
readV2Workspace(file) {
|
|
18444
|
+
let bytes;
|
|
18445
|
+
try {
|
|
18446
|
+
bytes = fs7.readFileSync(file, "utf8");
|
|
17294
18447
|
} catch {
|
|
17295
|
-
|
|
17296
|
-
fs5.unlinkSync(temporary);
|
|
17297
|
-
} catch {}
|
|
18448
|
+
return null;
|
|
17298
18449
|
}
|
|
17299
|
-
}
|
|
17300
|
-
indexTaskWrite(file, task) {
|
|
17301
18450
|
try {
|
|
17302
|
-
const
|
|
17303
|
-
if (
|
|
17304
|
-
return;
|
|
17305
|
-
const tasks = this.readIndex(task.workspaceId);
|
|
17306
|
-
tasks[task.id] = indexEntry(task, signature);
|
|
17307
|
-
this.writeIndex(task.workspaceId, Object.values(tasks));
|
|
18451
|
+
const value = JSON.parse(bytes);
|
|
18452
|
+
if (isObject4(value) && isObject4(value.store) && value.store.format === "workit-store")
|
|
18453
|
+
return TOMBSTONE;
|
|
17308
18454
|
} catch {}
|
|
18455
|
+
return { bytes };
|
|
18456
|
+
}
|
|
18457
|
+
writeTombstone(v2, workspaceId) {
|
|
18458
|
+
const dirs = this.paths();
|
|
18459
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
18460
|
+
const marker = {
|
|
18461
|
+
schemaVersion: SCHEMA_VERSION,
|
|
18462
|
+
id: workspaceId ?? newId(),
|
|
18463
|
+
revision: newRevision(),
|
|
18464
|
+
root: path6.dirname(v2),
|
|
18465
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
18466
|
+
writer: null,
|
|
18467
|
+
store: {
|
|
18468
|
+
format: "workit-store",
|
|
18469
|
+
version: STORE_FORMAT,
|
|
18470
|
+
path: dirs.dir,
|
|
18471
|
+
note: `workit ${STORE_MIN_RUNTIME}+ keeps task state in ${dirs.dir}; upgrade Workit`
|
|
18472
|
+
},
|
|
18473
|
+
critical: ["store"]
|
|
18474
|
+
};
|
|
18475
|
+
fs7.mkdirSync(v2, { recursive: true });
|
|
18476
|
+
const ignore = path6.join(v2, ".gitignore");
|
|
18477
|
+
if (!fs7.existsSync(ignore))
|
|
18478
|
+
fs7.writeFileSync(ignore, `*
|
|
18479
|
+
`);
|
|
18480
|
+
replaceFile(path6.join(v2, "workspace.json"), `${JSON.stringify(marker, null, 2)}
|
|
18481
|
+
`, true);
|
|
18482
|
+
}
|
|
18483
|
+
stubFor(taskId) {
|
|
18484
|
+
const version = isNewerVersion(runtimeVersion(), STORE_MIN_RUNTIME) ? runtimeVersion() : STORE_MIN_RUNTIME;
|
|
18485
|
+
return `${JSON.stringify({
|
|
18486
|
+
schemaVersion: SCHEMA_VERSION,
|
|
18487
|
+
id: taskId,
|
|
18488
|
+
runtime: { createdWith: version, updatedWith: version },
|
|
18489
|
+
store: { format: "workit-store", version: STORE_FORMAT, path: this.paths().dir },
|
|
18490
|
+
critical: ["store"]
|
|
18491
|
+
})}
|
|
18492
|
+
`;
|
|
17309
18493
|
}
|
|
17310
|
-
|
|
17311
|
-
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
17312
|
-
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
17313
|
-
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
17314
|
-
if (fs5.existsSync(destination)) {
|
|
17315
|
-
if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
|
|
17316
|
-
throw new Error("recovery copy already exists with different bytes");
|
|
17317
|
-
const stamp = new Date;
|
|
17318
|
-
retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
|
|
17319
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
17320
|
-
return;
|
|
17321
|
-
}
|
|
17322
|
-
let temporary;
|
|
18494
|
+
isStub(file) {
|
|
17323
18495
|
try {
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
fs5.fsyncSync(fd);
|
|
17329
|
-
} finally {
|
|
17330
|
-
fs5.closeSync(fd);
|
|
17331
|
-
}
|
|
17332
|
-
retryTransient(() => fs5.renameSync(temporary, destination));
|
|
17333
|
-
temporary = undefined;
|
|
17334
|
-
this.fsyncDirectory(this.recoveryDir);
|
|
17335
|
-
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
17336
|
-
} finally {
|
|
17337
|
-
if (temporary)
|
|
17338
|
-
try {
|
|
17339
|
-
fs5.unlinkSync(temporary);
|
|
17340
|
-
} catch {}
|
|
18496
|
+
const value = JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
18497
|
+
return isObject4(value) && isObject4(value.store) && value.store.format === "workit-store";
|
|
18498
|
+
} catch {
|
|
18499
|
+
return false;
|
|
17341
18500
|
}
|
|
17342
18501
|
}
|
|
17343
|
-
|
|
18502
|
+
writeTombstoneIfMissing() {
|
|
17344
18503
|
try {
|
|
17345
|
-
const
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
18504
|
+
const v2 = path6.join(this.root, ".workit");
|
|
18505
|
+
if (fs7.existsSync(path6.join(v2, "workspace.json")))
|
|
18506
|
+
return;
|
|
18507
|
+
let id = null;
|
|
18508
|
+
try {
|
|
18509
|
+
id = JSON.parse(fs7.readFileSync(this.paths().workspace, "utf8")).id ?? null;
|
|
18510
|
+
} catch {}
|
|
18511
|
+
this.writeTombstone(v2, id);
|
|
17349
18512
|
} catch {}
|
|
17350
18513
|
}
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
const file = path4.join(this.recoveryDir, name);
|
|
18514
|
+
initializeStorage() {
|
|
18515
|
+
const dirs = this.paths();
|
|
18516
|
+
retryTransient(() => fs7.mkdirSync(dirs.checkout, { recursive: true }));
|
|
18517
|
+
retryTransient(() => fs7.mkdirSync(dirs.tasks, { recursive: true }));
|
|
18518
|
+
if (!fs7.existsSync(dirs.marker))
|
|
18519
|
+
replaceFile(dirs.marker, `${JSON.stringify({ format: "workit-store", version: STORE_FORMAT, createdWith: runtimeVersion() })}
|
|
18520
|
+
`, false);
|
|
18521
|
+
if (!dirs.shared) {
|
|
18522
|
+
const ignore = path6.join(dirs.dir, ".gitignore");
|
|
18523
|
+
let current = null;
|
|
17362
18524
|
try {
|
|
17363
|
-
|
|
17364
|
-
if (!stat.isFile())
|
|
17365
|
-
continue;
|
|
17366
|
-
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
18525
|
+
current = fs7.readFileSync(ignore, "utf8");
|
|
17367
18526
|
} catch {}
|
|
18527
|
+
if (current !== `*
|
|
18528
|
+
`)
|
|
18529
|
+
retryTransient(() => fs7.writeFileSync(ignore, `*
|
|
18530
|
+
`));
|
|
17368
18531
|
}
|
|
17369
|
-
|
|
18532
|
+
this.writeTombstoneIfMissing();
|
|
17370
18533
|
}
|
|
17371
|
-
|
|
17372
|
-
const
|
|
17373
|
-
return
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
18534
|
+
lockOptions(lockPath) {
|
|
18535
|
+
const inProcess = heldInProcess.has(lockPath);
|
|
18536
|
+
return {
|
|
18537
|
+
lockPath,
|
|
18538
|
+
staleMs: Number.MAX_SAFE_INTEGER,
|
|
18539
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
18540
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
18541
|
+
staleRecovery: "remove-if-unchanged",
|
|
18542
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
18543
|
+
let ageMs = null;
|
|
18544
|
+
try {
|
|
18545
|
+
ageMs = nowMs - fs7.lstatSync(lockPath).mtimeMs;
|
|
18546
|
+
} catch {}
|
|
18547
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
18548
|
+
},
|
|
18549
|
+
shouldRemoveStaleLock: () => true,
|
|
18550
|
+
parsePayload: parseMetadataLockOrNull,
|
|
18551
|
+
payload: () => ({
|
|
18552
|
+
pid: process.pid,
|
|
18553
|
+
processStart: processStartOf(process.pid),
|
|
18554
|
+
host: localLockHost(),
|
|
18555
|
+
nonce: randomUUID3()
|
|
18556
|
+
})
|
|
17382
18557
|
};
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
18558
|
+
}
|
|
18559
|
+
acquire(lockPath, options) {
|
|
18560
|
+
clearAbandonedReclaimGuard(lockPath);
|
|
18561
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
18562
|
+
while (true) {
|
|
17386
18563
|
try {
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
for (const copies of groups.values()) {
|
|
17391
|
-
const surplus = this.surplusCopies(copies);
|
|
17392
|
-
report.recovery.kept += copies.length - surplus.length;
|
|
17393
|
-
for (const copy of surplus) {
|
|
17394
|
-
report.recovery.removed += 1;
|
|
17395
|
-
try {
|
|
17396
|
-
report.recovery.removedBytes += fs5.statSync(copy.path).size;
|
|
17397
|
-
} catch {}
|
|
17398
|
-
if (!dryRun)
|
|
17399
|
-
fs5.rmSync(copy.path, { force: true });
|
|
17400
|
-
}
|
|
17401
|
-
}
|
|
17402
|
-
const nowMs = Date.now();
|
|
17403
|
-
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
17404
|
-
if (!fs5.existsSync(directory))
|
|
17405
|
-
continue;
|
|
17406
|
-
for (const name of fs5.readdirSync(directory)) {
|
|
17407
|
-
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
17408
|
-
continue;
|
|
17409
|
-
const file = path4.join(directory, name);
|
|
17410
|
-
const stat = fs5.lstatSync(file);
|
|
17411
|
-
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
17412
|
-
continue;
|
|
17413
|
-
report.temporary.removed += 1;
|
|
17414
|
-
if (!dryRun)
|
|
17415
|
-
fs5.rmSync(file, { force: true });
|
|
17416
|
-
}
|
|
17417
|
-
}
|
|
17418
|
-
return success(null, null, null);
|
|
17419
|
-
} catch (error) {
|
|
17420
|
-
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
17421
|
-
path: this.recoveryDir
|
|
18564
|
+
return acquireFileLockSync(lockPath, {
|
|
18565
|
+
...options,
|
|
18566
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
17422
18567
|
});
|
|
18568
|
+
} catch (error) {
|
|
18569
|
+
const code = error?.code;
|
|
18570
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
18571
|
+
throw error;
|
|
18572
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
18573
|
+
throw error;
|
|
18574
|
+
if (code !== "file_lock_stale")
|
|
18575
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
17423
18576
|
}
|
|
17424
|
-
};
|
|
17425
|
-
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
17426
|
-
if (!pruned.ok)
|
|
17427
|
-
return pruned;
|
|
17428
|
-
const tasks = this.listTasks();
|
|
17429
|
-
if (!tasks.ok)
|
|
17430
|
-
return tasks;
|
|
17431
|
-
for (const task of tasks.data) {
|
|
17432
|
-
const deduped = dedupeCandidates(task.candidates);
|
|
17433
|
-
const removed = task.candidates.length - deduped.length;
|
|
17434
|
-
if (removed === 0)
|
|
17435
|
-
continue;
|
|
17436
|
-
if (task.status === "active") {
|
|
17437
|
-
report.candidates.skippedActive.push(task.id);
|
|
17438
|
-
continue;
|
|
17439
|
-
}
|
|
17440
|
-
if (task.status === "closed") {
|
|
17441
|
-
report.candidates.skippedClosed.push(task.id);
|
|
17442
|
-
continue;
|
|
17443
|
-
}
|
|
17444
|
-
if (!dryRun) {
|
|
17445
|
-
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
17446
|
-
...current,
|
|
17447
|
-
candidates: dedupeCandidates(current.candidates)
|
|
17448
|
-
}));
|
|
17449
|
-
if (!written.ok) {
|
|
17450
|
-
report.candidates.failed.push(task.id);
|
|
17451
|
-
continue;
|
|
17452
|
-
}
|
|
17453
|
-
}
|
|
17454
|
-
report.candidates.removed += removed;
|
|
17455
|
-
report.candidates.tasks.push(task.id);
|
|
17456
18577
|
}
|
|
17457
|
-
return success(null, null, report);
|
|
17458
18578
|
}
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
return
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
return null;
|
|
17468
|
-
for (const candidate of candidates.data) {
|
|
17469
|
-
if (candidate.target !== target || candidate.digest !== digest)
|
|
17470
|
-
continue;
|
|
17471
|
-
try {
|
|
17472
|
-
if (!fs5.lstatSync(candidate.path).isFile())
|
|
17473
|
-
continue;
|
|
17474
|
-
const bytes = fs5.readFileSync(candidate.path);
|
|
17475
|
-
if (digestBytes(bytes) !== digest)
|
|
17476
|
-
continue;
|
|
17477
|
-
if (target === "workspace") {
|
|
17478
|
-
const parsed = this.parseBytes(bytes, workspaceRecordSchema);
|
|
17479
|
-
if (parsed.ok && parsed.data.id === workspaceId && parsed.data.root === this.root)
|
|
17480
|
-
return bytes;
|
|
17481
|
-
} else {
|
|
17482
|
-
const parsed = this.parseBytes(bytes, taskRecordSchema);
|
|
17483
|
-
if (parsed.ok && parsed.data.id === taskId && parsed.data.workspaceId === workspace?.id)
|
|
17484
|
-
return bytes;
|
|
17485
|
-
}
|
|
17486
|
-
} catch {}
|
|
18579
|
+
withLock(lockPath, operation) {
|
|
18580
|
+
try {
|
|
18581
|
+
this.initializeStorage();
|
|
18582
|
+
fs7.mkdirSync(path6.dirname(lockPath), { recursive: true });
|
|
18583
|
+
} catch (error) {
|
|
18584
|
+
return failure2("storage_error", `unable to initialize store: ${String(error)}`, {
|
|
18585
|
+
path: this.paths().dir
|
|
18586
|
+
});
|
|
17487
18587
|
}
|
|
17488
|
-
|
|
17489
|
-
}
|
|
17490
|
-
fsyncDirectory(directory) {
|
|
18588
|
+
let handle;
|
|
17491
18589
|
try {
|
|
17492
|
-
|
|
17493
|
-
try {
|
|
17494
|
-
fs5.fsyncSync(fd);
|
|
17495
|
-
} finally {
|
|
17496
|
-
fs5.closeSync(fd);
|
|
17497
|
-
}
|
|
18590
|
+
handle = this.acquire(lockPath, this.lockOptions(lockPath));
|
|
17498
18591
|
} catch (error) {
|
|
17499
|
-
|
|
17500
|
-
throw error;
|
|
18592
|
+
return this.lockFailure(lockPath, error);
|
|
17501
18593
|
}
|
|
17502
|
-
|
|
17503
|
-
|
|
17504
|
-
if (!fs5.existsSync(this.tasksDir))
|
|
17505
|
-
return null;
|
|
17506
|
-
const ids = new Set;
|
|
18594
|
+
hold(lockPath);
|
|
18595
|
+
let result;
|
|
17507
18596
|
try {
|
|
17508
|
-
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
18597
|
+
if (!handle.verifyStillHeld())
|
|
18598
|
+
result = failure2("recovery_required", "metadata lock was compromised", { path: lockPath });
|
|
18599
|
+
else {
|
|
18600
|
+
try {
|
|
18601
|
+
result = operation();
|
|
18602
|
+
} catch (error) {
|
|
18603
|
+
result = failure2("storage_error", `mutation failed: ${String(error)}`);
|
|
18604
|
+
}
|
|
17515
18605
|
}
|
|
17516
|
-
} catch {
|
|
17517
|
-
|
|
18606
|
+
} catch (error) {
|
|
18607
|
+
result = this.lockFailure(lockPath, error);
|
|
18608
|
+
}
|
|
18609
|
+
drop(lockPath);
|
|
18610
|
+
try {
|
|
18611
|
+
retryTransient(() => handle.release());
|
|
18612
|
+
} catch (error) {
|
|
18613
|
+
const released = this.lockFailure(lockPath, error);
|
|
18614
|
+
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
18615
|
+
result = result.ok ? released : failure2("recovery_required", `${result.error}; ${releaseError}`, result.details);
|
|
17518
18616
|
}
|
|
17519
|
-
return
|
|
18617
|
+
return result;
|
|
17520
18618
|
}
|
|
17521
|
-
|
|
18619
|
+
lockDirWritable(lockPath) {
|
|
18620
|
+
const probe = `${lockPath}.${process.pid}.${randomUUID3()}.probe`;
|
|
17522
18621
|
try {
|
|
17523
|
-
|
|
18622
|
+
fs7.closeSync(fs7.openSync(probe, "wx", 384));
|
|
17524
18623
|
} catch {
|
|
17525
|
-
return
|
|
18624
|
+
return false;
|
|
17526
18625
|
}
|
|
18626
|
+
try {
|
|
18627
|
+
fs7.rmSync(probe, { force: true });
|
|
18628
|
+
} catch {}
|
|
18629
|
+
return true;
|
|
17527
18630
|
}
|
|
17528
|
-
|
|
18631
|
+
lockHolderPresent(lockPath) {
|
|
17529
18632
|
try {
|
|
17530
|
-
|
|
17531
|
-
return
|
|
18633
|
+
fs7.lstatSync(lockPath);
|
|
18634
|
+
return true;
|
|
17532
18635
|
} catch (error) {
|
|
17533
|
-
|
|
17534
|
-
return { exists: false, result: success(null, null, null) };
|
|
17535
|
-
return {
|
|
17536
|
-
exists: true,
|
|
17537
|
-
result: failure2("recovery_required", `snapshot cannot be read: ${String(error)}`, {
|
|
17538
|
-
path: file
|
|
17539
|
-
})
|
|
17540
|
-
};
|
|
18636
|
+
return error?.code !== "ENOENT";
|
|
17541
18637
|
}
|
|
17542
18638
|
}
|
|
17543
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
18639
|
+
lockFailure(lockPath, error) {
|
|
18640
|
+
const value = error;
|
|
18641
|
+
const code = typeof value?.code === "string" ? value.code : "";
|
|
18642
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent(lockPath) && !this.lockDirWritable(lockPath))
|
|
18643
|
+
return failure2("storage_error", `cannot create the workit lock (${code}); no other Workit call holds it`, {
|
|
18644
|
+
path: lockPath,
|
|
18645
|
+
guidance: `Check permissions and the read-only attribute on ${path6.dirname(lockPath)}.`
|
|
18646
|
+
});
|
|
18647
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
18648
|
+
let lock = null;
|
|
18649
|
+
try {
|
|
18650
|
+
lock = parseMetadataLockOrNull(fs7.readFileSync(lockPath, "utf8"));
|
|
18651
|
+
} catch {}
|
|
18652
|
+
if (lock?.externalAction)
|
|
18653
|
+
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
18654
|
+
outcome: "not_started",
|
|
18655
|
+
path: lockPath
|
|
18656
|
+
});
|
|
18657
|
+
return failure2("busy", `workit lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
18658
|
+
outcome: "not_started",
|
|
18659
|
+
path: lockPath,
|
|
18660
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
18661
|
+
});
|
|
17549
18662
|
}
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
const parsed = parseStoredRecord(schema, value);
|
|
17553
|
-
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
17554
|
-
return success(null, null, parsed.data);
|
|
17555
|
-
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
17556
|
-
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
17557
|
-
if (parsed.success)
|
|
17558
|
-
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
17559
|
-
if (parsed.critical.length > 0)
|
|
17560
|
-
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
17561
|
-
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
17562
|
-
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
17563
|
-
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
17564
|
-
}
|
|
17565
|
-
conflict(expected, actual) {
|
|
17566
|
-
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
17567
|
-
expectedRevision: expected,
|
|
17568
|
-
actualRevision: actual
|
|
17569
|
-
});
|
|
17570
|
-
}
|
|
17571
|
-
workspaceConflict(expected, actual) {
|
|
17572
|
-
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
17573
|
-
}
|
|
17574
|
-
taskPath(taskId) {
|
|
17575
|
-
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
17576
|
-
}
|
|
17577
|
-
get workitDir() {
|
|
17578
|
-
return path4.join(this.root, ".workit");
|
|
17579
|
-
}
|
|
17580
|
-
get tasksDir() {
|
|
17581
|
-
return path4.join(this.workitDir, "tasks");
|
|
17582
|
-
}
|
|
17583
|
-
get recoveryDir() {
|
|
17584
|
-
return path4.join(this.workitDir, "recovery");
|
|
17585
|
-
}
|
|
17586
|
-
get workspacePath() {
|
|
17587
|
-
return path4.join(this.workitDir, "workspace.json");
|
|
17588
|
-
}
|
|
17589
|
-
get indexPath() {
|
|
17590
|
-
return path4.join(this.workitDir, "index.json");
|
|
17591
|
-
}
|
|
17592
|
-
get lockPath() {
|
|
17593
|
-
return path4.join(this.workitDir, "metadata.lock");
|
|
17594
|
-
}
|
|
17595
|
-
get gitignorePath() {
|
|
17596
|
-
return path4.join(this.workitDir, ".gitignore");
|
|
18663
|
+
const recovery = code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
18664
|
+
return failure2(recovery ? "recovery_required" : "storage_error", `lock operation failed: ${String(error)}`, { path: lockPath });
|
|
17597
18665
|
}
|
|
17598
18666
|
}
|
|
17599
18667
|
// packages/workit-core/src/core/workers.ts
|
|
@@ -17657,30 +18725,30 @@ function assertProductWriteAllowed(input) {
|
|
|
17657
18725
|
}
|
|
17658
18726
|
|
|
17659
18727
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
17660
|
-
import { createHash as
|
|
17661
|
-
import * as
|
|
17662
|
-
import { spawnSync as
|
|
17663
|
-
import
|
|
18728
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
18729
|
+
import * as fs12 from "node:fs";
|
|
18730
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
18731
|
+
import path14 from "node:path";
|
|
17664
18732
|
|
|
17665
18733
|
// packages/workit-core/src/core/authority.ts
|
|
17666
|
-
import { createHash as
|
|
17667
|
-
import * as
|
|
17668
|
-
import
|
|
18734
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
18735
|
+
import * as fs9 from "node:fs";
|
|
18736
|
+
import path11 from "node:path";
|
|
17669
18737
|
|
|
17670
18738
|
// packages/workit-core/src/core/auto-approval.ts
|
|
17671
|
-
import { createHash as
|
|
18739
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
17672
18740
|
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
17673
|
-
import
|
|
18741
|
+
import path10 from "node:path";
|
|
17674
18742
|
|
|
17675
18743
|
// packages/workit-core/src/core/workspaces.ts
|
|
17676
|
-
import { createHash as
|
|
18744
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
17677
18745
|
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
17678
|
-
import
|
|
18746
|
+
import path8 from "node:path";
|
|
17679
18747
|
|
|
17680
18748
|
// packages/workit-core/src/core/config.ts
|
|
17681
18749
|
import {
|
|
17682
|
-
copyFileSync,
|
|
17683
|
-
cpSync,
|
|
18750
|
+
copyFileSync as copyFileSync2,
|
|
18751
|
+
cpSync as cpSync2,
|
|
17684
18752
|
existsSync as existsSync3,
|
|
17685
18753
|
mkdirSync as mkdirSync2,
|
|
17686
18754
|
readFileSync as readFileSync2,
|
|
@@ -17688,7 +18756,7 @@ import {
|
|
|
17688
18756
|
writeFileSync as writeFileSync2
|
|
17689
18757
|
} from "node:fs";
|
|
17690
18758
|
import os from "node:os";
|
|
17691
|
-
import
|
|
18759
|
+
import path7 from "node:path";
|
|
17692
18760
|
|
|
17693
18761
|
// packages/workit-core/src/core/boundary.ts
|
|
17694
18762
|
import { statSync as statSync2 } from "node:fs";
|
|
@@ -17747,7 +18815,7 @@ var mergePreset = (preset, input = {}, current = {
|
|
|
17747
18815
|
protected: preset === "custom" ? input.protectedNames ?? current.branchPolicy.protected : [...defs.protected]
|
|
17748
18816
|
};
|
|
17749
18817
|
};
|
|
17750
|
-
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
|
|
18818
|
+
var resolveConfigDir = () => process.env.WORKFLOW_TOOLKIT_CONFIG ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ?? path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workit");
|
|
17751
18819
|
var migratedDir = null;
|
|
17752
18820
|
var migrationFailed = false;
|
|
17753
18821
|
var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
@@ -17757,7 +18825,7 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
17757
18825
|
migratedDir = dir;
|
|
17758
18826
|
return dir;
|
|
17759
18827
|
}
|
|
17760
|
-
const legacy =
|
|
18828
|
+
const legacy = path7.join(process.env.XDG_CONFIG_HOME || path7.join(os.homedir(), ".config"), "workflow-toolkit");
|
|
17761
18829
|
if (!existsSync3(legacy)) {
|
|
17762
18830
|
migratedDir = dir;
|
|
17763
18831
|
return dir;
|
|
@@ -17770,15 +18838,15 @@ var ensureConfigDir = (dir = resolveConfigDir()) => {
|
|
|
17770
18838
|
mkdirSync2(dir, { recursive: true });
|
|
17771
18839
|
diagnosticLogger?.info(EVENT.migration, { from: legacy, to: dir });
|
|
17772
18840
|
for (const entry of readdirSync2(legacy, { withFileTypes: true })) {
|
|
17773
|
-
const src =
|
|
17774
|
-
const dest =
|
|
18841
|
+
const src = path7.join(legacy, entry.name);
|
|
18842
|
+
const dest = path7.join(dir, entry.name);
|
|
17775
18843
|
if (existsSync3(dest))
|
|
17776
18844
|
continue;
|
|
17777
18845
|
try {
|
|
17778
18846
|
if (entry.isDirectory())
|
|
17779
|
-
|
|
18847
|
+
cpSync2(src, dest, { recursive: true });
|
|
17780
18848
|
else if (entry.isFile())
|
|
17781
|
-
|
|
18849
|
+
copyFileSync2(src, dest);
|
|
17782
18850
|
} catch (err) {
|
|
17783
18851
|
migrationFailed = true;
|
|
17784
18852
|
diagnosticLogger?.warn(EVENT.migration, { from: src, ok: false, ...errorDetail(err) });
|
|
@@ -17843,7 +18911,7 @@ var parseConfigResult = (raw, file) => {
|
|
|
17843
18911
|
};
|
|
17844
18912
|
};
|
|
17845
18913
|
var readConfigTyped = (dir) => {
|
|
17846
|
-
const file =
|
|
18914
|
+
const file = path7.join(dir ?? configDir(), "config.json");
|
|
17847
18915
|
return parseConfigResult(readSafe(file), file);
|
|
17848
18916
|
};
|
|
17849
18917
|
var readConfig = () => {
|
|
@@ -17940,7 +19008,7 @@ var releaseTrackSchema = object({
|
|
|
17940
19008
|
context.addIssue({ code: "custom", path: [field], message: "entries must be unique" });
|
|
17941
19009
|
}
|
|
17942
19010
|
}
|
|
17943
|
-
if (track.versionSource.kind === "package-json" && (
|
|
19011
|
+
if (track.versionSource.kind === "package-json" && (path8.posix.isAbsolute(track.versionSource.path.replaceAll("\\", "/")) || /^[A-Za-z]:\//u.test(track.versionSource.path.replaceAll("\\", "/")) || track.versionSource.path.replaceAll("\\", "/").split("/").includes(".."))) {
|
|
17944
19012
|
context.addIssue({
|
|
17945
19013
|
code: "custom",
|
|
17946
19014
|
path: ["versionSource", "path"],
|
|
@@ -17994,7 +19062,7 @@ var workspacesFileSchema = object({ workspaces: array(workspaceConfigSchema).opt
|
|
|
17994
19062
|
names.add(workspace.name);
|
|
17995
19063
|
}
|
|
17996
19064
|
});
|
|
17997
|
-
var workspacesRevision = (contents) =>
|
|
19065
|
+
var workspacesRevision = (contents) => createHash5("sha256").update(contents ?? "<missing>").digest("hex");
|
|
17998
19066
|
var validateWorkspacesDocument = (parsed, file) => {
|
|
17999
19067
|
if (!isConfigObject(parsed)) {
|
|
18000
19068
|
return { status: "malformed", path: file, entries: [], error: `${file} is not a JSON object` };
|
|
@@ -18032,7 +19100,7 @@ var validateWorkspacesDocument = (parsed, file) => {
|
|
|
18032
19100
|
};
|
|
18033
19101
|
};
|
|
18034
19102
|
var readWorkspacesResult = (dir = configDir()) => {
|
|
18035
|
-
const file =
|
|
19103
|
+
const file = path8.join(dir, "workspaces.json");
|
|
18036
19104
|
let raw;
|
|
18037
19105
|
try {
|
|
18038
19106
|
raw = readFileSync3(file, "utf8");
|
|
@@ -18203,7 +19271,7 @@ var runtimeWorkspaceIndexSchema = object({
|
|
|
18203
19271
|
workspaces: array(object({ name: nonBlank, glob: nonBlank }).passthrough()).optional()
|
|
18204
19272
|
}).passthrough();
|
|
18205
19273
|
var resolveRuntimeWorkspaceCandidate = (cwd, kind) => {
|
|
18206
|
-
const file =
|
|
19274
|
+
const file = path8.join(configDir(), "workspaces.json");
|
|
18207
19275
|
let raw;
|
|
18208
19276
|
try {
|
|
18209
19277
|
raw = readFileSync3(file, "utf8");
|
|
@@ -18334,13 +19402,13 @@ var gitContext = (workspaceRoot, paths = []) => {
|
|
|
18334
19402
|
};
|
|
18335
19403
|
|
|
18336
19404
|
// packages/workit-core/src/core/vcs-config.ts
|
|
18337
|
-
import
|
|
18338
|
-
import
|
|
18339
|
-
import { spawnSync as
|
|
18340
|
-
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ??
|
|
19405
|
+
import fs8 from "node:fs";
|
|
19406
|
+
import path9 from "node:path";
|
|
19407
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
19408
|
+
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path9.join(configDir(), "vcs.json");
|
|
18341
19409
|
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
18342
19410
|
var remoteProvider = (cwd) => {
|
|
18343
|
-
const r =
|
|
19411
|
+
const r = spawnSync3("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
18344
19412
|
if (r.status !== 0)
|
|
18345
19413
|
return null;
|
|
18346
19414
|
const url = (r.stdout ?? "").trim();
|
|
@@ -18377,7 +19445,7 @@ var remoteEndpoint = (remote) => {
|
|
|
18377
19445
|
var configuredSshEndpoint = (host) => {
|
|
18378
19446
|
if (host.startsWith("-"))
|
|
18379
19447
|
return null;
|
|
18380
|
-
const resolved =
|
|
19448
|
+
const resolved = spawnSync3("ssh", ["-G", host], {
|
|
18381
19449
|
encoding: "utf8",
|
|
18382
19450
|
timeout: 5000,
|
|
18383
19451
|
maxBuffer: 1024 * 1024,
|
|
@@ -18427,7 +19495,7 @@ var readVcsConfig = () => {
|
|
|
18427
19495
|
const file = vcsConfigPath();
|
|
18428
19496
|
let raw;
|
|
18429
19497
|
try {
|
|
18430
|
-
raw =
|
|
19498
|
+
raw = fs8.readFileSync(file, "utf8");
|
|
18431
19499
|
} catch {
|
|
18432
19500
|
return { status: "missing", path: file, config: {} };
|
|
18433
19501
|
}
|
|
@@ -18493,7 +19561,7 @@ function vcsConfig(mode, cwd) {
|
|
|
18493
19561
|
const prov = cfg[provider] ?? {};
|
|
18494
19562
|
const out = {
|
|
18495
19563
|
ok: true,
|
|
18496
|
-
configPath:
|
|
19564
|
+
configPath: path9.resolve(cfgPath),
|
|
18497
19565
|
provider,
|
|
18498
19566
|
defaultTargetBranch: defaultTarget,
|
|
18499
19567
|
pr: cfg.pr ?? {},
|
|
@@ -18528,7 +19596,7 @@ function vcsCliIdentity(cwd) {
|
|
|
18528
19596
|
PATH: process.env.PATH ?? "",
|
|
18529
19597
|
...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
|
|
18530
19598
|
};
|
|
18531
|
-
const result =
|
|
19599
|
+
const result = spawnSync3(bin, ["api", "user"], {
|
|
18532
19600
|
cwd: root,
|
|
18533
19601
|
encoding: "utf8",
|
|
18534
19602
|
env
|
|
@@ -18570,7 +19638,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
18570
19638
|
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
18571
19639
|
descriptionPreview: desc.slice(0, 600)
|
|
18572
19640
|
});
|
|
18573
|
-
const remoteResult =
|
|
19641
|
+
const remoteResult = spawnSync3("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
18574
19642
|
cwd,
|
|
18575
19643
|
encoding: "utf8"
|
|
18576
19644
|
});
|
|
@@ -18586,7 +19654,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
18586
19654
|
if (!hostingApiHostMatches(remote, host))
|
|
18587
19655
|
return { ok: false, error: "PR style target does not match the provider host" };
|
|
18588
19656
|
if (provider === "gitlab") {
|
|
18589
|
-
const run = (args) =>
|
|
19657
|
+
const run = (args) => spawnSync3("glab", ["api", ...args], {
|
|
18590
19658
|
cwd,
|
|
18591
19659
|
encoding: "utf8",
|
|
18592
19660
|
env
|
|
@@ -18606,7 +19674,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
18606
19674
|
});
|
|
18607
19675
|
}
|
|
18608
19676
|
} else if (provider === "github") {
|
|
18609
|
-
const r =
|
|
19677
|
+
const r = spawnSync3("gh", [
|
|
18610
19678
|
"pr",
|
|
18611
19679
|
"list",
|
|
18612
19680
|
"--repo",
|
|
@@ -18678,7 +19746,7 @@ var operationAutoClass = (operation) => typeof operation === "string" ? OPERATIO
|
|
|
18678
19746
|
var digestOf = (configDirPath) => {
|
|
18679
19747
|
try {
|
|
18680
19748
|
const raw = readFileSync4(`${configDirPath}/workspaces.json`, "utf8");
|
|
18681
|
-
return
|
|
19749
|
+
return createHash6("sha256").update(raw).digest("hex");
|
|
18682
19750
|
} catch {
|
|
18683
19751
|
return null;
|
|
18684
19752
|
}
|
|
@@ -18722,7 +19790,7 @@ var targetOf = (root, descriptor) => {
|
|
|
18722
19790
|
const cwd = parsed?.payload?.cwd;
|
|
18723
19791
|
if (cwd === undefined)
|
|
18724
19792
|
return root;
|
|
18725
|
-
if (typeof cwd !== "string" || !
|
|
19793
|
+
if (typeof cwd !== "string" || !path10.isAbsolute(cwd))
|
|
18726
19794
|
return null;
|
|
18727
19795
|
return realpathSync3(cwd);
|
|
18728
19796
|
} catch {
|
|
@@ -18947,11 +20015,11 @@ function applicableDecision(task, purpose, binding, checkoutRoot) {
|
|
|
18947
20015
|
return [];
|
|
18948
20016
|
return task.decisions.filter((entry) => entry.provenance.kind !== "imported" && decisionMatches(entry, purpose, binding) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, entry.data.binding).ok : entry.data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map((entry) => entry.data);
|
|
18949
20017
|
}
|
|
18950
|
-
var
|
|
20018
|
+
var digestBytes = (bytes) => createHash7("sha256").update(bytes).digest("hex");
|
|
18951
20019
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
18952
20020
|
let root;
|
|
18953
20021
|
try {
|
|
18954
|
-
root =
|
|
20022
|
+
root = fs9.realpathSync(checkoutRoot);
|
|
18955
20023
|
} catch {
|
|
18956
20024
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
18957
20025
|
}
|
|
@@ -18960,23 +20028,23 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
18960
20028
|
continue;
|
|
18961
20029
|
if (!reference.digest)
|
|
18962
20030
|
return failure2("invalid_input", "document references require a byte digest");
|
|
18963
|
-
const target =
|
|
18964
|
-
if (target !== root && !target.startsWith(`${root}${
|
|
20031
|
+
const target = path11.resolve(root, reference.path);
|
|
20032
|
+
if (target !== root && !target.startsWith(`${root}${path11.sep}`))
|
|
18965
20033
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
18966
20034
|
fields: [
|
|
18967
20035
|
{ path: "contentRefs.path", reason: `${reference.path} is outside the checkout` }
|
|
18968
20036
|
]
|
|
18969
20037
|
});
|
|
18970
20038
|
try {
|
|
18971
|
-
const relative =
|
|
20039
|
+
const relative = path11.relative(root, target);
|
|
18972
20040
|
let current = root;
|
|
18973
|
-
for (const segment of relative.split(
|
|
20041
|
+
for (const segment of relative.split(path11.sep)) {
|
|
18974
20042
|
if (!segment)
|
|
18975
20043
|
continue;
|
|
18976
|
-
current =
|
|
18977
|
-
const stat =
|
|
18978
|
-
const real =
|
|
18979
|
-
if (real !== root && !real.startsWith(`${root}${
|
|
20044
|
+
current = path11.join(current, segment);
|
|
20045
|
+
const stat = fs9.lstatSync(current);
|
|
20046
|
+
const real = fs9.realpathSync(current);
|
|
20047
|
+
if (real !== root && !real.startsWith(`${root}${path11.sep}`))
|
|
18980
20048
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
18981
20049
|
fields: [
|
|
18982
20050
|
{
|
|
@@ -18992,7 +20060,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
18992
20060
|
if (current !== target && !stat.isDirectory())
|
|
18993
20061
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
18994
20062
|
}
|
|
18995
|
-
if (
|
|
20063
|
+
if (digestBytes(fs9.readFileSync(target)) !== reference.digest)
|
|
18996
20064
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
18997
20065
|
} catch {
|
|
18998
20066
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -19356,9 +20424,9 @@ var bindingCovers = scopeCovers;
|
|
|
19356
20424
|
var verifyDecisionContent = verifyContentRefs;
|
|
19357
20425
|
|
|
19358
20426
|
// packages/workit-core/src/check-config.ts
|
|
19359
|
-
import { spawnSync as
|
|
19360
|
-
import
|
|
19361
|
-
import
|
|
20427
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
20428
|
+
import fs10 from "node:fs";
|
|
20429
|
+
import path12 from "node:path";
|
|
19362
20430
|
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
19363
20431
|
var CHECKS_FILE = "workit.checks.json";
|
|
19364
20432
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -19400,7 +20468,7 @@ function splitCommand(command) {
|
|
|
19400
20468
|
return argv.length ? argv : new Error("empty command");
|
|
19401
20469
|
}
|
|
19402
20470
|
var git = (cwd, args) => {
|
|
19403
|
-
const run =
|
|
20471
|
+
const run = spawnSync4("git", args, {
|
|
19404
20472
|
cwd,
|
|
19405
20473
|
encoding: "utf8",
|
|
19406
20474
|
timeout: 20000,
|
|
@@ -19411,10 +20479,10 @@ var git = (cwd, args) => {
|
|
|
19411
20479
|
return value || null;
|
|
19412
20480
|
};
|
|
19413
20481
|
function checkRoot(cwd) {
|
|
19414
|
-
return
|
|
20482
|
+
return path12.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
19415
20483
|
}
|
|
19416
20484
|
var packageManager = (root) => {
|
|
19417
|
-
const has = (file) =>
|
|
20485
|
+
const has = (file) => fs10.existsSync(path12.join(root, file));
|
|
19418
20486
|
if (has("bun.lock") || has("bun.lockb"))
|
|
19419
20487
|
return "bun";
|
|
19420
20488
|
if (has("pnpm-lock.yaml"))
|
|
@@ -19434,7 +20502,7 @@ var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
|
19434
20502
|
function detectedChecks(root) {
|
|
19435
20503
|
const read = (file) => {
|
|
19436
20504
|
try {
|
|
19437
|
-
return
|
|
20505
|
+
return fs10.readFileSync(path12.join(root, file), "utf8");
|
|
19438
20506
|
} catch {
|
|
19439
20507
|
return null;
|
|
19440
20508
|
}
|
|
@@ -19473,7 +20541,7 @@ function detectedChecks(root) {
|
|
|
19473
20541
|
var GATES = ["testing", "verification"];
|
|
19474
20542
|
function loadCheckConfig(cwd) {
|
|
19475
20543
|
const root = checkRoot(cwd);
|
|
19476
|
-
const file =
|
|
20544
|
+
const file = path12.join(root, CHECKS_FILE);
|
|
19477
20545
|
const none = (error = null) => ({
|
|
19478
20546
|
source: error ? CHECKS_FILE : "none",
|
|
19479
20547
|
root,
|
|
@@ -19483,7 +20551,7 @@ function loadCheckConfig(cwd) {
|
|
|
19483
20551
|
});
|
|
19484
20552
|
let text = null;
|
|
19485
20553
|
try {
|
|
19486
|
-
text =
|
|
20554
|
+
text = fs10.readFileSync(file, "utf8");
|
|
19487
20555
|
} catch (error) {
|
|
19488
20556
|
if (error.code !== "ENOENT")
|
|
19489
20557
|
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
@@ -19546,10 +20614,10 @@ function workitAvailable(env = process.env) {
|
|
|
19546
20614
|
return workitOnPath;
|
|
19547
20615
|
const windows = process.platform === "win32";
|
|
19548
20616
|
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
19549
|
-
const dirs = (env.PATH ?? env.Path ?? "").split(
|
|
20617
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path12.delimiter).filter(Boolean);
|
|
19550
20618
|
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
19551
20619
|
try {
|
|
19552
|
-
return
|
|
20620
|
+
return fs10.statSync(path12.join(dir, `workit${ext}`)).isFile();
|
|
19553
20621
|
} catch {
|
|
19554
20622
|
return false;
|
|
19555
20623
|
}
|
|
@@ -19561,15 +20629,15 @@ function checkCommand(args, version, onPath = workitAvailable()) {
|
|
|
19561
20629
|
}
|
|
19562
20630
|
|
|
19563
20631
|
// packages/workit-core/src/git/rev.ts
|
|
19564
|
-
import { spawnSync as
|
|
19565
|
-
import { createHash as
|
|
19566
|
-
import
|
|
20632
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
20633
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
20634
|
+
import fs11 from "node:fs";
|
|
19567
20635
|
import os2 from "node:os";
|
|
19568
|
-
import
|
|
20636
|
+
import path13 from "node:path";
|
|
19569
20637
|
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
19570
20638
|
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
19571
20639
|
var git2 = (cwd, args, options = {}) => {
|
|
19572
|
-
const result =
|
|
20640
|
+
const result = spawnSync5("git", args, {
|
|
19573
20641
|
cwd,
|
|
19574
20642
|
encoding: "utf8",
|
|
19575
20643
|
env: options.env ?? process.env,
|
|
@@ -19599,10 +20667,10 @@ function headSha(cwd) {
|
|
|
19599
20667
|
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
19600
20668
|
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
19601
20669
|
var seedIndex = (from, to) => {
|
|
19602
|
-
|
|
19603
|
-
const { atimeNs, mtimeNs } =
|
|
20670
|
+
fs11.copyFileSync(from, to);
|
|
20671
|
+
const { atimeNs, mtimeNs } = fs11.statSync(from, { bigint: true });
|
|
19604
20672
|
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
19605
|
-
|
|
20673
|
+
fs11.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
19606
20674
|
};
|
|
19607
20675
|
function worktreeTree(cwd, options = {}) {
|
|
19608
20676
|
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
@@ -19611,10 +20679,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
19611
20679
|
return null;
|
|
19612
20680
|
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
19613
20681
|
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
19614
|
-
const scratch =
|
|
20682
|
+
const scratch = fs11.mkdtempSync(path13.join(os2.tmpdir(), "workit-tree-"));
|
|
19615
20683
|
try {
|
|
19616
|
-
const tempIndex =
|
|
19617
|
-
if (
|
|
20684
|
+
const tempIndex = path13.join(scratch, "index");
|
|
20685
|
+
if (fs11.existsSync(indexPath))
|
|
19618
20686
|
seedIndex(indexPath, tempIndex);
|
|
19619
20687
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
19620
20688
|
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
@@ -19626,7 +20694,7 @@ function worktreeTree(cwd, options = {}) {
|
|
|
19626
20694
|
const skipped = [];
|
|
19627
20695
|
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
19628
20696
|
try {
|
|
19629
|
-
const stat =
|
|
20697
|
+
const stat = fs11.statSync(path13.join(top, file));
|
|
19630
20698
|
if (stat.isFile() && stat.size > cap)
|
|
19631
20699
|
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
19632
20700
|
} catch {}
|
|
@@ -19643,10 +20711,10 @@ function worktreeTree(cwd, options = {}) {
|
|
|
19643
20711
|
if (!tree)
|
|
19644
20712
|
return null;
|
|
19645
20713
|
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
19646
|
-
const key = skipped.length ? `sha256:${
|
|
20714
|
+
const key = skipped.length ? `sha256:${createHash8("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
19647
20715
|
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
19648
20716
|
} finally {
|
|
19649
|
-
|
|
20717
|
+
fs11.rmSync(scratch, { recursive: true, force: true });
|
|
19650
20718
|
}
|
|
19651
20719
|
}
|
|
19652
20720
|
function worktreeSignal(cwd, options = {}) {
|
|
@@ -19666,16 +20734,16 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
19666
20734
|
});
|
|
19667
20735
|
if (!status.ok)
|
|
19668
20736
|
return null;
|
|
19669
|
-
const hash =
|
|
20737
|
+
const hash = createHash8("sha256");
|
|
19670
20738
|
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
19671
20739
|
for (const entry of status.stdout.split("\x00")) {
|
|
19672
20740
|
if (entry.length < 4)
|
|
19673
20741
|
continue;
|
|
19674
20742
|
const file = entry.slice(3);
|
|
19675
|
-
const absolute =
|
|
20743
|
+
const absolute = path13.join(top, file);
|
|
19676
20744
|
let stat = "-";
|
|
19677
20745
|
try {
|
|
19678
|
-
const info =
|
|
20746
|
+
const info = fs11.lstatSync(absolute, { bigint: true });
|
|
19679
20747
|
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
19680
20748
|
if (info.isDirectory())
|
|
19681
20749
|
stat += `:${headSha(absolute) ?? "-"}`;
|
|
@@ -19686,15 +20754,15 @@ function worktreeSignal(cwd, options = {}) {
|
|
|
19686
20754
|
}
|
|
19687
20755
|
|
|
19688
20756
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
19689
|
-
var
|
|
19690
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${
|
|
19691
|
-
var relative = (root, candidate) =>
|
|
20757
|
+
var digestBytes2 = (value) => createHash9("sha256").update(value).digest("hex");
|
|
20758
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path14.sep}`);
|
|
20759
|
+
var relative = (root, candidate) => path14.relative(root, candidate).split(path14.sep).join("/") || ".";
|
|
19692
20760
|
var canonicalPath = (value) => {
|
|
19693
20761
|
if (typeof value !== "string" || !value)
|
|
19694
20762
|
return null;
|
|
19695
20763
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
19696
20764
|
return null;
|
|
19697
|
-
const normalized =
|
|
20765
|
+
const normalized = path14.posix.normalize(value);
|
|
19698
20766
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
19699
20767
|
return null;
|
|
19700
20768
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -19730,7 +20798,7 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
19730
20798
|
return left.length - right.length;
|
|
19731
20799
|
};
|
|
19732
20800
|
var gitPaths = (root) => {
|
|
19733
|
-
const result =
|
|
20801
|
+
const result = spawnSync6("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
19734
20802
|
cwd: root,
|
|
19735
20803
|
encoding: "buffer"
|
|
19736
20804
|
});
|
|
@@ -19742,21 +20810,21 @@ var gitPaths = (root) => {
|
|
|
19742
20810
|
git: !/not a git repository/i.test(stderr)
|
|
19743
20811
|
};
|
|
19744
20812
|
}
|
|
19745
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
19746
|
-
const stagedDeleted =
|
|
20813
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")) : [];
|
|
20814
|
+
const stagedDeleted = spawnSync6("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
19747
20815
|
if (stagedDeleted.status !== 0) {
|
|
19748
20816
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
19749
20817
|
if (!/not a git repository/i.test(stderr))
|
|
19750
20818
|
return { paths, uncertain: true, git: true };
|
|
19751
20819
|
} else if (stagedDeleted.stdout) {
|
|
19752
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
20820
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path14.sep).join("/")));
|
|
19753
20821
|
}
|
|
19754
20822
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
19755
20823
|
};
|
|
19756
20824
|
var walk = (root, directory, output) => {
|
|
19757
20825
|
let names;
|
|
19758
20826
|
try {
|
|
19759
|
-
names =
|
|
20827
|
+
names = fs12.readdirSync(directory);
|
|
19760
20828
|
} catch {
|
|
19761
20829
|
return true;
|
|
19762
20830
|
}
|
|
@@ -19764,10 +20832,10 @@ var walk = (root, directory, output) => {
|
|
|
19764
20832
|
for (const name of names) {
|
|
19765
20833
|
if (name === ".git" || name === ".workit")
|
|
19766
20834
|
continue;
|
|
19767
|
-
const target =
|
|
20835
|
+
const target = path14.join(directory, name);
|
|
19768
20836
|
let stat;
|
|
19769
20837
|
try {
|
|
19770
|
-
stat =
|
|
20838
|
+
stat = fs12.lstatSync(target);
|
|
19771
20839
|
} catch {
|
|
19772
20840
|
uncertain = true;
|
|
19773
20841
|
continue;
|
|
@@ -19782,14 +20850,14 @@ var walk = (root, directory, output) => {
|
|
|
19782
20850
|
var scopeRoots = (root, scope) => {
|
|
19783
20851
|
const roots = [];
|
|
19784
20852
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
19785
|
-
const target =
|
|
20853
|
+
const target = path14.resolve(root, item);
|
|
19786
20854
|
if (!inside(root, target))
|
|
19787
20855
|
return failure2("invalid_input", `candidate scope escapes checkout: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} is outside the checkout` }] });
|
|
19788
20856
|
let ancestor = target;
|
|
19789
|
-
while (!
|
|
19790
|
-
ancestor =
|
|
20857
|
+
while (!fs12.existsSync(ancestor) && ancestor !== root)
|
|
20858
|
+
ancestor = path14.dirname(ancestor);
|
|
19791
20859
|
try {
|
|
19792
|
-
if (!inside(root,
|
|
20860
|
+
if (!inside(root, fs12.realpathSync(ancestor)))
|
|
19793
20861
|
return failure2("invalid_input", `candidate scope escapes checkout through a symlink: ${item} — keep one task per repository and coordinate a separate linked task in the other checkout`, { fields: [{ path: "scope.paths", reason: `${item} resolves outside the checkout` }] });
|
|
19794
20862
|
} catch {
|
|
19795
20863
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -19801,7 +20869,7 @@ var scopeRoots = (root, scope) => {
|
|
|
19801
20869
|
function captureCandidate(root, scope, environment = []) {
|
|
19802
20870
|
let checkout;
|
|
19803
20871
|
try {
|
|
19804
|
-
checkout =
|
|
20872
|
+
checkout = fs12.realpathSync(root);
|
|
19805
20873
|
} catch {
|
|
19806
20874
|
return failure2("invalid_input", "candidate root does not exist");
|
|
19807
20875
|
}
|
|
@@ -19819,11 +20887,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
19819
20887
|
if (item === ".git" || item === ".workit")
|
|
19820
20888
|
continue;
|
|
19821
20889
|
names.add(item);
|
|
19822
|
-
if (!
|
|
20890
|
+
if (!fs12.existsSync(target))
|
|
19823
20891
|
continue;
|
|
19824
20892
|
let stat;
|
|
19825
20893
|
try {
|
|
19826
|
-
stat =
|
|
20894
|
+
stat = fs12.lstatSync(target);
|
|
19827
20895
|
} catch {
|
|
19828
20896
|
uncertain = true;
|
|
19829
20897
|
continue;
|
|
@@ -19839,17 +20907,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
19839
20907
|
for (const item of [...names].toSorted()) {
|
|
19840
20908
|
if (!scopeMatches(item, normalizedScope))
|
|
19841
20909
|
continue;
|
|
19842
|
-
const target =
|
|
20910
|
+
const target = path14.join(checkout, item);
|
|
19843
20911
|
try {
|
|
19844
|
-
const stat =
|
|
20912
|
+
const stat = fs12.lstatSync(target);
|
|
19845
20913
|
if (stat.isSymbolicLink()) {
|
|
19846
|
-
const link =
|
|
19847
|
-
files.push({ path: item, kind: "symlink", digest:
|
|
20914
|
+
const link = fs12.readlinkSync(target);
|
|
20915
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes2(link), executable: null });
|
|
19848
20916
|
} else if (stat.isFile()) {
|
|
19849
20917
|
files.push({
|
|
19850
20918
|
path: item,
|
|
19851
20919
|
kind: "file",
|
|
19852
|
-
digest:
|
|
20920
|
+
digest: digestBytes2(fs12.readFileSync(target)),
|
|
19853
20921
|
executable: (stat.mode & 73) !== 0
|
|
19854
20922
|
});
|
|
19855
20923
|
} else if (!stat.isDirectory()) {
|
|
@@ -19874,7 +20942,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
19874
20942
|
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
19875
20943
|
if (values.some(({ value }) => value === null))
|
|
19876
20944
|
completeness = "uncertain";
|
|
19877
|
-
const headResult =
|
|
20945
|
+
const headResult = spawnSync6("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
19878
20946
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
19879
20947
|
completeness = "uncertain";
|
|
19880
20948
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -21045,6 +22113,38 @@ var applyWorkerLifecycle = (input) => {
|
|
|
21045
22113
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
21046
22114
|
};
|
|
21047
22115
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
22116
|
+
var TASK_SCOPED_ACTIONS = new Set([
|
|
22117
|
+
"task.inspect",
|
|
22118
|
+
"task.revise",
|
|
22119
|
+
"task.progress",
|
|
22120
|
+
"task.pause",
|
|
22121
|
+
"task.resume",
|
|
22122
|
+
"task.close",
|
|
22123
|
+
"policy.assess",
|
|
22124
|
+
"policy.preview",
|
|
22125
|
+
"policy.explain",
|
|
22126
|
+
"evidence.record",
|
|
22127
|
+
"finding.record",
|
|
22128
|
+
"finding.resolve",
|
|
22129
|
+
"decision.record",
|
|
22130
|
+
"decision.revoke",
|
|
22131
|
+
"worker.assign",
|
|
22132
|
+
"worker.report",
|
|
22133
|
+
"worker.cancel",
|
|
22134
|
+
"writer.acquire",
|
|
22135
|
+
"writer.release",
|
|
22136
|
+
"state.export"
|
|
22137
|
+
]);
|
|
22138
|
+
var CREATING_ACTIONS = new Set([
|
|
22139
|
+
"task.revise",
|
|
22140
|
+
"task.progress",
|
|
22141
|
+
"policy.assess",
|
|
22142
|
+
"evidence.record",
|
|
22143
|
+
"finding.record",
|
|
22144
|
+
"decision.record",
|
|
22145
|
+
"worker.assign",
|
|
22146
|
+
"writer.acquire"
|
|
22147
|
+
]);
|
|
21048
22148
|
var REVISION_RETRY_ATTEMPTS = 8;
|
|
21049
22149
|
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
21050
22150
|
var filledRevisions = (request) => {
|
|
@@ -21296,7 +22396,9 @@ class WorkitCore {
|
|
|
21296
22396
|
}
|
|
21297
22397
|
contextRootError() {
|
|
21298
22398
|
try {
|
|
21299
|
-
|
|
22399
|
+
realpathSync6(this.context.root);
|
|
22400
|
+
const root = checkoutRootOf(this.context.root);
|
|
22401
|
+
if (root !== this.store.root && !sameDirectoryIdentity(root, this.store.root))
|
|
21300
22402
|
return failure2("invalid_input", "operation context root does not match the task store root");
|
|
21301
22403
|
} catch {
|
|
21302
22404
|
return failure2("invalid_input", "operation context root cannot be resolved");
|
|
@@ -21318,6 +22420,30 @@ class WorkitCore {
|
|
|
21318
22420
|
retryOmittedRevisions(request, run) {
|
|
21319
22421
|
return retryFilledRevisions(filledRevisions(request), run);
|
|
21320
22422
|
}
|
|
22423
|
+
implicitTask(family, request) {
|
|
22424
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
22425
|
+
return success(null, null, request);
|
|
22426
|
+
const value = request;
|
|
22427
|
+
if (value.taskId !== undefined || typeof value.action !== "string")
|
|
22428
|
+
return success(null, null, request);
|
|
22429
|
+
const action = `${family}.${value.action}`;
|
|
22430
|
+
if (!TASK_SCOPED_ACTIONS.has(action))
|
|
22431
|
+
return success(null, null, request);
|
|
22432
|
+
const root = this.contextRootError();
|
|
22433
|
+
if (!root.ok)
|
|
22434
|
+
return root;
|
|
22435
|
+
const create = CREATING_ACTIONS.has(action) && (this.context.workerId ?? null) === null;
|
|
22436
|
+
const found = this.store.implicitTask({
|
|
22437
|
+
provenance: provenance(this.context),
|
|
22438
|
+
create,
|
|
22439
|
+
now: trustedNow(this.context)
|
|
22440
|
+
});
|
|
22441
|
+
if (!found.ok)
|
|
22442
|
+
return found;
|
|
22443
|
+
if (!found.data)
|
|
22444
|
+
return failure2("not_found", 'no task for this branch yet; pass taskId, or record something first (`workit task start "<objective>"`, a note, a check) to create it');
|
|
22445
|
+
return success(null, null, { ...request, taskId: found.data.task.id });
|
|
22446
|
+
}
|
|
21321
22447
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
21322
22448
|
const workerId = this.context.workerId ?? null;
|
|
21323
22449
|
if (workerId === null)
|
|
@@ -21349,7 +22475,11 @@ class WorkitCore {
|
|
|
21349
22475
|
return null;
|
|
21350
22476
|
}
|
|
21351
22477
|
state(request) {
|
|
21352
|
-
|
|
22478
|
+
const resolved = this.implicitTask("state", request);
|
|
22479
|
+
if (!resolved.ok)
|
|
22480
|
+
return resolved;
|
|
22481
|
+
const value = resolved.data;
|
|
22482
|
+
return value?.action === "import" ? this.retryOmittedRevisions(value, () => this.stateOnce(value)) : this.stateOnce(value);
|
|
21353
22483
|
}
|
|
21354
22484
|
stateOnce(request) {
|
|
21355
22485
|
const root = this.contextRootError();
|
|
@@ -21416,44 +22546,14 @@ class WorkitCore {
|
|
|
21416
22546
|
return imported;
|
|
21417
22547
|
return this.summary(imported.data);
|
|
21418
22548
|
}
|
|
21419
|
-
|
|
21420
|
-
return failure2("permission_denied", "native recovery authority is unavailable");
|
|
21421
|
-
const processEvidence = (lock, writer) => this.context.nativeRecovery({
|
|
21422
|
-
lock,
|
|
21423
|
-
writer,
|
|
21424
|
-
reason: input.reason,
|
|
21425
|
-
authorityRefs: input.authorityRefs
|
|
21426
|
-
});
|
|
21427
|
-
if (input.target === "workspace")
|
|
21428
|
-
return this.store.recoverWorkspace({
|
|
21429
|
-
expectedBytes: input.expectedBytes,
|
|
21430
|
-
snapshotDigest: input.snapshotDigest,
|
|
21431
|
-
reason: input.reason,
|
|
21432
|
-
authorityRefs: input.authorityRefs,
|
|
21433
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
21434
|
-
processEvidence
|
|
21435
|
-
});
|
|
21436
|
-
const workspace = this.store.readWorkspace();
|
|
21437
|
-
if (!workspace.ok)
|
|
21438
|
-
return workspace;
|
|
21439
|
-
if (!workspace.data)
|
|
21440
|
-
return failure2("not_found", "workspace not found");
|
|
21441
|
-
this.fillRevisions(input, undefined, workspace.data);
|
|
21442
|
-
if (input.target === "workspace" && input.taskId !== undefined)
|
|
21443
|
-
return failure2("invalid_input", "workspace recovery takes no task ID");
|
|
21444
|
-
if (input.target === "task" && input.taskId === undefined)
|
|
21445
|
-
return failure2("invalid_input", "task recovery requires a task ID");
|
|
21446
|
-
return this.store.recoverTask(input.taskId, {
|
|
21447
|
-
expectedBytes: input.expectedBytes,
|
|
21448
|
-
snapshotDigest: input.snapshotDigest,
|
|
21449
|
-
reason: input.reason,
|
|
21450
|
-
authorityRefs: input.authorityRefs,
|
|
21451
|
-
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
21452
|
-
processEvidence
|
|
21453
|
-
});
|
|
22549
|
+
return failure2("invalid_input", "unknown state action");
|
|
21454
22550
|
}
|
|
21455
22551
|
task(request) {
|
|
21456
|
-
|
|
22552
|
+
const resolved = this.implicitTask("task", request);
|
|
22553
|
+
if (!resolved.ok)
|
|
22554
|
+
return resolved;
|
|
22555
|
+
const value = resolved.data;
|
|
22556
|
+
return this.retryOmittedRevisions(value, () => this.taskOnce(value));
|
|
21457
22557
|
}
|
|
21458
22558
|
taskOnce(request) {
|
|
21459
22559
|
const root = this.contextRootError();
|
|
@@ -21555,7 +22655,11 @@ class WorkitCore {
|
|
|
21555
22655
|
}
|
|
21556
22656
|
}
|
|
21557
22657
|
policy(request) {
|
|
21558
|
-
|
|
22658
|
+
const resolved = this.implicitTask("policy", request);
|
|
22659
|
+
if (!resolved.ok)
|
|
22660
|
+
return resolved;
|
|
22661
|
+
const value = resolved.data;
|
|
22662
|
+
return this.retryOmittedRevisions(value, () => this.policyOnce(value));
|
|
21559
22663
|
}
|
|
21560
22664
|
policyOnce(request) {
|
|
21561
22665
|
const root = this.contextRootError();
|
|
@@ -21604,7 +22708,11 @@ class WorkitCore {
|
|
|
21604
22708
|
return success(changed.data.revision, null, changed.data.policy);
|
|
21605
22709
|
}
|
|
21606
22710
|
evidence(request) {
|
|
21607
|
-
|
|
22711
|
+
const resolved = this.implicitTask("evidence", request);
|
|
22712
|
+
if (!resolved.ok)
|
|
22713
|
+
return resolved;
|
|
22714
|
+
const value = resolved.data;
|
|
22715
|
+
return this.retryOmittedRevisions(value, () => this.evidenceOnce(value));
|
|
21608
22716
|
}
|
|
21609
22717
|
evidenceOnce(request) {
|
|
21610
22718
|
const root = this.contextRootError();
|
|
@@ -21745,13 +22853,16 @@ class WorkitCore {
|
|
|
21745
22853
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
21746
22854
|
}
|
|
21747
22855
|
decision(request) {
|
|
21748
|
-
|
|
22856
|
+
const resolved = this.implicitTask("decision", request);
|
|
22857
|
+
return resolved.ok ? this.recordDecision(resolved.data) : resolved;
|
|
21749
22858
|
}
|
|
21750
22859
|
observeDecision(request, observation) {
|
|
21751
|
-
|
|
22860
|
+
const resolved = this.implicitTask("decision", request);
|
|
22861
|
+
return resolved.ok ? this.recordDecision(resolved.data, observation, true) : resolved;
|
|
21752
22862
|
}
|
|
21753
22863
|
observeStandingDecision(request) {
|
|
21754
|
-
|
|
22864
|
+
const resolved = this.implicitTask("decision", request);
|
|
22865
|
+
return resolved.ok ? this.recordDecision(resolved.data, undefined, true, true) : resolved;
|
|
21755
22866
|
}
|
|
21756
22867
|
commitTask(taskId, expected, filled, recheck, update) {
|
|
21757
22868
|
let revision = expected;
|
|
@@ -21913,7 +23024,11 @@ class WorkitCore {
|
|
|
21913
23024
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
21914
23025
|
}
|
|
21915
23026
|
finding(request) {
|
|
21916
|
-
|
|
23027
|
+
const resolved = this.implicitTask("finding", request);
|
|
23028
|
+
if (!resolved.ok)
|
|
23029
|
+
return resolved;
|
|
23030
|
+
const value = resolved.data;
|
|
23031
|
+
return this.retryOmittedRevisions(value, () => this.findingOnce(value));
|
|
21917
23032
|
}
|
|
21918
23033
|
findingOnce(request) {
|
|
21919
23034
|
const root = this.contextRootError();
|
|
@@ -22039,7 +23154,11 @@ class WorkitCore {
|
|
|
22039
23154
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
22040
23155
|
}
|
|
22041
23156
|
worker(request) {
|
|
22042
|
-
|
|
23157
|
+
const resolved = this.implicitTask("worker", request);
|
|
23158
|
+
if (!resolved.ok)
|
|
23159
|
+
return resolved;
|
|
23160
|
+
const value = resolved.data;
|
|
23161
|
+
return this.retryOmittedRevisions(value, () => this.workerOnce(value));
|
|
22043
23162
|
}
|
|
22044
23163
|
workerOnce(request) {
|
|
22045
23164
|
const root = this.contextRootError();
|
|
@@ -22389,7 +23508,11 @@ class WorkitCore {
|
|
|
22389
23508
|
return applied;
|
|
22390
23509
|
}
|
|
22391
23510
|
writer(request) {
|
|
22392
|
-
|
|
23511
|
+
const resolved = this.implicitTask("writer", request);
|
|
23512
|
+
if (!resolved.ok)
|
|
23513
|
+
return resolved;
|
|
23514
|
+
const value = resolved.data;
|
|
23515
|
+
return this.retryOmittedRevisions(value, () => this.writerOnce(value));
|
|
22393
23516
|
}
|
|
22394
23517
|
writerOnce(request) {
|
|
22395
23518
|
const root = this.contextRootError();
|
|
@@ -23025,22 +24148,22 @@ function resolveGitRevision(root, value) {
|
|
|
23025
24148
|
}
|
|
23026
24149
|
function resolveInside(root, candidate) {
|
|
23027
24150
|
const base = realpathSync7(root);
|
|
23028
|
-
const target =
|
|
23029
|
-
if (target !== base && !target.startsWith(base +
|
|
24151
|
+
const target = path15.resolve(base, candidate);
|
|
24152
|
+
if (target !== base && !target.startsWith(base + path15.sep)) {
|
|
23030
24153
|
throw new Error("path must stay inside repository root");
|
|
23031
24154
|
}
|
|
23032
24155
|
let ancestor = target;
|
|
23033
24156
|
while (!existsSync5(ancestor))
|
|
23034
|
-
ancestor =
|
|
24157
|
+
ancestor = path15.dirname(ancestor);
|
|
23035
24158
|
const canonicalAncestor = realpathSync7(ancestor);
|
|
23036
|
-
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base +
|
|
24159
|
+
if (canonicalAncestor !== base && !canonicalAncestor.startsWith(base + path15.sep)) {
|
|
23037
24160
|
throw new Error("path must stay inside repository root");
|
|
23038
24161
|
}
|
|
23039
24162
|
return target;
|
|
23040
24163
|
}
|
|
23041
24164
|
function run2(root, executable, args, env = {}) {
|
|
23042
24165
|
const cwd = realpathSync7(root);
|
|
23043
|
-
const result =
|
|
24166
|
+
const result = spawnSync7(executable, args, {
|
|
23044
24167
|
cwd,
|
|
23045
24168
|
encoding: "utf8",
|
|
23046
24169
|
env: { ...process.env, ...env }
|
|
@@ -23065,7 +24188,7 @@ var SUPPORT_MATRIX = {
|
|
|
23065
24188
|
|
|
23066
24189
|
// packages/workit-mcp/src/server.ts
|
|
23067
24190
|
import { existsSync as existsSync7, readFileSync as readFileSync8, realpathSync as realpathSync8 } from "node:fs";
|
|
23068
|
-
import
|
|
24191
|
+
import path19 from "node:path";
|
|
23069
24192
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
|
23070
24193
|
function isZ4Schema(s) {
|
|
23071
24194
|
const schema = s;
|
|
@@ -24837,7 +25960,7 @@ class Protocol {
|
|
|
24837
25960
|
};
|
|
24838
25961
|
}
|
|
24839
25962
|
}
|
|
24840
|
-
function
|
|
25963
|
+
function isPlainObject3(value) {
|
|
24841
25964
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24842
25965
|
}
|
|
24843
25966
|
function mergeCapabilities(base, additional) {
|
|
@@ -24848,7 +25971,7 @@ function mergeCapabilities(base, additional) {
|
|
|
24848
25971
|
if (addValue === undefined)
|
|
24849
25972
|
continue;
|
|
24850
25973
|
const baseValue = result[k];
|
|
24851
|
-
if (
|
|
25974
|
+
if (isPlainObject3(baseValue) && isPlainObject3(addValue)) {
|
|
24852
25975
|
result[k] = { ...baseValue, ...addValue };
|
|
24853
25976
|
} else {
|
|
24854
25977
|
result[k] = addValue;
|
|
@@ -25501,9 +26624,9 @@ var applyPatterns = (value) => {
|
|
|
25501
26624
|
var redactSecrets = (value) => applyPatterns(value);
|
|
25502
26625
|
|
|
25503
26626
|
// packages/workit-core/src/core/pr-create.ts
|
|
25504
|
-
import
|
|
25505
|
-
import
|
|
25506
|
-
import { spawnSync as
|
|
26627
|
+
import fs13 from "node:fs";
|
|
26628
|
+
import path16 from "node:path";
|
|
26629
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
25507
26630
|
// packages/workit-core/src/forge/pr-body.ts
|
|
25508
26631
|
function parseGhIssue(value) {
|
|
25509
26632
|
const m = /issues\/(\d+)/.exec(value);
|
|
@@ -25513,13 +26636,13 @@ function parseGhIssue(value) {
|
|
|
25513
26636
|
// packages/workit-core/src/core/pr-create.ts
|
|
25514
26637
|
function whichOnPath(tool) {
|
|
25515
26638
|
const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
|
|
25516
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
26639
|
+
for (const dir of (process.env.PATH ?? "").split(path16.delimiter)) {
|
|
25517
26640
|
if (!dir)
|
|
25518
26641
|
continue;
|
|
25519
26642
|
for (const name of names) {
|
|
25520
|
-
const candidate =
|
|
26643
|
+
const candidate = path16.join(dir, name);
|
|
25521
26644
|
try {
|
|
25522
|
-
|
|
26645
|
+
fs13.accessSync(candidate, fs13.constants.X_OK);
|
|
25523
26646
|
return candidate;
|
|
25524
26647
|
} catch {}
|
|
25525
26648
|
}
|
|
@@ -25528,7 +26651,7 @@ function whichOnPath(tool) {
|
|
|
25528
26651
|
}
|
|
25529
26652
|
var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
|
|
25530
26653
|
var pushRemote = (cwd) => {
|
|
25531
|
-
const result =
|
|
26654
|
+
const result = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
25532
26655
|
cwd,
|
|
25533
26656
|
encoding: "utf8"
|
|
25534
26657
|
});
|
|
@@ -25539,9 +26662,9 @@ var pushRemote = (cwd) => {
|
|
|
25539
26662
|
};
|
|
25540
26663
|
|
|
25541
26664
|
// packages/workit-core/src/core/repo-context.ts
|
|
25542
|
-
import { spawnSync as
|
|
26665
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
25543
26666
|
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
25544
|
-
import
|
|
26667
|
+
import path17 from "node:path";
|
|
25545
26668
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
25546
26669
|
try {
|
|
25547
26670
|
gitRevisionParts(value);
|
|
@@ -25568,7 +26691,7 @@ var invalidRange = (cwd) => ({
|
|
|
25568
26691
|
cwd
|
|
25569
26692
|
});
|
|
25570
26693
|
var runGit = (cwd, args) => {
|
|
25571
|
-
const result =
|
|
26694
|
+
const result = spawnSync9("git", args, { cwd, encoding: "utf8" });
|
|
25572
26695
|
return {
|
|
25573
26696
|
stdout: (result.stdout ?? "").trimEnd(),
|
|
25574
26697
|
stderr: (result.stderr ?? "").trimEnd(),
|
|
@@ -25675,7 +26798,7 @@ var findPrTemplate = (cwd) => {
|
|
|
25675
26798
|
]) {
|
|
25676
26799
|
const parts = rel.split("/");
|
|
25677
26800
|
const base = parts[parts.length - 1];
|
|
25678
|
-
const dir = parts.length > 1 ?
|
|
26801
|
+
const dir = parts.length > 1 ? path17.join(cwd, ...parts.slice(0, -1)) : cwd;
|
|
25679
26802
|
const actual = probeCaseInsensitive(dir, base);
|
|
25680
26803
|
if (actual)
|
|
25681
26804
|
return [...parts.slice(0, -1), actual].join("/");
|
|
@@ -25706,7 +26829,7 @@ var diffStatForRange = (cwd, range) => {
|
|
|
25706
26829
|
return runGit(cwd, ["diff", "--stat", "--"]).stdout;
|
|
25707
26830
|
};
|
|
25708
26831
|
var packageScripts = (cwd, keys) => {
|
|
25709
|
-
const pkgPath =
|
|
26832
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
25710
26833
|
if (!existsSync6(pkgPath))
|
|
25711
26834
|
return [];
|
|
25712
26835
|
try {
|
|
@@ -25732,9 +26855,9 @@ var documentationFiles = (cwd) => {
|
|
|
25732
26855
|
if (entry.isDirectory()) {
|
|
25733
26856
|
if (excluded.has(entry.name))
|
|
25734
26857
|
continue;
|
|
25735
|
-
walk(
|
|
26858
|
+
walk(path17.join(dir, entry.name), depth + 1);
|
|
25736
26859
|
} else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
|
|
25737
|
-
matches.push("./" +
|
|
26860
|
+
matches.push("./" + path17.relative(cwd, path17.join(dir, entry.name)).replaceAll("\\", "/"));
|
|
25738
26861
|
}
|
|
25739
26862
|
}
|
|
25740
26863
|
};
|
|
@@ -25750,7 +26873,7 @@ var readTrimmed2 = (file, maxLines) => {
|
|
|
25750
26873
|
`);
|
|
25751
26874
|
};
|
|
25752
26875
|
function prReadyContext(root, range) {
|
|
25753
|
-
const cwd =
|
|
26876
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25754
26877
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25755
26878
|
return invalidRange(cwd);
|
|
25756
26879
|
let stdout = "";
|
|
@@ -25808,7 +26931,7 @@ ${status.stderr}` : "");
|
|
|
25808
26931
|
stdout += `template_path: ${template}
|
|
25809
26932
|
|
|
25810
26933
|
`;
|
|
25811
|
-
stdout += readTrimmed2(
|
|
26934
|
+
stdout += readTrimmed2(path17.join(cwd, template), 220);
|
|
25812
26935
|
} else {
|
|
25813
26936
|
stdout += `template_path: none
|
|
25814
26937
|
|
|
@@ -25818,7 +26941,7 @@ ${status.stderr}` : "");
|
|
|
25818
26941
|
stdout += `
|
|
25819
26942
|
`;
|
|
25820
26943
|
stdout += printSection("Recent Validation Signals");
|
|
25821
|
-
if (existsSync6(
|
|
26944
|
+
if (existsSync6(path17.join(cwd, "package.json"))) {
|
|
25822
26945
|
stdout += `package.json detected. Common scripts:
|
|
25823
26946
|
`;
|
|
25824
26947
|
const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
|
|
@@ -25827,7 +26950,7 @@ ${status.stderr}` : "");
|
|
|
25827
26950
|
`) + `
|
|
25828
26951
|
`;
|
|
25829
26952
|
}
|
|
25830
|
-
if (existsSync6(
|
|
26953
|
+
if (existsSync6(path17.join(cwd, "Cargo.toml")) || existsSync6(path17.join(cwd, "src-tauri/Cargo.toml"))) {
|
|
25831
26954
|
stdout += `Rust project detected.
|
|
25832
26955
|
`;
|
|
25833
26956
|
}
|
|
@@ -25860,7 +26983,7 @@ var CHANGELOG_RULES = `- Use an [Unreleased] section.
|
|
|
25860
26983
|
- Apply with the host-native editor, preserving the repository changelog conventions.
|
|
25861
26984
|
- If Unreleased already has duplicate category headings, normalize_only first.`;
|
|
25862
26985
|
function changelogContext(root, range) {
|
|
25863
|
-
const cwd =
|
|
26986
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25864
26987
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25865
26988
|
return invalidRange(cwd);
|
|
25866
26989
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -25878,7 +27001,7 @@ function changelogContext(root, range) {
|
|
|
25878
27001
|
stdout += CHANGELOG_RULES + `
|
|
25879
27002
|
`;
|
|
25880
27003
|
stdout += printSection("Existing CHANGELOG.md");
|
|
25881
|
-
const changelogPath =
|
|
27004
|
+
const changelogPath = path17.join(cwd, "CHANGELOG.md");
|
|
25882
27005
|
if (existsSync6(changelogPath)) {
|
|
25883
27006
|
stdout += readTrimmed2(changelogPath, 260) + `
|
|
25884
27007
|
`;
|
|
@@ -25898,7 +27021,7 @@ function changelogContext(root, range) {
|
|
|
25898
27021
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
25899
27022
|
}
|
|
25900
27023
|
function docsRefreshContext(root, range) {
|
|
25901
|
-
const cwd =
|
|
27024
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25902
27025
|
if (range !== undefined && !isResolvableContextRange(cwd, range))
|
|
25903
27026
|
return invalidRange(cwd);
|
|
25904
27027
|
const resolvedRange = rangeArgOrDefault(range, cwd);
|
|
@@ -25920,7 +27043,7 @@ function docsRefreshContext(root, range) {
|
|
|
25920
27043
|
`) + `
|
|
25921
27044
|
`;
|
|
25922
27045
|
stdout += printSection("README Preview");
|
|
25923
|
-
const readme =
|
|
27046
|
+
const readme = path17.join(cwd, "README.md");
|
|
25924
27047
|
if (existsSync6(readme)) {
|
|
25925
27048
|
stdout += readTrimmed2(readme, 220) + `
|
|
25926
27049
|
`;
|
|
@@ -25929,7 +27052,7 @@ function docsRefreshContext(root, range) {
|
|
|
25929
27052
|
`;
|
|
25930
27053
|
}
|
|
25931
27054
|
stdout += printSection("Package Scripts");
|
|
25932
|
-
const pkgPath =
|
|
27055
|
+
const pkgPath = path17.join(cwd, "package.json");
|
|
25933
27056
|
if (existsSync6(pkgPath)) {
|
|
25934
27057
|
try {
|
|
25935
27058
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
@@ -25943,7 +27066,7 @@ function docsRefreshContext(root, range) {
|
|
|
25943
27066
|
return { stdout, stderr: "", exitCode: 0, cwd };
|
|
25944
27067
|
}
|
|
25945
27068
|
function releaseNotesContext(root, rangeOrTag) {
|
|
25946
|
-
const cwd =
|
|
27069
|
+
const cwd = path17.resolve(repoRoot(root));
|
|
25947
27070
|
if (!rangeOrTag) {
|
|
25948
27071
|
return { stdout: "", stderr: `ERROR: release tag or range required
|
|
25949
27072
|
`, exitCode: 1, cwd };
|
|
@@ -25981,7 +27104,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
25981
27104
|
`;
|
|
25982
27105
|
stdout += printSection("Existing Release Files");
|
|
25983
27106
|
for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
|
|
25984
|
-
if (existsSync6(
|
|
27107
|
+
if (existsSync6(path17.join(cwd, rel)))
|
|
25985
27108
|
stdout += rel + `
|
|
25986
27109
|
`;
|
|
25987
27110
|
}
|
|
@@ -26009,7 +27132,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
26009
27132
|
}
|
|
26010
27133
|
|
|
26011
27134
|
// packages/workit-core/src/core/tracker-issues.ts
|
|
26012
|
-
import { spawnSync as
|
|
27135
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
26013
27136
|
var repoPathFromRemote = (remote) => {
|
|
26014
27137
|
let rest = (remote || "").trim().replace(/\/+$/, "");
|
|
26015
27138
|
if (!rest)
|
|
@@ -26030,7 +27153,7 @@ var defaultCli = (bin, available) => {
|
|
|
26030
27153
|
if (!available)
|
|
26031
27154
|
return null;
|
|
26032
27155
|
return (args, root) => {
|
|
26033
|
-
const out =
|
|
27156
|
+
const out = spawnSync10(bin, args, {
|
|
26034
27157
|
cwd: root,
|
|
26035
27158
|
encoding: "utf8",
|
|
26036
27159
|
env: { ...process.env, PATH: process.env.PATH ?? "" }
|
|
@@ -26108,8 +27231,8 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
|
|
|
26108
27231
|
};
|
|
26109
27232
|
|
|
26110
27233
|
// packages/workit-core/src/core/youtrack.ts
|
|
26111
|
-
import
|
|
26112
|
-
import
|
|
27234
|
+
import fs14 from "node:fs";
|
|
27235
|
+
import path18 from "node:path";
|
|
26113
27236
|
|
|
26114
27237
|
// packages/workit-core/src/core/templates.ts
|
|
26115
27238
|
var repoRoot2 = assetRoot();
|
|
@@ -26122,22 +27245,22 @@ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
|
|
|
26122
27245
|
// packages/workit-core/src/core/youtrack.ts
|
|
26123
27246
|
var ISSUE_RE = /^[A-Z]+-\d+$/;
|
|
26124
27247
|
var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
26125
|
-
var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
26126
|
-
var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ??
|
|
27248
|
+
var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(configDir(), "youtrack.json");
|
|
27249
|
+
var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(resolveConfigDir(), "youtrack.json");
|
|
26127
27250
|
var youTrackTokenModeOk = (p) => {
|
|
26128
27251
|
if (process.platform === "win32")
|
|
26129
27252
|
return true;
|
|
26130
|
-
const mode =
|
|
27253
|
+
const mode = fs14.statSync(p).mode & 511;
|
|
26131
27254
|
return mode === 384;
|
|
26132
27255
|
};
|
|
26133
27256
|
function readYouTrackConfig(required) {
|
|
26134
27257
|
const cfgPath = youTrackConfigPath();
|
|
26135
|
-
if (!
|
|
27258
|
+
if (!fs14.existsSync(cfgPath)) {
|
|
26136
27259
|
return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
|
|
26137
27260
|
}
|
|
26138
27261
|
let parsed;
|
|
26139
27262
|
try {
|
|
26140
|
-
parsed = JSON.parse(
|
|
27263
|
+
parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
|
|
26141
27264
|
} catch {
|
|
26142
27265
|
return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
|
|
26143
27266
|
}
|
|
@@ -26148,11 +27271,11 @@ function readYouTrackConfig(required) {
|
|
|
26148
27271
|
}
|
|
26149
27272
|
var readYouTrackContextConfig = () => {
|
|
26150
27273
|
const cfgPath = youTrackReadOnlyConfigPath();
|
|
26151
|
-
if (!
|
|
27274
|
+
if (!fs14.existsSync(cfgPath))
|
|
26152
27275
|
return { data: {} };
|
|
26153
27276
|
let parsed;
|
|
26154
27277
|
try {
|
|
26155
|
-
parsed = JSON.parse(
|
|
27278
|
+
parsed = JSON.parse(fs14.readFileSync(cfgPath, "utf8"));
|
|
26156
27279
|
} catch {
|
|
26157
27280
|
return { error: "YouTrack configuration is unavailable" };
|
|
26158
27281
|
}
|
|
@@ -26191,12 +27314,12 @@ var youTrackToken = () => {
|
|
|
26191
27314
|
if ("error" in loaded)
|
|
26192
27315
|
return loaded;
|
|
26193
27316
|
const tokenFile = String(loaded.config.tokenFile ?? "");
|
|
26194
|
-
const tokenPath = tokenFile ?
|
|
26195
|
-
if (!tokenPath || !
|
|
27317
|
+
const tokenPath = tokenFile ? path18.isAbsolute(tokenFile) ? path18.resolve(tokenFile) : path18.resolve(process.cwd(), tokenFile) : "";
|
|
27318
|
+
if (!tokenPath || !fs14.existsSync(tokenPath))
|
|
26196
27319
|
return { error: "missing youtrack.token" };
|
|
26197
27320
|
if (!youTrackTokenModeOk(tokenPath))
|
|
26198
27321
|
return { error: "youtrack.token mode must be 0600" };
|
|
26199
|
-
const token =
|
|
27322
|
+
const token = fs14.readFileSync(tokenPath, "utf8").trim();
|
|
26200
27323
|
if (!token)
|
|
26201
27324
|
return { error: "empty token file" };
|
|
26202
27325
|
if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
|
|
@@ -26261,9 +27384,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
|
|
|
26261
27384
|
} catch {
|
|
26262
27385
|
continue;
|
|
26263
27386
|
}
|
|
26264
|
-
if (!
|
|
27387
|
+
if (!fs14.existsSync(full))
|
|
26265
27388
|
continue;
|
|
26266
|
-
const text =
|
|
27389
|
+
const text = fs14.readFileSync(full, "utf8");
|
|
26267
27390
|
const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
|
|
26268
27391
|
if (m)
|
|
26269
27392
|
return m[1];
|
|
@@ -26557,13 +27680,13 @@ var rootVariants = (workspaceRoot) => {
|
|
|
26557
27680
|
if (!workspaceRoot)
|
|
26558
27681
|
return [];
|
|
26559
27682
|
const roots = new Set;
|
|
26560
|
-
for (const candidate of [workspaceRoot,
|
|
26561
|
-
if (
|
|
27683
|
+
for (const candidate of [workspaceRoot, path19.resolve(workspaceRoot)]) {
|
|
27684
|
+
if (path19.isAbsolute(candidate) && candidate !== path19.parse(candidate).root)
|
|
26562
27685
|
roots.add(candidate);
|
|
26563
27686
|
}
|
|
26564
27687
|
try {
|
|
26565
27688
|
const canonical = realpathSync8(workspaceRoot);
|
|
26566
|
-
if (canonical !==
|
|
27689
|
+
if (canonical !== path19.parse(canonical).root)
|
|
26567
27690
|
roots.add(canonical);
|
|
26568
27691
|
} catch {}
|
|
26569
27692
|
return [...roots].toSorted((left, right) => right.length - left.length);
|
|
@@ -26664,11 +27787,11 @@ var thrownResult = (tool, error, workspaceRoot) => {
|
|
|
26664
27787
|
};
|
|
26665
27788
|
function createMcpServer(host, contextProvider) {
|
|
26666
27789
|
assertMcpHost(host);
|
|
26667
|
-
const here =
|
|
27790
|
+
const here = path19.dirname(fileURLToPath2(import.meta.url));
|
|
26668
27791
|
const sourceMarker = markSourcesLoaded([
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
27792
|
+
path19.join(here, "server.ts"),
|
|
27793
|
+
path19.join(here, "..", "..", "workit-core", "src", "core", "task-contract.ts"),
|
|
27794
|
+
path19.join(here, "..", "..", "workit-core", "src", "core", "task-engine.ts")
|
|
26672
27795
|
].filter((file) => existsSync7(file)));
|
|
26673
27796
|
let staleWarned = false;
|
|
26674
27797
|
const server = new Server({ name: "workit", version: VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
@@ -27006,8 +28129,8 @@ var codexQualification = (host) => host === "codex_cli" ? { surface: host, cli:
|
|
|
27006
28129
|
var resolveCodexWorkspaceRoot = (pluginRoot, env) => {
|
|
27007
28130
|
const explicit = env.WORKFLOW_WORKSPACE_ROOT;
|
|
27008
28131
|
const inherited = env.PWD;
|
|
27009
|
-
const candidate = explicit ?? (inherited &&
|
|
27010
|
-
if (!candidate || !
|
|
28132
|
+
const candidate = explicit ?? (inherited && path20.resolve(inherited) !== path20.resolve(pluginRoot) ? inherited : undefined);
|
|
28133
|
+
if (!candidate || !path20.isAbsolute(candidate) || !existsSync8(candidate))
|
|
27011
28134
|
return null;
|
|
27012
28135
|
try {
|
|
27013
28136
|
if (!statSync3(candidate).isDirectory())
|