@brainervirus/workit-mcp 2.1.5 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1638 -1152
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16783,7 +16783,7 @@ class StdioServerTransport {
|
|
|
16783
16783
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16784
16784
|
|
|
16785
16785
|
// packages/workit-core/src/core.ts
|
|
16786
|
-
import { spawnSync as
|
|
16786
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
16787
16787
|
import { existsSync as existsSync5, realpathSync as realpathSync7 } from "node:fs";
|
|
16788
16788
|
import path11 from "node:path";
|
|
16789
16789
|
|
|
@@ -16863,7 +16863,8 @@ var hostSchema = _enum([
|
|
|
16863
16863
|
"codex_cli",
|
|
16864
16864
|
"codex_desktop",
|
|
16865
16865
|
"pi",
|
|
16866
|
-
"workit_cli"
|
|
16866
|
+
"workit_cli",
|
|
16867
|
+
"claude_code"
|
|
16867
16868
|
]);
|
|
16868
16869
|
var assuranceSchema = _enum(["enforced", "agent_guided", "unavailable"]);
|
|
16869
16870
|
var outcomeSchema = _enum(["verified", "accepted_limitations", "stopped"]);
|
|
@@ -17313,15 +17314,70 @@ var taskRecordSchema = object2({
|
|
|
17313
17314
|
decisions: array(entrySchema(decisionSchema)),
|
|
17314
17315
|
actionProgress: actionProgressListSchema.optional(),
|
|
17315
17316
|
findings: array(entrySchema(findingSchema)),
|
|
17316
|
-
workers: array(entrySchema(workerSchema))
|
|
17317
|
+
workers: array(entrySchema(workerSchema)),
|
|
17318
|
+
critical: array(nonEmpty).optional()
|
|
17317
17319
|
}).strict();
|
|
17320
|
+
var stripCount = (strips) => strips.reduce((sum, strip) => sum + strip.keys.length, 0);
|
|
17321
|
+
var strippable = (issues, prefix = []) => {
|
|
17322
|
+
const strips = [];
|
|
17323
|
+
for (const issue of issues) {
|
|
17324
|
+
if (issue.code === "unrecognized_keys") {
|
|
17325
|
+
strips.push({ path: [...prefix, ...issue.path], keys: issue.keys });
|
|
17326
|
+
continue;
|
|
17327
|
+
}
|
|
17328
|
+
if (issue.code !== "invalid_union")
|
|
17329
|
+
return null;
|
|
17330
|
+
const branch = issue.errors.map((errors) => strippable(errors, [...prefix, ...issue.path])).filter((found) => found !== null && found.length > 0).reduce((best, found) => best === null || stripCount(found) < stripCount(best) ? found : best, null);
|
|
17331
|
+
if (!branch)
|
|
17332
|
+
return null;
|
|
17333
|
+
strips.push(...branch);
|
|
17334
|
+
}
|
|
17335
|
+
return strips;
|
|
17336
|
+
};
|
|
17337
|
+
var recordPath = (path) => path.map((key) => typeof key === "number" ? "*" : String(key)).join(".");
|
|
17338
|
+
var overlaps = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
17339
|
+
var parseStoredRecord = (schema, value) => {
|
|
17340
|
+
let parsed = schema.safeParse(value);
|
|
17341
|
+
if (parsed.success)
|
|
17342
|
+
return { success: true, data: parsed.data, stripped: [] };
|
|
17343
|
+
const first = { success: false, error: parsed.error, critical: [] };
|
|
17344
|
+
const declared = typeof value === "object" && value !== null && Array.isArray(value.critical) ? value.critical.filter((item) => typeof item === "string").map((item) => item.split(".").map((key) => /^\d+$/.test(key) ? "*" : key).join(".")) : [];
|
|
17345
|
+
const stripped = [];
|
|
17346
|
+
let current = structuredClone(value);
|
|
17347
|
+
for (let round = 0;round < 8 && !parsed.success; round++) {
|
|
17348
|
+
const strips = strippable(parsed.error.issues);
|
|
17349
|
+
if (strips === null || strips.length === 0)
|
|
17350
|
+
return first;
|
|
17351
|
+
for (const strip of strips) {
|
|
17352
|
+
let target = current;
|
|
17353
|
+
for (const key of strip.path)
|
|
17354
|
+
target = typeof target === "object" && target !== null ? target[key] : undefined;
|
|
17355
|
+
if (typeof target !== "object" || target === null)
|
|
17356
|
+
return first;
|
|
17357
|
+
for (const key of strip.keys) {
|
|
17358
|
+
stripped.push(recordPath([...strip.path, key]));
|
|
17359
|
+
delete target[key];
|
|
17360
|
+
}
|
|
17361
|
+
}
|
|
17362
|
+
parsed = schema.safeParse(current);
|
|
17363
|
+
}
|
|
17364
|
+
if (!parsed.success)
|
|
17365
|
+
return first;
|
|
17366
|
+
const critical = [
|
|
17367
|
+
...new Set(stripped.filter((item) => declared.some((path) => overlaps(item, path))))
|
|
17368
|
+
];
|
|
17369
|
+
if (critical.length > 0)
|
|
17370
|
+
return { ...first, critical };
|
|
17371
|
+
return { success: true, data: parsed.data, stripped: [...new Set(stripped)] };
|
|
17372
|
+
};
|
|
17318
17373
|
var workspaceRecordSchema = object2({
|
|
17319
17374
|
schemaVersion: literal(1),
|
|
17320
17375
|
id,
|
|
17321
17376
|
revision,
|
|
17322
17377
|
root: nonEmpty,
|
|
17323
17378
|
runtime: runtimeSchema.optional(),
|
|
17324
|
-
writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable()
|
|
17379
|
+
writer: object2({ state: _enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc }).strict().nullable(),
|
|
17380
|
+
critical: array(nonEmpty).optional()
|
|
17325
17381
|
}).strict();
|
|
17326
17382
|
var capabilitySchema = object2({
|
|
17327
17383
|
name: nonEmpty,
|
|
@@ -17559,6 +17615,11 @@ var operationSchemas = {
|
|
|
17559
17615
|
writer: discriminatedUnion("action", Object.values(writerOperations)),
|
|
17560
17616
|
state: discriminatedUnion("action", Object.values(stateOperations))
|
|
17561
17617
|
};
|
|
17618
|
+
var { recover: _unadvertisedRecover, ...advertisedStateOperations } = stateOperations;
|
|
17619
|
+
var advertisedOperationSchemas = {
|
|
17620
|
+
...operationSchemas,
|
|
17621
|
+
state: discriminatedUnion("action", Object.values(advertisedStateOperations))
|
|
17622
|
+
};
|
|
17562
17623
|
var compiledOperationSchemas = Object.fromEntries(OPERATION_FAMILIES.map((family) => [family, compile(operationSchemas[family])]));
|
|
17563
17624
|
var success = (revisionValue, workspaceRevisionValue, data) => ({
|
|
17564
17625
|
ok: true,
|
|
@@ -17582,7 +17643,7 @@ function parseOperation(family, input) {
|
|
|
17582
17643
|
return failure2("invalid_input", "operation input is invalid", { fields });
|
|
17583
17644
|
}
|
|
17584
17645
|
function operationJsonSchema(family) {
|
|
17585
|
-
return toJSONSchema(
|
|
17646
|
+
return toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
|
|
17586
17647
|
}
|
|
17587
17648
|
var OPERATION_SCHEMA_DEPTH = 1;
|
|
17588
17649
|
var schemaShapeDescription = (value, depth = 0) => {
|
|
@@ -17774,8 +17835,7 @@ function requirementId(input) {
|
|
|
17774
17835
|
// packages/workit-core/src/core/task-store.ts
|
|
17775
17836
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
17776
17837
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17777
|
-
import * as
|
|
17778
|
-
import { hostname } from "node:os";
|
|
17838
|
+
import * as fs5 from "node:fs";
|
|
17779
17839
|
import path4 from "node:path";
|
|
17780
17840
|
|
|
17781
17841
|
// packages/workit-core/src/core/package-root.ts
|
|
@@ -18398,13 +18458,10 @@ function acquireFileLockSync(targetPath, options) {
|
|
|
18398
18458
|
}
|
|
18399
18459
|
}
|
|
18400
18460
|
}
|
|
18401
|
-
// packages/workit-core/src/core/
|
|
18402
|
-
|
|
18403
|
-
|
|
18404
|
-
|
|
18405
|
-
processStart: string2().nullable(),
|
|
18406
|
-
ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
|
|
18407
|
-
}).strict();
|
|
18461
|
+
// packages/workit-core/src/core/store-lock.ts
|
|
18462
|
+
import { spawnSync } from "node:child_process";
|
|
18463
|
+
import fs4 from "node:fs";
|
|
18464
|
+
import { hostname } from "node:os";
|
|
18408
18465
|
var metadataLockSchema = object2({
|
|
18409
18466
|
pid: number2().int().nonnegative().safe(),
|
|
18410
18467
|
processStart: string2().nullable(),
|
|
@@ -18412,6 +18469,143 @@ var metadataLockSchema = object2({
|
|
|
18412
18469
|
nonce: string2().min(1),
|
|
18413
18470
|
externalAction: literal(true).optional()
|
|
18414
18471
|
}).strict();
|
|
18472
|
+
var FOREIGN_LOCK_TTL_MS = 10 * 60000;
|
|
18473
|
+
var UNREADABLE_LOCK_TTL_MS = 30000;
|
|
18474
|
+
var RECLAIM_GUARD_TTL_MS = 30000;
|
|
18475
|
+
var defaultLockTimeoutMs = 250;
|
|
18476
|
+
var defaultLockTimeout = () => defaultLockTimeoutMs;
|
|
18477
|
+
var parseMetadataLock = (raw) => {
|
|
18478
|
+
let value;
|
|
18479
|
+
try {
|
|
18480
|
+
value = JSON.parse(raw);
|
|
18481
|
+
} catch {
|
|
18482
|
+
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
18483
|
+
}
|
|
18484
|
+
const parsed = metadataLockSchema.safeParse(value);
|
|
18485
|
+
if (!parsed.success)
|
|
18486
|
+
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
18487
|
+
return parsed.data;
|
|
18488
|
+
};
|
|
18489
|
+
var parseMetadataLockOrNull = (raw) => {
|
|
18490
|
+
try {
|
|
18491
|
+
return parseMetadataLock(raw);
|
|
18492
|
+
} catch {
|
|
18493
|
+
return null;
|
|
18494
|
+
}
|
|
18495
|
+
};
|
|
18496
|
+
var sameMetadataLock = (left, right) => {
|
|
18497
|
+
const parsed = metadataLockSchema.safeParse(left);
|
|
18498
|
+
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
18499
|
+
};
|
|
18500
|
+
var parseProcStatStart = (stat) => {
|
|
18501
|
+
const close = stat.lastIndexOf(")");
|
|
18502
|
+
if (close < 0)
|
|
18503
|
+
return null;
|
|
18504
|
+
return stat.slice(close + 1).trim().split(/\s+/)[19] ?? null;
|
|
18505
|
+
};
|
|
18506
|
+
var processStartOf = (pid) => {
|
|
18507
|
+
if (process.platform === "linux") {
|
|
18508
|
+
try {
|
|
18509
|
+
return parseProcStatStart(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
18510
|
+
} catch {
|
|
18511
|
+
return null;
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
18515
|
+
try {
|
|
18516
|
+
const run = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
18517
|
+
encoding: "utf8",
|
|
18518
|
+
timeout: 1000
|
|
18519
|
+
});
|
|
18520
|
+
const value = run.status === 0 ? run.stdout.trim() : "";
|
|
18521
|
+
return value || null;
|
|
18522
|
+
} catch {
|
|
18523
|
+
return null;
|
|
18524
|
+
}
|
|
18525
|
+
}
|
|
18526
|
+
return null;
|
|
18527
|
+
};
|
|
18528
|
+
var readTrimmed = (file) => {
|
|
18529
|
+
try {
|
|
18530
|
+
return fs4.readFileSync(file, "utf8").trim() || null;
|
|
18531
|
+
} catch {
|
|
18532
|
+
return null;
|
|
18533
|
+
}
|
|
18534
|
+
};
|
|
18535
|
+
var cachedLockHost = null;
|
|
18536
|
+
var localLockHost = () => {
|
|
18537
|
+
if (cachedLockHost !== null)
|
|
18538
|
+
return cachedLockHost;
|
|
18539
|
+
let pidns = null;
|
|
18540
|
+
try {
|
|
18541
|
+
pidns = /\[(\d+)\]/.exec(fs4.readlinkSync("/proc/self/ns/pid"))?.[1] ?? null;
|
|
18542
|
+
} catch {}
|
|
18543
|
+
const boot = readTrimmed("/proc/sys/kernel/random/boot_id");
|
|
18544
|
+
cachedLockHost = pidns || boot ? `${hostname()}#${pidns ?? "?"}:${boot ?? "?"}` : hostname();
|
|
18545
|
+
return cachedLockHost;
|
|
18546
|
+
};
|
|
18547
|
+
var pidAlive = (pid) => {
|
|
18548
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
18549
|
+
return false;
|
|
18550
|
+
try {
|
|
18551
|
+
process.kill(pid, 0);
|
|
18552
|
+
return true;
|
|
18553
|
+
} catch (error) {
|
|
18554
|
+
return error.code === "EPERM";
|
|
18555
|
+
}
|
|
18556
|
+
};
|
|
18557
|
+
var classifyLockOwner = (payload, ageMs, localHost = localLockHost()) => {
|
|
18558
|
+
const lock = metadataLockSchema.safeParse(payload);
|
|
18559
|
+
if (!lock.success)
|
|
18560
|
+
return ageMs !== null && ageMs > UNREADABLE_LOCK_TTL_MS ? { state: "stale", reason: "unreadable lock left behind" } : { state: "unknown", reason: "lock is being written" };
|
|
18561
|
+
const { pid, processStart, host } = lock.data;
|
|
18562
|
+
const [lockName, lockSpace] = host.split("#");
|
|
18563
|
+
const [localName, localSpace] = localHost.split("#");
|
|
18564
|
+
if (lockSpace && localSpace && lockName === localName) {
|
|
18565
|
+
const [lockNs, lockBoot] = lockSpace.split(":");
|
|
18566
|
+
const [localNs, localBoot] = localSpace.split(":");
|
|
18567
|
+
if (lockNs === localNs && lockNs !== "?" && lockBoot !== "?" && lockBoot !== localBoot)
|
|
18568
|
+
return { state: "stale", reason: "lock was taken before this machine rebooted" };
|
|
18569
|
+
}
|
|
18570
|
+
if (host !== localHost)
|
|
18571
|
+
return ageMs !== null && ageMs > FOREIGN_LOCK_TTL_MS ? { state: "stale", reason: `lock from ${host} is older than its TTL` } : { state: "unknown", reason: `lock is held from ${host}; its pid cannot be checked here` };
|
|
18572
|
+
if (!pidAlive(pid))
|
|
18573
|
+
return { state: "stale", reason: `pid ${pid} is not running` };
|
|
18574
|
+
const currentStart = processStartOf(pid);
|
|
18575
|
+
if (processStart !== null && currentStart !== null && processStart !== currentStart)
|
|
18576
|
+
return { state: "stale", reason: `pid ${pid} now belongs to a different process` };
|
|
18577
|
+
return { state: "live", reason: `held by running pid ${pid}` };
|
|
18578
|
+
};
|
|
18579
|
+
var ageOf = (file, nowMs) => {
|
|
18580
|
+
try {
|
|
18581
|
+
return nowMs - fs4.lstatSync(file).mtimeMs;
|
|
18582
|
+
} catch {
|
|
18583
|
+
return null;
|
|
18584
|
+
}
|
|
18585
|
+
};
|
|
18586
|
+
var clearAbandonedReclaimGuard = (lockPath, nowMs = Date.now()) => {
|
|
18587
|
+
const guard = `${lockPath}.reclaim`;
|
|
18588
|
+
const age = ageOf(guard, nowMs);
|
|
18589
|
+
if (age === null || age <= RECLAIM_GUARD_TTL_MS)
|
|
18590
|
+
return false;
|
|
18591
|
+
try {
|
|
18592
|
+
fs4.rmdirSync(guard);
|
|
18593
|
+
return true;
|
|
18594
|
+
} catch {
|
|
18595
|
+
return false;
|
|
18596
|
+
}
|
|
18597
|
+
};
|
|
18598
|
+
|
|
18599
|
+
// packages/workit-core/src/core/task-store.ts
|
|
18600
|
+
var RECOVERY_COPIES_PER_RECORD = 3;
|
|
18601
|
+
var STALE_TEMPORARY_MS = 60 * 60000;
|
|
18602
|
+
var RECOVERY_NAME = /^(task|workspace)\.([^.]+)\.([0-9a-f]{64})\.json$/;
|
|
18603
|
+
var processEvidenceSchema = object2({
|
|
18604
|
+
state: _enum(["stopped", "accounted_for"]),
|
|
18605
|
+
pid: number2().int().nonnegative(),
|
|
18606
|
+
processStart: string2().nullable(),
|
|
18607
|
+
ownerDigest: string2().regex(/^[0-9a-f]{64}$/).nullable()
|
|
18608
|
+
}).strict();
|
|
18415
18609
|
var INDEX_VERSION = 1;
|
|
18416
18610
|
var hostSession = (value) => isObject2(value) && value.kind === "host" && typeof value.host === "string" && typeof value.handle === "string" ? { host: value.host, handle: value.handle } : null;
|
|
18417
18611
|
var indexEntry = (task, file) => {
|
|
@@ -18439,7 +18633,7 @@ var indexEntry = (task, file) => {
|
|
|
18439
18633
|
};
|
|
18440
18634
|
var fileSignature = (file) => {
|
|
18441
18635
|
try {
|
|
18442
|
-
const stat =
|
|
18636
|
+
const stat = fs5.statSync(file, { bigint: true });
|
|
18443
18637
|
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
18444
18638
|
} catch {
|
|
18445
18639
|
return null;
|
|
@@ -18456,7 +18650,7 @@ var cachedRuntimeVersion = null;
|
|
|
18456
18650
|
var runtimeVersion = () => {
|
|
18457
18651
|
if (cachedRuntimeVersion === null) {
|
|
18458
18652
|
try {
|
|
18459
|
-
const pkg = JSON.parse(
|
|
18653
|
+
const pkg = JSON.parse(fs5.readFileSync(path4.join(packageRoot(), "package.json"), "utf8"));
|
|
18460
18654
|
cachedRuntimeVersion = typeof pkg.version === "string" && pkg.version ? pkg.version : "0.0.0";
|
|
18461
18655
|
} catch {
|
|
18462
18656
|
cachedRuntimeVersion = "0.0.0";
|
|
@@ -18485,36 +18679,20 @@ var digestBytes = (value) => createHash2("sha256").update(value).digest("hex");
|
|
|
18485
18679
|
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
18486
18680
|
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);
|
|
18487
18681
|
var validDigest = (value) => /^[0-9a-f]{64}$/.test(value);
|
|
18488
|
-
var parseMetadataLock = (raw) => {
|
|
18489
|
-
let value;
|
|
18490
|
-
try {
|
|
18491
|
-
value = JSON.parse(raw);
|
|
18492
|
-
} catch {
|
|
18493
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
18494
|
-
}
|
|
18495
|
-
const parsed = metadataLockSchema.safeParse(value);
|
|
18496
|
-
if (!parsed.success)
|
|
18497
|
-
throw Object.assign(new Error("metadata lock is invalid"), { code: "metadata_lock_invalid" });
|
|
18498
|
-
return parsed.data;
|
|
18499
|
-
};
|
|
18500
|
-
var sameMetadataLock = (left, right) => {
|
|
18501
|
-
const parsed = metadataLockSchema.safeParse(left);
|
|
18502
|
-
return parsed.success && canonicalJson(parsed.data) === canonicalJson(right);
|
|
18503
|
-
};
|
|
18504
18682
|
var sameDirectoryIdentity = (left, right) => {
|
|
18505
18683
|
if (!path4.isAbsolute(left) || !path4.isAbsolute(right))
|
|
18506
18684
|
return false;
|
|
18507
18685
|
const normalizedLeft = path4.resolve(left);
|
|
18508
18686
|
const normalizedRight = path4.resolve(right);
|
|
18509
18687
|
try {
|
|
18510
|
-
const a =
|
|
18511
|
-
const b =
|
|
18688
|
+
const a = fs5.statSync(normalizedLeft, { bigint: true });
|
|
18689
|
+
const b = fs5.statSync(normalizedRight, { bigint: true });
|
|
18512
18690
|
if (!a.isDirectory() || !b.isDirectory())
|
|
18513
18691
|
return false;
|
|
18514
18692
|
if (process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight)
|
|
18515
18693
|
return true;
|
|
18516
|
-
const canonicalLeft =
|
|
18517
|
-
const canonicalRight =
|
|
18694
|
+
const canonicalLeft = fs5.realpathSync(normalizedLeft);
|
|
18695
|
+
const canonicalRight = fs5.realpathSync(normalizedRight);
|
|
18518
18696
|
if (process.platform === "win32" ? canonicalLeft.toLowerCase() === canonicalRight.toLowerCase() : canonicalLeft === canonicalRight)
|
|
18519
18697
|
return true;
|
|
18520
18698
|
return a.dev === b.dev && a.ino !== 0n && a.ino === b.ino;
|
|
@@ -18522,12 +18700,47 @@ var sameDirectoryIdentity = (left, right) => {
|
|
|
18522
18700
|
return false;
|
|
18523
18701
|
}
|
|
18524
18702
|
};
|
|
18703
|
+
var TRANSIENT_WINDOWS_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
18704
|
+
var isTransientWindowsError = (error) => {
|
|
18705
|
+
if (process.platform !== "win32")
|
|
18706
|
+
return false;
|
|
18707
|
+
const code = error?.code;
|
|
18708
|
+
return typeof code === "string" && TRANSIENT_WINDOWS_CODES.has(code);
|
|
18709
|
+
};
|
|
18710
|
+
var retryTransient = (run) => {
|
|
18711
|
+
if (process.platform !== "win32")
|
|
18712
|
+
return run();
|
|
18713
|
+
for (let attempt = 0;; attempt += 1) {
|
|
18714
|
+
try {
|
|
18715
|
+
return run();
|
|
18716
|
+
} catch (error) {
|
|
18717
|
+
if (attempt >= 20 || !isTransientWindowsError(error))
|
|
18718
|
+
throw error;
|
|
18719
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5 * (attempt + 1));
|
|
18720
|
+
}
|
|
18721
|
+
}
|
|
18722
|
+
};
|
|
18525
18723
|
var externalActionLockRoots = new AsyncLocalStorage;
|
|
18724
|
+
var heldInProcess = new Map;
|
|
18725
|
+
var holdRoot = (root) => heldInProcess.set(root, (heldInProcess.get(root) ?? 0) + 1);
|
|
18726
|
+
var dropRoot = (root) => {
|
|
18727
|
+
const count = (heldInProcess.get(root) ?? 1) - 1;
|
|
18728
|
+
if (count > 0)
|
|
18729
|
+
heldInProcess.set(root, count);
|
|
18730
|
+
else
|
|
18731
|
+
heldInProcess.delete(root);
|
|
18732
|
+
};
|
|
18733
|
+
var dedupeCandidates = (candidates) => {
|
|
18734
|
+
const last = new Map(candidates.map((candidate, index) => [candidate.id, index]));
|
|
18735
|
+
return candidates.filter((candidate, index) => last.get(candidate.id) === index);
|
|
18736
|
+
};
|
|
18526
18737
|
|
|
18527
18738
|
class TaskStore {
|
|
18528
18739
|
root;
|
|
18529
|
-
|
|
18530
|
-
|
|
18740
|
+
lockTimeoutMs;
|
|
18741
|
+
constructor(root, options = {}) {
|
|
18742
|
+
this.root = fs5.existsSync(root) ? fs5.realpathSync(root) : path4.resolve(root);
|
|
18743
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? defaultLockTimeout();
|
|
18531
18744
|
}
|
|
18532
18745
|
readTask(taskId) {
|
|
18533
18746
|
if (!validId(taskId))
|
|
@@ -18547,11 +18760,11 @@ class TaskStore {
|
|
|
18547
18760
|
return result.result;
|
|
18548
18761
|
}
|
|
18549
18762
|
listTasks() {
|
|
18550
|
-
if (!
|
|
18763
|
+
if (!fs5.existsSync(this.tasksDir))
|
|
18551
18764
|
return success(null, null, []);
|
|
18552
18765
|
let names;
|
|
18553
18766
|
try {
|
|
18554
|
-
names =
|
|
18767
|
+
names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
18555
18768
|
} catch (error) {
|
|
18556
18769
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
18557
18770
|
path: this.tasksDir
|
|
@@ -18580,11 +18793,11 @@ class TaskStore {
|
|
|
18580
18793
|
return success(null, null, tasks);
|
|
18581
18794
|
}
|
|
18582
18795
|
listTaskIndex() {
|
|
18583
|
-
if (!
|
|
18796
|
+
if (!fs5.existsSync(this.tasksDir))
|
|
18584
18797
|
return success(null, null, []);
|
|
18585
18798
|
let names;
|
|
18586
18799
|
try {
|
|
18587
|
-
names =
|
|
18800
|
+
names = fs5.readdirSync(this.tasksDir).filter((name) => name.endsWith(".json"));
|
|
18588
18801
|
} catch (error) {
|
|
18589
18802
|
return failure2("storage_error", `unable to list tasks: ${String(error)}`, {
|
|
18590
18803
|
path: this.tasksDir
|
|
@@ -18846,7 +19059,7 @@ class TaskStore {
|
|
|
18846
19059
|
if (!current.data)
|
|
18847
19060
|
return failure2("not_found", "workspace not found");
|
|
18848
19061
|
if (current.data.revision !== expected)
|
|
18849
|
-
return this.
|
|
19062
|
+
return this.workspaceConflict(expected, current.data.revision);
|
|
18850
19063
|
const previousBytes = this.snapshotBytes(this.workspacePath);
|
|
18851
19064
|
if (!previousBytes)
|
|
18852
19065
|
return failure2("storage_error", "workspace snapshot disappeared during mutation");
|
|
@@ -18889,7 +19102,7 @@ class TaskStore {
|
|
|
18889
19102
|
if (task.data.revision !== expected)
|
|
18890
19103
|
return this.conflict(expected, task.data.revision);
|
|
18891
19104
|
if (workspace.data.revision !== input.expectedWorkspaceRevision)
|
|
18892
|
-
return this.
|
|
19105
|
+
return this.workspaceConflict(input.expectedWorkspaceRevision, workspace.data.revision);
|
|
18893
19106
|
const previousWorkspaceBytes = this.snapshotBytes(this.workspacePath);
|
|
18894
19107
|
const previousTaskBytes = this.snapshotBytes(this.taskPath(input.taskId));
|
|
18895
19108
|
if (!previousWorkspaceBytes || !previousTaskBytes)
|
|
@@ -18952,12 +19165,12 @@ class TaskStore {
|
|
|
18952
19165
|
});
|
|
18953
19166
|
}
|
|
18954
19167
|
recoveryCandidates() {
|
|
18955
|
-
if (!
|
|
19168
|
+
if (!fs5.existsSync(this.recoveryDir))
|
|
18956
19169
|
return success(null, null, []);
|
|
18957
19170
|
try {
|
|
18958
19171
|
const candidates = [];
|
|
18959
|
-
for (const name of
|
|
18960
|
-
const match =
|
|
19172
|
+
for (const name of fs5.readdirSync(this.recoveryDir)) {
|
|
19173
|
+
const match = RECOVERY_NAME.exec(name);
|
|
18961
19174
|
if (match)
|
|
18962
19175
|
candidates.push({
|
|
18963
19176
|
target: match[1],
|
|
@@ -19005,7 +19218,7 @@ class TaskStore {
|
|
|
19005
19218
|
return failure2("recovery_required", "validated recovery snapshot not found");
|
|
19006
19219
|
selectedBytes = candidate;
|
|
19007
19220
|
}
|
|
19008
|
-
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema) : this.parseBytes(selectedBytes, taskRecordSchema);
|
|
19221
|
+
const selectedRecord = target === "workspace" ? this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite") : this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
19009
19222
|
if (!selectedRecord.ok)
|
|
19010
19223
|
return selectedRecord;
|
|
19011
19224
|
if (target === "workspace") {
|
|
@@ -19051,14 +19264,14 @@ class TaskStore {
|
|
|
19051
19264
|
return this.conflict(input.expectedWorkspaceRevision, currentWorkspace.data.revision);
|
|
19052
19265
|
}
|
|
19053
19266
|
if (target === "workspace") {
|
|
19054
|
-
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema);
|
|
19267
|
+
const parsed = this.parseBytes(selectedBytes, workspaceRecordSchema, "rewrite");
|
|
19055
19268
|
if (!parsed.ok)
|
|
19056
19269
|
return parsed;
|
|
19057
19270
|
const value = { ...parsed.data, revision: newRevision(), writer: null };
|
|
19058
19271
|
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
19059
19272
|
return replaced.ok ? success(value.revision, value.revision, value) : replaced;
|
|
19060
19273
|
}
|
|
19061
|
-
const parsed = this.parseBytes(selectedBytes, taskRecordSchema);
|
|
19274
|
+
const parsed = this.parseBytes(selectedBytes, taskRecordSchema, "rewrite");
|
|
19062
19275
|
if (!parsed.ok)
|
|
19063
19276
|
return parsed;
|
|
19064
19277
|
const value = {
|
|
@@ -19069,7 +19282,7 @@ class TaskStore {
|
|
|
19069
19282
|
};
|
|
19070
19283
|
const replaced = this.replaceSnapshot(file, value, reacquiredBytes);
|
|
19071
19284
|
return replaced.ok ? success(value.revision, null, value) : replaced;
|
|
19072
|
-
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate));
|
|
19285
|
+
}, this.recoveryLockOptions(lock.data, evidence.data, recoveryGate), "recovery_required");
|
|
19073
19286
|
return result;
|
|
19074
19287
|
} catch (error) {
|
|
19075
19288
|
return failure2("recovery_required", `recovery protocol failed: ${String(error)}`);
|
|
@@ -19104,12 +19317,12 @@ class TaskStore {
|
|
|
19104
19317
|
}
|
|
19105
19318
|
readLockSnapshot() {
|
|
19106
19319
|
try {
|
|
19107
|
-
const stat =
|
|
19320
|
+
const stat = fs5.lstatSync(this.lockPath);
|
|
19108
19321
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
19109
19322
|
return failure2("recovery_required", "metadata lock is not a regular file", {
|
|
19110
19323
|
path: this.lockPath
|
|
19111
19324
|
});
|
|
19112
|
-
const raw =
|
|
19325
|
+
const raw = fs5.readFileSync(this.lockPath, "utf8");
|
|
19113
19326
|
return success(null, null, { raw, data: parseMetadataLock(raw) });
|
|
19114
19327
|
} catch (error) {
|
|
19115
19328
|
if (error?.code === "ENOENT")
|
|
@@ -19120,22 +19333,50 @@ class TaskStore {
|
|
|
19120
19333
|
}
|
|
19121
19334
|
}
|
|
19122
19335
|
metadataLockOptions() {
|
|
19336
|
+
const inProcess = heldInProcess.has(this.root);
|
|
19123
19337
|
return {
|
|
19124
19338
|
lockPath: this.lockPath,
|
|
19125
19339
|
staleMs: Number.MAX_SAFE_INTEGER,
|
|
19126
|
-
timeoutMs: 0,
|
|
19127
|
-
retry: { retries: 0 },
|
|
19128
|
-
staleRecovery: "
|
|
19129
|
-
shouldReclaim: () =>
|
|
19130
|
-
|
|
19340
|
+
timeoutMs: inProcess ? 0 : this.lockTimeoutMs,
|
|
19341
|
+
retry: inProcess ? { retries: 0 } : { minTimeout: 5, maxTimeout: 100, factor: 1.5, randomize: true },
|
|
19342
|
+
staleRecovery: "remove-if-unchanged",
|
|
19343
|
+
shouldReclaim: ({ payload, nowMs }) => {
|
|
19344
|
+
let ageMs = null;
|
|
19345
|
+
try {
|
|
19346
|
+
ageMs = nowMs - fs5.lstatSync(this.lockPath).mtimeMs;
|
|
19347
|
+
} catch {}
|
|
19348
|
+
return classifyLockOwner(payload, ageMs).state === "stale";
|
|
19349
|
+
},
|
|
19350
|
+
shouldRemoveStaleLock: () => true,
|
|
19351
|
+
parsePayload: parseMetadataLockOrNull,
|
|
19131
19352
|
payload: () => ({
|
|
19132
19353
|
pid: process.pid,
|
|
19133
|
-
processStart:
|
|
19134
|
-
host:
|
|
19354
|
+
processStart: processStartOf(process.pid),
|
|
19355
|
+
host: localLockHost(),
|
|
19135
19356
|
nonce: randomUUID2()
|
|
19136
19357
|
})
|
|
19137
19358
|
};
|
|
19138
19359
|
}
|
|
19360
|
+
acquireMetadataLock(options) {
|
|
19361
|
+
clearAbandonedReclaimGuard(this.lockPath);
|
|
19362
|
+
const deadline = Date.now() + (options.timeoutMs ?? 0);
|
|
19363
|
+
while (true) {
|
|
19364
|
+
try {
|
|
19365
|
+
return acquireFileLockSync(this.workspacePath, {
|
|
19366
|
+
...options,
|
|
19367
|
+
timeoutMs: Math.max(0, deadline - Date.now())
|
|
19368
|
+
});
|
|
19369
|
+
} catch (error) {
|
|
19370
|
+
const code = error?.code;
|
|
19371
|
+
if (code !== "file_lock_stale" && !isTransientWindowsError(error) || Date.now() >= deadline)
|
|
19372
|
+
throw error;
|
|
19373
|
+
if (code !== "file_lock_stale" && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
19374
|
+
throw error;
|
|
19375
|
+
if (code !== "file_lock_stale")
|
|
19376
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
19377
|
+
}
|
|
19378
|
+
}
|
|
19379
|
+
}
|
|
19139
19380
|
externalActionLockOptions() {
|
|
19140
19381
|
const options = this.metadataLockOptions();
|
|
19141
19382
|
return { ...options, payload: () => ({ ...options.payload(), externalAction: true }) };
|
|
@@ -19153,10 +19394,11 @@ class TaskStore {
|
|
|
19153
19394
|
}
|
|
19154
19395
|
let handle;
|
|
19155
19396
|
try {
|
|
19156
|
-
handle =
|
|
19397
|
+
handle = this.acquireMetadataLock(this.externalActionLockOptions());
|
|
19157
19398
|
} catch (error) {
|
|
19158
19399
|
return this.lockFailure(error);
|
|
19159
19400
|
}
|
|
19401
|
+
holdRoot(this.root);
|
|
19160
19402
|
let result;
|
|
19161
19403
|
try {
|
|
19162
19404
|
if (!handle.verifyStillHeld())
|
|
@@ -19191,8 +19433,9 @@ class TaskStore {
|
|
|
19191
19433
|
path: this.lockPath
|
|
19192
19434
|
});
|
|
19193
19435
|
}
|
|
19436
|
+
dropRoot(this.root);
|
|
19194
19437
|
try {
|
|
19195
|
-
handle.release();
|
|
19438
|
+
retryTransient(() => handle.release());
|
|
19196
19439
|
} catch (error) {
|
|
19197
19440
|
const released = this.lockFailure(error);
|
|
19198
19441
|
const releaseError = released.ok ? "metadata lock release failed" : released.error;
|
|
@@ -19205,6 +19448,9 @@ class TaskStore {
|
|
|
19205
19448
|
}
|
|
19206
19449
|
recoveryLockOptions(observed, evidence, gate) {
|
|
19207
19450
|
const options = this.metadataLockOptions();
|
|
19451
|
+
options.timeoutMs = 0;
|
|
19452
|
+
options.retry = { retries: 0 };
|
|
19453
|
+
options.parsePayload = parseMetadataLock;
|
|
19208
19454
|
options.staleRecovery = "remove-if-unchanged";
|
|
19209
19455
|
options.shouldReclaim = ({ payload }) => Boolean(observed && sameMetadataLock(payload, observed.data) && (evidence.state === "stopped" || evidence.state === "accounted_for"));
|
|
19210
19456
|
options.shouldRemoveStaleLock = ({ raw, payload }) => {
|
|
@@ -19226,7 +19472,7 @@ class TaskStore {
|
|
|
19226
19472
|
};
|
|
19227
19473
|
this.replaceSnapshot(this.workspacePath, value, workspace);
|
|
19228
19474
|
}
|
|
19229
|
-
withLock(operation, options = this.metadataLockOptions()) {
|
|
19475
|
+
withLock(operation, options = this.metadataLockOptions(), contention = "busy") {
|
|
19230
19476
|
try {
|
|
19231
19477
|
this.initializeMutationStorage();
|
|
19232
19478
|
} catch (error) {
|
|
@@ -19237,10 +19483,12 @@ class TaskStore {
|
|
|
19237
19483
|
let handle;
|
|
19238
19484
|
let result = failure2("storage_error", "metadata lock operation did not produce a result");
|
|
19239
19485
|
try {
|
|
19240
|
-
handle =
|
|
19486
|
+
handle = this.acquireMetadataLock(options);
|
|
19241
19487
|
} catch (error) {
|
|
19242
|
-
result = this.lockFailure(error);
|
|
19488
|
+
result = this.lockFailure(error, contention);
|
|
19243
19489
|
}
|
|
19490
|
+
if (handle)
|
|
19491
|
+
holdRoot(this.root);
|
|
19244
19492
|
if (handle) {
|
|
19245
19493
|
try {
|
|
19246
19494
|
if (!handle.verifyStillHeld())
|
|
@@ -19259,8 +19507,9 @@ class TaskStore {
|
|
|
19259
19507
|
}
|
|
19260
19508
|
}
|
|
19261
19509
|
if (handle) {
|
|
19510
|
+
dropRoot(this.root);
|
|
19262
19511
|
try {
|
|
19263
|
-
handle.release();
|
|
19512
|
+
retryTransient(() => handle.release());
|
|
19264
19513
|
} catch (error) {
|
|
19265
19514
|
const releaseFailure = this.lockFailure(error);
|
|
19266
19515
|
const releaseError = releaseFailure.ok ? "metadata lock release failed" : releaseFailure.error;
|
|
@@ -19269,25 +19518,65 @@ class TaskStore {
|
|
|
19269
19518
|
}
|
|
19270
19519
|
return result;
|
|
19271
19520
|
}
|
|
19272
|
-
|
|
19521
|
+
lockDirWritable() {
|
|
19522
|
+
const probe = `${this.lockPath}.${process.pid}.${randomUUID2()}.probe`;
|
|
19523
|
+
try {
|
|
19524
|
+
fs5.closeSync(fs5.openSync(probe, "wx", 384));
|
|
19525
|
+
} catch {
|
|
19526
|
+
return false;
|
|
19527
|
+
}
|
|
19528
|
+
try {
|
|
19529
|
+
fs5.rmSync(probe, { force: true });
|
|
19530
|
+
} catch {}
|
|
19531
|
+
return true;
|
|
19532
|
+
}
|
|
19533
|
+
lockHolderPresent() {
|
|
19534
|
+
try {
|
|
19535
|
+
fs5.lstatSync(this.lockPath);
|
|
19536
|
+
return true;
|
|
19537
|
+
} catch (error) {
|
|
19538
|
+
return error?.code !== "ENOENT";
|
|
19539
|
+
}
|
|
19540
|
+
}
|
|
19541
|
+
lockFailure(error, contention = "busy") {
|
|
19273
19542
|
const value = error;
|
|
19274
19543
|
const code = typeof value?.code === "string" ? value.code : "";
|
|
19275
|
-
if (
|
|
19276
|
-
|
|
19277
|
-
|
|
19544
|
+
if (isTransientWindowsError(error) && !this.lockHolderPresent() && !this.lockDirWritable())
|
|
19545
|
+
return failure2("storage_error", `cannot create the workspace metadata lock (${code}); no other Workit call holds it`, {
|
|
19546
|
+
path: this.lockPath,
|
|
19547
|
+
guidance: `Check permissions and the read-only attribute on ${path4.dirname(this.lockPath)}.`
|
|
19548
|
+
});
|
|
19549
|
+
if (code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || isTransientWindowsError(error)) {
|
|
19550
|
+
let lock = null;
|
|
19551
|
+
try {
|
|
19552
|
+
lock = parseMetadataLockOrNull(fs5.readFileSync(this.lockPath, "utf8"));
|
|
19553
|
+
} catch {}
|
|
19554
|
+
if (lock?.externalAction)
|
|
19278
19555
|
return failure2("writer_conflict", "workspace is reserved by a managed external action", {
|
|
19279
19556
|
outcome: "not_started",
|
|
19280
19557
|
path: this.lockPath
|
|
19281
19558
|
});
|
|
19559
|
+
if (contention === "busy")
|
|
19560
|
+
return failure2("busy", `workspace metadata lock is held by another Workit call${lock ? ` (pid ${lock.pid} on ${lock.host})` : ""}; retry shortly`, {
|
|
19561
|
+
outcome: "not_started",
|
|
19562
|
+
path: this.lockPath,
|
|
19563
|
+
guidance: "Retry the same call. If it stays busy, run `workit doctor --fix-lock` to clear a lock left by a dead process."
|
|
19564
|
+
});
|
|
19282
19565
|
}
|
|
19283
19566
|
const recovery = code === "EEXIST" || code === "file_lock_timeout" || code === "file_lock_stale" || code === "metadata_lock_invalid" || code === "not-file" || /metadata lock|file lock|reclaim/i.test(String(value?.message ?? error));
|
|
19284
19567
|
return failure2(recovery ? "recovery_required" : "storage_error", `metadata lock operation failed: ${String(error)}`, { path: this.lockPath });
|
|
19285
19568
|
}
|
|
19286
19569
|
initializeMutationStorage() {
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
|
|
19570
|
+
retryTransient(() => fs5.mkdirSync(this.tasksDir, { recursive: true }));
|
|
19571
|
+
retryTransient(() => fs5.mkdirSync(this.recoveryDir, { recursive: true }));
|
|
19572
|
+
let current = null;
|
|
19573
|
+
try {
|
|
19574
|
+
current = retryTransient(() => fs5.readFileSync(this.gitignorePath, "utf8"));
|
|
19575
|
+
} catch {}
|
|
19576
|
+
if (current !== `*
|
|
19577
|
+
`)
|
|
19578
|
+
retryTransient(() => fs5.writeFileSync(this.gitignorePath, `*
|
|
19579
|
+
`));
|
|
19291
19580
|
}
|
|
19292
19581
|
replaceSnapshot(file, value, previous) {
|
|
19293
19582
|
let temporary;
|
|
@@ -19299,14 +19588,14 @@ class TaskStore {
|
|
|
19299
19588
|
else if (typeof previous === "string")
|
|
19300
19589
|
this.saveRecovery(file, previous);
|
|
19301
19590
|
temporary = `${file}.${process.pid}.${randomUUID2()}.tmp`;
|
|
19302
|
-
const fd =
|
|
19591
|
+
const fd = fs5.openSync(temporary, "wx", 384);
|
|
19303
19592
|
try {
|
|
19304
|
-
|
|
19305
|
-
|
|
19593
|
+
fs5.writeSync(fd, jsonBytes(value));
|
|
19594
|
+
fs5.fsyncSync(fd);
|
|
19306
19595
|
} finally {
|
|
19307
|
-
|
|
19596
|
+
fs5.closeSync(fd);
|
|
19308
19597
|
}
|
|
19309
|
-
|
|
19598
|
+
retryTransient(() => fs5.renameSync(temporary, file));
|
|
19310
19599
|
temporary = undefined;
|
|
19311
19600
|
this.fsyncDirectory(path4.dirname(file));
|
|
19312
19601
|
if (path4.dirname(file) === this.tasksDir)
|
|
@@ -19319,13 +19608,13 @@ class TaskStore {
|
|
|
19319
19608
|
} finally {
|
|
19320
19609
|
if (temporary)
|
|
19321
19610
|
try {
|
|
19322
|
-
|
|
19611
|
+
fs5.unlinkSync(temporary);
|
|
19323
19612
|
} catch {}
|
|
19324
19613
|
}
|
|
19325
19614
|
}
|
|
19326
19615
|
readIndex(workspaceId) {
|
|
19327
19616
|
try {
|
|
19328
|
-
const value = JSON.parse(
|
|
19617
|
+
const value = JSON.parse(fs5.readFileSync(this.indexPath, "utf8"));
|
|
19329
19618
|
if (!isObject2(value) || value.version !== INDEX_VERSION || value.workspaceId !== workspaceId || !isObject2(value.tasks))
|
|
19330
19619
|
return {};
|
|
19331
19620
|
const tasks = {};
|
|
@@ -19340,15 +19629,15 @@ class TaskStore {
|
|
|
19340
19629
|
writeIndex(workspaceId, entries) {
|
|
19341
19630
|
const temporary = `${this.indexPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
19342
19631
|
try {
|
|
19343
|
-
|
|
19632
|
+
fs5.writeFileSync(temporary, JSON.stringify({
|
|
19344
19633
|
version: INDEX_VERSION,
|
|
19345
19634
|
workspaceId,
|
|
19346
19635
|
tasks: Object.fromEntries(entries.map((entry) => [entry.id, entry]))
|
|
19347
19636
|
}), { mode: 384, flag: "wx" });
|
|
19348
|
-
|
|
19637
|
+
retryTransient(() => fs5.renameSync(temporary, this.indexPath));
|
|
19349
19638
|
} catch {
|
|
19350
19639
|
try {
|
|
19351
|
-
|
|
19640
|
+
fs5.unlinkSync(temporary);
|
|
19352
19641
|
} catch {}
|
|
19353
19642
|
}
|
|
19354
19643
|
}
|
|
@@ -19366,33 +19655,153 @@ class TaskStore {
|
|
|
19366
19655
|
const target = path4.basename(file) === "workspace.json" ? "workspace" : "task";
|
|
19367
19656
|
const id = target === "task" ? path4.basename(file, ".json") : "workspace";
|
|
19368
19657
|
const destination = path4.join(this.recoveryDir, `${target}.${id}.${digestBytes(bytes)}.json`);
|
|
19369
|
-
if (
|
|
19370
|
-
if (digestBytes(
|
|
19371
|
-
|
|
19372
|
-
|
|
19658
|
+
if (fs5.existsSync(destination)) {
|
|
19659
|
+
if (digestBytes(retryTransient(() => fs5.readFileSync(destination))) !== digestBytes(bytes))
|
|
19660
|
+
throw new Error("recovery copy already exists with different bytes");
|
|
19661
|
+
const stamp = new Date;
|
|
19662
|
+
retryTransient(() => fs5.utimesSync(destination, stamp, stamp));
|
|
19663
|
+
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
19664
|
+
return;
|
|
19373
19665
|
}
|
|
19374
19666
|
let temporary;
|
|
19375
19667
|
try {
|
|
19376
19668
|
temporary = `${destination}.${process.pid}.${randomUUID2()}.tmp`;
|
|
19377
|
-
const fd =
|
|
19669
|
+
const fd = fs5.openSync(temporary, "wx", 384);
|
|
19378
19670
|
try {
|
|
19379
|
-
|
|
19380
|
-
|
|
19671
|
+
fs5.writeSync(fd, bytes);
|
|
19672
|
+
fs5.fsyncSync(fd);
|
|
19381
19673
|
} finally {
|
|
19382
|
-
|
|
19674
|
+
fs5.closeSync(fd);
|
|
19383
19675
|
}
|
|
19384
|
-
|
|
19676
|
+
retryTransient(() => fs5.renameSync(temporary, destination));
|
|
19385
19677
|
temporary = undefined;
|
|
19386
19678
|
this.fsyncDirectory(this.recoveryDir);
|
|
19679
|
+
this.pruneRecovery(`${target}.${id}.`, destination);
|
|
19387
19680
|
} finally {
|
|
19388
19681
|
if (temporary)
|
|
19389
19682
|
try {
|
|
19390
|
-
|
|
19683
|
+
fs5.unlinkSync(temporary);
|
|
19391
19684
|
} catch {}
|
|
19392
19685
|
}
|
|
19393
19686
|
}
|
|
19687
|
+
pruneRecovery(prefix, keep) {
|
|
19688
|
+
try {
|
|
19689
|
+
const copies = this.recoveryCopies(prefix);
|
|
19690
|
+
const surplus = this.surplusCopies(copies, path4.basename(keep));
|
|
19691
|
+
for (const copy of surplus)
|
|
19692
|
+
fs5.rmSync(copy.path, { force: true });
|
|
19693
|
+
} catch {}
|
|
19694
|
+
}
|
|
19695
|
+
recoveryCopies(prefix = "") {
|
|
19696
|
+
if (!fs5.existsSync(this.recoveryDir))
|
|
19697
|
+
return [];
|
|
19698
|
+
const copies = [];
|
|
19699
|
+
for (const name of fs5.readdirSync(this.recoveryDir)) {
|
|
19700
|
+
if (!name.startsWith(prefix))
|
|
19701
|
+
continue;
|
|
19702
|
+
const match = RECOVERY_NAME.exec(name);
|
|
19703
|
+
if (!match)
|
|
19704
|
+
continue;
|
|
19705
|
+
const file = path4.join(this.recoveryDir, name);
|
|
19706
|
+
try {
|
|
19707
|
+
const stat = fs5.lstatSync(file, { bigint: true });
|
|
19708
|
+
if (!stat.isFile())
|
|
19709
|
+
continue;
|
|
19710
|
+
copies.push({ name, path: file, group: `${match[1]}.${match[2]}`, mtimeNs: stat.mtimeNs });
|
|
19711
|
+
} catch {}
|
|
19712
|
+
}
|
|
19713
|
+
return copies;
|
|
19714
|
+
}
|
|
19715
|
+
surplusCopies(copies, keep) {
|
|
19716
|
+
const ordered = copies.toSorted((left, right) => left.name === keep ? -1 : right.name === keep ? 1 : left.mtimeNs === right.mtimeNs ? left.name.localeCompare(right.name) : left.mtimeNs > right.mtimeNs ? -1 : 1);
|
|
19717
|
+
return ordered.slice(RECOVERY_COPIES_PER_RECORD);
|
|
19718
|
+
}
|
|
19719
|
+
collectGarbage(options = {}) {
|
|
19720
|
+
const dryRun = options.dryRun === true;
|
|
19721
|
+
const report = {
|
|
19722
|
+
dryRun,
|
|
19723
|
+
recovery: { removed: 0, removedBytes: 0, kept: 0 },
|
|
19724
|
+
temporary: { removed: 0 },
|
|
19725
|
+
candidates: { removed: 0, tasks: [], skippedActive: [], skippedClosed: [], failed: [] }
|
|
19726
|
+
};
|
|
19727
|
+
if (!fs5.existsSync(this.workitDir))
|
|
19728
|
+
return success(null, null, report);
|
|
19729
|
+
const sweep = () => {
|
|
19730
|
+
try {
|
|
19731
|
+
const groups = new Map;
|
|
19732
|
+
for (const copy of this.recoveryCopies())
|
|
19733
|
+
groups.set(copy.group, [...groups.get(copy.group) ?? [], copy]);
|
|
19734
|
+
for (const copies of groups.values()) {
|
|
19735
|
+
const surplus = this.surplusCopies(copies);
|
|
19736
|
+
report.recovery.kept += copies.length - surplus.length;
|
|
19737
|
+
for (const copy of surplus) {
|
|
19738
|
+
report.recovery.removed += 1;
|
|
19739
|
+
try {
|
|
19740
|
+
report.recovery.removedBytes += fs5.statSync(copy.path).size;
|
|
19741
|
+
} catch {}
|
|
19742
|
+
if (!dryRun)
|
|
19743
|
+
fs5.rmSync(copy.path, { force: true });
|
|
19744
|
+
}
|
|
19745
|
+
}
|
|
19746
|
+
const nowMs = Date.now();
|
|
19747
|
+
for (const directory of [this.recoveryDir, this.tasksDir, this.workitDir]) {
|
|
19748
|
+
if (!fs5.existsSync(directory))
|
|
19749
|
+
continue;
|
|
19750
|
+
for (const name of fs5.readdirSync(directory)) {
|
|
19751
|
+
if (!name.endsWith(".tmp") && !name.endsWith(".probe"))
|
|
19752
|
+
continue;
|
|
19753
|
+
const file = path4.join(directory, name);
|
|
19754
|
+
const stat = fs5.lstatSync(file);
|
|
19755
|
+
if (!stat.isFile() || nowMs - stat.mtimeMs <= STALE_TEMPORARY_MS)
|
|
19756
|
+
continue;
|
|
19757
|
+
report.temporary.removed += 1;
|
|
19758
|
+
if (!dryRun)
|
|
19759
|
+
fs5.rmSync(file, { force: true });
|
|
19760
|
+
}
|
|
19761
|
+
}
|
|
19762
|
+
return success(null, null, null);
|
|
19763
|
+
} catch (error) {
|
|
19764
|
+
return failure2("storage_error", `garbage collection failed: ${String(error)}`, {
|
|
19765
|
+
path: this.recoveryDir
|
|
19766
|
+
});
|
|
19767
|
+
}
|
|
19768
|
+
};
|
|
19769
|
+
const pruned = dryRun ? sweep() : this.withLock(sweep);
|
|
19770
|
+
if (!pruned.ok)
|
|
19771
|
+
return pruned;
|
|
19772
|
+
const tasks = this.listTasks();
|
|
19773
|
+
if (!tasks.ok)
|
|
19774
|
+
return tasks;
|
|
19775
|
+
for (const task of tasks.data) {
|
|
19776
|
+
const deduped = dedupeCandidates(task.candidates);
|
|
19777
|
+
const removed = task.candidates.length - deduped.length;
|
|
19778
|
+
if (removed === 0)
|
|
19779
|
+
continue;
|
|
19780
|
+
if (task.status === "active") {
|
|
19781
|
+
report.candidates.skippedActive.push(task.id);
|
|
19782
|
+
continue;
|
|
19783
|
+
}
|
|
19784
|
+
if (task.status === "closed") {
|
|
19785
|
+
report.candidates.skippedClosed.push(task.id);
|
|
19786
|
+
continue;
|
|
19787
|
+
}
|
|
19788
|
+
if (!dryRun) {
|
|
19789
|
+
const written = this.mutateTask(task.id, task.revision, (current) => success(current.revision, null, {
|
|
19790
|
+
...current,
|
|
19791
|
+
candidates: dedupeCandidates(current.candidates)
|
|
19792
|
+
}));
|
|
19793
|
+
if (!written.ok) {
|
|
19794
|
+
report.candidates.failed.push(task.id);
|
|
19795
|
+
continue;
|
|
19796
|
+
}
|
|
19797
|
+
}
|
|
19798
|
+
report.candidates.removed += removed;
|
|
19799
|
+
report.candidates.tasks.push(task.id);
|
|
19800
|
+
}
|
|
19801
|
+
return success(null, null, report);
|
|
19802
|
+
}
|
|
19394
19803
|
findRecovery(target, taskId, digest, workspace) {
|
|
19395
|
-
if (!
|
|
19804
|
+
if (!fs5.existsSync(this.recoveryDir))
|
|
19396
19805
|
return null;
|
|
19397
19806
|
const workspaceId = workspace?.id ?? this.trustedWorkspaceId();
|
|
19398
19807
|
if (target === "workspace" && !workspaceId)
|
|
@@ -19404,9 +19813,9 @@ class TaskStore {
|
|
|
19404
19813
|
if (candidate.target !== target || candidate.digest !== digest)
|
|
19405
19814
|
continue;
|
|
19406
19815
|
try {
|
|
19407
|
-
if (!
|
|
19816
|
+
if (!fs5.lstatSync(candidate.path).isFile())
|
|
19408
19817
|
continue;
|
|
19409
|
-
const bytes =
|
|
19818
|
+
const bytes = fs5.readFileSync(candidate.path);
|
|
19410
19819
|
if (digestBytes(bytes) !== digest)
|
|
19411
19820
|
continue;
|
|
19412
19821
|
if (target === "workspace") {
|
|
@@ -19424,11 +19833,11 @@ class TaskStore {
|
|
|
19424
19833
|
}
|
|
19425
19834
|
fsyncDirectory(directory) {
|
|
19426
19835
|
try {
|
|
19427
|
-
const fd =
|
|
19836
|
+
const fd = fs5.openSync(directory, "r");
|
|
19428
19837
|
try {
|
|
19429
|
-
|
|
19838
|
+
fs5.fsyncSync(fd);
|
|
19430
19839
|
} finally {
|
|
19431
|
-
|
|
19840
|
+
fs5.closeSync(fd);
|
|
19432
19841
|
}
|
|
19433
19842
|
} catch (error) {
|
|
19434
19843
|
if (process.platform !== "win32")
|
|
@@ -19436,14 +19845,14 @@ class TaskStore {
|
|
|
19436
19845
|
}
|
|
19437
19846
|
}
|
|
19438
19847
|
trustedWorkspaceId() {
|
|
19439
|
-
if (!
|
|
19848
|
+
if (!fs5.existsSync(this.tasksDir))
|
|
19440
19849
|
return null;
|
|
19441
19850
|
const ids = new Set;
|
|
19442
19851
|
try {
|
|
19443
|
-
for (const name of
|
|
19852
|
+
for (const name of fs5.readdirSync(this.tasksDir)) {
|
|
19444
19853
|
if (!name.endsWith(".json") || !validId(name.slice(0, -5)))
|
|
19445
19854
|
return null;
|
|
19446
|
-
const parsed = this.parseBytes(
|
|
19855
|
+
const parsed = this.parseBytes(fs5.readFileSync(path4.join(this.tasksDir, name)), taskRecordSchema);
|
|
19447
19856
|
if (!parsed.ok || parsed.data.id !== name.slice(0, -5))
|
|
19448
19857
|
return null;
|
|
19449
19858
|
ids.add(parsed.data.workspaceId);
|
|
@@ -19455,14 +19864,14 @@ class TaskStore {
|
|
|
19455
19864
|
}
|
|
19456
19865
|
snapshotBytes(file) {
|
|
19457
19866
|
try {
|
|
19458
|
-
return
|
|
19867
|
+
return retryTransient(() => fs5.readFileSync(file));
|
|
19459
19868
|
} catch {
|
|
19460
19869
|
return null;
|
|
19461
19870
|
}
|
|
19462
19871
|
}
|
|
19463
19872
|
readRecord(file, schema) {
|
|
19464
19873
|
try {
|
|
19465
|
-
const bytes =
|
|
19874
|
+
const bytes = retryTransient(() => fs5.readFileSync(file, "utf8"));
|
|
19466
19875
|
return { exists: true, result: this.parseBytes(bytes, schema) };
|
|
19467
19876
|
} catch (error) {
|
|
19468
19877
|
if (error?.code === "ENOENT")
|
|
@@ -19475,7 +19884,7 @@ class TaskStore {
|
|
|
19475
19884
|
};
|
|
19476
19885
|
}
|
|
19477
19886
|
}
|
|
19478
|
-
parseBytes(bytes, schema) {
|
|
19887
|
+
parseBytes(bytes, schema, mode = "read") {
|
|
19479
19888
|
let value;
|
|
19480
19889
|
try {
|
|
19481
19890
|
value = JSON.parse(typeof bytes === "string" ? bytes : bytes.toString("utf8"));
|
|
@@ -19484,27 +19893,28 @@ class TaskStore {
|
|
|
19484
19893
|
}
|
|
19485
19894
|
if (isObject2(value) && "schemaVersion" in value && value.schemaVersion !== SCHEMA_VERSION)
|
|
19486
19895
|
return failure2("unsupported_version", "unsupported snapshot schema version");
|
|
19487
|
-
const parsed = schema
|
|
19488
|
-
if (parsed.success)
|
|
19896
|
+
const parsed = parseStoredRecord(schema, value);
|
|
19897
|
+
if (parsed.success && (mode === "read" || parsed.stripped.length === 0))
|
|
19489
19898
|
return success(null, null, parsed.data);
|
|
19490
19899
|
const writerVersion = isObject2(value) && isObject2(value.runtime) ? value.runtime.updatedWith : null;
|
|
19900
|
+
const upgrade = `upgrade Workit before ${parsed.success ? "recovering" : "mutating"} this checkout`;
|
|
19901
|
+
if (parsed.success)
|
|
19902
|
+
return failure2("recovery_required", `snapshot carries fields this Workit cannot preserve (${parsed.stripped.join(", ")}); ${upgrade}`);
|
|
19903
|
+
if (parsed.critical.length > 0)
|
|
19904
|
+
return failure2("recovery_required", `snapshot requires fields this Workit cannot read (${parsed.critical.join(", ")}); ${upgrade}`);
|
|
19491
19905
|
if (typeof writerVersion === "string" && isNewerVersion(writerVersion, runtimeVersion()))
|
|
19492
19906
|
return failure2("recovery_required", `snapshot was written by workit ${writerVersion}; upgrade Workit before mutating this checkout`);
|
|
19493
19907
|
return failure2("recovery_required", "snapshot does not satisfy its schema");
|
|
19494
19908
|
}
|
|
19495
|
-
processStart(pid) {
|
|
19496
|
-
try {
|
|
19497
|
-
return fs4.readFileSync(`/proc/${pid}/stat`, "utf8").split(" ")[21] ?? null;
|
|
19498
|
-
} catch {
|
|
19499
|
-
return null;
|
|
19500
|
-
}
|
|
19501
|
-
}
|
|
19502
19909
|
conflict(expected, actual) {
|
|
19503
19910
|
return failure2("revision_conflict", "snapshot revision does not match; omit expectedRevision to use the current record", {
|
|
19504
19911
|
expectedRevision: expected,
|
|
19505
19912
|
actualRevision: actual
|
|
19506
19913
|
});
|
|
19507
19914
|
}
|
|
19915
|
+
workspaceConflict(expected, actual) {
|
|
19916
|
+
return failure2("revision_conflict", "workspace revision does not match; omit expectedWorkspaceRevision to use the current record", { expectedWorkspaceRevision: expected, actualWorkspaceRevision: actual });
|
|
19917
|
+
}
|
|
19508
19918
|
taskPath(taskId) {
|
|
19509
19919
|
return path4.join(this.tasksDir, `${taskId}.json`);
|
|
19510
19920
|
}
|
|
@@ -19530,53 +19940,116 @@ class TaskStore {
|
|
|
19530
19940
|
return path4.join(this.workitDir, ".gitignore");
|
|
19531
19941
|
}
|
|
19532
19942
|
}
|
|
19533
|
-
// packages/workit-core/src/core/
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
// packages/workit-core/src/core/auto-approval.ts
|
|
19542
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
19543
|
-
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
19544
|
-
import path8 from "node:path";
|
|
19545
|
-
|
|
19546
|
-
// packages/workit-core/src/core/workspaces.ts
|
|
19547
|
-
import { createHash as createHash3 } from "node:crypto";
|
|
19548
|
-
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
19549
|
-
import path6 from "node:path";
|
|
19550
|
-
|
|
19551
|
-
// packages/workit-core/src/core/config.ts
|
|
19552
|
-
import {
|
|
19553
|
-
copyFileSync,
|
|
19554
|
-
cpSync,
|
|
19555
|
-
existsSync as existsSync3,
|
|
19556
|
-
mkdirSync as mkdirSync2,
|
|
19557
|
-
readFileSync as readFileSync2,
|
|
19558
|
-
readdirSync as readdirSync2,
|
|
19559
|
-
writeFileSync as writeFileSync2
|
|
19560
|
-
} from "node:fs";
|
|
19561
|
-
import os from "node:os";
|
|
19562
|
-
import path5 from "node:path";
|
|
19563
|
-
|
|
19564
|
-
// packages/workit-core/src/core/boundary.ts
|
|
19565
|
-
import { statSync as statSync2 } from "node:fs";
|
|
19566
|
-
var EVENT = {
|
|
19567
|
-
initialization: "initialization",
|
|
19568
|
-
provenance: "provenance",
|
|
19569
|
-
assets: "assets",
|
|
19570
|
-
configurationSource: "configuration_source",
|
|
19571
|
-
hooks: "hooks",
|
|
19572
|
-
mcpConnection: "mcp_connection",
|
|
19573
|
-
migration: "migration",
|
|
19574
|
-
installSteps: "install_steps",
|
|
19575
|
-
uncaughtFailure: "uncaught_failure",
|
|
19576
|
-
toolsFailed: "tools_failed",
|
|
19577
|
-
doctor: "doctor"
|
|
19943
|
+
// packages/workit-core/src/core/workers.ts
|
|
19944
|
+
var same = (left, right) => {
|
|
19945
|
+
try {
|
|
19946
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
19947
|
+
} catch {
|
|
19948
|
+
return false;
|
|
19949
|
+
}
|
|
19578
19950
|
};
|
|
19579
|
-
var
|
|
19951
|
+
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
19952
|
+
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
19953
|
+
var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
|
|
19954
|
+
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
19955
|
+
var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
|
|
19956
|
+
const code = char.charCodeAt(0);
|
|
19957
|
+
return code < 32 || code === 127;
|
|
19958
|
+
}) && !value.split(/[\\/]/).includes("..");
|
|
19959
|
+
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
19960
|
+
var workerIdOf = (value) => value.workerId ?? null;
|
|
19961
|
+
var currentWriterOwnsTask = (task, workspace, caller) => {
|
|
19962
|
+
const owner = workspace.writer?.owner;
|
|
19963
|
+
return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
|
|
19964
|
+
};
|
|
19965
|
+
function assertProductWriteAllowed(input) {
|
|
19966
|
+
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
19967
|
+
return failure2("invalid_input", "invalid product write path");
|
|
19968
|
+
if (!input.store)
|
|
19969
|
+
return failure2("permission_denied", "write authorization requires core state");
|
|
19970
|
+
const task = input.store.readTask(input.task.id);
|
|
19971
|
+
if (!task.ok)
|
|
19972
|
+
return task;
|
|
19973
|
+
const workspace = input.store.readWorkspace();
|
|
19974
|
+
if (!workspace.ok)
|
|
19975
|
+
return workspace;
|
|
19976
|
+
if (!workspace.data)
|
|
19977
|
+
return failure2("not_found", "workspace not found");
|
|
19978
|
+
const workspaceRecord = workspace.data;
|
|
19979
|
+
if (task.data.status !== "active")
|
|
19980
|
+
return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
19981
|
+
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
19982
|
+
return failure2("recovery_required", "task and workspace bindings are invalid");
|
|
19983
|
+
const writer = workspaceRecord.writer;
|
|
19984
|
+
if (!writer)
|
|
19985
|
+
return failure2("permission_denied", "checkout has no writer owner");
|
|
19986
|
+
if (writer.state === "uncertain")
|
|
19987
|
+
return failure2("recovery_required", "checkout writer requires recovery", {
|
|
19988
|
+
owner: writer.owner
|
|
19989
|
+
});
|
|
19990
|
+
const caller = callerValue(input.caller);
|
|
19991
|
+
const workerId = workerIdOf(input.caller);
|
|
19992
|
+
const owner = writer.owner;
|
|
19993
|
+
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
19994
|
+
return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
19995
|
+
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
19996
|
+
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
19997
|
+
return failure2("permission_denied", "only implementers can own product writes");
|
|
19998
|
+
if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
|
|
19999
|
+
return failure2("recovery_required", "worker session is not an active writer");
|
|
20000
|
+
return success(null, workspaceRecord.revision, owner);
|
|
20001
|
+
}
|
|
20002
|
+
|
|
20003
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
20004
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
20005
|
+
import * as fs8 from "node:fs";
|
|
20006
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
20007
|
+
import path10 from "node:path";
|
|
20008
|
+
|
|
20009
|
+
// packages/workit-core/src/core/authority.ts
|
|
20010
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
20011
|
+
import * as fs7 from "node:fs";
|
|
20012
|
+
import path9 from "node:path";
|
|
20013
|
+
|
|
20014
|
+
// packages/workit-core/src/core/auto-approval.ts
|
|
20015
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
20016
|
+
import { readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
20017
|
+
import path8 from "node:path";
|
|
20018
|
+
|
|
20019
|
+
// packages/workit-core/src/core/workspaces.ts
|
|
20020
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
20021
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
20022
|
+
import path6 from "node:path";
|
|
20023
|
+
|
|
20024
|
+
// packages/workit-core/src/core/config.ts
|
|
20025
|
+
import {
|
|
20026
|
+
copyFileSync,
|
|
20027
|
+
cpSync,
|
|
20028
|
+
existsSync as existsSync3,
|
|
20029
|
+
mkdirSync as mkdirSync2,
|
|
20030
|
+
readFileSync as readFileSync2,
|
|
20031
|
+
readdirSync as readdirSync2,
|
|
20032
|
+
writeFileSync as writeFileSync2
|
|
20033
|
+
} from "node:fs";
|
|
20034
|
+
import os from "node:os";
|
|
20035
|
+
import path5 from "node:path";
|
|
20036
|
+
|
|
20037
|
+
// packages/workit-core/src/core/boundary.ts
|
|
20038
|
+
import { statSync as statSync2 } from "node:fs";
|
|
20039
|
+
var EVENT = {
|
|
20040
|
+
initialization: "initialization",
|
|
20041
|
+
provenance: "provenance",
|
|
20042
|
+
assets: "assets",
|
|
20043
|
+
configurationSource: "configuration_source",
|
|
20044
|
+
hooks: "hooks",
|
|
20045
|
+
mcpConnection: "mcp_connection",
|
|
20046
|
+
migration: "migration",
|
|
20047
|
+
installSteps: "install_steps",
|
|
20048
|
+
uncaughtFailure: "uncaught_failure",
|
|
20049
|
+
toolsFailed: "tools_failed",
|
|
20050
|
+
doctor: "doctor"
|
|
20051
|
+
};
|
|
20052
|
+
var errorDetail = (err) => {
|
|
19580
20053
|
if (err instanceof Error) {
|
|
19581
20054
|
return { error_name: err.name, error: err.message };
|
|
19582
20055
|
}
|
|
@@ -20205,13 +20678,13 @@ var gitContext = (workspaceRoot, paths = []) => {
|
|
|
20205
20678
|
};
|
|
20206
20679
|
|
|
20207
20680
|
// packages/workit-core/src/core/vcs-config.ts
|
|
20208
|
-
import
|
|
20681
|
+
import fs6 from "node:fs";
|
|
20209
20682
|
import path7 from "node:path";
|
|
20210
|
-
import { spawnSync } from "node:child_process";
|
|
20683
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
20211
20684
|
var vcsConfigPath = () => process.env.WORKFLOW_VCS_CONFIG ?? path7.join(configDir(), "vcs.json");
|
|
20212
20685
|
var vcsCwd = (cwd) => cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
|
|
20213
20686
|
var remoteProvider = (cwd) => {
|
|
20214
|
-
const r =
|
|
20687
|
+
const r = spawnSync2("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
20215
20688
|
if (r.status !== 0)
|
|
20216
20689
|
return null;
|
|
20217
20690
|
const url = (r.stdout ?? "").trim();
|
|
@@ -20248,7 +20721,7 @@ var remoteEndpoint = (remote) => {
|
|
|
20248
20721
|
var configuredSshEndpoint = (host) => {
|
|
20249
20722
|
if (host.startsWith("-"))
|
|
20250
20723
|
return null;
|
|
20251
|
-
const resolved =
|
|
20724
|
+
const resolved = spawnSync2("ssh", ["-G", host], {
|
|
20252
20725
|
encoding: "utf8",
|
|
20253
20726
|
timeout: 5000,
|
|
20254
20727
|
maxBuffer: 1024 * 1024,
|
|
@@ -20298,7 +20771,7 @@ var readVcsConfig = () => {
|
|
|
20298
20771
|
const file = vcsConfigPath();
|
|
20299
20772
|
let raw;
|
|
20300
20773
|
try {
|
|
20301
|
-
raw =
|
|
20774
|
+
raw = fs6.readFileSync(file, "utf8");
|
|
20302
20775
|
} catch {
|
|
20303
20776
|
return { status: "missing", path: file, config: {} };
|
|
20304
20777
|
}
|
|
@@ -20399,7 +20872,7 @@ function vcsCliIdentity(cwd) {
|
|
|
20399
20872
|
PATH: process.env.PATH ?? "",
|
|
20400
20873
|
...provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }
|
|
20401
20874
|
};
|
|
20402
|
-
const result =
|
|
20875
|
+
const result = spawnSync2(bin, ["api", "user"], {
|
|
20403
20876
|
cwd: root,
|
|
20404
20877
|
encoding: "utf8",
|
|
20405
20878
|
env
|
|
@@ -20441,7 +20914,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20441
20914
|
`).filter((l) => l.startsWith("## ")).map((l) => l.trim()),
|
|
20442
20915
|
descriptionPreview: desc.slice(0, 600)
|
|
20443
20916
|
});
|
|
20444
|
-
const remoteResult =
|
|
20917
|
+
const remoteResult = spawnSync2("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
20445
20918
|
cwd,
|
|
20446
20919
|
encoding: "utf8"
|
|
20447
20920
|
});
|
|
@@ -20457,7 +20930,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20457
20930
|
if (!hostingApiHostMatches(remote, host))
|
|
20458
20931
|
return { ok: false, error: "PR style target does not match the provider host" };
|
|
20459
20932
|
if (provider === "gitlab") {
|
|
20460
|
-
const run = (args) =>
|
|
20933
|
+
const run = (args) => spawnSync2("glab", ["api", ...args], {
|
|
20461
20934
|
cwd,
|
|
20462
20935
|
encoding: "utf8",
|
|
20463
20936
|
env
|
|
@@ -20477,7 +20950,7 @@ function mergedPrStyle(limit = 6, cwd) {
|
|
|
20477
20950
|
});
|
|
20478
20951
|
}
|
|
20479
20952
|
} else if (provider === "github") {
|
|
20480
|
-
const r =
|
|
20953
|
+
const r = spawnSync2("gh", [
|
|
20481
20954
|
"pr",
|
|
20482
20955
|
"list",
|
|
20483
20956
|
"--repo",
|
|
@@ -20536,66 +21009,6 @@ var verifyPushIdentity = (root, provider, account) => {
|
|
|
20536
21009
|
return { ok: true };
|
|
20537
21010
|
};
|
|
20538
21011
|
|
|
20539
|
-
// packages/workit-core/src/core/workers.ts
|
|
20540
|
-
var same = (left, right) => {
|
|
20541
|
-
try {
|
|
20542
|
-
return canonicalJson(left) === canonicalJson(right);
|
|
20543
|
-
} catch {
|
|
20544
|
-
return false;
|
|
20545
|
-
}
|
|
20546
|
-
};
|
|
20547
|
-
var UNCERTAIN_WORKER_STATES = ["dispatching", "running", "cancelling", "unknown"];
|
|
20548
|
-
var isUncertainWorker = (state) => UNCERTAIN_WORKER_STATES.includes(state);
|
|
20549
|
-
var workerBlocksTransition = (transition, state) => transition === "pause" ? false : isUncertainWorker(state);
|
|
20550
|
-
var callerSession = (caller) => caller.session ?? { kind: "host", host: caller.host, handle: caller.actor };
|
|
20551
|
-
var validPath = (value) => typeof value === "string" && value.length > 0 && !Array.from(value).some((char) => {
|
|
20552
|
-
const code = char.charCodeAt(0);
|
|
20553
|
-
return code < 32 || code === 127;
|
|
20554
|
-
}) && !value.split(/[\\/]/).includes("..");
|
|
20555
|
-
var callerValue = (value) => ({ host: value.host, actor: value.actor });
|
|
20556
|
-
var workerIdOf = (value) => value.workerId ?? null;
|
|
20557
|
-
var currentWriterOwnsTask = (task, workspace, caller) => {
|
|
20558
|
-
const owner = workspace.writer?.owner;
|
|
20559
|
-
return task.status === "active" && task.workspaceId === workspace.id && owner?.taskId === task.id && owner.session.host === caller.host && owner.session.handle === caller.actor;
|
|
20560
|
-
};
|
|
20561
|
-
function assertProductWriteAllowed(input) {
|
|
20562
|
-
if (!Array.isArray(input.paths) || input.paths.some((path) => !validPath(path)))
|
|
20563
|
-
return failure2("invalid_input", "invalid product write path");
|
|
20564
|
-
if (!input.store)
|
|
20565
|
-
return failure2("permission_denied", "write authorization requires core state");
|
|
20566
|
-
const task = input.store.readTask(input.task.id);
|
|
20567
|
-
if (!task.ok)
|
|
20568
|
-
return task;
|
|
20569
|
-
const workspace = input.store.readWorkspace();
|
|
20570
|
-
if (!workspace.ok)
|
|
20571
|
-
return workspace;
|
|
20572
|
-
if (!workspace.data)
|
|
20573
|
-
return failure2("not_found", "workspace not found");
|
|
20574
|
-
const workspaceRecord = workspace.data;
|
|
20575
|
-
if (task.data.status !== "active")
|
|
20576
|
-
return failure2("invalid_transition", "paused or closed tasks cannot own product writes");
|
|
20577
|
-
if (workspaceRecord.id !== task.data.workspaceId || workspaceRecord.root.length === 0)
|
|
20578
|
-
return failure2("recovery_required", "task and workspace bindings are invalid");
|
|
20579
|
-
const writer = workspaceRecord.writer;
|
|
20580
|
-
if (!writer)
|
|
20581
|
-
return failure2("permission_denied", "checkout has no writer owner");
|
|
20582
|
-
if (writer.state === "uncertain")
|
|
20583
|
-
return failure2("recovery_required", "checkout writer requires recovery", {
|
|
20584
|
-
owner: writer.owner
|
|
20585
|
-
});
|
|
20586
|
-
const caller = callerValue(input.caller);
|
|
20587
|
-
const workerId = workerIdOf(input.caller);
|
|
20588
|
-
const owner = writer.owner;
|
|
20589
|
-
if (owner.taskId !== task.data.id || owner.workerId !== workerId || owner.session.host !== caller.host || owner.session.handle !== caller.actor)
|
|
20590
|
-
return failure2("writer_conflict", "checkout is owned by another validated actor", { owner });
|
|
20591
|
-
const worker = workerId === null ? null : task.data.workers.find((entry) => entry.id === workerId);
|
|
20592
|
-
if (workerId !== null && (!worker || worker.data.assignment.role !== "implementer"))
|
|
20593
|
-
return failure2("permission_denied", "only implementers can own product writes");
|
|
20594
|
-
if (workerId !== null && (!worker?.data.session || worker.data.state === "cancelling" || worker.data.state === "unknown" || worker.data.state === "stopped" || !same(worker.data.session, callerSession(input.caller))))
|
|
20595
|
-
return failure2("recovery_required", "worker session is not an active writer");
|
|
20596
|
-
return success(null, workspaceRecord.revision, owner);
|
|
20597
|
-
}
|
|
20598
|
-
|
|
20599
21012
|
// packages/workit-core/src/core/auto-approval.ts
|
|
20600
21013
|
var AUTO_CLASSES = ["branch", "commit", "push", "pr", "merge"];
|
|
20601
21014
|
var OPERATION_CLASS = {
|
|
@@ -20882,7 +21295,7 @@ var digestBytes2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
|
20882
21295
|
var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
20883
21296
|
let root;
|
|
20884
21297
|
try {
|
|
20885
|
-
root =
|
|
21298
|
+
root = fs7.realpathSync(checkoutRoot);
|
|
20886
21299
|
} catch {
|
|
20887
21300
|
return failure2("permission_denied", "checkout root is unavailable");
|
|
20888
21301
|
}
|
|
@@ -20905,8 +21318,8 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
20905
21318
|
if (!segment)
|
|
20906
21319
|
continue;
|
|
20907
21320
|
current = path9.join(current, segment);
|
|
20908
|
-
const stat =
|
|
20909
|
-
const real =
|
|
21321
|
+
const stat = fs7.lstatSync(current);
|
|
21322
|
+
const real = fs7.realpathSync(current);
|
|
20910
21323
|
if (real !== root && !real.startsWith(`${root}${path9.sep}`))
|
|
20911
21324
|
return failure2("invalid_input", "document reference escapes checkout; cite it as an external file:// reference instead", {
|
|
20912
21325
|
fields: [
|
|
@@ -20923,7 +21336,7 @@ var verifyDecisionContentAtRoot = (checkoutRoot, binding) => {
|
|
|
20923
21336
|
if (current !== target && !stat.isDirectory())
|
|
20924
21337
|
return failure2("invalid_input", "document reference ancestor is not a directory");
|
|
20925
21338
|
}
|
|
20926
|
-
if (digestBytes2(
|
|
21339
|
+
if (digestBytes2(fs7.readFileSync(target)) !== reference.digest)
|
|
20927
21340
|
return failure2("permission_denied", "approved document bytes have changed");
|
|
20928
21341
|
} catch {
|
|
20929
21342
|
return failure2("invalid_input", "approved document is unavailable");
|
|
@@ -21286,18 +21699,41 @@ function reconcileAction(input) {
|
|
|
21286
21699
|
var bindingCovers = scopeCovers;
|
|
21287
21700
|
var verifyDecisionContent = verifyContentRefs;
|
|
21288
21701
|
|
|
21289
|
-
// packages/workit-core/src/core/
|
|
21290
|
-
var
|
|
21291
|
-
|
|
21292
|
-
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21297
|
-
|
|
21298
|
-
|
|
21299
|
-
|
|
21300
|
-
|
|
21702
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
21703
|
+
var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
|
|
21704
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
|
|
21705
|
+
var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
|
|
21706
|
+
var canonicalPath = (value) => {
|
|
21707
|
+
if (typeof value !== "string" || !value)
|
|
21708
|
+
return null;
|
|
21709
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
21710
|
+
return null;
|
|
21711
|
+
const normalized = path10.posix.normalize(value);
|
|
21712
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
21713
|
+
return null;
|
|
21714
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
21715
|
+
};
|
|
21716
|
+
var canonicalScope = (scope) => {
|
|
21717
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
21718
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
21719
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
21720
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
21721
|
+
return null;
|
|
21722
|
+
return {
|
|
21723
|
+
description: scope.description,
|
|
21724
|
+
paths: [...new Set(normalizedPaths)],
|
|
21725
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
21726
|
+
};
|
|
21727
|
+
};
|
|
21728
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
21729
|
+
var scopeMatches = (value, scope) => {
|
|
21730
|
+
const normalized = canonicalScope(scope);
|
|
21731
|
+
if (!normalized)
|
|
21732
|
+
return false;
|
|
21733
|
+
const paths = normalized.paths;
|
|
21734
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
21735
|
+
};
|
|
21736
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
21301
21737
|
var compareCodeUnits2 = (left, right) => {
|
|
21302
21738
|
const length = Math.min(left.length, right.length);
|
|
21303
21739
|
for (let index = 0;index < length; index += 1) {
|
|
@@ -21307,362 +21743,8 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
21307
21743
|
}
|
|
21308
21744
|
return left.length - right.length;
|
|
21309
21745
|
};
|
|
21310
|
-
var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits2);
|
|
21311
|
-
var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits2(canonicalJson(left), canonicalJson(right)));
|
|
21312
|
-
var normalizedScope = (scope) => ({
|
|
21313
|
-
description: scope.description,
|
|
21314
|
-
paths: stableUnique(scope.paths),
|
|
21315
|
-
exclusions: stableUnique(scope.exclusions)
|
|
21316
|
-
});
|
|
21317
|
-
var normalizedRefList = (values) => stableList(values);
|
|
21318
|
-
var normalizeAssessment = (assessment) => ({
|
|
21319
|
-
facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
|
|
21320
|
-
signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
|
|
21321
|
-
name,
|
|
21322
|
-
{ ...signal, refs: normalizedRefList(signal.refs) }
|
|
21323
|
-
])),
|
|
21324
|
-
consequences: stableList(assessment.consequences.map((consequence) => ({
|
|
21325
|
-
...consequence,
|
|
21326
|
-
fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
|
|
21327
|
-
}))),
|
|
21328
|
-
verification: stableList(assessment.verification.map((verification) => ({
|
|
21329
|
-
...verification,
|
|
21330
|
-
scope: normalizedScope(verification.scope),
|
|
21331
|
-
availableChecks: normalizedRefList(verification.availableChecks),
|
|
21332
|
-
gaps: stableUnique(verification.gaps)
|
|
21333
|
-
})))
|
|
21334
|
-
});
|
|
21335
|
-
var normalizeConstraint = (constraint) => ({
|
|
21336
|
-
...constraint,
|
|
21337
|
-
source: constraint.source,
|
|
21338
|
-
requires: stableList(constraint.requires.map((requirement) => ({
|
|
21339
|
-
...requirement,
|
|
21340
|
-
scope: normalizedScope(requirement.scope),
|
|
21341
|
-
refs: normalizedRefList(requirement.refs)
|
|
21342
|
-
})))
|
|
21343
|
-
});
|
|
21344
|
-
var normalizeDecision = (decision) => ({
|
|
21345
|
-
...decision,
|
|
21346
|
-
binding: {
|
|
21347
|
-
...decision.binding,
|
|
21348
|
-
scope: normalizedScope(decision.binding.scope),
|
|
21349
|
-
contentRefs: normalizedRefList(decision.binding.contentRefs)
|
|
21350
|
-
},
|
|
21351
|
-
requirementIds: stableUnique(decision.requirementIds)
|
|
21352
|
-
});
|
|
21353
|
-
var normalizeIntent = (intent) => ({
|
|
21354
|
-
...intent,
|
|
21355
|
-
scope: normalizedScope(intent.scope),
|
|
21356
|
-
authorityRefs: normalizedRefList(intent.authorityRefs)
|
|
21357
|
-
});
|
|
21358
|
-
var normalizePrior = (prior) => ({
|
|
21359
|
-
decisions: stableList(prior.decisions.map(normalizeDecision)),
|
|
21360
|
-
findings: stableList(prior.findings.map((finding) => ({
|
|
21361
|
-
...finding,
|
|
21362
|
-
scope: normalizedScope(finding.scope),
|
|
21363
|
-
refs: normalizedRefList(finding.refs),
|
|
21364
|
-
resolution: finding.resolution ? {
|
|
21365
|
-
...finding.resolution,
|
|
21366
|
-
evidenceIds: stableUnique(finding.resolution.evidenceIds),
|
|
21367
|
-
decisionIds: stableUnique(finding.resolution.decisionIds)
|
|
21368
|
-
} : null
|
|
21369
|
-
}))),
|
|
21370
|
-
requirements: stableList(prior.requirements.map((requirement) => ({
|
|
21371
|
-
...requirement,
|
|
21372
|
-
scope: normalizedScope(requirement.scope)
|
|
21373
|
-
})))
|
|
21374
|
-
});
|
|
21375
|
-
var invalidFields = (error) => ({
|
|
21376
|
-
fields: error.issues.map((issue) => ({
|
|
21377
|
-
path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
|
|
21378
|
-
reason: issue.message
|
|
21379
|
-
}))
|
|
21380
|
-
});
|
|
21381
|
-
function normalizeResolverInput(input) {
|
|
21382
|
-
const parsed = resolverInputSchema.safeParse(input);
|
|
21383
|
-
if (!parsed.success)
|
|
21384
|
-
return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
|
|
21385
|
-
const value = parsed.data;
|
|
21386
|
-
const signals = value.assessment.signals;
|
|
21387
|
-
if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
|
|
21388
|
-
return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
|
|
21389
|
-
fields: [
|
|
21390
|
-
{
|
|
21391
|
-
path: "assessment.signals",
|
|
21392
|
-
reason: "contradictory behaviorChange and mechanicalLowRisk"
|
|
21393
|
-
}
|
|
21394
|
-
]
|
|
21395
|
-
});
|
|
21396
|
-
const ids = value.constraints.map((constraint) => constraint.id);
|
|
21397
|
-
if (new Set(ids).size !== ids.length)
|
|
21398
|
-
return failure2("invalid_input", "constraint IDs must be unique", {
|
|
21399
|
-
fields: [{ path: "constraints", reason: "duplicate constraint id" }]
|
|
21400
|
-
});
|
|
21401
|
-
if (value.preferences?.fast && value.preferences.thorough)
|
|
21402
|
-
return failure2("invalid_input", "fast and thorough preferences contradict", {
|
|
21403
|
-
fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
|
|
21404
|
-
});
|
|
21405
|
-
const normalized = {
|
|
21406
|
-
intent: normalizeIntent(value.intent),
|
|
21407
|
-
assessment: normalizeAssessment(value.assessment),
|
|
21408
|
-
constraints: stableList(value.constraints.map(normalizeConstraint)),
|
|
21409
|
-
prior: normalizePrior(value.prior),
|
|
21410
|
-
...value.preferences ? { preferences: { ...value.preferences } } : {},
|
|
21411
|
-
policyVersion: POLICY_VERSION
|
|
21412
|
-
};
|
|
21413
|
-
return success(null, null, normalized);
|
|
21414
|
-
}
|
|
21415
|
-
var requirement = (spec) => ({
|
|
21416
|
-
...spec,
|
|
21417
|
-
id: requirementId({
|
|
21418
|
-
ruleId: spec.ruleId,
|
|
21419
|
-
scope: spec.scope,
|
|
21420
|
-
satisfaction: spec.satisfaction,
|
|
21421
|
-
before: spec.before,
|
|
21422
|
-
dependentAction: spec.dependentAction
|
|
21423
|
-
})
|
|
21424
|
-
});
|
|
21425
|
-
var signalReason = (input, name) => input.assessment.signals[name].reason;
|
|
21426
|
-
function resolveRequirements(input) {
|
|
21427
|
-
const { signals } = input.assessment;
|
|
21428
|
-
const scope = input.intent.scope;
|
|
21429
|
-
const requirements = [];
|
|
21430
|
-
const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
|
|
21431
|
-
const hasUnknownFact = [
|
|
21432
|
-
...input.assessment.facts,
|
|
21433
|
-
...input.assessment.consequences.map(({ fact }) => fact)
|
|
21434
|
-
].some((fact) => fact.basis === "unknown");
|
|
21435
|
-
if (hasUnknownSignal || hasUnknownFact)
|
|
21436
|
-
requirements.push(requirement({
|
|
21437
|
-
ruleId: "consequential-unknown",
|
|
21438
|
-
dimension: "investigation",
|
|
21439
|
-
scope,
|
|
21440
|
-
reason: `Resolve consequential uncertainty before the dependent action: ${[
|
|
21441
|
-
...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
|
|
21442
|
-
...hasUnknownFact ? ["unknown fact"] : []
|
|
21443
|
-
].join(", ")}`,
|
|
21444
|
-
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
21445
|
-
before: "dependent_action",
|
|
21446
|
-
dependentAction: null,
|
|
21447
|
-
acceptanceAllowed: false
|
|
21448
|
-
}));
|
|
21449
|
-
if (signals.productChoiceOpen.value === true)
|
|
21450
|
-
requirements.push(requirement({
|
|
21451
|
-
ruleId: "product-decision",
|
|
21452
|
-
dimension: "decisions",
|
|
21453
|
-
scope,
|
|
21454
|
-
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
21455
|
-
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
21456
|
-
before: "dependent_action",
|
|
21457
|
-
dependentAction: null,
|
|
21458
|
-
acceptanceAllowed: false
|
|
21459
|
-
}));
|
|
21460
|
-
const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
|
|
21461
|
-
if (signals.behaviorChange.value === true)
|
|
21462
|
-
requirements.push(requirement({
|
|
21463
|
-
ruleId: "behavioral-verification",
|
|
21464
|
-
dimension: "testing",
|
|
21465
|
-
scope,
|
|
21466
|
-
reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
|
|
21467
|
-
satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
|
|
21468
|
-
before: "close",
|
|
21469
|
-
dependentAction: null,
|
|
21470
|
-
acceptanceAllowed: false
|
|
21471
|
-
}), requirement({
|
|
21472
|
-
ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
|
|
21473
|
-
dimension: "review",
|
|
21474
|
-
scope,
|
|
21475
|
-
reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
|
|
21476
|
-
satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
|
|
21477
|
-
before: "close",
|
|
21478
|
-
dependentAction: null,
|
|
21479
|
-
acceptanceAllowed: !independentBehaviorReview
|
|
21480
|
-
}));
|
|
21481
|
-
else if (signals.mechanicalLowRisk.value === true)
|
|
21482
|
-
requirements.push(requirement({
|
|
21483
|
-
ruleId: "mechanical-existing-checks",
|
|
21484
|
-
dimension: "verification",
|
|
21485
|
-
scope,
|
|
21486
|
-
reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
|
|
21487
|
-
satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
|
|
21488
|
-
before: "close",
|
|
21489
|
-
dependentAction: null,
|
|
21490
|
-
acceptanceAllowed: true
|
|
21491
|
-
}), requirement({
|
|
21492
|
-
ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
|
|
21493
|
-
dimension: "review",
|
|
21494
|
-
scope,
|
|
21495
|
-
reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
|
|
21496
|
-
satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
|
|
21497
|
-
before: "close",
|
|
21498
|
-
dependentAction: null,
|
|
21499
|
-
acceptanceAllowed: !input.preferences?.thorough
|
|
21500
|
-
}));
|
|
21501
|
-
if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
|
|
21502
|
-
requirements.push(requirement({
|
|
21503
|
-
ruleId: "pre-pr-cleanup",
|
|
21504
|
-
dimension: "verification",
|
|
21505
|
-
scope,
|
|
21506
|
-
reason: "Implementation output must be deslopped before it is delivered.",
|
|
21507
|
-
satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
|
|
21508
|
-
before: "dependent_action",
|
|
21509
|
-
dependentAction: "hosting.pull_request",
|
|
21510
|
-
acceptanceAllowed: true
|
|
21511
|
-
}));
|
|
21512
|
-
if (signals.durableAgreementNeeded.value === true)
|
|
21513
|
-
requirements.push(requirement({
|
|
21514
|
-
ruleId: "durable-spec",
|
|
21515
|
-
dimension: "artifacts",
|
|
21516
|
-
scope,
|
|
21517
|
-
reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
|
|
21518
|
-
satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
|
|
21519
|
-
before: "write",
|
|
21520
|
-
dependentAction: null,
|
|
21521
|
-
acceptanceAllowed: true
|
|
21522
|
-
}));
|
|
21523
|
-
if (signals.coordinationPlanNeeded.value === true)
|
|
21524
|
-
requirements.push(requirement({
|
|
21525
|
-
ruleId: "coordination-plan",
|
|
21526
|
-
dimension: "continuity",
|
|
21527
|
-
scope,
|
|
21528
|
-
reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
|
|
21529
|
-
satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
|
|
21530
|
-
before: "dependent_action",
|
|
21531
|
-
dependentAction: null,
|
|
21532
|
-
acceptanceAllowed: true
|
|
21533
|
-
}));
|
|
21534
|
-
if (signals.helperUseful.value === true && !input.preferences?.fast)
|
|
21535
|
-
requirements.push(requirement({
|
|
21536
|
-
ruleId: "helper-usefulness",
|
|
21537
|
-
dimension: "delegation",
|
|
21538
|
-
scope,
|
|
21539
|
-
reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
|
|
21540
|
-
satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
|
|
21541
|
-
before: "dependent_action",
|
|
21542
|
-
dependentAction: null,
|
|
21543
|
-
acceptanceAllowed: true
|
|
21544
|
-
}));
|
|
21545
|
-
for (const constraint of input.constraints)
|
|
21546
|
-
for (const obligation of constraint.requires) {
|
|
21547
|
-
const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
|
|
21548
|
-
requirements.push(requirement({
|
|
21549
|
-
ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
|
|
21550
|
-
kind: obligation.kind,
|
|
21551
|
-
scope: normalizedScope(obligation.scope),
|
|
21552
|
-
refs: normalizedRefList(obligation.refs),
|
|
21553
|
-
method: obligation.method,
|
|
21554
|
-
before: obligation.before,
|
|
21555
|
-
dependentAction: obligation.dependentAction
|
|
21556
|
-
})}`,
|
|
21557
|
-
dimension,
|
|
21558
|
-
scope: obligation.scope,
|
|
21559
|
-
reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
|
|
21560
|
-
satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
|
|
21561
|
-
before: obligation.before,
|
|
21562
|
-
dependentAction: obligation.dependentAction,
|
|
21563
|
-
acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
|
|
21564
|
-
}));
|
|
21565
|
-
}
|
|
21566
|
-
const seen = new Set;
|
|
21567
|
-
return requirements.filter((item) => {
|
|
21568
|
-
if (seen.has(item.id))
|
|
21569
|
-
return false;
|
|
21570
|
-
seen.add(item.id);
|
|
21571
|
-
return true;
|
|
21572
|
-
});
|
|
21573
|
-
}
|
|
21574
|
-
function resolvePolicy(input) {
|
|
21575
|
-
const normalized = normalizeResolverInput(input);
|
|
21576
|
-
if (!normalized.ok)
|
|
21577
|
-
return normalized;
|
|
21578
|
-
const requirements = resolveRequirements(normalized.data);
|
|
21579
|
-
const policy = {
|
|
21580
|
-
policyVersion: POLICY_VERSION,
|
|
21581
|
-
inputDigest: sha256(canonicalJson(normalized.data)),
|
|
21582
|
-
requirements
|
|
21583
|
-
};
|
|
21584
|
-
const parsed = policySchema.safeParse(policy);
|
|
21585
|
-
if (!parsed.success)
|
|
21586
|
-
return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
|
|
21587
|
-
return success(null, null, parsed.data);
|
|
21588
|
-
}
|
|
21589
|
-
function diffPolicy(previous, next, reason, now) {
|
|
21590
|
-
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
21591
|
-
throw new TypeError("previous policy is invalid");
|
|
21592
|
-
if (!policySchema.safeParse(next).success)
|
|
21593
|
-
throw new TypeError("next policy is invalid");
|
|
21594
|
-
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
21595
|
-
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
21596
|
-
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
|
|
21597
|
-
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
|
|
21598
|
-
if (added.length === 0 && retired.length === 0)
|
|
21599
|
-
return null;
|
|
21600
|
-
const normalizedReason = reason.trim();
|
|
21601
|
-
const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
|
|
21602
|
-
const change = {
|
|
21603
|
-
recordedAt: now,
|
|
21604
|
-
fromInputDigest: previous?.inputDigest ?? null,
|
|
21605
|
-
toInputDigest: next.inputDigest,
|
|
21606
|
-
added,
|
|
21607
|
-
retired,
|
|
21608
|
-
reason: changeReason
|
|
21609
|
-
};
|
|
21610
|
-
const parsed = policyChangeSchema.safeParse(change);
|
|
21611
|
-
if (!parsed.success)
|
|
21612
|
-
throw new TypeError("policy change is invalid");
|
|
21613
|
-
return parsed.data;
|
|
21614
|
-
}
|
|
21615
|
-
|
|
21616
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
21617
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
21618
|
-
import * as fs7 from "node:fs";
|
|
21619
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
21620
|
-
import path10 from "node:path";
|
|
21621
|
-
var digestBytes3 = (value) => createHash6("sha256").update(value).digest("hex");
|
|
21622
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
|
|
21623
|
-
var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
|
|
21624
|
-
var canonicalPath = (value) => {
|
|
21625
|
-
if (typeof value !== "string" || !value)
|
|
21626
|
-
return null;
|
|
21627
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
21628
|
-
return null;
|
|
21629
|
-
const normalized = path10.posix.normalize(value);
|
|
21630
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
21631
|
-
return null;
|
|
21632
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
21633
|
-
};
|
|
21634
|
-
var canonicalScope = (scope) => {
|
|
21635
|
-
const paths = scope.paths.length ? scope.paths : ["."];
|
|
21636
|
-
const normalizedPaths = paths.map(canonicalPath);
|
|
21637
|
-
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
21638
|
-
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
21639
|
-
return null;
|
|
21640
|
-
return {
|
|
21641
|
-
description: scope.description,
|
|
21642
|
-
paths: [...new Set(normalizedPaths)],
|
|
21643
|
-
exclusions: [...new Set(normalizedExclusions)]
|
|
21644
|
-
};
|
|
21645
|
-
};
|
|
21646
|
-
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
21647
|
-
var scopeMatches = (value, scope) => {
|
|
21648
|
-
const normalized = canonicalScope(scope);
|
|
21649
|
-
if (!normalized)
|
|
21650
|
-
return false;
|
|
21651
|
-
const paths = normalized.paths;
|
|
21652
|
-
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
21653
|
-
};
|
|
21654
|
-
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
21655
|
-
var compareCodeUnits3 = (left, right) => {
|
|
21656
|
-
const length = Math.min(left.length, right.length);
|
|
21657
|
-
for (let index = 0;index < length; index += 1) {
|
|
21658
|
-
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
21659
|
-
if (difference !== 0)
|
|
21660
|
-
return difference;
|
|
21661
|
-
}
|
|
21662
|
-
return left.length - right.length;
|
|
21663
|
-
};
|
|
21664
21746
|
var gitPaths = (root) => {
|
|
21665
|
-
const result =
|
|
21747
|
+
const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
21666
21748
|
cwd: root,
|
|
21667
21749
|
encoding: "buffer"
|
|
21668
21750
|
});
|
|
@@ -21675,7 +21757,7 @@ var gitPaths = (root) => {
|
|
|
21675
21757
|
};
|
|
21676
21758
|
}
|
|
21677
21759
|
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
|
|
21678
|
-
const stagedDeleted =
|
|
21760
|
+
const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
21679
21761
|
if (stagedDeleted.status !== 0) {
|
|
21680
21762
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
21681
21763
|
if (!/not a git repository/i.test(stderr))
|
|
@@ -21688,7 +21770,7 @@ var gitPaths = (root) => {
|
|
|
21688
21770
|
var walk = (root, directory, output) => {
|
|
21689
21771
|
let names;
|
|
21690
21772
|
try {
|
|
21691
|
-
names =
|
|
21773
|
+
names = fs8.readdirSync(directory);
|
|
21692
21774
|
} catch {
|
|
21693
21775
|
return true;
|
|
21694
21776
|
}
|
|
@@ -21699,7 +21781,7 @@ var walk = (root, directory, output) => {
|
|
|
21699
21781
|
const target = path10.join(directory, name);
|
|
21700
21782
|
let stat;
|
|
21701
21783
|
try {
|
|
21702
|
-
stat =
|
|
21784
|
+
stat = fs8.lstatSync(target);
|
|
21703
21785
|
} catch {
|
|
21704
21786
|
uncertain = true;
|
|
21705
21787
|
continue;
|
|
@@ -21718,10 +21800,10 @@ var scopeRoots = (root, scope) => {
|
|
|
21718
21800
|
if (!inside(root, target))
|
|
21719
21801
|
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` }] });
|
|
21720
21802
|
let ancestor = target;
|
|
21721
|
-
while (!
|
|
21803
|
+
while (!fs8.existsSync(ancestor) && ancestor !== root)
|
|
21722
21804
|
ancestor = path10.dirname(ancestor);
|
|
21723
21805
|
try {
|
|
21724
|
-
if (!inside(root,
|
|
21806
|
+
if (!inside(root, fs8.realpathSync(ancestor)))
|
|
21725
21807
|
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` }] });
|
|
21726
21808
|
} catch {
|
|
21727
21809
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -21733,7 +21815,7 @@ var scopeRoots = (root, scope) => {
|
|
|
21733
21815
|
function captureCandidate(root, scope, environment = []) {
|
|
21734
21816
|
let checkout;
|
|
21735
21817
|
try {
|
|
21736
|
-
checkout =
|
|
21818
|
+
checkout = fs8.realpathSync(root);
|
|
21737
21819
|
} catch {
|
|
21738
21820
|
return failure2("invalid_input", "candidate root does not exist");
|
|
21739
21821
|
}
|
|
@@ -21751,11 +21833,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
21751
21833
|
if (item === ".git" || item === ".workit")
|
|
21752
21834
|
continue;
|
|
21753
21835
|
names.add(item);
|
|
21754
|
-
if (!
|
|
21836
|
+
if (!fs8.existsSync(target))
|
|
21755
21837
|
continue;
|
|
21756
21838
|
let stat;
|
|
21757
21839
|
try {
|
|
21758
|
-
stat =
|
|
21840
|
+
stat = fs8.lstatSync(target);
|
|
21759
21841
|
} catch {
|
|
21760
21842
|
uncertain = true;
|
|
21761
21843
|
continue;
|
|
@@ -21773,15 +21855,15 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
21773
21855
|
continue;
|
|
21774
21856
|
const target = path10.join(checkout, item);
|
|
21775
21857
|
try {
|
|
21776
|
-
const stat =
|
|
21858
|
+
const stat = fs8.lstatSync(target);
|
|
21777
21859
|
if (stat.isSymbolicLink()) {
|
|
21778
|
-
const link =
|
|
21860
|
+
const link = fs8.readlinkSync(target);
|
|
21779
21861
|
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
21780
21862
|
} else if (stat.isFile()) {
|
|
21781
21863
|
files.push({
|
|
21782
21864
|
path: item,
|
|
21783
21865
|
kind: "file",
|
|
21784
|
-
digest: digestBytes3(
|
|
21866
|
+
digest: digestBytes3(fs8.readFileSync(target)),
|
|
21785
21867
|
executable: (stat.mode & 73) !== 0
|
|
21786
21868
|
});
|
|
21787
21869
|
} else if (!stat.isDirectory()) {
|
|
@@ -21803,10 +21885,10 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
21803
21885
|
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
21804
21886
|
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
21805
21887
|
return failure2("invalid_input", "candidate environment names must be unique and non-empty");
|
|
21806
|
-
const values = supplied.toSorted((left, right) =>
|
|
21888
|
+
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
21807
21889
|
if (values.some(({ value }) => value === null))
|
|
21808
21890
|
completeness = "uncertain";
|
|
21809
|
-
const headResult =
|
|
21891
|
+
const headResult = spawnSync3("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
21810
21892
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
21811
21893
|
completeness = "uncertain";
|
|
21812
21894
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -21857,420 +21939,862 @@ function evaluateEvidence(task, candidate) {
|
|
|
21857
21939
|
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
21858
21940
|
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
21859
21941
|
return {
|
|
21860
|
-
evidenceId: entry.id,
|
|
21861
|
-
status: "stale",
|
|
21862
|
-
reason: "check or review evidence lacks start and end candidates"
|
|
21942
|
+
evidenceId: entry.id,
|
|
21943
|
+
status: "stale",
|
|
21944
|
+
reason: "check or review evidence lacks start and end candidates"
|
|
21945
|
+
};
|
|
21946
|
+
if (evidence.beforeCandidateId !== evidence.candidateId)
|
|
21947
|
+
return {
|
|
21948
|
+
evidenceId: entry.id,
|
|
21949
|
+
status: "stale",
|
|
21950
|
+
reason: "candidate changed during the check or review"
|
|
21951
|
+
};
|
|
21952
|
+
}
|
|
21953
|
+
if (!evidence.candidateId || !current)
|
|
21954
|
+
return {
|
|
21955
|
+
evidenceId: entry.id,
|
|
21956
|
+
status: evidence.result,
|
|
21957
|
+
reason: "evidence has no candidate binding"
|
|
21958
|
+
};
|
|
21959
|
+
const observed = task.candidates.find((item) => item.id === evidence.candidateId) ?? (current.id === evidence.candidateId ? current : undefined);
|
|
21960
|
+
if (!observed || observed.completeness === "uncertain" || current.completeness === "uncertain")
|
|
21961
|
+
return { evidenceId: entry.id, status: "stale", reason: "candidate is missing or uncertain" };
|
|
21962
|
+
if (observed.id === current.id)
|
|
21963
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "candidate is unchanged" };
|
|
21964
|
+
const scopes = evidenceScopes(task, evidence.requirementIds);
|
|
21965
|
+
const changed = changedPaths(observed, current);
|
|
21966
|
+
const environmentChanged = JSON.stringify(observed.environment) !== JSON.stringify(current.environment);
|
|
21967
|
+
if (!scopes.length || environmentChanged || changed.some((item) => scopes.some((scope) => pathRelevant(item, scope))))
|
|
21968
|
+
return { evidenceId: entry.id, status: "stale", reason: "relevant candidate state changed" };
|
|
21969
|
+
return {
|
|
21970
|
+
evidenceId: entry.id,
|
|
21971
|
+
status: evidence.result,
|
|
21972
|
+
reason: "candidate changed outside evidence scope"
|
|
21973
|
+
};
|
|
21974
|
+
});
|
|
21975
|
+
}
|
|
21976
|
+
var scopeCovers2 = scopeCovers;
|
|
21977
|
+
var checkPin = (assignment, beforeCandidateId, candidateId) => {
|
|
21978
|
+
const pin = assignment.candidateId ?? null;
|
|
21979
|
+
if (pin === null)
|
|
21980
|
+
return true;
|
|
21981
|
+
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
21982
|
+
};
|
|
21983
|
+
var resolveBinding = (evidence, pin, currentCandidateId) => {
|
|
21984
|
+
const bound = evidence.kind === "check" || evidence.kind === "review";
|
|
21985
|
+
const fallback = bound ? pin ?? currentCandidateId : null;
|
|
21986
|
+
return {
|
|
21987
|
+
beforeCandidateId: evidence.beforeCandidateId ?? fallback,
|
|
21988
|
+
candidateId: evidence.candidateId ?? fallback
|
|
21989
|
+
};
|
|
21990
|
+
};
|
|
21991
|
+
var findingVerificationPasses = (findingCandidateId, evidence) => evidence.status === "passed" && (evidence.kind === "check" || evidence.kind === "review") && ((findingCandidateId ?? null) === null || evidence.candidateId != null);
|
|
21992
|
+
var applicableDecision2 = (task, workspace, requirement, checkoutRoot) => task.decisions.filter((entry) => entry.provenance.kind !== "imported").map((entry) => entry.data).filter((decision) => decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.binding.taskId === task.id && decision.binding.workspaceId === workspace.id && decision.requirementIds.includes(requirement.id) && decision.binding.scope && scopeCovers2(decision.binding.scope, requirement.scope) && decision.digest === decisionDigest(decision) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, decision.binding).ok : decision.binding.contentRefs.every((reference) => reference.kind !== "file")));
|
|
21993
|
+
var applicableRequirementDecision = (task, workspace, requirement, checkoutRoot) => task.decisions.filter(({ data, provenance }) => provenance.kind !== "imported" && data.purpose !== "limitation" && (data.response === "approved" || data.response === "stated") && data.revoked === null && data.binding.taskId === task.id && data.binding.workspaceId === workspace.id && data.requirementIds.includes(requirement.id) && scopeCovers2(data.binding.scope, requirement.scope) && data.digest === decisionDigest(data) && (checkoutRoot ? verifyDecisionContentAtRoot(checkoutRoot, data.binding).ok : data.binding.contentRefs.every((reference) => reference.kind !== "file"))).map(({ id }) => ({ id }));
|
|
21994
|
+
var evidenceMatchesRequirement = (kind, evidenceKind, dimension, ruleId) => {
|
|
21995
|
+
if (kind !== "passed")
|
|
21996
|
+
return false;
|
|
21997
|
+
if (dimension === "testing" || dimension === "verification")
|
|
21998
|
+
return evidenceKind === "check" || ruleId === "pre-pr-cleanup" && evidenceKind === "artifact";
|
|
21999
|
+
if (dimension === "review")
|
|
22000
|
+
return evidenceKind === "review";
|
|
22001
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
22002
|
+
return evidenceKind === "investigation";
|
|
22003
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
22004
|
+
return evidenceKind === "artifact";
|
|
22005
|
+
return false;
|
|
22006
|
+
};
|
|
22007
|
+
var expectedKinds = (dimension, ruleId) => {
|
|
22008
|
+
if (dimension === "testing")
|
|
22009
|
+
return "check";
|
|
22010
|
+
if (dimension === "verification")
|
|
22011
|
+
return ruleId === "pre-pr-cleanup" ? "check or artifact" : "check";
|
|
22012
|
+
if (dimension === "review")
|
|
22013
|
+
return "review";
|
|
22014
|
+
if (dimension === "investigation" || dimension === "challenge")
|
|
22015
|
+
return "investigation";
|
|
22016
|
+
if (dimension === "artifacts" || dimension === "continuity")
|
|
22017
|
+
return "artifact";
|
|
22018
|
+
if (dimension === "decisions")
|
|
22019
|
+
return "decision";
|
|
22020
|
+
return "worker report";
|
|
22021
|
+
};
|
|
22022
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller) {
|
|
22023
|
+
const evidence = evaluateEvidence(task, candidate);
|
|
22024
|
+
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
22025
|
+
return task.policy.requirements.map((requirement) => ({
|
|
22026
|
+
requirementId: requirement.id,
|
|
22027
|
+
status: "unsatisfied",
|
|
22028
|
+
evidenceIds: [],
|
|
22029
|
+
decisionIds: [],
|
|
22030
|
+
reason: "stored policy version is unsupported; reassessment is required"
|
|
22031
|
+
}));
|
|
22032
|
+
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
22033
|
+
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
22034
|
+
const passed = related.filter(({ entry, evaluation }) => {
|
|
22035
|
+
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
22036
|
+
return false;
|
|
22037
|
+
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
22038
|
+
return false;
|
|
22039
|
+
if (requirement.dimension !== "review")
|
|
22040
|
+
return true;
|
|
22041
|
+
const reviewSession = sessionFromRef(entry.data.reviewContext);
|
|
22042
|
+
const sameImplementation = sameSession(reviewSession, task.intent.provenance.session);
|
|
22043
|
+
const sameEvidenceSession = related.some(({ entry: other }) => other.id !== entry.id && other.data.kind === "review" && other.data.candidateId !== null && other.data.candidateId === entry.data.candidateId && sameSession(reviewSession, other.provenance.session));
|
|
22044
|
+
return entry.data.kind === "review" && reviewSession !== null && sameSession(reviewSession, entry.provenance.session) && (requirement.ruleId === "self-review" || !sameImplementation && !sameEvidenceSession);
|
|
22045
|
+
});
|
|
22046
|
+
if (passed.length) {
|
|
22047
|
+
const currentId = candidate?.id ?? null;
|
|
22048
|
+
const withRed = requirement.dimension === "testing" ? passed.filter(({ entry }) => related.some((other) => other.entry.data.kind === "check" && other.evaluation.status === "failed" && other.entry.data.requirementIds.includes(requirement.id) && other.entry.recordedAt <= entry.recordedAt) || related.some((baseline) => baseline.entry.id !== entry.id && baseline.entry.data.kind === "check" && baseline.entry.data.result === "passed" && baseline.entry.data.candidateId !== null && currentId !== null && baseline.entry.data.candidateId !== currentId)) : passed;
|
|
22049
|
+
if (withRed.length)
|
|
22050
|
+
return {
|
|
22051
|
+
requirementId: requirement.id,
|
|
22052
|
+
status: "satisfied",
|
|
22053
|
+
evidenceIds: withRed.map(({ entry }) => entry.id),
|
|
22054
|
+
decisionIds: [],
|
|
22055
|
+
reason: "fresh applicable evidence passed"
|
|
21863
22056
|
};
|
|
21864
|
-
if (
|
|
22057
|
+
if (requirement.dimension === "testing")
|
|
21865
22058
|
return {
|
|
21866
|
-
|
|
21867
|
-
status: "
|
|
21868
|
-
|
|
22059
|
+
requirementId: requirement.id,
|
|
22060
|
+
status: "unsatisfied",
|
|
22061
|
+
evidenceIds: [],
|
|
22062
|
+
decisionIds: [],
|
|
22063
|
+
reason: "testing requirement has GREEN evidence but no preceding RED"
|
|
21869
22064
|
};
|
|
21870
22065
|
}
|
|
21871
|
-
if (
|
|
22066
|
+
if (requirement.dimension === "delegation") {
|
|
22067
|
+
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
22068
|
+
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
22069
|
+
if (reconciled)
|
|
22070
|
+
return {
|
|
22071
|
+
requirementId: requirement.id,
|
|
22072
|
+
status: "satisfied",
|
|
22073
|
+
evidenceIds: [],
|
|
22074
|
+
decisionIds: [],
|
|
22075
|
+
reason: "bounded helper completed and its result is recorded"
|
|
22076
|
+
};
|
|
22077
|
+
}
|
|
22078
|
+
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
22079
|
+
if (decisions.length)
|
|
21872
22080
|
return {
|
|
21873
|
-
|
|
21874
|
-
status:
|
|
21875
|
-
|
|
22081
|
+
requirementId: requirement.id,
|
|
22082
|
+
status: "satisfied",
|
|
22083
|
+
evidenceIds: [],
|
|
22084
|
+
decisionIds: decisions.map((decision) => decision.id),
|
|
22085
|
+
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
21876
22086
|
};
|
|
21877
|
-
const
|
|
21878
|
-
if (
|
|
21879
|
-
return {
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
22087
|
+
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
22088
|
+
if (limitations.length)
|
|
22089
|
+
return {
|
|
22090
|
+
requirementId: requirement.id,
|
|
22091
|
+
status: "accepted_limitation",
|
|
22092
|
+
evidenceIds: [],
|
|
22093
|
+
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
22094
|
+
reason: "an applicable approved limitation permits the missing evidence"
|
|
22095
|
+
};
|
|
22096
|
+
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
22097
|
+
if (!unavailable && related.length > 0 && !passed.length)
|
|
22098
|
+
return {
|
|
22099
|
+
requirementId: requirement.id,
|
|
22100
|
+
status: "unsatisfied",
|
|
22101
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
22102
|
+
decisionIds: [],
|
|
22103
|
+
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
22104
|
+
};
|
|
22105
|
+
return {
|
|
22106
|
+
requirementId: requirement.id,
|
|
22107
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
22108
|
+
evidenceIds: related.map(({ entry }) => entry.id),
|
|
22109
|
+
decisionIds: [],
|
|
22110
|
+
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
22111
|
+
};
|
|
22112
|
+
});
|
|
22113
|
+
}
|
|
22114
|
+
function evaluateClosure(requestedOutcome, view) {
|
|
22115
|
+
if (requestedOutcome !== "stopped" && !view.task.policy)
|
|
22116
|
+
return failure2("requirements_unsatisfied", "task has not been assessed");
|
|
22117
|
+
if (view.task.policy && view.task.policy.policyVersion !== POLICY_VERSION)
|
|
22118
|
+
return failure2("requirements_unsatisfied", "stored policy version is unsupported; reassessment is required");
|
|
22119
|
+
const evaluations = view.requirements;
|
|
22120
|
+
const openFindings = view.task.findings.filter((entry) => entry.data.disposition === "open");
|
|
22121
|
+
if (requestedOutcome !== "stopped" && openFindings.length)
|
|
22122
|
+
return failure2("requirements_unsatisfied", "open findings must be resolved before closure");
|
|
22123
|
+
if (requestedOutcome !== "stopped") {
|
|
22124
|
+
const evidenceById = new Map(view.evidence.map((entry) => [entry.evidenceId, entry.status]));
|
|
22125
|
+
for (const entry of view.task.findings) {
|
|
22126
|
+
if (entry.data.disposition === "fixed") {
|
|
22127
|
+
const verified = entry.data.resolution?.evidenceIds.some((id) => {
|
|
22128
|
+
const evidence = view.task.evidence.find((item) => item.id === id);
|
|
22129
|
+
if (!evidence)
|
|
22130
|
+
return false;
|
|
22131
|
+
return findingVerificationPasses(entry.data.candidateId, {
|
|
22132
|
+
kind: evidence.data.kind,
|
|
22133
|
+
candidateId: evidence.data.candidateId,
|
|
22134
|
+
status: evidenceById.get(id) ?? "missing"
|
|
22135
|
+
});
|
|
22136
|
+
});
|
|
22137
|
+
if (!verified)
|
|
22138
|
+
return failure2("requirements_unsatisfied", "fixed findings require current verification");
|
|
22139
|
+
}
|
|
22140
|
+
if (entry.data.disposition === "deferred") {
|
|
22141
|
+
const valid = entry.data.resolution?.decisionIds.some((id) => {
|
|
22142
|
+
const decisionEntry = view.task.decisions.find((item) => item.id === id);
|
|
22143
|
+
const decision = decisionEntry?.data;
|
|
22144
|
+
return Boolean(decision && decisionEntry?.provenance.kind !== "imported" && decision.purpose === "limitation" && decision.response === "approved" && decision.revoked === null && decision.digest === decisionDigest(decision) && verifyDecisionContentAtRoot(view.workspace.root, decision.binding).ok && decision.binding.taskId === view.task.id && decision.binding.workspaceId === view.workspace.id && scopeCovers2(decision.binding.scope, entry.data.scope) && decision.requirementIds.some((requirementId) => view.task.policy?.requirements.some((requirement) => requirement.id === requirementId && requirement.acceptanceAllowed)));
|
|
22145
|
+
});
|
|
22146
|
+
if (!valid)
|
|
22147
|
+
return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
22148
|
+
}
|
|
22149
|
+
}
|
|
22150
|
+
}
|
|
22151
|
+
const blocking = evaluations.filter((item) => {
|
|
22152
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
22153
|
+
return false;
|
|
22154
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
22155
|
+
return !requirement || requirement.before === "close";
|
|
22156
|
+
});
|
|
22157
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
22158
|
+
return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
22159
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
22160
|
+
requirements: blocking.map((item) => {
|
|
22161
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
22162
|
+
return {
|
|
22163
|
+
requirementId: item.requirementId,
|
|
22164
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
22165
|
+
reason: item.reason,
|
|
22166
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
22167
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
22168
|
+
};
|
|
22169
|
+
})
|
|
22170
|
+
});
|
|
22171
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
22172
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
22173
|
+
return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
22174
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
22175
|
+
});
|
|
22176
|
+
const evidenceIds = [
|
|
22177
|
+
...new Set([
|
|
22178
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
22179
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
22180
|
+
])
|
|
22181
|
+
];
|
|
22182
|
+
const decisionIds = [
|
|
22183
|
+
...new Set([
|
|
22184
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
22185
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
22186
|
+
])
|
|
22187
|
+
];
|
|
22188
|
+
return success(null, null, {
|
|
22189
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
22190
|
+
evidenceIds,
|
|
22191
|
+
decisionIds,
|
|
22192
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
22193
|
+
});
|
|
22194
|
+
}
|
|
22195
|
+
|
|
22196
|
+
// packages/workit-core/src/core/methods.ts
|
|
22197
|
+
var METHODS = {
|
|
22198
|
+
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
22199
|
+
"workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
|
|
22200
|
+
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
22201
|
+
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
22202
|
+
"workit-implement": { dimensions: ["delegation"] },
|
|
22203
|
+
"workit-debug": {},
|
|
22204
|
+
"workit-handoff": {},
|
|
22205
|
+
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
22206
|
+
};
|
|
22207
|
+
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
22208
|
+
var assuranceFor = (id, definition, capabilities) => {
|
|
22209
|
+
const keys = new Set([
|
|
22210
|
+
id,
|
|
22211
|
+
...definition.dimensions ?? [],
|
|
22212
|
+
...definition.ruleIds ?? []
|
|
22213
|
+
]);
|
|
22214
|
+
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
22215
|
+
if (id === "workit-review" && matches.length === 0)
|
|
22216
|
+
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
22217
|
+
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
22218
|
+
return {
|
|
22219
|
+
assurance: "unavailable",
|
|
22220
|
+
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
22221
|
+
};
|
|
22222
|
+
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
22223
|
+
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
22224
|
+
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
22225
|
+
};
|
|
22226
|
+
function selectMethods(policy, capabilities) {
|
|
22227
|
+
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
22228
|
+
const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
|
|
22229
|
+
if (requirements.length === 0)
|
|
22230
|
+
return [];
|
|
22231
|
+
const capability = assuranceFor(id, definition, capabilities);
|
|
22232
|
+
const reasons = [
|
|
22233
|
+
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
22234
|
+
...capability.reasons
|
|
22235
|
+
];
|
|
22236
|
+
if (capability.assurance === "unavailable")
|
|
22237
|
+
reasons.unshift("required capability is unavailable");
|
|
22238
|
+
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
22239
|
+
});
|
|
22240
|
+
}
|
|
22241
|
+
|
|
22242
|
+
// packages/workit-core/src/core/task-context.ts
|
|
22243
|
+
function reconcileResume(view, observations = []) {
|
|
22244
|
+
if (observations.length > 0)
|
|
22245
|
+
return failure2("permission_denied", "worker observations require native host verification");
|
|
22246
|
+
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
22247
|
+
if (!candidate.ok)
|
|
22248
|
+
return candidate;
|
|
22249
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
22250
|
+
const blockers = [...view.task.progress.blockers];
|
|
22251
|
+
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
22252
|
+
blockers.push({
|
|
22253
|
+
reason: "worker state requires reconciliation",
|
|
22254
|
+
dependentAction: "resume",
|
|
22255
|
+
refs: []
|
|
22256
|
+
});
|
|
22257
|
+
return success(null, null, {
|
|
22258
|
+
candidate: candidate.data,
|
|
22259
|
+
staleEvidenceIds,
|
|
22260
|
+
workerUpdates: [],
|
|
22261
|
+
blockers,
|
|
22262
|
+
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
22263
|
+
});
|
|
22264
|
+
}
|
|
22265
|
+
var COMPACT_MAX_BYTES = 4096;
|
|
22266
|
+
var COMPACT_MAX_ITEMS = 8;
|
|
22267
|
+
var COMPACT_TEXT_BYTES = 128;
|
|
22268
|
+
var sensitiveUrl = /\bhttps?:\/\/[^/\s:@]+:[^@\s]+@/gi;
|
|
22269
|
+
var sensitiveValue = /(?:token|secret|password|passwd|api[-_ ]?key|private[-_ ]?key|authorization)\s*[:=]\s*(?:bearer\s+)?\S+|\bbearer\s+\S+/gi;
|
|
22270
|
+
var privateKey = /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi;
|
|
22271
|
+
var compactText = (value, limit) => value === null ? null : value.replace(privateKey, "<redacted-private-key>").replace(sensitiveUrl, "<redacted-url>").replace(sensitiveValue, (match) => {
|
|
22272
|
+
const separator = match.search(/[:=]/u);
|
|
22273
|
+
if (separator >= 0)
|
|
22274
|
+
return `${match.slice(0, separator)}=<redacted>`;
|
|
22275
|
+
const space = match.search(/\s/u);
|
|
22276
|
+
return `${match.slice(0, space)} <redacted>`;
|
|
22277
|
+
}).slice(0, limit);
|
|
22278
|
+
var compactBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
22279
|
+
var compactTimestamp = (value) => `${value.slice(0, 19)}.${value.slice(20, -1).padEnd(9, "0")}`;
|
|
22280
|
+
var compactDecisionChoice = (entry) => {
|
|
22281
|
+
const stated = entry.data.binding.statedChoice?.text;
|
|
22282
|
+
const approved = entry.data.response === "approved" ? entry.data.binding.approvedContent : "";
|
|
22283
|
+
const source = stated?.trim() || approved.trim() || null;
|
|
22284
|
+
return compactText(source, COMPACT_TEXT_BYTES);
|
|
22285
|
+
};
|
|
22286
|
+
var compactReference = (ref) => {
|
|
22287
|
+
if (ref.kind === "file")
|
|
21887
22288
|
return {
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
reason: "candidate changed outside evidence scope"
|
|
22289
|
+
kind: "file",
|
|
22290
|
+
path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
|
|
21891
22291
|
};
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
return (beforeCandidateId ?? null) === pin || (candidateId ?? null) === pin;
|
|
22292
|
+
if (ref.kind === "record")
|
|
22293
|
+
return {
|
|
22294
|
+
kind: "record",
|
|
22295
|
+
collection: ref.collection,
|
|
22296
|
+
id: ref.id
|
|
22297
|
+
};
|
|
22298
|
+
return { kind: ref.kind };
|
|
21900
22299
|
};
|
|
21901
|
-
|
|
21902
|
-
const
|
|
21903
|
-
|
|
21904
|
-
|
|
21905
|
-
|
|
21906
|
-
|
|
22300
|
+
function compactTaskContext(view) {
|
|
22301
|
+
const decisions = view.task.decisions.slice().toSorted((left, right) => {
|
|
22302
|
+
const leftAt = compactTimestamp(left.recordedAt);
|
|
22303
|
+
const rightAt = compactTimestamp(right.recordedAt);
|
|
22304
|
+
if (leftAt !== rightAt)
|
|
22305
|
+
return leftAt > rightAt ? -1 : 1;
|
|
22306
|
+
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
22307
|
+
}).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
|
|
22308
|
+
id: entry.id,
|
|
22309
|
+
purpose: entry.data.purpose,
|
|
22310
|
+
status: entry.data.response,
|
|
22311
|
+
digest: entry.data.digest,
|
|
22312
|
+
choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
|
|
22313
|
+
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
22314
|
+
}));
|
|
22315
|
+
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
22316
|
+
id: method.id,
|
|
22317
|
+
assurance: method.assurance,
|
|
22318
|
+
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
22319
|
+
})) : [];
|
|
22320
|
+
const gaps = Array.from(new Set([
|
|
22321
|
+
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
22322
|
+
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
22323
|
+
...view.task.progress.blockers.map((entry) => entry.reason)
|
|
22324
|
+
])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
|
|
22325
|
+
const context = {
|
|
22326
|
+
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
22327
|
+
status: view.task.status,
|
|
22328
|
+
decisions,
|
|
22329
|
+
methods,
|
|
22330
|
+
gaps,
|
|
22331
|
+
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
21907
22332
|
};
|
|
22333
|
+
let encoded = JSON.stringify(context);
|
|
22334
|
+
while (compactBytes(encoded) > COMPACT_MAX_BYTES && (context.decisions.length || context.methods.length || context.gaps.length)) {
|
|
22335
|
+
if (context.gaps.length)
|
|
22336
|
+
context.gaps.pop();
|
|
22337
|
+
else if (context.decisions.length)
|
|
22338
|
+
context.decisions.pop();
|
|
22339
|
+
else
|
|
22340
|
+
context.methods.pop();
|
|
22341
|
+
encoded = JSON.stringify(context);
|
|
22342
|
+
}
|
|
22343
|
+
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
22344
|
+
context.objective = compactText(context.objective, 64) ?? "";
|
|
22345
|
+
context.nextAction = compactText(context.nextAction, 64);
|
|
22346
|
+
encoded = JSON.stringify(context);
|
|
22347
|
+
}
|
|
22348
|
+
return encoded;
|
|
22349
|
+
}
|
|
22350
|
+
var exportDigest = (bundle) => sha256({
|
|
22351
|
+
schemaVersion: bundle.schemaVersion,
|
|
22352
|
+
exportedAt: bundle.exportedAt,
|
|
22353
|
+
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
22354
|
+
task: bundle.task
|
|
22355
|
+
});
|
|
22356
|
+
// packages/workit-core/src/core/external-action.ts
|
|
22357
|
+
var chainStepSchema = union([
|
|
22358
|
+
string2().min(1),
|
|
22359
|
+
object2({ branch: string2().min(1) }).strict(),
|
|
22360
|
+
object2({ pr: literal(true) }).strict()
|
|
22361
|
+
]);
|
|
22362
|
+
var externalActionSchema = discriminatedUnion("operation", [
|
|
22363
|
+
object2({
|
|
22364
|
+
operation: literal("git.branch_setup"),
|
|
22365
|
+
payload: object2({
|
|
22366
|
+
action: _enum(["setup", "reapply_stash"]).optional(),
|
|
22367
|
+
sdd_dir: string2().optional(),
|
|
22368
|
+
target_branch: string2().optional(),
|
|
22369
|
+
stash: _enum(["yes", "no"]).optional(),
|
|
22370
|
+
cwd: string2().min(1).optional()
|
|
22371
|
+
}).strict()
|
|
22372
|
+
}).strict(),
|
|
22373
|
+
object2({
|
|
22374
|
+
operation: literal("git.commit"),
|
|
22375
|
+
payload: object2({
|
|
22376
|
+
message: string2().min(1).optional(),
|
|
22377
|
+
plan_steps: array(chainStepSchema).min(1).max(32).optional(),
|
|
22378
|
+
plan_branch: string2().min(1).optional(),
|
|
22379
|
+
cwd: string2().min(1).optional()
|
|
22380
|
+
}).strict()
|
|
22381
|
+
}).strict(),
|
|
22382
|
+
object2({
|
|
22383
|
+
operation: literal("git.push"),
|
|
22384
|
+
payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
|
|
22385
|
+
}).strict(),
|
|
22386
|
+
object2({
|
|
22387
|
+
operation: literal("hosting.pull_request"),
|
|
22388
|
+
payload: object2({
|
|
22389
|
+
title: string2().min(1),
|
|
22390
|
+
body: string2().optional(),
|
|
22391
|
+
draft: boolean2().optional(),
|
|
22392
|
+
target_branch: string2().optional(),
|
|
22393
|
+
babysit: boolean2().optional(),
|
|
22394
|
+
cwd: string2().min(1).optional()
|
|
22395
|
+
}).strict()
|
|
22396
|
+
}).strict(),
|
|
22397
|
+
object2({
|
|
22398
|
+
operation: literal("hosting.merge"),
|
|
22399
|
+
payload: object2({
|
|
22400
|
+
target_branch: string2().min(1).optional(),
|
|
22401
|
+
source_branch: string2().min(1).optional(),
|
|
22402
|
+
cwd: string2().min(1).optional()
|
|
22403
|
+
}).strict()
|
|
22404
|
+
}).strict(),
|
|
22405
|
+
object2({
|
|
22406
|
+
operation: literal("hosting.delete_branch"),
|
|
22407
|
+
payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
|
|
22408
|
+
}).strict(),
|
|
22409
|
+
object2({
|
|
22410
|
+
operation: literal("youtrack.update"),
|
|
22411
|
+
payload: object2({
|
|
22412
|
+
issueId: string2().min(1),
|
|
22413
|
+
markdown: string2().min(1),
|
|
22414
|
+
minutes: number2().positive().optional()
|
|
22415
|
+
}).strict()
|
|
22416
|
+
}).strict(),
|
|
22417
|
+
object2({
|
|
22418
|
+
operation: literal("youtrack.time"),
|
|
22419
|
+
payload: object2({
|
|
22420
|
+
issueId: string2().min(1),
|
|
22421
|
+
minutes: number2().positive(),
|
|
22422
|
+
text: string2().optional(),
|
|
22423
|
+
dateMs: number2().finite().optional()
|
|
22424
|
+
}).strict()
|
|
22425
|
+
}).strict(),
|
|
22426
|
+
object2({
|
|
22427
|
+
operation: literal("youtrack.meeting"),
|
|
22428
|
+
payload: object2({
|
|
22429
|
+
issueId: string2().min(1),
|
|
22430
|
+
minutes: number2().positive(),
|
|
22431
|
+
text: string2().min(1)
|
|
22432
|
+
}).strict()
|
|
22433
|
+
}).strict(),
|
|
22434
|
+
object2({
|
|
22435
|
+
operation: literal("changelog.apply"),
|
|
22436
|
+
payload: object2({
|
|
22437
|
+
entries: union([
|
|
22438
|
+
record(string2(), array(string2())),
|
|
22439
|
+
array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
|
|
22440
|
+
]).optional(),
|
|
22441
|
+
path: string2().optional(),
|
|
22442
|
+
normalize_only: boolean2().optional()
|
|
22443
|
+
}).strict()
|
|
22444
|
+
}).strict(),
|
|
22445
|
+
object2({
|
|
22446
|
+
operation: literal("context.read"),
|
|
22447
|
+
payload: object2({
|
|
22448
|
+
kind: _enum([
|
|
22449
|
+
"git",
|
|
22450
|
+
"pr",
|
|
22451
|
+
"youtrack",
|
|
22452
|
+
"github_issue",
|
|
22453
|
+
"gitlab_issue",
|
|
22454
|
+
"changelog",
|
|
22455
|
+
"release",
|
|
22456
|
+
"affected"
|
|
22457
|
+
]),
|
|
22458
|
+
range: string2().optional(),
|
|
22459
|
+
issueId: string2().optional(),
|
|
22460
|
+
issueUrl: string2().optional(),
|
|
22461
|
+
issueRef: string2().optional(),
|
|
22462
|
+
mode: string2().optional(),
|
|
22463
|
+
specPath: string2().optional(),
|
|
22464
|
+
planPath: string2().optional(),
|
|
22465
|
+
cwd: string2().min(1).optional()
|
|
22466
|
+
}).strict()
|
|
22467
|
+
}).strict()
|
|
22468
|
+
]);
|
|
22469
|
+
// packages/workit-core/src/core/task-engine.ts
|
|
22470
|
+
import { realpathSync as realpathSync6 } from "node:fs";
|
|
22471
|
+
|
|
22472
|
+
// packages/workit-core/src/core/policy-resolver.ts
|
|
22473
|
+
var resolverInputSchema = object2({
|
|
22474
|
+
intent: intentSchema,
|
|
22475
|
+
assessment: assessmentSchema,
|
|
22476
|
+
constraints: array(constraintSchema),
|
|
22477
|
+
prior: object2({
|
|
22478
|
+
decisions: array(decisionSchema),
|
|
22479
|
+
findings: array(findingSchema),
|
|
22480
|
+
requirements: array(requirementSchema)
|
|
22481
|
+
}).strict(),
|
|
22482
|
+
preferences: object2({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
|
|
22483
|
+
}).strict();
|
|
22484
|
+
var compareCodeUnits3 = (left, right) => {
|
|
22485
|
+
const length = Math.min(left.length, right.length);
|
|
22486
|
+
for (let index = 0;index < length; index += 1) {
|
|
22487
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
22488
|
+
if (difference !== 0)
|
|
22489
|
+
return difference;
|
|
22490
|
+
}
|
|
22491
|
+
return left.length - right.length;
|
|
21908
22492
|
};
|
|
21909
|
-
var
|
|
21910
|
-
var
|
|
21911
|
-
var
|
|
21912
|
-
|
|
21913
|
-
|
|
21914
|
-
|
|
21915
|
-
|
|
21916
|
-
|
|
21917
|
-
|
|
21918
|
-
|
|
21919
|
-
|
|
21920
|
-
|
|
21921
|
-
|
|
21922
|
-
|
|
21923
|
-
|
|
21924
|
-
|
|
21925
|
-
|
|
21926
|
-
|
|
21927
|
-
|
|
21928
|
-
|
|
21929
|
-
|
|
21930
|
-
|
|
21931
|
-
|
|
21932
|
-
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
21937
|
-
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
|
|
21943
|
-
|
|
21944
|
-
|
|
21945
|
-
|
|
21946
|
-
|
|
21947
|
-
|
|
21948
|
-
|
|
21949
|
-
|
|
21950
|
-
|
|
21951
|
-
|
|
21952
|
-
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
22493
|
+
var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
|
|
22494
|
+
var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
|
|
22495
|
+
var normalizedScope = (scope) => ({
|
|
22496
|
+
description: scope.description,
|
|
22497
|
+
paths: stableUnique(scope.paths),
|
|
22498
|
+
exclusions: stableUnique(scope.exclusions)
|
|
22499
|
+
});
|
|
22500
|
+
var normalizedRefList = (values) => stableList(values);
|
|
22501
|
+
var normalizeAssessment = (assessment) => ({
|
|
22502
|
+
facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
|
|
22503
|
+
signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
|
|
22504
|
+
name,
|
|
22505
|
+
{ ...signal, refs: normalizedRefList(signal.refs) }
|
|
22506
|
+
])),
|
|
22507
|
+
consequences: stableList(assessment.consequences.map((consequence) => ({
|
|
22508
|
+
...consequence,
|
|
22509
|
+
fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
|
|
22510
|
+
}))),
|
|
22511
|
+
verification: stableList(assessment.verification.map((verification) => ({
|
|
22512
|
+
...verification,
|
|
22513
|
+
scope: normalizedScope(verification.scope),
|
|
22514
|
+
availableChecks: normalizedRefList(verification.availableChecks),
|
|
22515
|
+
gaps: stableUnique(verification.gaps)
|
|
22516
|
+
})))
|
|
22517
|
+
});
|
|
22518
|
+
var normalizeConstraint = (constraint) => ({
|
|
22519
|
+
...constraint,
|
|
22520
|
+
source: constraint.source,
|
|
22521
|
+
requires: stableList(constraint.requires.map((requirement) => ({
|
|
22522
|
+
...requirement,
|
|
22523
|
+
scope: normalizedScope(requirement.scope),
|
|
22524
|
+
refs: normalizedRefList(requirement.refs)
|
|
22525
|
+
})))
|
|
22526
|
+
});
|
|
22527
|
+
var normalizeDecision = (decision) => ({
|
|
22528
|
+
...decision,
|
|
22529
|
+
binding: {
|
|
22530
|
+
...decision.binding,
|
|
22531
|
+
scope: normalizedScope(decision.binding.scope),
|
|
22532
|
+
contentRefs: normalizedRefList(decision.binding.contentRefs)
|
|
22533
|
+
},
|
|
22534
|
+
requirementIds: stableUnique(decision.requirementIds)
|
|
22535
|
+
});
|
|
22536
|
+
var normalizeIntent = (intent) => ({
|
|
22537
|
+
...intent,
|
|
22538
|
+
scope: normalizedScope(intent.scope),
|
|
22539
|
+
authorityRefs: normalizedRefList(intent.authorityRefs)
|
|
22540
|
+
});
|
|
22541
|
+
var normalizePrior = (prior) => ({
|
|
22542
|
+
decisions: stableList(prior.decisions.map(normalizeDecision)),
|
|
22543
|
+
findings: stableList(prior.findings.map((finding) => ({
|
|
22544
|
+
...finding,
|
|
22545
|
+
scope: normalizedScope(finding.scope),
|
|
22546
|
+
refs: normalizedRefList(finding.refs),
|
|
22547
|
+
resolution: finding.resolution ? {
|
|
22548
|
+
...finding.resolution,
|
|
22549
|
+
evidenceIds: stableUnique(finding.resolution.evidenceIds),
|
|
22550
|
+
decisionIds: stableUnique(finding.resolution.decisionIds)
|
|
22551
|
+
} : null
|
|
22552
|
+
}))),
|
|
22553
|
+
requirements: stableList(prior.requirements.map((requirement) => ({
|
|
22554
|
+
...requirement,
|
|
22555
|
+
scope: normalizedScope(requirement.scope)
|
|
22556
|
+
})))
|
|
22557
|
+
});
|
|
22558
|
+
var invalidFields = (error) => ({
|
|
22559
|
+
fields: error.issues.map((issue) => ({
|
|
22560
|
+
path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
|
|
22561
|
+
reason: issue.message
|
|
22562
|
+
}))
|
|
22563
|
+
});
|
|
22564
|
+
function normalizeResolverInput(input) {
|
|
22565
|
+
const parsed = resolverInputSchema.safeParse(input);
|
|
22566
|
+
if (!parsed.success)
|
|
22567
|
+
return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
|
|
22568
|
+
const value = parsed.data;
|
|
22569
|
+
const signals = value.assessment.signals;
|
|
22570
|
+
if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
|
|
22571
|
+
return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
|
|
22572
|
+
fields: [
|
|
22573
|
+
{
|
|
22574
|
+
path: "assessment.signals",
|
|
22575
|
+
reason: "contradictory behaviorChange and mechanicalLowRisk"
|
|
22576
|
+
}
|
|
22577
|
+
]
|
|
21963
22578
|
});
|
|
21964
|
-
|
|
21965
|
-
|
|
21966
|
-
|
|
21967
|
-
|
|
21968
|
-
|
|
21969
|
-
|
|
21970
|
-
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
21977
|
-
|
|
21978
|
-
|
|
21979
|
-
|
|
21980
|
-
|
|
21981
|
-
|
|
21982
|
-
};
|
|
21983
|
-
}
|
|
21984
|
-
if (requirement.dimension === "delegation") {
|
|
21985
|
-
const dot = (scope) => scope.paths.length > 0 ? scope : { ...scope, paths: ["."] };
|
|
21986
|
-
const reconciled = task.workers.some((worker) => worker.data.report?.outcome === "completed" && (worker.data.assignment.requirementIds.includes(requirement.id) || scopeCovers2(dot(requirement.scope), dot(worker.data.assignment.scope))));
|
|
21987
|
-
if (reconciled)
|
|
21988
|
-
return {
|
|
21989
|
-
requirementId: requirement.id,
|
|
21990
|
-
status: "satisfied",
|
|
21991
|
-
evidenceIds: [],
|
|
21992
|
-
decisionIds: [],
|
|
21993
|
-
reason: "bounded helper completed and its result is recorded"
|
|
21994
|
-
};
|
|
21995
|
-
}
|
|
21996
|
-
const decisions = requirement.dimension === "decisions" ? applicableRequirementDecision(task, workspace, requirement, checkoutRoot) : [];
|
|
21997
|
-
if (decisions.length)
|
|
21998
|
-
return {
|
|
21999
|
-
requirementId: requirement.id,
|
|
22000
|
-
status: "satisfied",
|
|
22001
|
-
evidenceIds: [],
|
|
22002
|
-
decisionIds: decisions.map((decision) => decision.id),
|
|
22003
|
-
reason: "an applicable approved or stated decision satisfies the requirement"
|
|
22004
|
-
};
|
|
22005
|
-
const limitations = requirement.acceptanceAllowed ? applicableDecision2(task, workspace, requirement, checkoutRoot) : [];
|
|
22006
|
-
if (limitations.length)
|
|
22007
|
-
return {
|
|
22008
|
-
requirementId: requirement.id,
|
|
22009
|
-
status: "accepted_limitation",
|
|
22010
|
-
evidenceIds: [],
|
|
22011
|
-
decisionIds: limitations.map((decision) => task.decisions.find((entry) => entry.data === decision).id),
|
|
22012
|
-
reason: "an applicable approved limitation permits the missing evidence"
|
|
22013
|
-
};
|
|
22014
|
-
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
22015
|
-
if (!unavailable && related.length > 0 && !passed.length)
|
|
22016
|
-
return {
|
|
22017
|
-
requirementId: requirement.id,
|
|
22018
|
-
status: "unsatisfied",
|
|
22019
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
22020
|
-
decisionIds: [],
|
|
22021
|
-
reason: `no passing evidence (needs kind:${expectedKinds(requirement.dimension, requirement.ruleId)})`
|
|
22022
|
-
};
|
|
22023
|
-
return {
|
|
22024
|
-
requirementId: requirement.id,
|
|
22025
|
-
status: unavailable ? "unavailable" : "unsatisfied",
|
|
22026
|
-
evidenceIds: related.map(({ entry }) => entry.id),
|
|
22027
|
-
decisionIds: [],
|
|
22028
|
-
reason: unavailable ? "required capability is unavailable" : "no fresh passing evidence"
|
|
22029
|
-
};
|
|
22030
|
-
});
|
|
22579
|
+
const ids = value.constraints.map((constraint) => constraint.id);
|
|
22580
|
+
if (new Set(ids).size !== ids.length)
|
|
22581
|
+
return failure2("invalid_input", "constraint IDs must be unique", {
|
|
22582
|
+
fields: [{ path: "constraints", reason: "duplicate constraint id" }]
|
|
22583
|
+
});
|
|
22584
|
+
if (value.preferences?.fast && value.preferences.thorough)
|
|
22585
|
+
return failure2("invalid_input", "fast and thorough preferences contradict", {
|
|
22586
|
+
fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
|
|
22587
|
+
});
|
|
22588
|
+
const normalized = {
|
|
22589
|
+
intent: normalizeIntent(value.intent),
|
|
22590
|
+
assessment: normalizeAssessment(value.assessment),
|
|
22591
|
+
constraints: stableList(value.constraints.map(normalizeConstraint)),
|
|
22592
|
+
prior: normalizePrior(value.prior),
|
|
22593
|
+
...value.preferences ? { preferences: { ...value.preferences } } : {},
|
|
22594
|
+
policyVersion: POLICY_VERSION
|
|
22595
|
+
};
|
|
22596
|
+
return success(null, null, normalized);
|
|
22031
22597
|
}
|
|
22032
|
-
|
|
22033
|
-
|
|
22034
|
-
|
|
22035
|
-
|
|
22036
|
-
|
|
22037
|
-
|
|
22038
|
-
|
|
22039
|
-
|
|
22040
|
-
|
|
22041
|
-
|
|
22042
|
-
|
|
22043
|
-
|
|
22044
|
-
|
|
22045
|
-
|
|
22046
|
-
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
|
|
22050
|
-
|
|
22051
|
-
|
|
22052
|
-
|
|
22053
|
-
|
|
22054
|
-
|
|
22055
|
-
|
|
22056
|
-
|
|
22057
|
-
|
|
22058
|
-
|
|
22059
|
-
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
|
|
22066
|
-
|
|
22598
|
+
var requirement = (spec) => ({
|
|
22599
|
+
...spec,
|
|
22600
|
+
id: requirementId({
|
|
22601
|
+
ruleId: spec.ruleId,
|
|
22602
|
+
scope: spec.scope,
|
|
22603
|
+
satisfaction: spec.satisfaction,
|
|
22604
|
+
before: spec.before,
|
|
22605
|
+
dependentAction: spec.dependentAction
|
|
22606
|
+
})
|
|
22607
|
+
});
|
|
22608
|
+
var signalReason = (input, name) => input.assessment.signals[name].reason;
|
|
22609
|
+
function resolveRequirements(input) {
|
|
22610
|
+
const { signals } = input.assessment;
|
|
22611
|
+
const scope = input.intent.scope;
|
|
22612
|
+
const requirements = [];
|
|
22613
|
+
const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
|
|
22614
|
+
const hasUnknownFact = [
|
|
22615
|
+
...input.assessment.facts,
|
|
22616
|
+
...input.assessment.consequences.map(({ fact }) => fact)
|
|
22617
|
+
].some((fact) => fact.basis === "unknown");
|
|
22618
|
+
if (hasUnknownSignal || hasUnknownFact)
|
|
22619
|
+
requirements.push(requirement({
|
|
22620
|
+
ruleId: "consequential-unknown",
|
|
22621
|
+
dimension: "investigation",
|
|
22622
|
+
scope,
|
|
22623
|
+
reason: `Resolve consequential uncertainty before the dependent action: ${[
|
|
22624
|
+
...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
|
|
22625
|
+
...hasUnknownFact ? ["unknown fact"] : []
|
|
22626
|
+
].join(", ")}`,
|
|
22627
|
+
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
22628
|
+
before: "dependent_action",
|
|
22629
|
+
dependentAction: null,
|
|
22630
|
+
acceptanceAllowed: false
|
|
22631
|
+
}));
|
|
22632
|
+
if (signals.productChoiceOpen.value === true)
|
|
22633
|
+
requirements.push(requirement({
|
|
22634
|
+
ruleId: "product-decision",
|
|
22635
|
+
dimension: "decisions",
|
|
22636
|
+
scope,
|
|
22637
|
+
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
22638
|
+
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
22639
|
+
before: "dependent_action",
|
|
22640
|
+
dependentAction: null,
|
|
22641
|
+
acceptanceAllowed: false
|
|
22642
|
+
}));
|
|
22643
|
+
const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
|
|
22644
|
+
if (signals.behaviorChange.value === true)
|
|
22645
|
+
requirements.push(requirement({
|
|
22646
|
+
ruleId: "behavioral-verification",
|
|
22647
|
+
dimension: "testing",
|
|
22648
|
+
scope,
|
|
22649
|
+
reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
|
|
22650
|
+
satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
|
|
22651
|
+
before: "close",
|
|
22652
|
+
dependentAction: null,
|
|
22653
|
+
acceptanceAllowed: false
|
|
22654
|
+
}), requirement({
|
|
22655
|
+
ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
|
|
22656
|
+
dimension: "review",
|
|
22657
|
+
scope,
|
|
22658
|
+
reason: independentBehaviorReview ? "Consequential behavior or the thorough preference requires fresh-context review." : "Bounded behavior changes use self-review unless consequential risks or trusted rules require more.",
|
|
22659
|
+
satisfaction: independentBehaviorReview ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
|
|
22660
|
+
before: "close",
|
|
22661
|
+
dependentAction: null,
|
|
22662
|
+
acceptanceAllowed: !independentBehaviorReview
|
|
22663
|
+
}));
|
|
22664
|
+
else if (signals.mechanicalLowRisk.value === true)
|
|
22665
|
+
requirements.push(requirement({
|
|
22666
|
+
ruleId: "mechanical-existing-checks",
|
|
22667
|
+
dimension: "verification",
|
|
22668
|
+
scope,
|
|
22669
|
+
reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
|
|
22670
|
+
satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
|
|
22671
|
+
before: "close",
|
|
22672
|
+
dependentAction: null,
|
|
22673
|
+
acceptanceAllowed: true
|
|
22674
|
+
}), requirement({
|
|
22675
|
+
ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
|
|
22676
|
+
dimension: "review",
|
|
22677
|
+
scope,
|
|
22678
|
+
reason: input.preferences?.thorough ? "The thorough preference upgrades mechanical self-review to fresh-context review." : "Mechanical low-risk work uses self-review rather than mandatory fresh-context review.",
|
|
22679
|
+
satisfaction: input.preferences?.thorough ? "A separate review context examines the current candidate and records its result." : "The lead reviews the resulting diff and records the relevant scope and checks.",
|
|
22680
|
+
before: "close",
|
|
22681
|
+
dependentAction: null,
|
|
22682
|
+
acceptanceAllowed: !input.preferences?.thorough
|
|
22683
|
+
}));
|
|
22684
|
+
if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
|
|
22685
|
+
requirements.push(requirement({
|
|
22686
|
+
ruleId: "pre-pr-cleanup",
|
|
22687
|
+
dimension: "verification",
|
|
22688
|
+
scope,
|
|
22689
|
+
reason: "Implementation output must be deslopped before it is delivered.",
|
|
22690
|
+
satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
|
|
22691
|
+
before: "dependent_action",
|
|
22692
|
+
dependentAction: "hosting.pull_request",
|
|
22693
|
+
acceptanceAllowed: true
|
|
22694
|
+
}));
|
|
22695
|
+
if (signals.durableAgreementNeeded.value === true)
|
|
22696
|
+
requirements.push(requirement({
|
|
22697
|
+
ruleId: "durable-spec",
|
|
22698
|
+
dimension: "artifacts",
|
|
22699
|
+
scope,
|
|
22700
|
+
reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
|
|
22701
|
+
satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
|
|
22702
|
+
before: "write",
|
|
22703
|
+
dependentAction: null,
|
|
22704
|
+
acceptanceAllowed: true
|
|
22705
|
+
}));
|
|
22706
|
+
if (signals.coordinationPlanNeeded.value === true)
|
|
22707
|
+
requirements.push(requirement({
|
|
22708
|
+
ruleId: "coordination-plan",
|
|
22709
|
+
dimension: "continuity",
|
|
22710
|
+
scope,
|
|
22711
|
+
reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
|
|
22712
|
+
satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
|
|
22713
|
+
before: "dependent_action",
|
|
22714
|
+
dependentAction: null,
|
|
22715
|
+
acceptanceAllowed: true
|
|
22716
|
+
}));
|
|
22717
|
+
if (signals.helperUseful.value === true && !input.preferences?.fast)
|
|
22718
|
+
requirements.push(requirement({
|
|
22719
|
+
ruleId: "helper-usefulness",
|
|
22720
|
+
dimension: "delegation",
|
|
22721
|
+
scope,
|
|
22722
|
+
reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
|
|
22723
|
+
satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
|
|
22724
|
+
before: "dependent_action",
|
|
22725
|
+
dependentAction: null,
|
|
22726
|
+
acceptanceAllowed: true
|
|
22727
|
+
}));
|
|
22728
|
+
for (const constraint of input.constraints)
|
|
22729
|
+
for (const obligation of constraint.requires) {
|
|
22730
|
+
const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
|
|
22731
|
+
requirements.push(requirement({
|
|
22732
|
+
ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
|
|
22733
|
+
kind: obligation.kind,
|
|
22734
|
+
scope: normalizedScope(obligation.scope),
|
|
22735
|
+
refs: normalizedRefList(obligation.refs),
|
|
22736
|
+
method: obligation.method,
|
|
22737
|
+
before: obligation.before,
|
|
22738
|
+
dependentAction: obligation.dependentAction
|
|
22739
|
+
})}`,
|
|
22740
|
+
dimension,
|
|
22741
|
+
scope: obligation.scope,
|
|
22742
|
+
reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
|
|
22743
|
+
satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
|
|
22744
|
+
before: obligation.before,
|
|
22745
|
+
dependentAction: obligation.dependentAction,
|
|
22746
|
+
acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
|
|
22747
|
+
}));
|
|
22067
22748
|
}
|
|
22068
|
-
|
|
22069
|
-
|
|
22070
|
-
if (
|
|
22749
|
+
const seen = new Set;
|
|
22750
|
+
return requirements.filter((item) => {
|
|
22751
|
+
if (seen.has(item.id))
|
|
22071
22752
|
return false;
|
|
22072
|
-
|
|
22073
|
-
return
|
|
22074
|
-
});
|
|
22075
|
-
if (requestedOutcome !== "stopped" && blocking.length)
|
|
22076
|
-
return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
22077
|
-
requirementIds: blocking.map((item) => item.requirementId),
|
|
22078
|
-
requirements: blocking.map((item) => {
|
|
22079
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
22080
|
-
return {
|
|
22081
|
-
requirementId: item.requirementId,
|
|
22082
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
22083
|
-
reason: item.reason,
|
|
22084
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
22085
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
22086
|
-
};
|
|
22087
|
-
})
|
|
22088
|
-
});
|
|
22089
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
22090
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
22091
|
-
return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
22092
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
22093
|
-
});
|
|
22094
|
-
const evidenceIds = [
|
|
22095
|
-
...new Set([
|
|
22096
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
22097
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
22098
|
-
])
|
|
22099
|
-
];
|
|
22100
|
-
const decisionIds = [
|
|
22101
|
-
...new Set([
|
|
22102
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
22103
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
22104
|
-
])
|
|
22105
|
-
];
|
|
22106
|
-
return success(null, null, {
|
|
22107
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
22108
|
-
evidenceIds,
|
|
22109
|
-
decisionIds,
|
|
22110
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
22111
|
-
});
|
|
22112
|
-
}
|
|
22113
|
-
|
|
22114
|
-
// packages/workit-core/src/core/methods.ts
|
|
22115
|
-
var METHODS = {
|
|
22116
|
-
"workit-challenge": { dimensions: ["challenge", "decisions"] },
|
|
22117
|
-
"workit-behavioral-tdd": { dimensions: ["testing"], ruleIds: ["mechanical-existing-checks"] },
|
|
22118
|
-
"workit-review": { dimensions: ["review"], ruleIds: ["fresh-context-review", "self-review"] },
|
|
22119
|
-
"workit-plan": { dimensions: ["artifacts", "continuity"] },
|
|
22120
|
-
"workit-implement": { dimensions: ["delegation"] },
|
|
22121
|
-
"workit-debug": {},
|
|
22122
|
-
"workit-handoff": {},
|
|
22123
|
-
"workit-deslop": { ruleIds: ["pre-pr-cleanup"] }
|
|
22124
|
-
};
|
|
22125
|
-
var methodMatches = (definition, requirement) => definition.dimensions?.includes(requirement.dimension) === true || definition.ruleIds?.includes(requirement.ruleId) === true;
|
|
22126
|
-
var assuranceFor = (id, definition, capabilities) => {
|
|
22127
|
-
const keys = new Set([
|
|
22128
|
-
id,
|
|
22129
|
-
...definition.dimensions ?? [],
|
|
22130
|
-
...definition.ruleIds ?? []
|
|
22131
|
-
]);
|
|
22132
|
-
const matches = capabilities.filter((capability) => keys.has(capability.name) || keys.has(capability.surface));
|
|
22133
|
-
if (id === "workit-review" && matches.length === 0)
|
|
22134
|
-
return { assurance: "unavailable", reasons: ["independent review capability is unavailable"] };
|
|
22135
|
-
if (matches.some((capability) => capability.assurance === "unavailable"))
|
|
22136
|
-
return {
|
|
22137
|
-
assurance: "unavailable",
|
|
22138
|
-
reasons: matches.filter((capability) => capability.assurance === "unavailable").map((capability) => capability.reason)
|
|
22139
|
-
};
|
|
22140
|
-
if (matches.some((capability) => capability.assurance === "enforced"))
|
|
22141
|
-
return { assurance: "enforced", reasons: matches.map((capability) => capability.reason) };
|
|
22142
|
-
return { assurance: "agent_guided", reasons: matches.map((capability) => capability.reason) };
|
|
22143
|
-
};
|
|
22144
|
-
function selectMethods(policy, capabilities) {
|
|
22145
|
-
return Object.entries(METHODS).flatMap(([id, definition]) => {
|
|
22146
|
-
const requirements = policy.requirements.filter((requirement) => methodMatches(definition, requirement));
|
|
22147
|
-
if (requirements.length === 0)
|
|
22148
|
-
return [];
|
|
22149
|
-
const capability = assuranceFor(id, definition, capabilities);
|
|
22150
|
-
const reasons = [
|
|
22151
|
-
...new Set(requirements.map((requirement) => requirement.reason)),
|
|
22152
|
-
...capability.reasons
|
|
22153
|
-
];
|
|
22154
|
-
if (capability.assurance === "unavailable")
|
|
22155
|
-
reasons.unshift("required capability is unavailable");
|
|
22156
|
-
return [{ id, reason: reasons.join("; "), assurance: capability.assurance }];
|
|
22753
|
+
seen.add(item.id);
|
|
22754
|
+
return true;
|
|
22157
22755
|
});
|
|
22158
22756
|
}
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
22163
|
-
|
|
22164
|
-
const
|
|
22165
|
-
|
|
22166
|
-
|
|
22167
|
-
|
|
22168
|
-
|
|
22169
|
-
|
|
22170
|
-
|
|
22171
|
-
|
|
22172
|
-
|
|
22173
|
-
refs: []
|
|
22174
|
-
});
|
|
22175
|
-
return success(null, null, {
|
|
22176
|
-
candidate: candidate.data,
|
|
22177
|
-
staleEvidenceIds,
|
|
22178
|
-
workerUpdates: [],
|
|
22179
|
-
blockers,
|
|
22180
|
-
reassessmentRequired: view.task.policy?.policyVersion !== POLICY_VERSION
|
|
22181
|
-
});
|
|
22757
|
+
function resolvePolicy(input) {
|
|
22758
|
+
const normalized = normalizeResolverInput(input);
|
|
22759
|
+
if (!normalized.ok)
|
|
22760
|
+
return normalized;
|
|
22761
|
+
const requirements = resolveRequirements(normalized.data);
|
|
22762
|
+
const policy = {
|
|
22763
|
+
policyVersion: POLICY_VERSION,
|
|
22764
|
+
inputDigest: sha256(canonicalJson(normalized.data)),
|
|
22765
|
+
requirements
|
|
22766
|
+
};
|
|
22767
|
+
const parsed = policySchema.safeParse(policy);
|
|
22768
|
+
if (!parsed.success)
|
|
22769
|
+
return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
|
|
22770
|
+
return success(null, null, parsed.data);
|
|
22182
22771
|
}
|
|
22183
|
-
|
|
22184
|
-
|
|
22185
|
-
|
|
22186
|
-
|
|
22187
|
-
|
|
22188
|
-
|
|
22189
|
-
|
|
22190
|
-
const
|
|
22191
|
-
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
}
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
|
|
22201
|
-
|
|
22202
|
-
|
|
22203
|
-
};
|
|
22204
|
-
var compactReference = (ref) => {
|
|
22205
|
-
if (ref.kind === "file")
|
|
22206
|
-
return {
|
|
22207
|
-
kind: "file",
|
|
22208
|
-
path: compactText(ref.path, COMPACT_TEXT_BYTES) ?? ""
|
|
22209
|
-
};
|
|
22210
|
-
if (ref.kind === "record")
|
|
22211
|
-
return {
|
|
22212
|
-
kind: "record",
|
|
22213
|
-
collection: ref.collection,
|
|
22214
|
-
id: ref.id
|
|
22215
|
-
};
|
|
22216
|
-
return { kind: ref.kind };
|
|
22217
|
-
};
|
|
22218
|
-
function compactTaskContext(view) {
|
|
22219
|
-
const decisions = view.task.decisions.slice().toSorted((left, right) => {
|
|
22220
|
-
const leftAt = compactTimestamp(left.recordedAt);
|
|
22221
|
-
const rightAt = compactTimestamp(right.recordedAt);
|
|
22222
|
-
if (leftAt !== rightAt)
|
|
22223
|
-
return leftAt > rightAt ? -1 : 1;
|
|
22224
|
-
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
22225
|
-
}).slice(0, COMPACT_MAX_ITEMS).map((entry) => ({
|
|
22226
|
-
id: entry.id,
|
|
22227
|
-
purpose: entry.data.purpose,
|
|
22228
|
-
status: entry.data.response,
|
|
22229
|
-
digest: entry.data.digest,
|
|
22230
|
-
choice: compactDecisionChoice(entry) || `task.inspect taskId=${view.task.id} view=full; decision=${entry.id}`,
|
|
22231
|
-
references: entry.data.binding.contentRefs.slice(0, 1).map(compactReference)
|
|
22232
|
-
}));
|
|
22233
|
-
const methods = view.task.policy ? selectMethods(view.task.policy, view.capabilities).map((method) => ({
|
|
22234
|
-
id: method.id,
|
|
22235
|
-
assurance: method.assurance,
|
|
22236
|
-
reason: compactText(method.reason, COMPACT_TEXT_BYTES) ?? ""
|
|
22237
|
-
})) : [];
|
|
22238
|
-
const gaps = Array.from(new Set([
|
|
22239
|
-
...view.evidence.filter((entry) => entry.status === "stale").map((entry) => `stale evidence: ${entry.evidenceId}`),
|
|
22240
|
-
...view.requirements.filter((entry) => entry.status !== "satisfied").map((entry) => entry.reason),
|
|
22241
|
-
...view.task.progress.blockers.map((entry) => entry.reason)
|
|
22242
|
-
])).toSorted().slice(0, COMPACT_MAX_ITEMS).map((gap) => compactText(gap, COMPACT_TEXT_BYTES));
|
|
22243
|
-
const context = {
|
|
22244
|
-
objective: compactText(view.task.intent.data.objective, COMPACT_TEXT_BYTES) ?? "",
|
|
22245
|
-
status: view.task.status,
|
|
22246
|
-
decisions,
|
|
22247
|
-
methods,
|
|
22248
|
-
gaps,
|
|
22249
|
-
nextAction: compactText(view.task.progress.nextAction, COMPACT_TEXT_BYTES)
|
|
22772
|
+
function diffPolicy(previous, next, reason, now) {
|
|
22773
|
+
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
22774
|
+
throw new TypeError("previous policy is invalid");
|
|
22775
|
+
if (!policySchema.safeParse(next).success)
|
|
22776
|
+
throw new TypeError("next policy is invalid");
|
|
22777
|
+
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
22778
|
+
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
22779
|
+
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
|
|
22780
|
+
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
|
|
22781
|
+
if (added.length === 0 && retired.length === 0)
|
|
22782
|
+
return null;
|
|
22783
|
+
const normalizedReason = reason.trim();
|
|
22784
|
+
const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
|
|
22785
|
+
const change = {
|
|
22786
|
+
recordedAt: now,
|
|
22787
|
+
fromInputDigest: previous?.inputDigest ?? null,
|
|
22788
|
+
toInputDigest: next.inputDigest,
|
|
22789
|
+
added,
|
|
22790
|
+
retired,
|
|
22791
|
+
reason: changeReason
|
|
22250
22792
|
};
|
|
22251
|
-
|
|
22252
|
-
|
|
22253
|
-
|
|
22254
|
-
|
|
22255
|
-
else if (context.decisions.length)
|
|
22256
|
-
context.decisions.pop();
|
|
22257
|
-
else
|
|
22258
|
-
context.methods.pop();
|
|
22259
|
-
encoded = JSON.stringify(context);
|
|
22260
|
-
}
|
|
22261
|
-
if (compactBytes(encoded) > COMPACT_MAX_BYTES) {
|
|
22262
|
-
context.objective = compactText(context.objective, 64) ?? "";
|
|
22263
|
-
context.nextAction = compactText(context.nextAction, 64);
|
|
22264
|
-
encoded = JSON.stringify(context);
|
|
22265
|
-
}
|
|
22266
|
-
return encoded;
|
|
22793
|
+
const parsed = policyChangeSchema.safeParse(change);
|
|
22794
|
+
if (!parsed.success)
|
|
22795
|
+
throw new TypeError("policy change is invalid");
|
|
22796
|
+
return parsed.data;
|
|
22267
22797
|
}
|
|
22268
|
-
var exportDigest = (bundle) => sha256({
|
|
22269
|
-
schemaVersion: bundle.schemaVersion,
|
|
22270
|
-
exportedAt: bundle.exportedAt,
|
|
22271
|
-
sourceWorkspaceId: bundle.sourceWorkspaceId,
|
|
22272
|
-
task: bundle.task
|
|
22273
|
-
});
|
|
22274
22798
|
|
|
22275
22799
|
// packages/workit-core/src/core/task-engine.ts
|
|
22276
22800
|
var provenance = (context, kind = context.provenanceKind ?? "host_observed") => ({
|
|
@@ -22443,6 +22967,29 @@ var applyWorkerLifecycle = (input) => {
|
|
|
22443
22967
|
return updated ? success(changed.data.task.revision, changed.data.workspace.revision, updated) : failure2("recovery_required", "worker disappeared during lifecycle observation");
|
|
22444
22968
|
};
|
|
22445
22969
|
var refsWithinScope = (refs, scope) => refs.every((ref) => ref.kind !== "file" || bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }));
|
|
22970
|
+
var REVISION_RETRY_ATTEMPTS = 8;
|
|
22971
|
+
var retryPause = new Int32Array(new SharedArrayBuffer(4));
|
|
22972
|
+
var filledRevisions = (request) => {
|
|
22973
|
+
const value = typeof request === "object" && request !== null ? request : {};
|
|
22974
|
+
return {
|
|
22975
|
+
task: value.expectedRevision === undefined,
|
|
22976
|
+
workspace: value.expectedWorkspaceRevision === undefined
|
|
22977
|
+
};
|
|
22978
|
+
};
|
|
22979
|
+
var filledConflict = (result, filled) => !result.ok && result.code === "revision_conflict" && (("actualRevision" in result.details) && filled.task || ("actualWorkspaceRevision" in result.details) && filled.workspace);
|
|
22980
|
+
var retryFilledRevisions = (filled, run) => {
|
|
22981
|
+
if (!filled.task && !filled.workspace)
|
|
22982
|
+
return run();
|
|
22983
|
+
let result = run();
|
|
22984
|
+
const deadline = Date.now() + defaultLockTimeout();
|
|
22985
|
+
for (let attempt = 1;filledConflict(result, filled); attempt += 1) {
|
|
22986
|
+
if (attempt >= REVISION_RETRY_ATTEMPTS || Date.now() >= deadline)
|
|
22987
|
+
return failure2("busy", "records kept changing under concurrent writers; retry the call");
|
|
22988
|
+
Atomics.wait(retryPause, 0, 0, Math.floor(Math.random() * 4 * attempt) + 1);
|
|
22989
|
+
result = run();
|
|
22990
|
+
}
|
|
22991
|
+
return result;
|
|
22992
|
+
};
|
|
22446
22993
|
var trustedNow = (context) => typeof context.now === "function" ? context.now() : context.now;
|
|
22447
22994
|
var mapRef = (ref, ids) => {
|
|
22448
22995
|
if (ref.kind !== "record")
|
|
@@ -22690,6 +23237,9 @@ class WorkitCore {
|
|
|
22690
23237
|
if (input.expectedWorkspaceRevision === undefined)
|
|
22691
23238
|
input.expectedWorkspaceRevision = workspace ? workspace.revision : null;
|
|
22692
23239
|
}
|
|
23240
|
+
retryOmittedRevisions(request, run) {
|
|
23241
|
+
return retryFilledRevisions(filledRevisions(request), run);
|
|
23242
|
+
}
|
|
22693
23243
|
helperEntry(task, requireSession = false, requireActive = false) {
|
|
22694
23244
|
const workerId = this.context.workerId ?? null;
|
|
22695
23245
|
if (workerId === null)
|
|
@@ -22721,6 +23271,9 @@ class WorkitCore {
|
|
|
22721
23271
|
return null;
|
|
22722
23272
|
}
|
|
22723
23273
|
state(request) {
|
|
23274
|
+
return request?.action === "import" ? this.retryOmittedRevisions(request, () => this.stateOnce(request)) : this.stateOnce(request);
|
|
23275
|
+
}
|
|
23276
|
+
stateOnce(request) {
|
|
22724
23277
|
const root = this.contextRootError();
|
|
22725
23278
|
if (!root.ok)
|
|
22726
23279
|
return root;
|
|
@@ -22822,6 +23375,9 @@ class WorkitCore {
|
|
|
22822
23375
|
});
|
|
22823
23376
|
}
|
|
22824
23377
|
task(request) {
|
|
23378
|
+
return this.retryOmittedRevisions(request, () => this.taskOnce(request));
|
|
23379
|
+
}
|
|
23380
|
+
taskOnce(request) {
|
|
22825
23381
|
const root = this.contextRootError();
|
|
22826
23382
|
if (!root.ok)
|
|
22827
23383
|
return root;
|
|
@@ -22921,6 +23477,9 @@ class WorkitCore {
|
|
|
22921
23477
|
}
|
|
22922
23478
|
}
|
|
22923
23479
|
policy(request) {
|
|
23480
|
+
return this.retryOmittedRevisions(request, () => this.policyOnce(request));
|
|
23481
|
+
}
|
|
23482
|
+
policyOnce(request) {
|
|
22924
23483
|
const root = this.contextRootError();
|
|
22925
23484
|
if (!root.ok)
|
|
22926
23485
|
return root;
|
|
@@ -22967,6 +23526,9 @@ class WorkitCore {
|
|
|
22967
23526
|
return success(changed.data.revision, null, changed.data.policy);
|
|
22968
23527
|
}
|
|
22969
23528
|
evidence(request) {
|
|
23529
|
+
return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
|
|
23530
|
+
}
|
|
23531
|
+
evidenceOnce(request) {
|
|
22970
23532
|
const root = this.contextRootError();
|
|
22971
23533
|
if (!root.ok)
|
|
22972
23534
|
return root;
|
|
@@ -23051,6 +23613,23 @@ class WorkitCore {
|
|
|
23051
23613
|
observeStandingDecision(request) {
|
|
23052
23614
|
return this.recordDecision(request, undefined, true, true);
|
|
23053
23615
|
}
|
|
23616
|
+
commitTask(taskId, expected, filled, recheck, update) {
|
|
23617
|
+
let revision = expected;
|
|
23618
|
+
let first = true;
|
|
23619
|
+
return retryFilledRevisions({ task: filled, workspace: false }, () => {
|
|
23620
|
+
if (!first) {
|
|
23621
|
+
const fresh = this.store.readTask(taskId);
|
|
23622
|
+
if (!fresh.ok)
|
|
23623
|
+
return fresh;
|
|
23624
|
+
const checked = recheck(fresh.data);
|
|
23625
|
+
if (!checked.ok)
|
|
23626
|
+
return checked;
|
|
23627
|
+
revision = fresh.data.revision;
|
|
23628
|
+
}
|
|
23629
|
+
first = false;
|
|
23630
|
+
return this.store.mutateTask(taskId, revision, update, trustedNow(this.context));
|
|
23631
|
+
});
|
|
23632
|
+
}
|
|
23054
23633
|
recordDecision(request, nativeObservation, nativeRequired = false, standing = false) {
|
|
23055
23634
|
const root = this.contextRootError();
|
|
23056
23635
|
if (!root.ok)
|
|
@@ -23075,6 +23654,7 @@ class WorkitCore {
|
|
|
23075
23654
|
return task;
|
|
23076
23655
|
if ((this.context.workerId ?? null) !== null)
|
|
23077
23656
|
return failure2("permission_denied", "helpers cannot record or revoke decisions");
|
|
23657
|
+
const filled = input.expectedRevision === undefined;
|
|
23078
23658
|
this.fillRevisions(input, task.data);
|
|
23079
23659
|
if (input.action === "record") {
|
|
23080
23660
|
if (task.data.status === "closed")
|
|
@@ -23123,7 +23703,20 @@ class WorkitCore {
|
|
|
23123
23703
|
const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
|
|
23124
23704
|
if (native?.ok && !nativeProvenance)
|
|
23125
23705
|
return failure2("permission_denied", "native decision authority was retired");
|
|
23126
|
-
|
|
23706
|
+
let standingProvenance = standingApproval?.ok === true ? standingApproval.data : null;
|
|
23707
|
+
const recheck = (fresh) => {
|
|
23708
|
+
if (fresh.status === "closed")
|
|
23709
|
+
return failure2("invalid_transition", "closed task cannot record a decision");
|
|
23710
|
+
if (!standingProvenance)
|
|
23711
|
+
return success(null, null, null);
|
|
23712
|
+
const again = verifyStandingApproval(this.store.root, fresh, this.context.caller, input.binding);
|
|
23713
|
+
if (again.ok)
|
|
23714
|
+
standingProvenance = again.data;
|
|
23715
|
+
return again;
|
|
23716
|
+
};
|
|
23717
|
+
const changed = this.commitTask(task.data.id, input.expectedRevision, filled, recheck, (current, mutation) => {
|
|
23718
|
+
if (current.status === "closed")
|
|
23719
|
+
return failure2("invalid_transition", "closed task cannot record a decision");
|
|
23127
23720
|
const latestContent = verifyDecisionContent(this.store, input.binding);
|
|
23128
23721
|
if (!latestContent.ok)
|
|
23129
23722
|
return latestContent;
|
|
@@ -23135,14 +23728,14 @@ class WorkitCore {
|
|
|
23135
23728
|
const entry = {
|
|
23136
23729
|
id: newId(),
|
|
23137
23730
|
recordedAt: mutation.now,
|
|
23138
|
-
provenance:
|
|
23731
|
+
provenance: standingProvenance ?? nativeProvenance ?? provenance(this.context, "agent_reported"),
|
|
23139
23732
|
data
|
|
23140
23733
|
};
|
|
23141
23734
|
return success(mutation.revision, null, {
|
|
23142
23735
|
...current,
|
|
23143
23736
|
decisions: [...current.decisions, entry]
|
|
23144
23737
|
});
|
|
23145
|
-
}
|
|
23738
|
+
});
|
|
23146
23739
|
if (!changed.ok)
|
|
23147
23740
|
return changed;
|
|
23148
23741
|
return success(changed.data.revision, null, changed.data.decisions.at(-1));
|
|
@@ -23156,7 +23749,7 @@ class WorkitCore {
|
|
|
23156
23749
|
return failure2("invalid_transition", "decision is already revoked");
|
|
23157
23750
|
if (decision.data.consumption)
|
|
23158
23751
|
return failure2("permission_denied", "consumed decision cannot be revoked");
|
|
23159
|
-
const changed = this.
|
|
23752
|
+
const changed = this.commitTask(task.data.id, input.expectedRevision, filled, () => success(null, null, null), (current, mutation) => {
|
|
23160
23753
|
const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23161
23754
|
if (!entry)
|
|
23162
23755
|
return failure2("not_found", "decision not found");
|
|
@@ -23173,13 +23766,16 @@ class WorkitCore {
|
|
|
23173
23766
|
...current,
|
|
23174
23767
|
decisions: current.decisions.map((candidate) => candidate.id === input.decisionId ? updated : candidate)
|
|
23175
23768
|
});
|
|
23176
|
-
}
|
|
23769
|
+
});
|
|
23177
23770
|
if (!changed.ok)
|
|
23178
23771
|
return changed;
|
|
23179
23772
|
const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
|
|
23180
23773
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "decision disappeared");
|
|
23181
23774
|
}
|
|
23182
23775
|
finding(request) {
|
|
23776
|
+
return this.retryOmittedRevisions(request, () => this.findingOnce(request));
|
|
23777
|
+
}
|
|
23778
|
+
findingOnce(request) {
|
|
23183
23779
|
const root = this.contextRootError();
|
|
23184
23780
|
if (!root.ok)
|
|
23185
23781
|
return root;
|
|
@@ -23303,6 +23899,9 @@ class WorkitCore {
|
|
|
23303
23899
|
return entry ? success(changed.data.revision, null, entry) : failure2("recovery_required", "finding disappeared");
|
|
23304
23900
|
}
|
|
23305
23901
|
worker(request) {
|
|
23902
|
+
return this.retryOmittedRevisions(request, () => this.workerOnce(request));
|
|
23903
|
+
}
|
|
23904
|
+
workerOnce(request) {
|
|
23306
23905
|
const root = this.contextRootError();
|
|
23307
23906
|
if (!root.ok)
|
|
23308
23907
|
return root;
|
|
@@ -23650,6 +24249,9 @@ class WorkitCore {
|
|
|
23650
24249
|
return applied;
|
|
23651
24250
|
}
|
|
23652
24251
|
writer(request) {
|
|
24252
|
+
return this.retryOmittedRevisions(request, () => this.writerOnce(request));
|
|
24253
|
+
}
|
|
24254
|
+
writerOnce(request) {
|
|
23653
24255
|
const root = this.contextRootError();
|
|
23654
24256
|
if (!root.ok)
|
|
23655
24257
|
return root;
|
|
@@ -24252,122 +24854,6 @@ class WorkitCore {
|
|
|
24252
24854
|
return this.summary(changed.data.task);
|
|
24253
24855
|
}
|
|
24254
24856
|
}
|
|
24255
|
-
|
|
24256
|
-
// packages/workit-core/src/core/session-context.ts
|
|
24257
|
-
var cache = new Map;
|
|
24258
|
-
// packages/workit-core/src/core/external-action.ts
|
|
24259
|
-
var chainStepSchema = union([
|
|
24260
|
-
string2().min(1),
|
|
24261
|
-
object2({ branch: string2().min(1) }).strict(),
|
|
24262
|
-
object2({ pr: literal(true) }).strict()
|
|
24263
|
-
]);
|
|
24264
|
-
var externalActionSchema = discriminatedUnion("operation", [
|
|
24265
|
-
object2({
|
|
24266
|
-
operation: literal("git.branch_setup"),
|
|
24267
|
-
payload: object2({
|
|
24268
|
-
action: _enum(["setup", "reapply_stash"]).optional(),
|
|
24269
|
-
sdd_dir: string2().optional(),
|
|
24270
|
-
target_branch: string2().optional(),
|
|
24271
|
-
stash: _enum(["yes", "no"]).optional(),
|
|
24272
|
-
cwd: string2().min(1).optional()
|
|
24273
|
-
}).strict()
|
|
24274
|
-
}).strict(),
|
|
24275
|
-
object2({
|
|
24276
|
-
operation: literal("git.commit"),
|
|
24277
|
-
payload: object2({
|
|
24278
|
-
message: string2().min(1).optional(),
|
|
24279
|
-
plan_steps: array(chainStepSchema).min(1).max(32).optional(),
|
|
24280
|
-
plan_branch: string2().min(1).optional(),
|
|
24281
|
-
cwd: string2().min(1).optional()
|
|
24282
|
-
}).strict()
|
|
24283
|
-
}).strict(),
|
|
24284
|
-
object2({
|
|
24285
|
-
operation: literal("git.push"),
|
|
24286
|
-
payload: object2({ branch: string2().min(1).optional(), cwd: string2().min(1).optional() }).strict()
|
|
24287
|
-
}).strict(),
|
|
24288
|
-
object2({
|
|
24289
|
-
operation: literal("hosting.pull_request"),
|
|
24290
|
-
payload: object2({
|
|
24291
|
-
title: string2().min(1),
|
|
24292
|
-
body: string2().optional(),
|
|
24293
|
-
draft: boolean2().optional(),
|
|
24294
|
-
target_branch: string2().optional(),
|
|
24295
|
-
babysit: boolean2().optional(),
|
|
24296
|
-
cwd: string2().min(1).optional()
|
|
24297
|
-
}).strict()
|
|
24298
|
-
}).strict(),
|
|
24299
|
-
object2({
|
|
24300
|
-
operation: literal("hosting.merge"),
|
|
24301
|
-
payload: object2({
|
|
24302
|
-
target_branch: string2().min(1).optional(),
|
|
24303
|
-
source_branch: string2().min(1).optional(),
|
|
24304
|
-
cwd: string2().min(1).optional()
|
|
24305
|
-
}).strict()
|
|
24306
|
-
}).strict(),
|
|
24307
|
-
object2({
|
|
24308
|
-
operation: literal("hosting.delete_branch"),
|
|
24309
|
-
payload: object2({ branch: string2().min(1), cwd: string2().min(1).optional() }).strict()
|
|
24310
|
-
}).strict(),
|
|
24311
|
-
object2({
|
|
24312
|
-
operation: literal("youtrack.update"),
|
|
24313
|
-
payload: object2({
|
|
24314
|
-
issueId: string2().min(1),
|
|
24315
|
-
markdown: string2().min(1),
|
|
24316
|
-
minutes: number2().positive().optional()
|
|
24317
|
-
}).strict()
|
|
24318
|
-
}).strict(),
|
|
24319
|
-
object2({
|
|
24320
|
-
operation: literal("youtrack.time"),
|
|
24321
|
-
payload: object2({
|
|
24322
|
-
issueId: string2().min(1),
|
|
24323
|
-
minutes: number2().positive(),
|
|
24324
|
-
text: string2().optional(),
|
|
24325
|
-
dateMs: number2().finite().optional()
|
|
24326
|
-
}).strict()
|
|
24327
|
-
}).strict(),
|
|
24328
|
-
object2({
|
|
24329
|
-
operation: literal("youtrack.meeting"),
|
|
24330
|
-
payload: object2({
|
|
24331
|
-
issueId: string2().min(1),
|
|
24332
|
-
minutes: number2().positive(),
|
|
24333
|
-
text: string2().min(1)
|
|
24334
|
-
}).strict()
|
|
24335
|
-
}).strict(),
|
|
24336
|
-
object2({
|
|
24337
|
-
operation: literal("changelog.apply"),
|
|
24338
|
-
payload: object2({
|
|
24339
|
-
entries: union([
|
|
24340
|
-
record(string2(), array(string2())),
|
|
24341
|
-
array(object2({ category: string2().min(1), text: string2().min(1) }).strict())
|
|
24342
|
-
]).optional(),
|
|
24343
|
-
path: string2().optional(),
|
|
24344
|
-
normalize_only: boolean2().optional()
|
|
24345
|
-
}).strict()
|
|
24346
|
-
}).strict(),
|
|
24347
|
-
object2({
|
|
24348
|
-
operation: literal("context.read"),
|
|
24349
|
-
payload: object2({
|
|
24350
|
-
kind: _enum([
|
|
24351
|
-
"git",
|
|
24352
|
-
"pr",
|
|
24353
|
-
"youtrack",
|
|
24354
|
-
"github_issue",
|
|
24355
|
-
"gitlab_issue",
|
|
24356
|
-
"changelog",
|
|
24357
|
-
"release",
|
|
24358
|
-
"affected"
|
|
24359
|
-
]),
|
|
24360
|
-
range: string2().optional(),
|
|
24361
|
-
issueId: string2().optional(),
|
|
24362
|
-
issueUrl: string2().optional(),
|
|
24363
|
-
issueRef: string2().optional(),
|
|
24364
|
-
mode: string2().optional(),
|
|
24365
|
-
specPath: string2().optional(),
|
|
24366
|
-
planPath: string2().optional(),
|
|
24367
|
-
cwd: string2().min(1).optional()
|
|
24368
|
-
}).strict()
|
|
24369
|
-
}).strict()
|
|
24370
|
-
]);
|
|
24371
24857
|
// packages/workit-core/src/core.ts
|
|
24372
24858
|
var revision2 = /^[A-Za-z0-9@][A-Za-z0-9@._/~^{}-]*$/;
|
|
24373
24859
|
function gitRevisionParts(value) {
|
|
@@ -24411,7 +24897,7 @@ function resolveInside(root, candidate) {
|
|
|
24411
24897
|
}
|
|
24412
24898
|
function run2(root, executable, args, env = {}) {
|
|
24413
24899
|
const cwd = realpathSync7(root);
|
|
24414
|
-
const result =
|
|
24900
|
+
const result = spawnSync4(executable, args, {
|
|
24415
24901
|
cwd,
|
|
24416
24902
|
encoding: "utf8",
|
|
24417
24903
|
env: { ...process.env, ...env }
|
|
@@ -24482,9 +24968,9 @@ var applyPatterns = (value) => {
|
|
|
24482
24968
|
var redactSecrets = (value) => applyPatterns(value);
|
|
24483
24969
|
|
|
24484
24970
|
// packages/workit-core/src/core/pr-create.ts
|
|
24485
|
-
import
|
|
24971
|
+
import fs9 from "node:fs";
|
|
24486
24972
|
import path12 from "node:path";
|
|
24487
|
-
import { spawnSync as
|
|
24973
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
24488
24974
|
function parseGhIssue(value) {
|
|
24489
24975
|
const m = /issues\/(\d+)/.exec(value);
|
|
24490
24976
|
return m ? m[1] : String(value).trim().replace(/^#/, "");
|
|
@@ -24497,7 +24983,7 @@ function whichOnPath(tool) {
|
|
|
24497
24983
|
for (const name of names) {
|
|
24498
24984
|
const candidate = path12.join(dir, name);
|
|
24499
24985
|
try {
|
|
24500
|
-
|
|
24986
|
+
fs9.accessSync(candidate, fs9.constants.X_OK);
|
|
24501
24987
|
return candidate;
|
|
24502
24988
|
} catch {}
|
|
24503
24989
|
}
|
|
@@ -24506,7 +24992,7 @@ function whichOnPath(tool) {
|
|
|
24506
24992
|
}
|
|
24507
24993
|
var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
|
|
24508
24994
|
var pushRemote = (cwd) => {
|
|
24509
|
-
const result =
|
|
24995
|
+
const result = spawnSync5("git", ["remote", "get-url", "--push", "--all", "origin"], {
|
|
24510
24996
|
cwd,
|
|
24511
24997
|
encoding: "utf8"
|
|
24512
24998
|
});
|
|
@@ -24517,7 +25003,7 @@ var pushRemote = (cwd) => {
|
|
|
24517
25003
|
};
|
|
24518
25004
|
|
|
24519
25005
|
// packages/workit-core/src/core/repo-context.ts
|
|
24520
|
-
import { spawnSync as
|
|
25006
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
24521
25007
|
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "node:fs";
|
|
24522
25008
|
import path13 from "node:path";
|
|
24523
25009
|
var isSafeContextRange = (value) => value.length <= 4096 && (() => {
|
|
@@ -24546,7 +25032,7 @@ var invalidRange = (cwd) => ({
|
|
|
24546
25032
|
cwd
|
|
24547
25033
|
});
|
|
24548
25034
|
var runGit = (cwd, args) => {
|
|
24549
|
-
const result =
|
|
25035
|
+
const result = spawnSync6("git", args, { cwd, encoding: "utf8" });
|
|
24550
25036
|
return {
|
|
24551
25037
|
stdout: (result.stdout ?? "").trimEnd(),
|
|
24552
25038
|
stderr: (result.stderr ?? "").trimEnd(),
|
|
@@ -24719,7 +25205,7 @@ var documentationFiles = (cwd) => {
|
|
|
24719
25205
|
walk(cwd, 1);
|
|
24720
25206
|
return matches.toSorted().slice(0, 200);
|
|
24721
25207
|
};
|
|
24722
|
-
var
|
|
25208
|
+
var readTrimmed2 = (file, maxLines) => {
|
|
24723
25209
|
if (!existsSync6(file))
|
|
24724
25210
|
return "";
|
|
24725
25211
|
const lines = readFileSync7(file, "utf8").split(`
|
|
@@ -24786,7 +25272,7 @@ ${status.stderr}` : "");
|
|
|
24786
25272
|
stdout += `template_path: ${template}
|
|
24787
25273
|
|
|
24788
25274
|
`;
|
|
24789
|
-
stdout +=
|
|
25275
|
+
stdout += readTrimmed2(path13.join(cwd, template), 220);
|
|
24790
25276
|
} else {
|
|
24791
25277
|
stdout += `template_path: none
|
|
24792
25278
|
|
|
@@ -24858,7 +25344,7 @@ function changelogContext(root, range) {
|
|
|
24858
25344
|
stdout += printSection("Existing CHANGELOG.md");
|
|
24859
25345
|
const changelogPath = path13.join(cwd, "CHANGELOG.md");
|
|
24860
25346
|
if (existsSync6(changelogPath)) {
|
|
24861
|
-
stdout +=
|
|
25347
|
+
stdout += readTrimmed2(changelogPath, 260) + `
|
|
24862
25348
|
`;
|
|
24863
25349
|
} else {
|
|
24864
25350
|
stdout += `CHANGELOG.md not found.
|
|
@@ -24900,7 +25386,7 @@ function docsRefreshContext(root, range) {
|
|
|
24900
25386
|
stdout += printSection("README Preview");
|
|
24901
25387
|
const readme = path13.join(cwd, "README.md");
|
|
24902
25388
|
if (existsSync6(readme)) {
|
|
24903
|
-
stdout +=
|
|
25389
|
+
stdout += readTrimmed2(readme, 220) + `
|
|
24904
25390
|
`;
|
|
24905
25391
|
} else {
|
|
24906
25392
|
stdout += `README.md not found.
|
|
@@ -24987,7 +25473,7 @@ function releaseNotesContext(root, rangeOrTag) {
|
|
|
24987
25473
|
}
|
|
24988
25474
|
|
|
24989
25475
|
// packages/workit-core/src/core/tracker-issues.ts
|
|
24990
|
-
import { spawnSync as
|
|
25476
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
24991
25477
|
var repoPathFromRemote = (remote) => {
|
|
24992
25478
|
let rest = (remote || "").trim().replace(/\/+$/, "");
|
|
24993
25479
|
if (!rest)
|
|
@@ -25008,7 +25494,7 @@ var defaultCli = (bin, available) => {
|
|
|
25008
25494
|
if (!available)
|
|
25009
25495
|
return null;
|
|
25010
25496
|
return (args, root) => {
|
|
25011
|
-
const out =
|
|
25497
|
+
const out = spawnSync7(bin, args, {
|
|
25012
25498
|
cwd: root,
|
|
25013
25499
|
encoding: "utf8",
|
|
25014
25500
|
env: { ...process.env, PATH: process.env.PATH ?? "" }
|
|
@@ -25086,7 +25572,7 @@ var fetchGitLabIssueBody = async (ref, root, deps = {}) => {
|
|
|
25086
25572
|
};
|
|
25087
25573
|
|
|
25088
25574
|
// packages/workit-core/src/core/youtrack.ts
|
|
25089
|
-
import
|
|
25575
|
+
import fs10 from "node:fs";
|
|
25090
25576
|
import path14 from "node:path";
|
|
25091
25577
|
|
|
25092
25578
|
// packages/workit-core/src/core/templates.ts
|
|
@@ -25105,17 +25591,17 @@ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? p
|
|
|
25105
25591
|
var youTrackTokenModeOk = (p) => {
|
|
25106
25592
|
if (process.platform === "win32")
|
|
25107
25593
|
return true;
|
|
25108
|
-
const mode =
|
|
25594
|
+
const mode = fs10.statSync(p).mode & 511;
|
|
25109
25595
|
return mode === 384;
|
|
25110
25596
|
};
|
|
25111
25597
|
function readYouTrackConfig(required) {
|
|
25112
25598
|
const cfgPath = youTrackConfigPath();
|
|
25113
|
-
if (!
|
|
25599
|
+
if (!fs10.existsSync(cfgPath)) {
|
|
25114
25600
|
return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
|
|
25115
25601
|
}
|
|
25116
25602
|
let parsed;
|
|
25117
25603
|
try {
|
|
25118
|
-
parsed = JSON.parse(
|
|
25604
|
+
parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
|
|
25119
25605
|
} catch {
|
|
25120
25606
|
return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
|
|
25121
25607
|
}
|
|
@@ -25126,11 +25612,11 @@ function readYouTrackConfig(required) {
|
|
|
25126
25612
|
}
|
|
25127
25613
|
var readYouTrackContextConfig = () => {
|
|
25128
25614
|
const cfgPath = youTrackReadOnlyConfigPath();
|
|
25129
|
-
if (!
|
|
25615
|
+
if (!fs10.existsSync(cfgPath))
|
|
25130
25616
|
return { data: {} };
|
|
25131
25617
|
let parsed;
|
|
25132
25618
|
try {
|
|
25133
|
-
parsed = JSON.parse(
|
|
25619
|
+
parsed = JSON.parse(fs10.readFileSync(cfgPath, "utf8"));
|
|
25134
25620
|
} catch {
|
|
25135
25621
|
return { error: "YouTrack configuration is unavailable" };
|
|
25136
25622
|
}
|
|
@@ -25170,11 +25656,11 @@ var youTrackToken = () => {
|
|
|
25170
25656
|
return loaded;
|
|
25171
25657
|
const tokenFile = String(loaded.config.tokenFile ?? "");
|
|
25172
25658
|
const tokenPath = tokenFile ? path14.isAbsolute(tokenFile) ? path14.resolve(tokenFile) : path14.resolve(process.cwd(), tokenFile) : "";
|
|
25173
|
-
if (!tokenPath || !
|
|
25659
|
+
if (!tokenPath || !fs10.existsSync(tokenPath))
|
|
25174
25660
|
return { error: "missing youtrack.token" };
|
|
25175
25661
|
if (!youTrackTokenModeOk(tokenPath))
|
|
25176
25662
|
return { error: "youtrack.token mode must be 0600" };
|
|
25177
|
-
const token =
|
|
25663
|
+
const token = fs10.readFileSync(tokenPath, "utf8").trim();
|
|
25178
25664
|
if (!token)
|
|
25179
25665
|
return { error: "empty token file" };
|
|
25180
25666
|
if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
|
|
@@ -25239,9 +25725,9 @@ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
|
|
|
25239
25725
|
} catch {
|
|
25240
25726
|
continue;
|
|
25241
25727
|
}
|
|
25242
|
-
if (!
|
|
25728
|
+
if (!fs10.existsSync(full))
|
|
25243
25729
|
continue;
|
|
25244
|
-
const text =
|
|
25730
|
+
const text = fs10.readFileSync(full, "utf8");
|
|
25245
25731
|
const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
|
|
25246
25732
|
if (m)
|
|
25247
25733
|
return m[1];
|