@brainervirus/workit-codex 2.6.0 → 2.7.0

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