@brainervirus/workit-codex 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/dist/launch-mcp.js +629 -112
- package/dist/workit-hook.js +1138 -594
- package/package.json +3 -3
- package/skills/workit-behavioral-tdd/SKILL.md +15 -9
- package/skills/workit-debug/SKILL.md +2 -1
package/dist/workit-hook.js
CHANGED
|
@@ -26,7 +26,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
26
26
|
};
|
|
27
27
|
});
|
|
28
28
|
// packages/workit-core/src/hooks/context.ts
|
|
29
|
-
import
|
|
29
|
+
import path13 from "node:path";
|
|
30
30
|
|
|
31
31
|
// packages/workit-core/src/core/methods.ts
|
|
32
32
|
var METHODS = {
|
|
@@ -105,8 +105,11 @@ policy, and reapplies the call, returning busy under persistent contention. A
|
|
|
105
105
|
revision_conflict means a revision you passed is stale: re-read the record
|
|
106
106
|
before deciding whether to retry.
|
|
107
107
|
A solo edit does not need writer acquisition; use it when concurrent checkout
|
|
108
|
-
writers need coordination. Record only observed facts and checks.
|
|
109
|
-
|
|
108
|
+
writers need coordination. Record only observed facts and checks. Close-time
|
|
109
|
+
testing and verification gates accept only a configured check the CLI ran:
|
|
110
|
+
\`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
|
|
111
|
+
\`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
|
|
112
|
+
\`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
|
|
110
113
|
current candidate before recording completion.
|
|
111
114
|
|
|
112
115
|
Workit validates domain policy against the actual action target, configured
|
|
@@ -8057,7 +8060,37 @@ var candidateSchema = object({
|
|
|
8057
8060
|
path: ["id"]
|
|
8058
8061
|
});
|
|
8059
8062
|
});
|
|
8060
|
-
var
|
|
8063
|
+
var checkObservationSchema = object({
|
|
8064
|
+
observer: literal("workit_cli"),
|
|
8065
|
+
name: nonEmpty.nullable(),
|
|
8066
|
+
configured: boolean2(),
|
|
8067
|
+
argv: array(text).min(1),
|
|
8068
|
+
shell: boolean2(),
|
|
8069
|
+
cwd: nonEmpty,
|
|
8070
|
+
exitCode: safeInteger,
|
|
8071
|
+
durationMs: safeInteger.min(0),
|
|
8072
|
+
timedOut: boolean2(),
|
|
8073
|
+
head: text.nullable(),
|
|
8074
|
+
tree: text.nullable(),
|
|
8075
|
+
dirty: boolean2().nullable(),
|
|
8076
|
+
signal: text.nullable(),
|
|
8077
|
+
treeAfter: text.nullable(),
|
|
8078
|
+
modifiedWorktree: boolean2(),
|
|
8079
|
+
base: text.nullable(),
|
|
8080
|
+
patchId: text.nullable(),
|
|
8081
|
+
environment: object({
|
|
8082
|
+
platform: nonEmpty,
|
|
8083
|
+
arch: nonEmpty,
|
|
8084
|
+
vars: record(nonEmpty, text.nullable())
|
|
8085
|
+
}).strict(),
|
|
8086
|
+
logDigest: text.nullable(),
|
|
8087
|
+
logRef: text.nullable(),
|
|
8088
|
+
logTail: array(text).max(40),
|
|
8089
|
+
ledgerRowId: text.nullable(),
|
|
8090
|
+
attestation: object({ host: nonEmpty, session: text.nullable(), agentId: text.nullable() }).strict().nullable()
|
|
8091
|
+
}).strict();
|
|
8092
|
+
var CHECK_OBSERVATION_PATH = "evidence.*.data.observation";
|
|
8093
|
+
var evidenceFields = {
|
|
8061
8094
|
kind: _enum(["check", "review", "investigation", "artifact"]),
|
|
8062
8095
|
claim: text,
|
|
8063
8096
|
requirementIds: array(digest),
|
|
@@ -8068,7 +8101,9 @@ var evidenceSchema = object({
|
|
|
8068
8101
|
refs: array(refSchema),
|
|
8069
8102
|
exitCode: safeInteger.nullable(),
|
|
8070
8103
|
reviewContext: refSchema.nullable()
|
|
8071
|
-
}
|
|
8104
|
+
};
|
|
8105
|
+
var evidenceSchema = object({ ...evidenceFields, observation: checkObservationSchema.optional() }).strict();
|
|
8106
|
+
var reportedEvidenceSchema = object(evidenceFields).strict();
|
|
8072
8107
|
var evidenceEvaluationSchema = object({
|
|
8073
8108
|
evidenceId: id,
|
|
8074
8109
|
status: _enum(["passed", "failed", "missing", "skipped", "stale"]),
|
|
@@ -8379,7 +8414,7 @@ var evidenceOperations = {
|
|
|
8379
8414
|
action: literal("record"),
|
|
8380
8415
|
...taskId,
|
|
8381
8416
|
expectedRevision: revision.optional(),
|
|
8382
|
-
evidence:
|
|
8417
|
+
evidence: reportedEvidenceSchema
|
|
8383
8418
|
})
|
|
8384
8419
|
};
|
|
8385
8420
|
var findingOperations = {
|
|
@@ -8614,8 +8649,11 @@ function requirementId(input) {
|
|
|
8614
8649
|
});
|
|
8615
8650
|
}
|
|
8616
8651
|
|
|
8617
|
-
// packages/workit-core/src/core/task-
|
|
8618
|
-
import {
|
|
8652
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
8653
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
8654
|
+
import * as fs10 from "node:fs";
|
|
8655
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
8656
|
+
import path12 from "node:path";
|
|
8619
8657
|
|
|
8620
8658
|
// packages/workit-core/src/core/authority.ts
|
|
8621
8659
|
import { createHash as createHash6 } from "node:crypto";
|
|
@@ -12292,18 +12330,371 @@ function reconcileAction(input) {
|
|
|
12292
12330
|
var bindingCovers = scopeCovers;
|
|
12293
12331
|
var verifyDecisionContent = verifyContentRefs;
|
|
12294
12332
|
|
|
12295
|
-
// packages/workit-core/src/
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12333
|
+
// packages/workit-core/src/check-config.ts
|
|
12334
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
12335
|
+
import fs8 from "node:fs";
|
|
12336
|
+
import path10 from "node:path";
|
|
12337
|
+
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
12338
|
+
var CHECKS_FILE = "workit.checks.json";
|
|
12339
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12340
|
+
var SHELL_META = /[|&;<>()$`\\*?[\]{}~!#]/u;
|
|
12341
|
+
function splitCommand(command) {
|
|
12342
|
+
const argv = [];
|
|
12343
|
+
let current = "";
|
|
12344
|
+
let quote = null;
|
|
12345
|
+
let started = false;
|
|
12346
|
+
for (const char of command) {
|
|
12347
|
+
if (quote) {
|
|
12348
|
+
if (char === quote)
|
|
12349
|
+
quote = null;
|
|
12350
|
+
else
|
|
12351
|
+
current += char;
|
|
12352
|
+
continue;
|
|
12353
|
+
}
|
|
12354
|
+
if (char === '"' || char === "'") {
|
|
12355
|
+
quote = char;
|
|
12356
|
+
started = true;
|
|
12357
|
+
continue;
|
|
12358
|
+
}
|
|
12359
|
+
if (/\s/u.test(char)) {
|
|
12360
|
+
if (started)
|
|
12361
|
+
argv.push(current);
|
|
12362
|
+
current = "";
|
|
12363
|
+
started = false;
|
|
12364
|
+
continue;
|
|
12365
|
+
}
|
|
12366
|
+
if (SHELL_META.test(char))
|
|
12367
|
+
return new Error(`"${command}" uses shell syntax (${char}); configure an argv array instead`);
|
|
12368
|
+
current += char;
|
|
12369
|
+
started = true;
|
|
12370
|
+
}
|
|
12371
|
+
if (quote)
|
|
12372
|
+
return new Error(`"${command}" has an unterminated quote`);
|
|
12373
|
+
if (started)
|
|
12374
|
+
argv.push(current);
|
|
12375
|
+
return argv.length ? argv : new Error("empty command");
|
|
12376
|
+
}
|
|
12377
|
+
var git = (cwd, args) => {
|
|
12378
|
+
const run = spawnSync3("git", args, {
|
|
12379
|
+
cwd,
|
|
12380
|
+
encoding: "utf8",
|
|
12381
|
+
timeout: 20000,
|
|
12382
|
+
killSignal: "SIGKILL",
|
|
12383
|
+
windowsHide: true
|
|
12384
|
+
});
|
|
12385
|
+
const value = run.status === 0 ? (run.stdout ?? "").trim() : "";
|
|
12386
|
+
return value || null;
|
|
12387
|
+
};
|
|
12388
|
+
function checkRoot(cwd) {
|
|
12389
|
+
return path10.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12390
|
+
}
|
|
12391
|
+
var packageManager = (root) => {
|
|
12392
|
+
const has = (file) => fs8.existsSync(path10.join(root, file));
|
|
12393
|
+
if (has("bun.lock") || has("bun.lockb"))
|
|
12394
|
+
return "bun";
|
|
12395
|
+
if (has("pnpm-lock.yaml"))
|
|
12396
|
+
return "pnpm";
|
|
12397
|
+
if (has("yarn.lock"))
|
|
12398
|
+
return "yarn";
|
|
12399
|
+
return "npm";
|
|
12400
|
+
};
|
|
12401
|
+
var scriptCheck = (pm, name) => {
|
|
12402
|
+
const argv = [pm, "run", name];
|
|
12403
|
+
const accepts = [argv];
|
|
12404
|
+
if (pm === "pnpm" || pm === "yarn" || pm === "npm" && name === "test")
|
|
12405
|
+
accepts.push([pm, name]);
|
|
12406
|
+
return { name, argv, accepts };
|
|
12407
|
+
};
|
|
12408
|
+
var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
12409
|
+
function detectedChecks(root) {
|
|
12410
|
+
const read = (file) => {
|
|
12411
|
+
try {
|
|
12412
|
+
return fs8.readFileSync(path10.join(root, file), "utf8");
|
|
12413
|
+
} catch {
|
|
12414
|
+
return null;
|
|
12415
|
+
}
|
|
12416
|
+
};
|
|
12417
|
+
const checks = [];
|
|
12418
|
+
const add = (check) => {
|
|
12419
|
+
if (!checks.some((item) => item.name === check.name))
|
|
12420
|
+
checks.push(check);
|
|
12421
|
+
};
|
|
12422
|
+
const manifestText = read("package.json");
|
|
12423
|
+
if (manifestText !== null) {
|
|
12424
|
+
let manifest = null;
|
|
12425
|
+
try {
|
|
12426
|
+
manifest = JSON.parse(manifestText);
|
|
12427
|
+
} catch {}
|
|
12428
|
+
const scripts = isRecord2(manifest) && isRecord2(manifest.scripts) ? manifest.scripts : {};
|
|
12429
|
+
const pm = packageManager(root);
|
|
12430
|
+
for (const name of SCRIPT_CHECKS) {
|
|
12431
|
+
const script = scripts[name];
|
|
12432
|
+
if (typeof script === "string" && script.trim() !== "")
|
|
12433
|
+
add(scriptCheck(pm, name));
|
|
12434
|
+
}
|
|
12435
|
+
}
|
|
12436
|
+
const plain = (name, argv) => ({ name, argv, accepts: [argv] });
|
|
12437
|
+
if (read("go.mod") !== null)
|
|
12438
|
+
add(plain("test", ["go", "test", "./..."]));
|
|
12439
|
+
if (read("Cargo.toml") !== null)
|
|
12440
|
+
add(plain("test", ["cargo", "test"]));
|
|
12441
|
+
if (read("pytest.ini") !== null || /\bpytest\b/u.test(read("pyproject.toml") ?? ""))
|
|
12442
|
+
add(plain("test", ["pytest"]));
|
|
12443
|
+
const makefile = read("Makefile") ?? read("makefile") ?? read("GNUmakefile");
|
|
12444
|
+
if (makefile !== null && /^test\s*:(?!=)/mu.test(makefile))
|
|
12445
|
+
add(plain("test", ["make", "test"]));
|
|
12446
|
+
return checks;
|
|
12447
|
+
}
|
|
12448
|
+
var GATES = ["testing", "verification"];
|
|
12449
|
+
function loadCheckConfig(cwd) {
|
|
12450
|
+
const root = checkRoot(cwd);
|
|
12451
|
+
const file = path10.join(root, CHECKS_FILE);
|
|
12452
|
+
const none = (error = null) => ({
|
|
12453
|
+
source: error ? CHECKS_FILE : "none",
|
|
12454
|
+
root,
|
|
12455
|
+
checks: [],
|
|
12456
|
+
gates: {},
|
|
12457
|
+
error
|
|
12458
|
+
});
|
|
12459
|
+
let text = null;
|
|
12460
|
+
try {
|
|
12461
|
+
text = fs8.readFileSync(file, "utf8");
|
|
12462
|
+
} catch (error) {
|
|
12463
|
+
if (error.code !== "ENOENT")
|
|
12464
|
+
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
12465
|
+
}
|
|
12466
|
+
if (text === null) {
|
|
12467
|
+
const checks = detectedChecks(root);
|
|
12468
|
+
return checks.length ? { source: "defaults", root, checks, gates: {}, error: null } : none();
|
|
12469
|
+
}
|
|
12470
|
+
let parsed;
|
|
12471
|
+
try {
|
|
12472
|
+
parsed = JSON.parse(text);
|
|
12473
|
+
} catch {
|
|
12474
|
+
return none(`${CHECKS_FILE} is not valid JSON`);
|
|
12475
|
+
}
|
|
12476
|
+
if (!isRecord2(parsed) || !isRecord2(parsed.checks))
|
|
12477
|
+
return none(`${CHECKS_FILE} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
|
|
12478
|
+
const checks = [];
|
|
12479
|
+
for (const [name, value] of Object.entries(parsed.checks)) {
|
|
12480
|
+
if (!CHECK_NAME.test(name))
|
|
12481
|
+
return none(`${CHECKS_FILE}: check name "${name}" must match ${CHECK_NAME.source}`);
|
|
12482
|
+
const argv = typeof value === "string" ? splitCommand(value) : Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0) ? value : new Error("expected a command string or a non-empty argv array");
|
|
12483
|
+
if (argv instanceof Error)
|
|
12484
|
+
return none(`${CHECKS_FILE}: check "${name}": ${argv.message}`);
|
|
12485
|
+
checks.push({ name, argv, accepts: [argv] });
|
|
12486
|
+
}
|
|
12487
|
+
const gates = {};
|
|
12488
|
+
if (parsed.gates !== undefined) {
|
|
12489
|
+
if (!isRecord2(parsed.gates))
|
|
12490
|
+
return none(`${CHECKS_FILE}: gates must be {"testing":"<name>"|["<name>",…]}`);
|
|
12491
|
+
for (const [gate, value] of Object.entries(parsed.gates)) {
|
|
12492
|
+
if (!GATES.includes(gate))
|
|
12493
|
+
return none(`${CHECKS_FILE}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
|
|
12494
|
+
const names = typeof value === "string" ? [value] : value;
|
|
12495
|
+
if (!Array.isArray(names) || names.length === 0 || !names.every((name) => typeof name === "string" && checks.some((c) => c.name === name)))
|
|
12496
|
+
return none(`${CHECKS_FILE}: gates.${gate} must name configured checks`);
|
|
12497
|
+
gates[gate] = names;
|
|
12498
|
+
}
|
|
12499
|
+
}
|
|
12500
|
+
return { source: CHECKS_FILE, root, checks, gates, error: null };
|
|
12501
|
+
}
|
|
12502
|
+
var sameArgv = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
|
|
12503
|
+
function matchesNamedCheck(config, name, argv, cwd) {
|
|
12504
|
+
if (config.error || name === null || cwd !== ".")
|
|
12505
|
+
return false;
|
|
12506
|
+
const check = config.checks.find((item) => item.name === name);
|
|
12507
|
+
return Boolean(check?.accepts.some((accepted) => sameArgv(accepted, argv)));
|
|
12508
|
+
}
|
|
12509
|
+
function gateCheckNames(config, dimension) {
|
|
12510
|
+
const explicit = config.gates[dimension];
|
|
12511
|
+
if (explicit)
|
|
12512
|
+
return explicit;
|
|
12513
|
+
const names = config.checks.map((check) => check.name);
|
|
12514
|
+
if (dimension === "testing")
|
|
12515
|
+
return names.includes("test") ? ["test"] : [];
|
|
12516
|
+
return names;
|
|
12517
|
+
}
|
|
12518
|
+
var workitOnPath;
|
|
12519
|
+
function workitAvailable(env = process.env) {
|
|
12520
|
+
if (workitOnPath !== undefined)
|
|
12521
|
+
return workitOnPath;
|
|
12522
|
+
const windows = process.platform === "win32";
|
|
12523
|
+
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12524
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path10.delimiter).filter(Boolean);
|
|
12525
|
+
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12526
|
+
try {
|
|
12527
|
+
return fs8.statSync(path10.join(dir, `workit${ext}`)).isFile();
|
|
12528
|
+
} catch {
|
|
12529
|
+
return false;
|
|
12530
|
+
}
|
|
12531
|
+
}));
|
|
12532
|
+
return workitOnPath;
|
|
12533
|
+
}
|
|
12534
|
+
function checkCommand(args, version, onPath = workitAvailable()) {
|
|
12535
|
+
return onPath ? `workit check ${args}` : `npx -y @brainervirus/workit-cli@${version} check ${args}`;
|
|
12536
|
+
}
|
|
12537
|
+
|
|
12538
|
+
// packages/workit-core/src/git/rev.ts
|
|
12539
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
12540
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
12541
|
+
import fs9 from "node:fs";
|
|
12542
|
+
import os2 from "node:os";
|
|
12543
|
+
import path11 from "node:path";
|
|
12544
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
12545
|
+
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
12546
|
+
var git2 = (cwd, args, options = {}) => {
|
|
12547
|
+
const result = spawnSync4("git", args, {
|
|
12548
|
+
cwd,
|
|
12549
|
+
encoding: "utf8",
|
|
12550
|
+
env: options.env ?? process.env,
|
|
12551
|
+
input: options.input,
|
|
12552
|
+
maxBuffer: MAX_BUFFER,
|
|
12553
|
+
timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
|
|
12554
|
+
killSignal: "SIGKILL",
|
|
12555
|
+
windowsHide: true
|
|
12556
|
+
});
|
|
12557
|
+
const error = result.error;
|
|
12558
|
+
return {
|
|
12559
|
+
ok: result.status === 0,
|
|
12560
|
+
stdout: result.stdout ?? "",
|
|
12561
|
+
stderr: result.stderr ?? "",
|
|
12562
|
+
timedOut: error?.code === "ETIMEDOUT"
|
|
12563
|
+
};
|
|
12564
|
+
};
|
|
12565
|
+
var line = (run) => {
|
|
12566
|
+
if (!run.ok)
|
|
12567
|
+
return null;
|
|
12568
|
+
const value = run.stdout.trim();
|
|
12569
|
+
return value ? value : null;
|
|
12570
|
+
};
|
|
12571
|
+
function headSha(cwd) {
|
|
12572
|
+
return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
12573
|
+
}
|
|
12574
|
+
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
12575
|
+
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
12576
|
+
var seedIndex = (from, to) => {
|
|
12577
|
+
fs9.copyFileSync(from, to);
|
|
12578
|
+
const { atimeNs, mtimeNs } = fs9.statSync(from, { bigint: true });
|
|
12579
|
+
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12580
|
+
fs9.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12581
|
+
};
|
|
12582
|
+
function worktreeTree(cwd, options = {}) {
|
|
12583
|
+
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
12584
|
+
const indexPath = line(git2(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
|
|
12585
|
+
if (!top || !indexPath)
|
|
12586
|
+
return null;
|
|
12587
|
+
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12588
|
+
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12589
|
+
const scratch = fs9.mkdtempSync(path11.join(os2.tmpdir(), "workit-tree-"));
|
|
12590
|
+
try {
|
|
12591
|
+
const tempIndex = path11.join(scratch, "index");
|
|
12592
|
+
if (fs9.existsSync(indexPath))
|
|
12593
|
+
seedIndex(indexPath, tempIndex);
|
|
12594
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12595
|
+
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
12596
|
+
env,
|
|
12597
|
+
timeoutMs
|
|
12598
|
+
});
|
|
12599
|
+
if (!untracked.ok)
|
|
12600
|
+
return null;
|
|
12601
|
+
const skipped = [];
|
|
12602
|
+
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12603
|
+
try {
|
|
12604
|
+
const stat = fs9.statSync(path11.join(top, file));
|
|
12605
|
+
if (stat.isFile() && stat.size > cap)
|
|
12606
|
+
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12607
|
+
} catch {}
|
|
12608
|
+
}
|
|
12609
|
+
skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
12610
|
+
const added = skipped.length ? git2(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
|
|
12611
|
+
env,
|
|
12612
|
+
timeoutMs,
|
|
12613
|
+
input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
|
|
12614
|
+
}) : git2(top, ["add", "-A"], { env, timeoutMs });
|
|
12615
|
+
if (!added.ok)
|
|
12616
|
+
return null;
|
|
12617
|
+
const tree = line(git2(top, ["write-tree"], { env, timeoutMs }));
|
|
12618
|
+
if (!tree)
|
|
12619
|
+
return null;
|
|
12620
|
+
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12621
|
+
const key = skipped.length ? `sha256:${createHash7("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12622
|
+
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12623
|
+
} finally {
|
|
12624
|
+
fs9.rmSync(scratch, { recursive: true, force: true });
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
function worktreeSignal(cwd, options = {}) {
|
|
12628
|
+
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
12629
|
+
if (!top)
|
|
12630
|
+
return null;
|
|
12631
|
+
const status = git2(top, [
|
|
12632
|
+
"status",
|
|
12633
|
+
"--porcelain=v1",
|
|
12634
|
+
"-z",
|
|
12635
|
+
"--untracked-files=all",
|
|
12636
|
+
"--no-renames",
|
|
12637
|
+
"--ignore-submodules=dirty"
|
|
12638
|
+
], {
|
|
12639
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
12640
|
+
timeoutMs: options.timeoutMs ?? GIT_TIMEOUTS.worktree
|
|
12641
|
+
});
|
|
12642
|
+
if (!status.ok)
|
|
12643
|
+
return null;
|
|
12644
|
+
const hash = createHash7("sha256");
|
|
12645
|
+
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12646
|
+
for (const entry of status.stdout.split("\x00")) {
|
|
12647
|
+
if (entry.length < 4)
|
|
12648
|
+
continue;
|
|
12649
|
+
const file = entry.slice(3);
|
|
12650
|
+
const absolute = path11.join(top, file);
|
|
12651
|
+
let stat = "-";
|
|
12652
|
+
try {
|
|
12653
|
+
const info = fs9.lstatSync(absolute, { bigint: true });
|
|
12654
|
+
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12655
|
+
if (info.isDirectory())
|
|
12656
|
+
stat += `:${headSha(absolute) ?? "-"}`;
|
|
12657
|
+
} catch {}
|
|
12658
|
+
hash.update(`${entry.slice(0, 2)}\x00${file}\x00${stat}\x00`);
|
|
12659
|
+
}
|
|
12660
|
+
return `sig:${hash.digest("hex")}`;
|
|
12661
|
+
}
|
|
12662
|
+
|
|
12663
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
12664
|
+
var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex");
|
|
12665
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path12.sep}`);
|
|
12666
|
+
var relative = (root, candidate) => path12.relative(root, candidate).split(path12.sep).join("/") || ".";
|
|
12667
|
+
var canonicalPath = (value) => {
|
|
12668
|
+
if (typeof value !== "string" || !value)
|
|
12669
|
+
return null;
|
|
12670
|
+
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12671
|
+
return null;
|
|
12672
|
+
const normalized = path12.posix.normalize(value);
|
|
12673
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12674
|
+
return null;
|
|
12675
|
+
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
12676
|
+
};
|
|
12677
|
+
var canonicalScope = (scope) => {
|
|
12678
|
+
const paths = scope.paths.length ? scope.paths : ["."];
|
|
12679
|
+
const normalizedPaths = paths.map(canonicalPath);
|
|
12680
|
+
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
12681
|
+
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
12682
|
+
return null;
|
|
12683
|
+
return {
|
|
12684
|
+
description: scope.description,
|
|
12685
|
+
paths: [...new Set(normalizedPaths)],
|
|
12686
|
+
exclusions: [...new Set(normalizedExclusions)]
|
|
12687
|
+
};
|
|
12688
|
+
};
|
|
12689
|
+
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
12690
|
+
var scopeMatches = (value, scope) => {
|
|
12691
|
+
const normalized = canonicalScope(scope);
|
|
12692
|
+
if (!normalized)
|
|
12693
|
+
return false;
|
|
12694
|
+
const paths = normalized.paths;
|
|
12695
|
+
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
12696
|
+
};
|
|
12697
|
+
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
12307
12698
|
var compareCodeUnits2 = (left, right) => {
|
|
12308
12699
|
const length = Math.min(left.length, right.length);
|
|
12309
12700
|
for (let index = 0;index < length; index += 1) {
|
|
@@ -12313,506 +12704,152 @@ var compareCodeUnits2 = (left, right) => {
|
|
|
12313
12704
|
}
|
|
12314
12705
|
return left.length - right.length;
|
|
12315
12706
|
};
|
|
12316
|
-
var
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12349
|
-
|
|
12350
|
-
|
|
12351
|
-
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
}
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
}
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
}
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
return failure2("invalid_input", "
|
|
12391
|
-
|
|
12392
|
-
const
|
|
12393
|
-
if (
|
|
12394
|
-
return failure2("invalid_input", "
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
dependentAction: null,
|
|
12453
|
-
acceptanceAllowed: false
|
|
12454
|
-
}));
|
|
12455
|
-
if (signals.productChoiceOpen.value === true)
|
|
12456
|
-
requirements.push(requirement({
|
|
12457
|
-
ruleId: "product-decision",
|
|
12458
|
-
dimension: "decisions",
|
|
12459
|
-
scope,
|
|
12460
|
-
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
12461
|
-
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
12462
|
-
before: "dependent_action",
|
|
12463
|
-
dependentAction: null,
|
|
12464
|
-
acceptanceAllowed: false
|
|
12465
|
-
}));
|
|
12466
|
-
const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
|
|
12467
|
-
if (signals.behaviorChange.value === true)
|
|
12468
|
-
requirements.push(requirement({
|
|
12469
|
-
ruleId: "behavioral-verification",
|
|
12470
|
-
dimension: "testing",
|
|
12471
|
-
scope,
|
|
12472
|
-
reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
|
|
12473
|
-
satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
|
|
12474
|
-
before: "close",
|
|
12475
|
-
dependentAction: null,
|
|
12476
|
-
acceptanceAllowed: false
|
|
12477
|
-
}), requirement({
|
|
12478
|
-
ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
|
|
12479
|
-
dimension: "review",
|
|
12480
|
-
scope,
|
|
12481
|
-
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.",
|
|
12482
|
-
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.",
|
|
12483
|
-
before: "close",
|
|
12484
|
-
dependentAction: null,
|
|
12485
|
-
acceptanceAllowed: !independentBehaviorReview
|
|
12486
|
-
}));
|
|
12487
|
-
else if (signals.mechanicalLowRisk.value === true)
|
|
12488
|
-
requirements.push(requirement({
|
|
12489
|
-
ruleId: "mechanical-existing-checks",
|
|
12490
|
-
dimension: "verification",
|
|
12491
|
-
scope,
|
|
12492
|
-
reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
|
|
12493
|
-
satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
|
|
12494
|
-
before: "close",
|
|
12495
|
-
dependentAction: null,
|
|
12496
|
-
acceptanceAllowed: true
|
|
12497
|
-
}), requirement({
|
|
12498
|
-
ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
|
|
12499
|
-
dimension: "review",
|
|
12500
|
-
scope,
|
|
12501
|
-
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.",
|
|
12502
|
-
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.",
|
|
12503
|
-
before: "close",
|
|
12504
|
-
dependentAction: null,
|
|
12505
|
-
acceptanceAllowed: !input.preferences?.thorough
|
|
12506
|
-
}));
|
|
12507
|
-
if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
|
|
12508
|
-
requirements.push(requirement({
|
|
12509
|
-
ruleId: "pre-pr-cleanup",
|
|
12510
|
-
dimension: "verification",
|
|
12511
|
-
scope,
|
|
12512
|
-
reason: "Implementation output must be deslopped before it is delivered.",
|
|
12513
|
-
satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
|
|
12514
|
-
before: "dependent_action",
|
|
12515
|
-
dependentAction: "hosting.pull_request",
|
|
12516
|
-
acceptanceAllowed: true
|
|
12517
|
-
}));
|
|
12518
|
-
if (signals.durableAgreementNeeded.value === true)
|
|
12519
|
-
requirements.push(requirement({
|
|
12520
|
-
ruleId: "durable-spec",
|
|
12521
|
-
dimension: "artifacts",
|
|
12522
|
-
scope,
|
|
12523
|
-
reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
|
|
12524
|
-
satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
|
|
12525
|
-
before: "write",
|
|
12526
|
-
dependentAction: null,
|
|
12527
|
-
acceptanceAllowed: true
|
|
12528
|
-
}));
|
|
12529
|
-
if (signals.coordinationPlanNeeded.value === true)
|
|
12530
|
-
requirements.push(requirement({
|
|
12531
|
-
ruleId: "coordination-plan",
|
|
12532
|
-
dimension: "continuity",
|
|
12533
|
-
scope,
|
|
12534
|
-
reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
|
|
12535
|
-
satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
|
|
12536
|
-
before: "dependent_action",
|
|
12537
|
-
dependentAction: null,
|
|
12538
|
-
acceptanceAllowed: true
|
|
12539
|
-
}));
|
|
12540
|
-
if (signals.helperUseful.value === true && !input.preferences?.fast)
|
|
12541
|
-
requirements.push(requirement({
|
|
12542
|
-
ruleId: "helper-usefulness",
|
|
12543
|
-
dimension: "delegation",
|
|
12544
|
-
scope,
|
|
12545
|
-
reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
|
|
12546
|
-
satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
|
|
12547
|
-
before: "dependent_action",
|
|
12548
|
-
dependentAction: null,
|
|
12549
|
-
acceptanceAllowed: true
|
|
12550
|
-
}));
|
|
12551
|
-
for (const constraint of input.constraints)
|
|
12552
|
-
for (const obligation of constraint.requires) {
|
|
12553
|
-
const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
|
|
12554
|
-
requirements.push(requirement({
|
|
12555
|
-
ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
|
|
12556
|
-
kind: obligation.kind,
|
|
12557
|
-
scope: normalizedScope(obligation.scope),
|
|
12558
|
-
refs: normalizedRefList(obligation.refs),
|
|
12559
|
-
method: obligation.method,
|
|
12560
|
-
before: obligation.before,
|
|
12561
|
-
dependentAction: obligation.dependentAction
|
|
12562
|
-
})}`,
|
|
12563
|
-
dimension,
|
|
12564
|
-
scope: obligation.scope,
|
|
12565
|
-
reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
|
|
12566
|
-
satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
|
|
12567
|
-
before: obligation.before,
|
|
12568
|
-
dependentAction: obligation.dependentAction,
|
|
12569
|
-
acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
|
|
12570
|
-
}));
|
|
12571
|
-
}
|
|
12572
|
-
const seen = new Set;
|
|
12573
|
-
return requirements.filter((item) => {
|
|
12574
|
-
if (seen.has(item.id))
|
|
12575
|
-
return false;
|
|
12576
|
-
seen.add(item.id);
|
|
12577
|
-
return true;
|
|
12578
|
-
});
|
|
12579
|
-
}
|
|
12580
|
-
function resolvePolicy(input) {
|
|
12581
|
-
const normalized = normalizeResolverInput(input);
|
|
12582
|
-
if (!normalized.ok)
|
|
12583
|
-
return normalized;
|
|
12584
|
-
const requirements = resolveRequirements(normalized.data);
|
|
12585
|
-
const policy = {
|
|
12586
|
-
policyVersion: POLICY_VERSION,
|
|
12587
|
-
inputDigest: sha256(canonicalJson(normalized.data)),
|
|
12588
|
-
requirements
|
|
12589
|
-
};
|
|
12590
|
-
const parsed = policySchema.safeParse(policy);
|
|
12591
|
-
if (!parsed.success)
|
|
12592
|
-
return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
|
|
12593
|
-
return success(null, null, parsed.data);
|
|
12594
|
-
}
|
|
12595
|
-
function diffPolicy(previous, next, reason, now) {
|
|
12596
|
-
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
12597
|
-
throw new TypeError("previous policy is invalid");
|
|
12598
|
-
if (!policySchema.safeParse(next).success)
|
|
12599
|
-
throw new TypeError("next policy is invalid");
|
|
12600
|
-
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
12601
|
-
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
12602
|
-
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits2);
|
|
12603
|
-
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits2);
|
|
12604
|
-
if (added.length === 0 && retired.length === 0)
|
|
12605
|
-
return null;
|
|
12606
|
-
const normalizedReason = reason.trim();
|
|
12607
|
-
const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
|
|
12608
|
-
const change = {
|
|
12609
|
-
recordedAt: now,
|
|
12610
|
-
fromInputDigest: previous?.inputDigest ?? null,
|
|
12611
|
-
toInputDigest: next.inputDigest,
|
|
12612
|
-
added,
|
|
12613
|
-
retired,
|
|
12614
|
-
reason: changeReason
|
|
12615
|
-
};
|
|
12616
|
-
const parsed = policyChangeSchema.safeParse(change);
|
|
12617
|
-
if (!parsed.success)
|
|
12618
|
-
throw new TypeError("policy change is invalid");
|
|
12619
|
-
return parsed.data;
|
|
12620
|
-
}
|
|
12621
|
-
|
|
12622
|
-
// packages/workit-core/src/core/task-evaluation.ts
|
|
12623
|
-
import { createHash as createHash7 } from "node:crypto";
|
|
12624
|
-
import * as fs8 from "node:fs";
|
|
12625
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
12626
|
-
import path10 from "node:path";
|
|
12627
|
-
var digestBytes3 = (value) => createHash7("sha256").update(value).digest("hex");
|
|
12628
|
-
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path10.sep}`);
|
|
12629
|
-
var relative = (root, candidate) => path10.relative(root, candidate).split(path10.sep).join("/") || ".";
|
|
12630
|
-
var canonicalPath = (value) => {
|
|
12631
|
-
if (typeof value !== "string" || !value)
|
|
12632
|
-
return null;
|
|
12633
|
-
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12634
|
-
return null;
|
|
12635
|
-
const normalized = path10.posix.normalize(value);
|
|
12636
|
-
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12637
|
-
return null;
|
|
12638
|
-
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
12639
|
-
};
|
|
12640
|
-
var canonicalScope = (scope) => {
|
|
12641
|
-
const paths = scope.paths.length ? scope.paths : ["."];
|
|
12642
|
-
const normalizedPaths = paths.map(canonicalPath);
|
|
12643
|
-
const normalizedExclusions = scope.exclusions.map(canonicalPath);
|
|
12644
|
-
if (normalizedPaths.some((value) => value === null) || normalizedExclusions.some((value) => value === null))
|
|
12645
|
-
return null;
|
|
12646
|
-
return {
|
|
12647
|
-
description: scope.description,
|
|
12648
|
-
paths: [...new Set(normalizedPaths)],
|
|
12649
|
-
exclusions: [...new Set(normalizedExclusions)]
|
|
12650
|
-
};
|
|
12651
|
-
};
|
|
12652
|
-
var excluded = (value, exclusions) => exclusions.some((item) => item === value || item !== "." && value.startsWith(`${item}/`));
|
|
12653
|
-
var scopeMatches = (value, scope) => {
|
|
12654
|
-
const normalized = canonicalScope(scope);
|
|
12655
|
-
if (!normalized)
|
|
12656
|
-
return false;
|
|
12657
|
-
const paths = normalized.paths;
|
|
12658
|
-
return paths.some((item) => item === "." || value === item || value.startsWith(`${item}/`)) && !excluded(value, normalized.exclusions);
|
|
12659
|
-
};
|
|
12660
|
-
var pathRelevant = (value, scope) => scopeMatches(value, scope);
|
|
12661
|
-
var compareCodeUnits3 = (left, right) => {
|
|
12662
|
-
const length = Math.min(left.length, right.length);
|
|
12663
|
-
for (let index = 0;index < length; index += 1) {
|
|
12664
|
-
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
12665
|
-
if (difference !== 0)
|
|
12666
|
-
return difference;
|
|
12667
|
-
}
|
|
12668
|
-
return left.length - right.length;
|
|
12669
|
-
};
|
|
12670
|
-
var gitPaths = (root) => {
|
|
12671
|
-
const result = spawnSync3("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12672
|
-
cwd: root,
|
|
12673
|
-
encoding: "buffer"
|
|
12674
|
-
});
|
|
12675
|
-
if (result.status !== 0) {
|
|
12676
|
-
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
12677
|
-
return {
|
|
12678
|
-
paths: [],
|
|
12679
|
-
uncertain: !/not a git repository/i.test(stderr),
|
|
12680
|
-
git: !/not a git repository/i.test(stderr)
|
|
12681
|
-
};
|
|
12682
|
-
}
|
|
12683
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")) : [];
|
|
12684
|
-
const stagedDeleted = spawnSync3("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12685
|
-
if (stagedDeleted.status !== 0) {
|
|
12686
|
-
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12687
|
-
if (!/not a git repository/i.test(stderr))
|
|
12688
|
-
return { paths, uncertain: true, git: true };
|
|
12689
|
-
} else if (stagedDeleted.stdout) {
|
|
12690
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path10.sep).join("/")));
|
|
12691
|
-
}
|
|
12692
|
-
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12693
|
-
};
|
|
12694
|
-
var walk = (root, directory, output) => {
|
|
12695
|
-
let names;
|
|
12696
|
-
try {
|
|
12697
|
-
names = fs8.readdirSync(directory);
|
|
12698
|
-
} catch {
|
|
12699
|
-
return true;
|
|
12700
|
-
}
|
|
12701
|
-
let uncertain = false;
|
|
12702
|
-
for (const name of names) {
|
|
12703
|
-
if (name === ".git" || name === ".workit")
|
|
12704
|
-
continue;
|
|
12705
|
-
const target = path10.join(directory, name);
|
|
12706
|
-
let stat;
|
|
12707
|
-
try {
|
|
12708
|
-
stat = fs8.lstatSync(target);
|
|
12709
|
-
} catch {
|
|
12710
|
-
uncertain = true;
|
|
12711
|
-
continue;
|
|
12712
|
-
}
|
|
12713
|
-
const item = relative(root, target);
|
|
12714
|
-
output.add(item);
|
|
12715
|
-
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
12716
|
-
uncertain = walk(root, target, output) || uncertain;
|
|
12717
|
-
}
|
|
12718
|
-
return uncertain;
|
|
12719
|
-
};
|
|
12720
|
-
var scopeRoots = (root, scope) => {
|
|
12721
|
-
const roots = [];
|
|
12722
|
-
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12723
|
-
const target = path10.resolve(root, item);
|
|
12724
|
-
if (!inside(root, target))
|
|
12725
|
-
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` }] });
|
|
12726
|
-
let ancestor = target;
|
|
12727
|
-
while (!fs8.existsSync(ancestor) && ancestor !== root)
|
|
12728
|
-
ancestor = path10.dirname(ancestor);
|
|
12729
|
-
try {
|
|
12730
|
-
if (!inside(root, fs8.realpathSync(ancestor)))
|
|
12731
|
-
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` }] });
|
|
12732
|
-
} catch {
|
|
12733
|
-
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
12734
|
-
}
|
|
12735
|
-
roots.push(target);
|
|
12736
|
-
}
|
|
12737
|
-
return success(null, null, roots);
|
|
12738
|
-
};
|
|
12739
|
-
function captureCandidate(root, scope, environment = []) {
|
|
12740
|
-
let checkout;
|
|
12741
|
-
try {
|
|
12742
|
-
checkout = fs8.realpathSync(root);
|
|
12743
|
-
} catch {
|
|
12744
|
-
return failure2("invalid_input", "candidate root does not exist");
|
|
12745
|
-
}
|
|
12746
|
-
const normalizedScope = canonicalScope(scope);
|
|
12747
|
-
if (!normalizedScope)
|
|
12748
|
-
return failure2("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
12749
|
-
const roots = scopeRoots(checkout, normalizedScope);
|
|
12750
|
-
if (!roots.ok)
|
|
12751
|
-
return roots;
|
|
12752
|
-
const git = gitPaths(checkout);
|
|
12753
|
-
const names = new Set;
|
|
12754
|
-
let uncertain = git.uncertain;
|
|
12755
|
-
for (const target of roots.data) {
|
|
12756
|
-
const item = relative(checkout, target);
|
|
12757
|
-
if (item === ".git" || item === ".workit")
|
|
12758
|
-
continue;
|
|
12759
|
-
names.add(item);
|
|
12760
|
-
if (!fs8.existsSync(target))
|
|
12761
|
-
continue;
|
|
12762
|
-
let stat;
|
|
12763
|
-
try {
|
|
12764
|
-
stat = fs8.lstatSync(target);
|
|
12765
|
-
} catch {
|
|
12766
|
-
uncertain = true;
|
|
12767
|
-
continue;
|
|
12768
|
-
}
|
|
12769
|
-
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
12770
|
-
uncertain = walk(checkout, target, names) || uncertain;
|
|
12771
|
-
}
|
|
12772
|
-
for (const item of git.paths)
|
|
12773
|
-
if (scopeMatches(item, normalizedScope))
|
|
12774
|
-
names.add(item);
|
|
12775
|
-
const files = [];
|
|
12776
|
-
let completeness = uncertain ? "uncertain" : "known";
|
|
12777
|
-
for (const item of [...names].toSorted()) {
|
|
12778
|
-
if (!scopeMatches(item, normalizedScope))
|
|
12779
|
-
continue;
|
|
12780
|
-
const target = path10.join(checkout, item);
|
|
12781
|
-
try {
|
|
12782
|
-
const stat = fs8.lstatSync(target);
|
|
12783
|
-
if (stat.isSymbolicLink()) {
|
|
12784
|
-
const link = fs8.readlinkSync(target);
|
|
12785
|
-
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
12786
|
-
} else if (stat.isFile()) {
|
|
12787
|
-
files.push({
|
|
12788
|
-
path: item,
|
|
12789
|
-
kind: "file",
|
|
12790
|
-
digest: digestBytes3(fs8.readFileSync(target)),
|
|
12791
|
-
executable: (stat.mode & 73) !== 0
|
|
12792
|
-
});
|
|
12793
|
-
} else if (!stat.isDirectory()) {
|
|
12794
|
-
completeness = "uncertain";
|
|
12795
|
-
} else if (git.git && git.paths.includes(item)) {
|
|
12796
|
-
completeness = "uncertain";
|
|
12797
|
-
}
|
|
12798
|
-
} catch (error) {
|
|
12799
|
-
if (error?.code === "ENOENT") {
|
|
12800
|
-
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
12801
|
-
} else
|
|
12802
|
-
completeness = "uncertain";
|
|
12803
|
-
}
|
|
12804
|
-
}
|
|
12805
|
-
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
12707
|
+
var gitPaths = (root) => {
|
|
12708
|
+
const result = spawnSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12709
|
+
cwd: root,
|
|
12710
|
+
encoding: "buffer"
|
|
12711
|
+
});
|
|
12712
|
+
if (result.status !== 0) {
|
|
12713
|
+
const stderr = result.stderr?.toString("utf8") ?? "";
|
|
12714
|
+
return {
|
|
12715
|
+
paths: [],
|
|
12716
|
+
uncertain: !/not a git repository/i.test(stderr),
|
|
12717
|
+
git: !/not a git repository/i.test(stderr)
|
|
12718
|
+
};
|
|
12719
|
+
}
|
|
12720
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")) : [];
|
|
12721
|
+
const stagedDeleted = spawnSync5("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12722
|
+
if (stagedDeleted.status !== 0) {
|
|
12723
|
+
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12724
|
+
if (!/not a git repository/i.test(stderr))
|
|
12725
|
+
return { paths, uncertain: true, git: true };
|
|
12726
|
+
} else if (stagedDeleted.stdout) {
|
|
12727
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")));
|
|
12728
|
+
}
|
|
12729
|
+
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12730
|
+
};
|
|
12731
|
+
var walk = (root, directory, output) => {
|
|
12732
|
+
let names;
|
|
12733
|
+
try {
|
|
12734
|
+
names = fs10.readdirSync(directory);
|
|
12735
|
+
} catch {
|
|
12736
|
+
return true;
|
|
12737
|
+
}
|
|
12738
|
+
let uncertain = false;
|
|
12739
|
+
for (const name of names) {
|
|
12740
|
+
if (name === ".git" || name === ".workit")
|
|
12741
|
+
continue;
|
|
12742
|
+
const target = path12.join(directory, name);
|
|
12743
|
+
let stat;
|
|
12744
|
+
try {
|
|
12745
|
+
stat = fs10.lstatSync(target);
|
|
12746
|
+
} catch {
|
|
12747
|
+
uncertain = true;
|
|
12748
|
+
continue;
|
|
12749
|
+
}
|
|
12750
|
+
const item = relative(root, target);
|
|
12751
|
+
output.add(item);
|
|
12752
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
12753
|
+
uncertain = walk(root, target, output) || uncertain;
|
|
12754
|
+
}
|
|
12755
|
+
return uncertain;
|
|
12756
|
+
};
|
|
12757
|
+
var scopeRoots = (root, scope) => {
|
|
12758
|
+
const roots = [];
|
|
12759
|
+
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12760
|
+
const target = path12.resolve(root, item);
|
|
12761
|
+
if (!inside(root, target))
|
|
12762
|
+
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` }] });
|
|
12763
|
+
let ancestor = target;
|
|
12764
|
+
while (!fs10.existsSync(ancestor) && ancestor !== root)
|
|
12765
|
+
ancestor = path12.dirname(ancestor);
|
|
12766
|
+
try {
|
|
12767
|
+
if (!inside(root, fs10.realpathSync(ancestor)))
|
|
12768
|
+
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` }] });
|
|
12769
|
+
} catch {
|
|
12770
|
+
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
12771
|
+
}
|
|
12772
|
+
roots.push(target);
|
|
12773
|
+
}
|
|
12774
|
+
return success(null, null, roots);
|
|
12775
|
+
};
|
|
12776
|
+
function captureCandidate(root, scope, environment = []) {
|
|
12777
|
+
let checkout;
|
|
12778
|
+
try {
|
|
12779
|
+
checkout = fs10.realpathSync(root);
|
|
12780
|
+
} catch {
|
|
12781
|
+
return failure2("invalid_input", "candidate root does not exist");
|
|
12782
|
+
}
|
|
12783
|
+
const normalizedScope = canonicalScope(scope);
|
|
12784
|
+
if (!normalizedScope)
|
|
12785
|
+
return failure2("invalid_input", "candidate scope is invalid: paths must be checkout-relative — to work in another repository, keep this task here and coordinate a separate linked task in that checkout", { fields: [{ path: "scope.paths", reason: "must be checkout-relative" }] });
|
|
12786
|
+
const roots = scopeRoots(checkout, normalizedScope);
|
|
12787
|
+
if (!roots.ok)
|
|
12788
|
+
return roots;
|
|
12789
|
+
const git = gitPaths(checkout);
|
|
12790
|
+
const names = new Set;
|
|
12791
|
+
let uncertain = git.uncertain;
|
|
12792
|
+
for (const target of roots.data) {
|
|
12793
|
+
const item = relative(checkout, target);
|
|
12794
|
+
if (item === ".git" || item === ".workit")
|
|
12795
|
+
continue;
|
|
12796
|
+
names.add(item);
|
|
12797
|
+
if (!fs10.existsSync(target))
|
|
12798
|
+
continue;
|
|
12799
|
+
let stat;
|
|
12800
|
+
try {
|
|
12801
|
+
stat = fs10.lstatSync(target);
|
|
12802
|
+
} catch {
|
|
12803
|
+
uncertain = true;
|
|
12804
|
+
continue;
|
|
12805
|
+
}
|
|
12806
|
+
if (!git.git && stat.isDirectory() && !stat.isSymbolicLink())
|
|
12807
|
+
uncertain = walk(checkout, target, names) || uncertain;
|
|
12808
|
+
}
|
|
12809
|
+
for (const item of git.paths)
|
|
12810
|
+
if (scopeMatches(item, normalizedScope))
|
|
12811
|
+
names.add(item);
|
|
12812
|
+
const files = [];
|
|
12813
|
+
let completeness = uncertain ? "uncertain" : "known";
|
|
12814
|
+
for (const item of [...names].toSorted()) {
|
|
12815
|
+
if (!scopeMatches(item, normalizedScope))
|
|
12816
|
+
continue;
|
|
12817
|
+
const target = path12.join(checkout, item);
|
|
12818
|
+
try {
|
|
12819
|
+
const stat = fs10.lstatSync(target);
|
|
12820
|
+
if (stat.isSymbolicLink()) {
|
|
12821
|
+
const link = fs10.readlinkSync(target);
|
|
12822
|
+
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
12823
|
+
} else if (stat.isFile()) {
|
|
12824
|
+
files.push({
|
|
12825
|
+
path: item,
|
|
12826
|
+
kind: "file",
|
|
12827
|
+
digest: digestBytes3(fs10.readFileSync(target)),
|
|
12828
|
+
executable: (stat.mode & 73) !== 0
|
|
12829
|
+
});
|
|
12830
|
+
} else if (!stat.isDirectory()) {
|
|
12831
|
+
completeness = "uncertain";
|
|
12832
|
+
} else if (git.git && git.paths.includes(item)) {
|
|
12833
|
+
completeness = "uncertain";
|
|
12834
|
+
}
|
|
12835
|
+
} catch (error) {
|
|
12836
|
+
if (error?.code === "ENOENT") {
|
|
12837
|
+
files.push({ path: item, kind: "absent", digest: null, executable: null });
|
|
12838
|
+
} else
|
|
12839
|
+
completeness = "uncertain";
|
|
12840
|
+
}
|
|
12841
|
+
}
|
|
12842
|
+
const supplied = Array.isArray(environment) ? environment.map((value) => typeof value === "string" ? { name: value, value: process.env[value] ?? null } : { name: value.name, value: value.value ?? null }) : Object.keys(environment).map((name) => ({
|
|
12806
12843
|
name,
|
|
12807
12844
|
value: environment[name] ?? null
|
|
12808
12845
|
}));
|
|
12809
12846
|
const namesByEnvironment = supplied.map(({ name }) => name);
|
|
12810
12847
|
if (new Set(namesByEnvironment).size !== namesByEnvironment.length || namesByEnvironment.some((name) => typeof name !== "string" || !name))
|
|
12811
12848
|
return failure2("invalid_input", "candidate environment names must be unique and non-empty");
|
|
12812
|
-
const values = supplied.toSorted((left, right) =>
|
|
12849
|
+
const values = supplied.toSorted((left, right) => compareCodeUnits2(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
12813
12850
|
if (values.some(({ value }) => value === null))
|
|
12814
12851
|
completeness = "uncertain";
|
|
12815
|
-
const headResult =
|
|
12852
|
+
const headResult = spawnSync5("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
12816
12853
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
12817
12854
|
completeness = "uncertain";
|
|
12818
12855
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -12850,7 +12887,24 @@ var sameSession = (left, right) => {
|
|
|
12850
12887
|
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
12851
12888
|
};
|
|
12852
12889
|
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
12853
|
-
|
|
12890
|
+
var memo2 = (compute) => {
|
|
12891
|
+
let value;
|
|
12892
|
+
return () => {
|
|
12893
|
+
if (value === undefined)
|
|
12894
|
+
value = compute();
|
|
12895
|
+
return value;
|
|
12896
|
+
};
|
|
12897
|
+
};
|
|
12898
|
+
var treeFreshness = (root) => ({
|
|
12899
|
+
mode: "tree",
|
|
12900
|
+
current: memo2(() => worktreeTree(root)?.key ?? null)
|
|
12901
|
+
});
|
|
12902
|
+
var signalFreshness = (root) => ({
|
|
12903
|
+
mode: "signal",
|
|
12904
|
+
current: memo2(() => worktreeSignal(root))
|
|
12905
|
+
});
|
|
12906
|
+
var isObservedCheck = (entry) => entry.data.kind === "check" && entry.data.observation?.observer === "workit_cli" && entry.provenance?.kind === "host_observed" && entry.provenance.host === "workit_cli";
|
|
12907
|
+
function evaluateEvidence(task, candidate, freshness) {
|
|
12854
12908
|
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
12855
12909
|
return task.evidence.map((entry) => {
|
|
12856
12910
|
const evidence = entry.data;
|
|
@@ -12860,6 +12914,25 @@ function evaluateEvidence(task, candidate) {
|
|
|
12860
12914
|
status: "failed",
|
|
12861
12915
|
reason: "failed evidence remains historical"
|
|
12862
12916
|
};
|
|
12917
|
+
const observation = isObservedCheck(entry) ? evidence.observation : undefined;
|
|
12918
|
+
if (observation && observation.tree !== null) {
|
|
12919
|
+
if (task.status === "closed")
|
|
12920
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "recorded before close" };
|
|
12921
|
+
if (observation.modifiedWorktree)
|
|
12922
|
+
return {
|
|
12923
|
+
evidenceId: entry.id,
|
|
12924
|
+
status: "stale",
|
|
12925
|
+
reason: "the check changed the worktree while it ran; ignore or clean its outputs"
|
|
12926
|
+
};
|
|
12927
|
+
const recorded = freshness?.mode === "signal" ? observation.signal : observation.tree;
|
|
12928
|
+
if (recorded !== null && freshness && recorded === freshness.current())
|
|
12929
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "worktree is unchanged" };
|
|
12930
|
+
return {
|
|
12931
|
+
evidenceId: entry.id,
|
|
12932
|
+
status: "stale",
|
|
12933
|
+
reason: freshness?.mode === "signal" ? "the worktree may have changed since the check ran (quick signal; inspect re-checks exactly)" : "the worktree changed since the check ran"
|
|
12934
|
+
};
|
|
12935
|
+
}
|
|
12863
12936
|
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
12864
12937
|
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
12865
12938
|
return {
|
|
@@ -12943,8 +13016,45 @@ var expectedKinds = (dimension, ruleId) => {
|
|
|
12943
13016
|
return "decision";
|
|
12944
13017
|
return "worker report";
|
|
12945
13018
|
};
|
|
12946
|
-
|
|
12947
|
-
|
|
13019
|
+
var observedGate = (requirement) => (requirement.dimension === "testing" || requirement.dimension === "verification") && requirement.before === "close";
|
|
13020
|
+
function evaluateObservedGate(task, requirement, statuses, config) {
|
|
13021
|
+
const names = gateCheckNames(config, requirement.dimension);
|
|
13022
|
+
const observed = task.evidence.filter(isObservedCheck);
|
|
13023
|
+
const fresh = observed.filter((entry) => statuses.get(entry.id) === "passed" && entry.data.observation?.exitCode === 0);
|
|
13024
|
+
const named = (entry) => {
|
|
13025
|
+
const observation = entry.data.observation;
|
|
13026
|
+
return observation.configured && !observation.shell && names.includes(observation.name ?? "") && matchesNamedCheck(config, observation.name, observation.argv, observation.cwd);
|
|
13027
|
+
};
|
|
13028
|
+
const considered = observed.map((entry) => entry.id);
|
|
13029
|
+
const run = checkCommand(names[0] ?? "<name>", runtimeVersion());
|
|
13030
|
+
if (config.error)
|
|
13031
|
+
return {
|
|
13032
|
+
satisfied: [],
|
|
13033
|
+
considered,
|
|
13034
|
+
reason: `check config is invalid (${config.error}); fix it, then run ${run}`
|
|
13035
|
+
};
|
|
13036
|
+
if (!names.length)
|
|
13037
|
+
return {
|
|
13038
|
+
satisfied: [],
|
|
13039
|
+
considered,
|
|
13040
|
+
reason: requirement.dimension === "testing" && config.checks.length ? `no \`test\` check is configured; add one (or gates.testing) to ${CHECKS_FILE}, then run ${checkCommand("test", runtimeVersion())}` : `no checks are configured or detected; add ${CHECKS_FILE} (committed, so it shows in the diff), e.g. {"checks":{"test":"<your test command>"}}, then run ${checkCommand("test", runtimeVersion())}`
|
|
13041
|
+
};
|
|
13042
|
+
const satisfied = fresh.filter(named);
|
|
13043
|
+
if (satisfied.length)
|
|
13044
|
+
return {
|
|
13045
|
+
satisfied: satisfied.map((entry) => entry.id),
|
|
13046
|
+
considered,
|
|
13047
|
+
reason: `a fresh passing configured check (${names.join("|")}) was observed by workit check`
|
|
13048
|
+
};
|
|
13049
|
+
const reported = task.evidence.some((entry) => entry.data.kind === "check" && !isObservedCheck(entry) && entry.data.requirementIds.includes(requirement.id));
|
|
13050
|
+
const reason = fresh.length ? `ad-hoc checks do not satisfy a configured gate; run ${run}` : observed.some((entry) => statuses.get(entry.id) === "stale") ? `observed checks are stale (the worktree changed); run ${run}` : observed.some((entry) => statuses.get(entry.id) === "failed") ? `the observed check failed; fix it and run ${run}` : reported ? `agent-reported checks are notes, not evidence; run ${run}` : `no CLI-observed check (needs kind:check observed by workit check); run ${run}`;
|
|
13051
|
+
return { satisfied: [], considered, reason };
|
|
13052
|
+
}
|
|
13053
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller, freshness = treeFreshness(checkoutRoot)) {
|
|
13054
|
+
const evidence = evaluateEvidence(task, candidate, freshness);
|
|
13055
|
+
const statuses = new Map(evidence.map((item) => [item.evidenceId, item.status]));
|
|
13056
|
+
let checkConfig;
|
|
13057
|
+
const configFor = () => checkConfig ??= loadCheckConfig(checkoutRoot);
|
|
12948
13058
|
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
12949
13059
|
return task.policy.requirements.map((requirement) => ({
|
|
12950
13060
|
requirementId: requirement.id,
|
|
@@ -12954,8 +13064,19 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
12954
13064
|
reason: "stored policy version is unsupported; reassessment is required"
|
|
12955
13065
|
}));
|
|
12956
13066
|
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
13067
|
+
const gate = observedGate(requirement) ? evaluateObservedGate(task, requirement, statuses, configFor()) : null;
|
|
13068
|
+
if (gate?.satisfied.length)
|
|
13069
|
+
return {
|
|
13070
|
+
requirementId: requirement.id,
|
|
13071
|
+
status: "satisfied",
|
|
13072
|
+
evidenceIds: gate.satisfied,
|
|
13073
|
+
decisionIds: [],
|
|
13074
|
+
reason: gate.reason
|
|
13075
|
+
};
|
|
12957
13076
|
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
12958
13077
|
const passed = related.filter(({ entry, evaluation }) => {
|
|
13078
|
+
if (gate)
|
|
13079
|
+
return false;
|
|
12959
13080
|
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
12960
13081
|
return false;
|
|
12961
13082
|
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
@@ -13018,6 +13139,14 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
13018
13139
|
reason: "an applicable approved limitation permits the missing evidence"
|
|
13019
13140
|
};
|
|
13020
13141
|
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
13142
|
+
if (gate)
|
|
13143
|
+
return {
|
|
13144
|
+
requirementId: requirement.id,
|
|
13145
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
13146
|
+
evidenceIds: [...new Set([...gate.considered, ...related.map(({ entry }) => entry.id)])],
|
|
13147
|
+
decisionIds: [],
|
|
13148
|
+
reason: unavailable ? "required capability is unavailable" : gate.reason
|
|
13149
|
+
};
|
|
13021
13150
|
if (!unavailable && related.length > 0 && !passed.length)
|
|
13022
13151
|
return {
|
|
13023
13152
|
requirementId: requirement.id,
|
|
@@ -13071,51 +13200,381 @@ function evaluateClosure(requestedOutcome, view) {
|
|
|
13071
13200
|
return failure2("requirements_unsatisfied", "deferred findings require an approved limitation decision that references an acceptanceAllowed requirement and covers the finding scope");
|
|
13072
13201
|
}
|
|
13073
13202
|
}
|
|
13074
|
-
}
|
|
13075
|
-
const blocking = evaluations.filter((item) => {
|
|
13076
|
-
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
13203
|
+
}
|
|
13204
|
+
const blocking = evaluations.filter((item) => {
|
|
13205
|
+
if (item.status !== "unsatisfied" && item.status !== "unavailable")
|
|
13206
|
+
return false;
|
|
13207
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
13208
|
+
return !requirement || requirement.before === "close";
|
|
13209
|
+
});
|
|
13210
|
+
if (requestedOutcome !== "stopped" && blocking.length)
|
|
13211
|
+
return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
13212
|
+
requirementIds: blocking.map((item) => item.requirementId),
|
|
13213
|
+
requirements: blocking.map((item) => {
|
|
13214
|
+
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
13215
|
+
return {
|
|
13216
|
+
requirementId: item.requirementId,
|
|
13217
|
+
ruleId: requirement?.ruleId ?? "unknown",
|
|
13218
|
+
reason: item.reason,
|
|
13219
|
+
satisfaction: requirement?.satisfaction ?? "",
|
|
13220
|
+
dependentAction: requirement?.dependentAction ?? null
|
|
13221
|
+
};
|
|
13222
|
+
})
|
|
13223
|
+
});
|
|
13224
|
+
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
13225
|
+
if (requestedOutcome === "verified" && accepted.length)
|
|
13226
|
+
return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
13227
|
+
requirementIds: accepted.map((item) => item.requirementId)
|
|
13228
|
+
});
|
|
13229
|
+
const evidenceIds = [
|
|
13230
|
+
...new Set([
|
|
13231
|
+
...evaluations.flatMap((item) => item.evidenceIds),
|
|
13232
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
13233
|
+
])
|
|
13234
|
+
];
|
|
13235
|
+
const decisionIds = [
|
|
13236
|
+
...new Set([
|
|
13237
|
+
...evaluations.flatMap((item) => item.decisionIds),
|
|
13238
|
+
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
13239
|
+
])
|
|
13240
|
+
];
|
|
13241
|
+
return success(null, null, {
|
|
13242
|
+
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
13243
|
+
evidenceIds,
|
|
13244
|
+
decisionIds,
|
|
13245
|
+
requirementIds: evaluations.map((item) => item.requirementId)
|
|
13246
|
+
});
|
|
13247
|
+
}
|
|
13248
|
+
|
|
13249
|
+
// packages/workit-core/src/core/task-engine.ts
|
|
13250
|
+
import { realpathSync as realpathSync6 } from "node:fs";
|
|
13251
|
+
|
|
13252
|
+
// packages/workit-core/src/core/policy-resolver.ts
|
|
13253
|
+
var resolverInputSchema = object({
|
|
13254
|
+
intent: intentSchema,
|
|
13255
|
+
assessment: assessmentSchema,
|
|
13256
|
+
constraints: array(constraintSchema),
|
|
13257
|
+
prior: object({
|
|
13258
|
+
decisions: array(decisionSchema),
|
|
13259
|
+
findings: array(findingSchema),
|
|
13260
|
+
requirements: array(requirementSchema)
|
|
13261
|
+
}).strict(),
|
|
13262
|
+
preferences: object({ fast: boolean2().optional(), thorough: boolean2().optional() }).strict().optional()
|
|
13263
|
+
}).strict();
|
|
13264
|
+
var compareCodeUnits3 = (left, right) => {
|
|
13265
|
+
const length = Math.min(left.length, right.length);
|
|
13266
|
+
for (let index = 0;index < length; index += 1) {
|
|
13267
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
13268
|
+
if (difference !== 0)
|
|
13269
|
+
return difference;
|
|
13270
|
+
}
|
|
13271
|
+
return left.length - right.length;
|
|
13272
|
+
};
|
|
13273
|
+
var stableUnique = (values) => [...new Set(values)].toSorted(compareCodeUnits3);
|
|
13274
|
+
var stableList = (values) => [...values].toSorted((left, right) => compareCodeUnits3(canonicalJson(left), canonicalJson(right)));
|
|
13275
|
+
var normalizedScope = (scope) => ({
|
|
13276
|
+
description: scope.description,
|
|
13277
|
+
paths: stableUnique(scope.paths),
|
|
13278
|
+
exclusions: stableUnique(scope.exclusions)
|
|
13279
|
+
});
|
|
13280
|
+
var normalizedRefList = (values) => stableList(values);
|
|
13281
|
+
var normalizeAssessment = (assessment) => ({
|
|
13282
|
+
facts: stableList(assessment.facts.map((fact) => ({ ...fact, refs: normalizedRefList(fact.refs) }))),
|
|
13283
|
+
signals: Object.fromEntries(Object.entries(assessment.signals).map(([name, signal]) => [
|
|
13284
|
+
name,
|
|
13285
|
+
{ ...signal, refs: normalizedRefList(signal.refs) }
|
|
13286
|
+
])),
|
|
13287
|
+
consequences: stableList(assessment.consequences.map((consequence) => ({
|
|
13288
|
+
...consequence,
|
|
13289
|
+
fact: { ...consequence.fact, refs: normalizedRefList(consequence.fact.refs) }
|
|
13290
|
+
}))),
|
|
13291
|
+
verification: stableList(assessment.verification.map((verification) => ({
|
|
13292
|
+
...verification,
|
|
13293
|
+
scope: normalizedScope(verification.scope),
|
|
13294
|
+
availableChecks: normalizedRefList(verification.availableChecks),
|
|
13295
|
+
gaps: stableUnique(verification.gaps)
|
|
13296
|
+
})))
|
|
13297
|
+
});
|
|
13298
|
+
var normalizeConstraint = (constraint) => ({
|
|
13299
|
+
...constraint,
|
|
13300
|
+
source: constraint.source,
|
|
13301
|
+
requires: stableList(constraint.requires.map((requirement) => ({
|
|
13302
|
+
...requirement,
|
|
13303
|
+
scope: normalizedScope(requirement.scope),
|
|
13304
|
+
refs: normalizedRefList(requirement.refs)
|
|
13305
|
+
})))
|
|
13306
|
+
});
|
|
13307
|
+
var normalizeDecision = (decision) => ({
|
|
13308
|
+
...decision,
|
|
13309
|
+
binding: {
|
|
13310
|
+
...decision.binding,
|
|
13311
|
+
scope: normalizedScope(decision.binding.scope),
|
|
13312
|
+
contentRefs: normalizedRefList(decision.binding.contentRefs)
|
|
13313
|
+
},
|
|
13314
|
+
requirementIds: stableUnique(decision.requirementIds)
|
|
13315
|
+
});
|
|
13316
|
+
var normalizeIntent = (intent) => ({
|
|
13317
|
+
...intent,
|
|
13318
|
+
scope: normalizedScope(intent.scope),
|
|
13319
|
+
authorityRefs: normalizedRefList(intent.authorityRefs)
|
|
13320
|
+
});
|
|
13321
|
+
var normalizePrior = (prior) => ({
|
|
13322
|
+
decisions: stableList(prior.decisions.map(normalizeDecision)),
|
|
13323
|
+
findings: stableList(prior.findings.map((finding) => ({
|
|
13324
|
+
...finding,
|
|
13325
|
+
scope: normalizedScope(finding.scope),
|
|
13326
|
+
refs: normalizedRefList(finding.refs),
|
|
13327
|
+
resolution: finding.resolution ? {
|
|
13328
|
+
...finding.resolution,
|
|
13329
|
+
evidenceIds: stableUnique(finding.resolution.evidenceIds),
|
|
13330
|
+
decisionIds: stableUnique(finding.resolution.decisionIds)
|
|
13331
|
+
} : null
|
|
13332
|
+
}))),
|
|
13333
|
+
requirements: stableList(prior.requirements.map((requirement) => ({
|
|
13334
|
+
...requirement,
|
|
13335
|
+
scope: normalizedScope(requirement.scope)
|
|
13336
|
+
})))
|
|
13337
|
+
});
|
|
13338
|
+
var invalidFields = (error) => ({
|
|
13339
|
+
fields: error.issues.map((issue) => ({
|
|
13340
|
+
path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
|
|
13341
|
+
reason: issue.message
|
|
13342
|
+
}))
|
|
13343
|
+
});
|
|
13344
|
+
function normalizeResolverInput(input) {
|
|
13345
|
+
const parsed = resolverInputSchema.safeParse(input);
|
|
13346
|
+
if (!parsed.success)
|
|
13347
|
+
return failure2("invalid_input", "resolver input is invalid", invalidFields(parsed.error));
|
|
13348
|
+
const value = parsed.data;
|
|
13349
|
+
const signals = value.assessment.signals;
|
|
13350
|
+
if (signals.behaviorChange.value === true && signals.mechanicalLowRisk.value === true)
|
|
13351
|
+
return failure2("invalid_input", "behavioral change cannot be wholly mechanical", {
|
|
13352
|
+
fields: [
|
|
13353
|
+
{
|
|
13354
|
+
path: "assessment.signals",
|
|
13355
|
+
reason: "contradictory behaviorChange and mechanicalLowRisk"
|
|
13356
|
+
}
|
|
13357
|
+
]
|
|
13358
|
+
});
|
|
13359
|
+
const ids = value.constraints.map((constraint) => constraint.id);
|
|
13360
|
+
if (new Set(ids).size !== ids.length)
|
|
13361
|
+
return failure2("invalid_input", "constraint IDs must be unique", {
|
|
13362
|
+
fields: [{ path: "constraints", reason: "duplicate constraint id" }]
|
|
13363
|
+
});
|
|
13364
|
+
if (value.preferences?.fast && value.preferences.thorough)
|
|
13365
|
+
return failure2("invalid_input", "fast and thorough preferences contradict", {
|
|
13366
|
+
fields: [{ path: "preferences", reason: "fast and thorough cannot both be enabled" }]
|
|
13367
|
+
});
|
|
13368
|
+
const normalized = {
|
|
13369
|
+
intent: normalizeIntent(value.intent),
|
|
13370
|
+
assessment: normalizeAssessment(value.assessment),
|
|
13371
|
+
constraints: stableList(value.constraints.map(normalizeConstraint)),
|
|
13372
|
+
prior: normalizePrior(value.prior),
|
|
13373
|
+
...value.preferences ? { preferences: { ...value.preferences } } : {},
|
|
13374
|
+
policyVersion: POLICY_VERSION
|
|
13375
|
+
};
|
|
13376
|
+
return success(null, null, normalized);
|
|
13377
|
+
}
|
|
13378
|
+
var requirement = (spec) => ({
|
|
13379
|
+
...spec,
|
|
13380
|
+
id: requirementId({
|
|
13381
|
+
ruleId: spec.ruleId,
|
|
13382
|
+
scope: spec.scope,
|
|
13383
|
+
satisfaction: spec.satisfaction,
|
|
13384
|
+
before: spec.before,
|
|
13385
|
+
dependentAction: spec.dependentAction
|
|
13386
|
+
})
|
|
13387
|
+
});
|
|
13388
|
+
var signalReason = (input, name) => input.assessment.signals[name].reason;
|
|
13389
|
+
function resolveRequirements(input) {
|
|
13390
|
+
const { signals } = input.assessment;
|
|
13391
|
+
const scope = input.intent.scope;
|
|
13392
|
+
const requirements = [];
|
|
13393
|
+
const hasUnknownSignal = Object.values(signals).some((signal) => signal.value === "unknown");
|
|
13394
|
+
const hasUnknownFact = [
|
|
13395
|
+
...input.assessment.facts,
|
|
13396
|
+
...input.assessment.consequences.map(({ fact }) => fact)
|
|
13397
|
+
].some((fact) => fact.basis === "unknown");
|
|
13398
|
+
if (hasUnknownSignal || hasUnknownFact)
|
|
13399
|
+
requirements.push(requirement({
|
|
13400
|
+
ruleId: "consequential-unknown",
|
|
13401
|
+
dimension: "investigation",
|
|
13402
|
+
scope,
|
|
13403
|
+
reason: `Resolve consequential uncertainty before the dependent action: ${[
|
|
13404
|
+
...Object.entries(signals).filter(([, signal]) => signal.value === "unknown").map(([name]) => name),
|
|
13405
|
+
...hasUnknownFact ? ["unknown fact"] : []
|
|
13406
|
+
].join(", ")}`,
|
|
13407
|
+
satisfaction: "An observed or inferred fact resolves the uncertainty and records its supporting references.",
|
|
13408
|
+
before: "dependent_action",
|
|
13409
|
+
dependentAction: null,
|
|
13410
|
+
acceptanceAllowed: false
|
|
13411
|
+
}));
|
|
13412
|
+
if (signals.productChoiceOpen.value === true)
|
|
13413
|
+
requirements.push(requirement({
|
|
13414
|
+
ruleId: "product-decision",
|
|
13415
|
+
dimension: "decisions",
|
|
13416
|
+
scope,
|
|
13417
|
+
reason: `A consequential product choice remains open: ${signalReason(input, "productChoiceOpen")}`,
|
|
13418
|
+
satisfaction: "Present the material alternatives and record the user's decision before the dependent action.",
|
|
13419
|
+
before: "dependent_action",
|
|
13420
|
+
dependentAction: null,
|
|
13421
|
+
acceptanceAllowed: false
|
|
13422
|
+
}));
|
|
13423
|
+
const independentBehaviorReview = input.preferences?.thorough === true || input.assessment.consequences.some((consequence) => ["security", "data", "public_contract", "operations"].includes(consequence.area));
|
|
13424
|
+
if (signals.behaviorChange.value === true)
|
|
13425
|
+
requirements.push(requirement({
|
|
13426
|
+
ruleId: "behavioral-verification",
|
|
13427
|
+
dimension: "testing",
|
|
13428
|
+
scope,
|
|
13429
|
+
reason: `Behavior, side effects, permissions, or data handling may change: ${signalReason(input, "behaviorChange")}`,
|
|
13430
|
+
satisfaction: "A useful behavioral check passes against the affected behavior and current candidate.",
|
|
13431
|
+
before: "close",
|
|
13432
|
+
dependentAction: null,
|
|
13433
|
+
acceptanceAllowed: false
|
|
13434
|
+
}), requirement({
|
|
13435
|
+
ruleId: independentBehaviorReview ? "fresh-context-review" : "self-review",
|
|
13436
|
+
dimension: "review",
|
|
13437
|
+
scope,
|
|
13438
|
+
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.",
|
|
13439
|
+
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.",
|
|
13440
|
+
before: "close",
|
|
13441
|
+
dependentAction: null,
|
|
13442
|
+
acceptanceAllowed: !independentBehaviorReview
|
|
13443
|
+
}));
|
|
13444
|
+
else if (signals.mechanicalLowRisk.value === true)
|
|
13445
|
+
requirements.push(requirement({
|
|
13446
|
+
ruleId: "mechanical-existing-checks",
|
|
13447
|
+
dimension: "verification",
|
|
13448
|
+
scope,
|
|
13449
|
+
reason: `The change is demonstrably mechanical and low-risk: ${signalReason(input, "mechanicalLowRisk")}`,
|
|
13450
|
+
satisfaction: "Run the relevant existing checks for affected consumers; do not add tautological tests.",
|
|
13451
|
+
before: "close",
|
|
13452
|
+
dependentAction: null,
|
|
13453
|
+
acceptanceAllowed: true
|
|
13454
|
+
}), requirement({
|
|
13455
|
+
ruleId: input.preferences?.thorough ? "fresh-context-review" : "self-review",
|
|
13456
|
+
dimension: "review",
|
|
13457
|
+
scope,
|
|
13458
|
+
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.",
|
|
13459
|
+
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.",
|
|
13460
|
+
before: "close",
|
|
13461
|
+
dependentAction: null,
|
|
13462
|
+
acceptanceAllowed: !input.preferences?.thorough
|
|
13463
|
+
}));
|
|
13464
|
+
if (signals.behaviorChange.value === true || signals.mechanicalLowRisk.value === true)
|
|
13465
|
+
requirements.push(requirement({
|
|
13466
|
+
ruleId: "pre-pr-cleanup",
|
|
13467
|
+
dimension: "verification",
|
|
13468
|
+
scope,
|
|
13469
|
+
reason: "Implementation output must be deslopped before it is delivered.",
|
|
13470
|
+
satisfaction: "Run the workit-deslop pass and record passing check evidence, or record an approved limitation decision waiving cleanup.",
|
|
13471
|
+
before: "dependent_action",
|
|
13472
|
+
dependentAction: "hosting.pull_request",
|
|
13473
|
+
acceptanceAllowed: true
|
|
13474
|
+
}));
|
|
13475
|
+
if (signals.durableAgreementNeeded.value === true)
|
|
13476
|
+
requirements.push(requirement({
|
|
13477
|
+
ruleId: "durable-spec",
|
|
13478
|
+
dimension: "artifacts",
|
|
13479
|
+
scope,
|
|
13480
|
+
reason: `The work needs a durable agreement about intended behavior: ${signalReason(input, "durableAgreementNeeded")}`,
|
|
13481
|
+
satisfaction: "Record the agreed behavior in a durable specification before implementation writes.",
|
|
13482
|
+
before: "write",
|
|
13483
|
+
dependentAction: null,
|
|
13484
|
+
acceptanceAllowed: true
|
|
13485
|
+
}));
|
|
13486
|
+
if (signals.coordinationPlanNeeded.value === true)
|
|
13487
|
+
requirements.push(requirement({
|
|
13488
|
+
ruleId: "coordination-plan",
|
|
13489
|
+
dimension: "continuity",
|
|
13490
|
+
scope,
|
|
13491
|
+
reason: `Dependencies, sequencing, or resumption need durable coordination: ${signalReason(input, "coordinationPlanNeeded")}`,
|
|
13492
|
+
satisfaction: "Record useful sequencing, dependencies, and the next action in a compact plan.",
|
|
13493
|
+
before: "dependent_action",
|
|
13494
|
+
dependentAction: null,
|
|
13495
|
+
acceptanceAllowed: true
|
|
13496
|
+
}));
|
|
13497
|
+
if (signals.helperUseful.value === true && !input.preferences?.fast)
|
|
13498
|
+
requirements.push(requirement({
|
|
13499
|
+
ruleId: "helper-usefulness",
|
|
13500
|
+
dimension: "delegation",
|
|
13501
|
+
scope,
|
|
13502
|
+
reason: `Independent judgment or investigation justifies a scoped helper: ${signalReason(input, "helperUseful")}`,
|
|
13503
|
+
satisfaction: "Use a bounded helper only for the independent objective and reconcile its result.",
|
|
13504
|
+
before: "dependent_action",
|
|
13505
|
+
dependentAction: null,
|
|
13506
|
+
acceptanceAllowed: true
|
|
13507
|
+
}));
|
|
13508
|
+
for (const constraint of input.constraints)
|
|
13509
|
+
for (const obligation of constraint.requires) {
|
|
13510
|
+
const dimension = obligation.method ?? (obligation.kind === "check" ? "verification" : obligation.kind === "decision" ? "decisions" : "challenge");
|
|
13511
|
+
requirements.push(requirement({
|
|
13512
|
+
ruleId: `project-constraint:${constraint.id}:${obligation.kind}:${sha256({
|
|
13513
|
+
kind: obligation.kind,
|
|
13514
|
+
scope: normalizedScope(obligation.scope),
|
|
13515
|
+
refs: normalizedRefList(obligation.refs),
|
|
13516
|
+
method: obligation.method,
|
|
13517
|
+
before: obligation.before,
|
|
13518
|
+
dependentAction: obligation.dependentAction
|
|
13519
|
+
})}`,
|
|
13520
|
+
dimension,
|
|
13521
|
+
scope: obligation.scope,
|
|
13522
|
+
reason: `${constraint.kind} constraint requires ${obligation.kind}: ${constraint.statement}`,
|
|
13523
|
+
satisfaction: `Satisfy the constraint obligation using its referenced ${obligation.kind}.`,
|
|
13524
|
+
before: obligation.before,
|
|
13525
|
+
dependentAction: obligation.dependentAction,
|
|
13526
|
+
acceptanceAllowed: constraint.acceptanceAllowed && obligation.kind !== "decision"
|
|
13527
|
+
}));
|
|
13528
|
+
}
|
|
13529
|
+
const seen = new Set;
|
|
13530
|
+
return requirements.filter((item) => {
|
|
13531
|
+
if (seen.has(item.id))
|
|
13077
13532
|
return false;
|
|
13078
|
-
|
|
13079
|
-
return
|
|
13080
|
-
});
|
|
13081
|
-
if (requestedOutcome !== "stopped" && blocking.length)
|
|
13082
|
-
return failure2("requirements_unsatisfied", "applicable requirements are unsatisfied", {
|
|
13083
|
-
requirementIds: blocking.map((item) => item.requirementId),
|
|
13084
|
-
requirements: blocking.map((item) => {
|
|
13085
|
-
const requirement = view.task.policy?.requirements.find((candidate) => candidate.id === item.requirementId);
|
|
13086
|
-
return {
|
|
13087
|
-
requirementId: item.requirementId,
|
|
13088
|
-
ruleId: requirement?.ruleId ?? "unknown",
|
|
13089
|
-
reason: item.reason,
|
|
13090
|
-
satisfaction: requirement?.satisfaction ?? "",
|
|
13091
|
-
dependentAction: requirement?.dependentAction ?? null
|
|
13092
|
-
};
|
|
13093
|
-
})
|
|
13094
|
-
});
|
|
13095
|
-
const accepted = evaluations.filter((item) => item.status === "accepted_limitation");
|
|
13096
|
-
if (requestedOutcome === "verified" && accepted.length)
|
|
13097
|
-
return failure2("requirements_unsatisfied", "accepted limitations cannot be reported as verified", {
|
|
13098
|
-
requirementIds: accepted.map((item) => item.requirementId)
|
|
13099
|
-
});
|
|
13100
|
-
const evidenceIds = [
|
|
13101
|
-
...new Set([
|
|
13102
|
-
...evaluations.flatMap((item) => item.evidenceIds),
|
|
13103
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.evidenceIds ?? [])
|
|
13104
|
-
])
|
|
13105
|
-
];
|
|
13106
|
-
const decisionIds = [
|
|
13107
|
-
...new Set([
|
|
13108
|
-
...evaluations.flatMap((item) => item.decisionIds),
|
|
13109
|
-
...view.task.findings.flatMap((item) => item.data.resolution?.decisionIds ?? [])
|
|
13110
|
-
])
|
|
13111
|
-
];
|
|
13112
|
-
return success(null, null, {
|
|
13113
|
-
outcome: requestedOutcome === "stopped" ? "stopped" : accepted.length ? "accepted_limitations" : "verified",
|
|
13114
|
-
evidenceIds,
|
|
13115
|
-
decisionIds,
|
|
13116
|
-
requirementIds: evaluations.map((item) => item.requirementId)
|
|
13533
|
+
seen.add(item.id);
|
|
13534
|
+
return true;
|
|
13117
13535
|
});
|
|
13118
13536
|
}
|
|
13537
|
+
function resolvePolicy(input) {
|
|
13538
|
+
const normalized = normalizeResolverInput(input);
|
|
13539
|
+
if (!normalized.ok)
|
|
13540
|
+
return normalized;
|
|
13541
|
+
const requirements = resolveRequirements(normalized.data);
|
|
13542
|
+
const policy = {
|
|
13543
|
+
policyVersion: POLICY_VERSION,
|
|
13544
|
+
inputDigest: sha256(canonicalJson(normalized.data)),
|
|
13545
|
+
requirements
|
|
13546
|
+
};
|
|
13547
|
+
const parsed = policySchema.safeParse(policy);
|
|
13548
|
+
if (!parsed.success)
|
|
13549
|
+
return failure2("invalid_input", "resolved policy is invalid", invalidFields(parsed.error));
|
|
13550
|
+
return success(null, null, parsed.data);
|
|
13551
|
+
}
|
|
13552
|
+
function diffPolicy(previous, next, reason, now) {
|
|
13553
|
+
if (!policySchema.safeParse(previous).success && previous !== null)
|
|
13554
|
+
throw new TypeError("previous policy is invalid");
|
|
13555
|
+
if (!policySchema.safeParse(next).success)
|
|
13556
|
+
throw new TypeError("next policy is invalid");
|
|
13557
|
+
const oldIds = new Set(previous?.requirements.map((item) => item.id) ?? []);
|
|
13558
|
+
const newIds = new Set(next.requirements.map((item) => item.id));
|
|
13559
|
+
const added = [...newIds].filter((id) => !oldIds.has(id)).toSorted(compareCodeUnits3);
|
|
13560
|
+
const retired = [...oldIds].filter((id) => !newIds.has(id)).toSorted(compareCodeUnits3);
|
|
13561
|
+
if (added.length === 0 && retired.length === 0)
|
|
13562
|
+
return null;
|
|
13563
|
+
const normalizedReason = reason.trim();
|
|
13564
|
+
const changeReason = retired.length ? `${normalizedReason || "policy reassessed"} (retired: ${retired.join(",")})` : normalizedReason || "policy reassessed";
|
|
13565
|
+
const change = {
|
|
13566
|
+
recordedAt: now,
|
|
13567
|
+
fromInputDigest: previous?.inputDigest ?? null,
|
|
13568
|
+
toInputDigest: next.inputDigest,
|
|
13569
|
+
added,
|
|
13570
|
+
retired,
|
|
13571
|
+
reason: changeReason
|
|
13572
|
+
};
|
|
13573
|
+
const parsed = policyChangeSchema.safeParse(change);
|
|
13574
|
+
if (!parsed.success)
|
|
13575
|
+
throw new TypeError("policy change is invalid");
|
|
13576
|
+
return parsed.data;
|
|
13577
|
+
}
|
|
13119
13578
|
|
|
13120
13579
|
// packages/workit-core/src/core/task-context.ts
|
|
13121
13580
|
function reconcileResume(view, observations = []) {
|
|
@@ -13124,7 +13583,7 @@ function reconcileResume(view, observations = []) {
|
|
|
13124
13583
|
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
13125
13584
|
if (!candidate.ok)
|
|
13126
13585
|
return candidate;
|
|
13127
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13586
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data, treeFreshness(view.workspace.root)).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13128
13587
|
const blockers = [...view.task.progress.blockers];
|
|
13129
13588
|
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13130
13589
|
blockers.push({
|
|
@@ -14022,10 +14481,7 @@ class WorkitCore {
|
|
|
14022
14481
|
if (finding.data.disposition !== "fixed")
|
|
14023
14482
|
return finding;
|
|
14024
14483
|
const withEntry = { ...current, candidates, evidence: [...current.evidence, entry] };
|
|
14025
|
-
const statuses = new Map(evaluateEvidence(withEntry, currentCandidate.data).map((item) => [
|
|
14026
|
-
item.evidenceId,
|
|
14027
|
-
item.status
|
|
14028
|
-
]));
|
|
14484
|
+
const statuses = new Map(evaluateEvidence(withEntry, currentCandidate.data, treeFreshness(this.store.root)).map((item) => [item.evidenceId, item.status]));
|
|
14029
14485
|
const verified = withEntry.evidence.some((item) => findingVerificationPasses(finding.data.candidateId, {
|
|
14030
14486
|
kind: item.data.kind,
|
|
14031
14487
|
candidateId: item.data.candidateId,
|
|
@@ -14040,6 +14496,71 @@ class WorkitCore {
|
|
|
14040
14496
|
return changed;
|
|
14041
14497
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14042
14498
|
}
|
|
14499
|
+
observeCheck(request) {
|
|
14500
|
+
return this.retryOmittedRevisions(request, () => this.observeCheckOnce(request));
|
|
14501
|
+
}
|
|
14502
|
+
observeCheckOnce(request) {
|
|
14503
|
+
const root = this.contextRootError();
|
|
14504
|
+
if (!root.ok)
|
|
14505
|
+
return root;
|
|
14506
|
+
if (this.context.caller.host !== "workit_cli" || (this.context.workerId ?? null) !== null)
|
|
14507
|
+
return failure2("permission_denied", "only the workit CLI records observed checks");
|
|
14508
|
+
const observation = checkObservationSchema.safeParse(request?.observation);
|
|
14509
|
+
if (!observation.success)
|
|
14510
|
+
return failure2("invalid_input", "check observation is invalid", {
|
|
14511
|
+
fields: observation.error.issues.map((issue) => ({
|
|
14512
|
+
path: issue.path.join("."),
|
|
14513
|
+
reason: issue.message
|
|
14514
|
+
}))
|
|
14515
|
+
});
|
|
14516
|
+
const task = this.store.readTask(request.taskId);
|
|
14517
|
+
if (!task.ok)
|
|
14518
|
+
return task;
|
|
14519
|
+
if (task.data.status === "closed")
|
|
14520
|
+
return failure2("invalid_transition", "closed task cannot record evidence");
|
|
14521
|
+
const observed = observation.data;
|
|
14522
|
+
let candidate = null;
|
|
14523
|
+
if (observed.tree === null) {
|
|
14524
|
+
const captured = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14525
|
+
if (!captured.ok)
|
|
14526
|
+
return captured;
|
|
14527
|
+
candidate = captured.data;
|
|
14528
|
+
}
|
|
14529
|
+
const label = observed.name ?? observed.argv.join(" ");
|
|
14530
|
+
const data = {
|
|
14531
|
+
kind: "check",
|
|
14532
|
+
claim: `workit check ${label}`.slice(0, 500),
|
|
14533
|
+
requirementIds: [],
|
|
14534
|
+
...candidate ? { beforeCandidateId: candidate.id, candidateId: candidate.id } : {},
|
|
14535
|
+
result: observed.exitCode === 0 ? "passed" : "failed",
|
|
14536
|
+
summary: `${observed.configured ? "configured" : "ad-hoc"} check exited ${observed.exitCode}${observed.timedOut ? " (timed out)" : ""} in ${observed.durationMs} ms`,
|
|
14537
|
+
refs: [],
|
|
14538
|
+
exitCode: observed.exitCode,
|
|
14539
|
+
reviewContext: null,
|
|
14540
|
+
observation: observed
|
|
14541
|
+
};
|
|
14542
|
+
const changed = this.store.mutateTask(task.data.id, request.expectedRevision ?? task.data.revision, (current, mutation) => {
|
|
14543
|
+
const candidates = candidate === null || current.candidates.some((item) => item.id === candidate.id) ? current.candidates : [...current.candidates, candidate];
|
|
14544
|
+
const critical = [...new Set([...current.critical ?? [], CHECK_OBSERVATION_PATH])];
|
|
14545
|
+
return success(mutation.revision, null, {
|
|
14546
|
+
...current,
|
|
14547
|
+
critical,
|
|
14548
|
+
candidates,
|
|
14549
|
+
evidence: [
|
|
14550
|
+
...current.evidence,
|
|
14551
|
+
{
|
|
14552
|
+
id: newId(),
|
|
14553
|
+
recordedAt: mutation.now,
|
|
14554
|
+
provenance: provenance(this.context, "host_observed"),
|
|
14555
|
+
data
|
|
14556
|
+
}
|
|
14557
|
+
]
|
|
14558
|
+
});
|
|
14559
|
+
}, trustedNow(this.context));
|
|
14560
|
+
if (!changed.ok)
|
|
14561
|
+
return changed;
|
|
14562
|
+
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14563
|
+
}
|
|
14043
14564
|
decision(request) {
|
|
14044
14565
|
return this.recordDecision(request);
|
|
14045
14566
|
}
|
|
@@ -14275,7 +14796,7 @@ class WorkitCore {
|
|
|
14275
14796
|
const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14276
14797
|
if (!current.ok)
|
|
14277
14798
|
return current;
|
|
14278
|
-
const evaluations = evaluateEvidence(task.data, current.data);
|
|
14799
|
+
const evaluations = evaluateEvidence(task.data, current.data, treeFreshness(this.store.root));
|
|
14279
14800
|
const verified = evidence.some((entry) => {
|
|
14280
14801
|
const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
|
|
14281
14802
|
return entry.recordedAt >= finding.recordedAt && findingVerificationPasses(finding.data.candidateId, {
|
|
@@ -14979,7 +15500,8 @@ class WorkitCore {
|
|
|
14979
15500
|
if (!current.ok)
|
|
14980
15501
|
return current;
|
|
14981
15502
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
14982
|
-
const
|
|
15503
|
+
const tree = treeFreshness(this.store.root);
|
|
15504
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
14983
15505
|
return success(task.revision, workspace.data.revision, {
|
|
14984
15506
|
id: task.id,
|
|
14985
15507
|
revision: task.revision,
|
|
@@ -14996,7 +15518,7 @@ class WorkitCore {
|
|
|
14996
15518
|
writer: evaluationWorkspace.writer
|
|
14997
15519
|
});
|
|
14998
15520
|
}
|
|
14999
|
-
compactContext(taskId) {
|
|
15521
|
+
compactContext(taskId, freshness) {
|
|
15000
15522
|
const root = this.contextRootError();
|
|
15001
15523
|
if (!root.ok)
|
|
15002
15524
|
return root;
|
|
@@ -15006,12 +15528,12 @@ class WorkitCore {
|
|
|
15006
15528
|
const helper = this.helperTaskGuard(task.data, true);
|
|
15007
15529
|
if (!helper.ok)
|
|
15008
15530
|
return helper;
|
|
15009
|
-
const view = this.view(task.data, false);
|
|
15531
|
+
const view = this.view(task.data, false, freshness ?? signalFreshness(this.store.root));
|
|
15010
15532
|
if (!view.ok)
|
|
15011
15533
|
return view;
|
|
15012
15534
|
return success(view.revision, view.workspaceRevision, compactTaskContext(view.data));
|
|
15013
15535
|
}
|
|
15014
|
-
view(task, capture = true) {
|
|
15536
|
+
view(task, capture = true, freshness = treeFreshness(this.store.root)) {
|
|
15015
15537
|
const workspace = this.store.readWorkspace();
|
|
15016
15538
|
if (!workspace.ok)
|
|
15017
15539
|
return workspace;
|
|
@@ -15022,13 +15544,14 @@ class WorkitCore {
|
|
|
15022
15544
|
if (!current.ok)
|
|
15023
15545
|
return current;
|
|
15024
15546
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
15025
|
-
const
|
|
15547
|
+
const tree = freshness;
|
|
15548
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
15026
15549
|
return success(task.revision, workspace.data.revision, {
|
|
15027
15550
|
task,
|
|
15028
15551
|
workspace: evaluationWorkspace,
|
|
15029
15552
|
capabilities: this.context.capabilities,
|
|
15030
15553
|
requirements,
|
|
15031
|
-
evidence: evaluateEvidence(task, current.data)
|
|
15554
|
+
evidence: evaluateEvidence(task, current.data, tree)
|
|
15032
15555
|
});
|
|
15033
15556
|
}
|
|
15034
15557
|
reconcileResume(view, observations = []) {
|
|
@@ -15255,13 +15778,14 @@ class WorkitCore {
|
|
|
15255
15778
|
return current;
|
|
15256
15779
|
const withCandidate = current.data;
|
|
15257
15780
|
const taskForView = withCandidate.id === task.data.candidates.at(-1)?.id ? task.data : { ...task.data, candidates: [...task.data.candidates, withCandidate] };
|
|
15258
|
-
const
|
|
15781
|
+
const tree = treeFreshness(this.store.root);
|
|
15782
|
+
const requirements = evaluateRequirements(taskForView, workspace.data, this.context.capabilities, withCandidate, this.store.root, this.context.caller, tree);
|
|
15259
15783
|
const view = {
|
|
15260
15784
|
task: taskForView,
|
|
15261
15785
|
workspace: workspace.data,
|
|
15262
15786
|
capabilities: this.context.capabilities,
|
|
15263
15787
|
requirements,
|
|
15264
|
-
evidence: evaluateEvidence(taskForView, withCandidate)
|
|
15788
|
+
evidence: evaluateEvidence(taskForView, withCandidate, tree)
|
|
15265
15789
|
};
|
|
15266
15790
|
const closure = evaluateClosure(input.outcome, view);
|
|
15267
15791
|
if (!closure.ok)
|
|
@@ -15316,6 +15840,19 @@ var filesKey = (files) => {
|
|
|
15316
15840
|
return signatures.some((item) => item !== "-" && racySignature(item)) ? null : signatures.join("\x00");
|
|
15317
15841
|
};
|
|
15318
15842
|
var CACHE_LIMIT = 64;
|
|
15843
|
+
var SIGNAL_TTL_MS = 1500;
|
|
15844
|
+
var signals = new Map;
|
|
15845
|
+
var recentSignal = (root) => {
|
|
15846
|
+
const now = Date.now();
|
|
15847
|
+
const cached = signals.get(root);
|
|
15848
|
+
if (cached && now - cached.at < SIGNAL_TTL_MS)
|
|
15849
|
+
return cached.value;
|
|
15850
|
+
const value = worktreeSignal(root);
|
|
15851
|
+
signals.set(root, { at: now, value });
|
|
15852
|
+
if (signals.size > CACHE_LIMIT)
|
|
15853
|
+
signals.delete(signals.keys().next().value);
|
|
15854
|
+
return value;
|
|
15855
|
+
};
|
|
15319
15856
|
var cache = new Map;
|
|
15320
15857
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15321
15858
|
const listed = store.listTaskIndex();
|
|
@@ -15339,18 +15876,26 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15339
15876
|
capabilities: context.capabilities
|
|
15340
15877
|
});
|
|
15341
15878
|
const hit = cache.get(slot);
|
|
15342
|
-
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files))
|
|
15879
|
+
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files) && (hit.signal === null || hit.signal === recentSignal(store.root)))
|
|
15343
15880
|
return hit.value;
|
|
15344
15881
|
const task = store.readTask(entry.id);
|
|
15345
15882
|
if (!task.ok)
|
|
15346
15883
|
return null;
|
|
15347
|
-
const
|
|
15348
|
-
const observed =
|
|
15349
|
-
const
|
|
15884
|
+
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15885
|
+
const observed = task.data.evidence.some(isObservedCheck);
|
|
15886
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path13.resolve(store.root, ref.path)] : []));
|
|
15887
|
+
const observedFiles = filesKey(files);
|
|
15888
|
+
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15350
15889
|
if (!compact.ok)
|
|
15351
15890
|
return null;
|
|
15352
15891
|
cache.delete(slot);
|
|
15353
|
-
cache.set(slot, {
|
|
15892
|
+
cache.set(slot, {
|
|
15893
|
+
key,
|
|
15894
|
+
files,
|
|
15895
|
+
filesKey: observedFiles,
|
|
15896
|
+
signal: observed ? freshness.current() : null,
|
|
15897
|
+
value: compact.data
|
|
15898
|
+
});
|
|
15354
15899
|
if (cache.size > CACHE_LIMIT)
|
|
15355
15900
|
cache.delete(cache.keys().next().value);
|
|
15356
15901
|
return compact.data;
|
|
@@ -15423,6 +15968,8 @@ var shellPolicy = (cwd, command) => {
|
|
|
15423
15968
|
var usable2 = (value) => value === "native" || value === "partial";
|
|
15424
15969
|
var NONE2 = { kind: "none" };
|
|
15425
15970
|
var offered = new Set;
|
|
15971
|
+
var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
|
|
15972
|
+
var subagentStartText = (host, descriptor, event) => host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER ? `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch (\`git switch -c <type>/<slug>\`, e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never push, open a PR, or merge unless the brief asks for it.` : `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
|
|
15426
15973
|
function handleHook(input, deps) {
|
|
15427
15974
|
const { descriptor } = deps;
|
|
15428
15975
|
const event = input.event;
|
|
@@ -15448,10 +15995,7 @@ function handleHook(input, deps) {
|
|
|
15448
15995
|
case "shell.pre":
|
|
15449
15996
|
return usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE2;
|
|
15450
15997
|
case "subagent.start":
|
|
15451
|
-
return {
|
|
15452
|
-
kind: "context",
|
|
15453
|
-
text: `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`
|
|
15454
|
-
};
|
|
15998
|
+
return { kind: "context", text: subagentStartText(input.host, descriptor, event) };
|
|
15455
15999
|
case "compact.pre":
|
|
15456
16000
|
return {
|
|
15457
16001
|
kind: "notice",
|
|
@@ -15496,12 +16040,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
15496
16040
|
};
|
|
15497
16041
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
15498
16042
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
15499
|
-
import
|
|
15500
|
-
var
|
|
16043
|
+
import path14 from "node:path";
|
|
16044
|
+
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15501
16045
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
15502
16046
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
15503
16047
|
var existingDirectory = (value) => {
|
|
15504
|
-
if (!nonEmpty2(value) || !
|
|
16048
|
+
if (!nonEmpty2(value) || !path14.isAbsolute(value) || !existsSync5(value))
|
|
15505
16049
|
return null;
|
|
15506
16050
|
try {
|
|
15507
16051
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -15638,7 +16182,7 @@ var EVENTS = {
|
|
|
15638
16182
|
};
|
|
15639
16183
|
var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
|
|
15640
16184
|
var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
|
|
15641
|
-
var toolCommand = (value) =>
|
|
16185
|
+
var toolCommand = (value) => isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15642
16186
|
var eventOf = (name, value) => {
|
|
15643
16187
|
const toolUseId = optionalText(value.tool_use_id);
|
|
15644
16188
|
switch (name) {
|
|
@@ -15658,7 +16202,7 @@ var eventOf = (name, value) => {
|
|
|
15658
16202
|
const command = toolCommand(value);
|
|
15659
16203
|
if (!SHELL_TOOLS.has(String(value.tool_name)) || !command)
|
|
15660
16204
|
return { ok: false, error: "only shell PostToolUse is mapped" };
|
|
15661
|
-
const response =
|
|
16205
|
+
const response = isRecord3(value.tool_response) ? value.tool_response : {};
|
|
15662
16206
|
return {
|
|
15663
16207
|
ok: true,
|
|
15664
16208
|
event: {
|
|
@@ -15734,14 +16278,14 @@ var render = (decision, native) => {
|
|
|
15734
16278
|
var claudeCodeAdapter = {
|
|
15735
16279
|
descriptor: CLAUDE_CODE_DESCRIPTOR,
|
|
15736
16280
|
parse(raw) {
|
|
15737
|
-
const native =
|
|
16281
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15738
16282
|
const fail = (error) => ({
|
|
15739
16283
|
ok: false,
|
|
15740
16284
|
error,
|
|
15741
16285
|
native,
|
|
15742
16286
|
event: native ? EVENTS[native] : null
|
|
15743
16287
|
});
|
|
15744
|
-
if (!
|
|
16288
|
+
if (!isRecord3(raw) || !native)
|
|
15745
16289
|
return fail("hook_event_name is required");
|
|
15746
16290
|
if (!nonEmpty2(raw.session_id))
|
|
15747
16291
|
return fail("session_id is required");
|
|
@@ -15891,7 +16435,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
|
|
|
15891
16435
|
var isShellTool = (name) => SHELL_TOOLS2.has(String(name).toLowerCase());
|
|
15892
16436
|
var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
|
|
15893
16437
|
var parseCodexHookInput = (value) => {
|
|
15894
|
-
if (!
|
|
16438
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS2, String(value.hook_event_name)))
|
|
15895
16439
|
return { ok: false, error: "hook_event_name is required" };
|
|
15896
16440
|
const event = value.hook_event_name;
|
|
15897
16441
|
const cwd = existingDirectory(value.cwd);
|
|
@@ -15899,7 +16443,7 @@ var parseCodexHookInput = (value) => {
|
|
|
15899
16443
|
return { ok: false, error: "cwd must be an existing absolute directory" };
|
|
15900
16444
|
if (event === "PreToolUse" && !nonEmpty2(value.tool_name))
|
|
15901
16445
|
return { ok: false, error: "tool_name is required" };
|
|
15902
|
-
const command =
|
|
16446
|
+
const command = isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15903
16447
|
if (event === "PreToolUse" && isShellTool(value.tool_name) && !command)
|
|
15904
16448
|
return { ok: false, error: "tool_input.command is required for shell tools" };
|
|
15905
16449
|
if (event === "SubagentStart" && !nonEmpty2(value.agent_id))
|
|
@@ -15982,7 +16526,7 @@ var codexAdapter = {
|
|
|
15982
16526
|
const host = detectCodexSurface(env);
|
|
15983
16527
|
const parsed = parseCodexHookInput(raw);
|
|
15984
16528
|
if (!parsed.ok) {
|
|
15985
|
-
const native =
|
|
16529
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS2, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15986
16530
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS2[native] : null };
|
|
15987
16531
|
}
|
|
15988
16532
|
const input = parsed.data;
|
|
@@ -16009,7 +16553,7 @@ var codexAdapter = {
|
|
|
16009
16553
|
};
|
|
16010
16554
|
|
|
16011
16555
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16012
|
-
import
|
|
16556
|
+
import path15 from "node:path";
|
|
16013
16557
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16014
16558
|
var none = { support: "none", native: null };
|
|
16015
16559
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16128,7 +16672,7 @@ var EVENTS3 = {
|
|
|
16128
16672
|
preCompact: "compact.pre"
|
|
16129
16673
|
};
|
|
16130
16674
|
var parseCursorHookInput = (value) => {
|
|
16131
|
-
if (!
|
|
16675
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
|
|
16132
16676
|
return { ok: false, error: "hook_event_name is required" };
|
|
16133
16677
|
const roots = value.workspace_roots;
|
|
16134
16678
|
if (!Array.isArray(roots) || roots.length === 0 || !roots.every(nonEmpty2))
|
|
@@ -16192,7 +16736,7 @@ var protocolEvent2 = (input) => {
|
|
|
16192
16736
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16193
16737
|
}
|
|
16194
16738
|
};
|
|
16195
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
16739
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path15.sep) ? root : `${root}${path15.sep}`);
|
|
16196
16740
|
var hookCwd = (input) => {
|
|
16197
16741
|
const cwd = existingDirectory(input.cwd);
|
|
16198
16742
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|
|
@@ -16229,7 +16773,7 @@ var cursorAdapter = {
|
|
|
16229
16773
|
parse(raw) {
|
|
16230
16774
|
const parsed = parseCursorHookInput(raw);
|
|
16231
16775
|
if (!parsed.ok) {
|
|
16232
|
-
const native =
|
|
16776
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS3, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
16233
16777
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS3[native] : null };
|
|
16234
16778
|
}
|
|
16235
16779
|
const input = parsed.data;
|