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