@brainervirus/workit-cursor 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cursor-session-start.js +607 -75
- package/dist/mcp-server.js +628 -112
- package/dist/workit-hook.js +615 -71
- package/package.json +3 -3
- package/skills/workit-behavioral-tdd/SKILL.md +15 -9
- package/skills/workit-debug/SKILL.md +2 -1
package/dist/workit-hook.js
CHANGED
|
@@ -7899,7 +7899,37 @@ var candidateSchema = object({
|
|
|
7899
7899
|
path: ["id"]
|
|
7900
7900
|
});
|
|
7901
7901
|
});
|
|
7902
|
-
var
|
|
7902
|
+
var checkObservationSchema = object({
|
|
7903
|
+
observer: literal("workit_cli"),
|
|
7904
|
+
name: nonEmpty.nullable(),
|
|
7905
|
+
configured: boolean2(),
|
|
7906
|
+
argv: array(text).min(1),
|
|
7907
|
+
shell: boolean2(),
|
|
7908
|
+
cwd: nonEmpty,
|
|
7909
|
+
exitCode: safeInteger,
|
|
7910
|
+
durationMs: safeInteger.min(0),
|
|
7911
|
+
timedOut: boolean2(),
|
|
7912
|
+
head: text.nullable(),
|
|
7913
|
+
tree: text.nullable(),
|
|
7914
|
+
dirty: boolean2().nullable(),
|
|
7915
|
+
signal: text.nullable(),
|
|
7916
|
+
treeAfter: text.nullable(),
|
|
7917
|
+
modifiedWorktree: boolean2(),
|
|
7918
|
+
base: text.nullable(),
|
|
7919
|
+
patchId: text.nullable(),
|
|
7920
|
+
environment: object({
|
|
7921
|
+
platform: nonEmpty,
|
|
7922
|
+
arch: nonEmpty,
|
|
7923
|
+
vars: record(nonEmpty, text.nullable())
|
|
7924
|
+
}).strict(),
|
|
7925
|
+
logDigest: text.nullable(),
|
|
7926
|
+
logRef: text.nullable(),
|
|
7927
|
+
logTail: array(text).max(40),
|
|
7928
|
+
ledgerRowId: text.nullable(),
|
|
7929
|
+
attestation: object({ host: nonEmpty, session: text.nullable(), agentId: text.nullable() }).strict().nullable()
|
|
7930
|
+
}).strict();
|
|
7931
|
+
var CHECK_OBSERVATION_PATH = "evidence.*.data.observation";
|
|
7932
|
+
var evidenceFields = {
|
|
7903
7933
|
kind: _enum(["check", "review", "investigation", "artifact"]),
|
|
7904
7934
|
claim: text,
|
|
7905
7935
|
requirementIds: array(digest),
|
|
@@ -7910,7 +7940,9 @@ var evidenceSchema = object({
|
|
|
7910
7940
|
refs: array(refSchema),
|
|
7911
7941
|
exitCode: safeInteger.nullable(),
|
|
7912
7942
|
reviewContext: refSchema.nullable()
|
|
7913
|
-
}
|
|
7943
|
+
};
|
|
7944
|
+
var evidenceSchema = object({ ...evidenceFields, observation: checkObservationSchema.optional() }).strict();
|
|
7945
|
+
var reportedEvidenceSchema = object(evidenceFields).strict();
|
|
7914
7946
|
var evidenceEvaluationSchema = object({
|
|
7915
7947
|
evidenceId: id,
|
|
7916
7948
|
status: _enum(["passed", "failed", "missing", "skipped", "stale"]),
|
|
@@ -8221,7 +8253,7 @@ var evidenceOperations = {
|
|
|
8221
8253
|
action: literal("record"),
|
|
8222
8254
|
...taskId,
|
|
8223
8255
|
expectedRevision: revision.optional(),
|
|
8224
|
-
evidence:
|
|
8256
|
+
evidence: reportedEvidenceSchema
|
|
8225
8257
|
})
|
|
8226
8258
|
};
|
|
8227
8259
|
var findingOperations = {
|
|
@@ -12462,19 +12494,351 @@ function diffPolicy(previous, next, reason, now) {
|
|
|
12462
12494
|
}
|
|
12463
12495
|
|
|
12464
12496
|
// packages/workit-core/src/core/task-evaluation.ts
|
|
12465
|
-
import { createHash as
|
|
12466
|
-
import * as
|
|
12497
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
12498
|
+
import * as fs10 from "node:fs";
|
|
12499
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
12500
|
+
import path12 from "node:path";
|
|
12501
|
+
|
|
12502
|
+
// packages/workit-core/src/check-config.ts
|
|
12467
12503
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
12504
|
+
import fs8 from "node:fs";
|
|
12468
12505
|
import path10 from "node:path";
|
|
12469
|
-
var
|
|
12470
|
-
var
|
|
12471
|
-
var
|
|
12506
|
+
var SCRIPT_CHECKS = ["test", "lint", "typecheck", "check"];
|
|
12507
|
+
var CHECKS_FILE = "workit.checks.json";
|
|
12508
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12509
|
+
var SHELL_META = /[|&;<>()$`\\*?[\]{}~!#]/u;
|
|
12510
|
+
function splitCommand(command) {
|
|
12511
|
+
const argv = [];
|
|
12512
|
+
let current = "";
|
|
12513
|
+
let quote = null;
|
|
12514
|
+
let started = false;
|
|
12515
|
+
for (const char of command) {
|
|
12516
|
+
if (quote) {
|
|
12517
|
+
if (char === quote)
|
|
12518
|
+
quote = null;
|
|
12519
|
+
else
|
|
12520
|
+
current += char;
|
|
12521
|
+
continue;
|
|
12522
|
+
}
|
|
12523
|
+
if (char === '"' || char === "'") {
|
|
12524
|
+
quote = char;
|
|
12525
|
+
started = true;
|
|
12526
|
+
continue;
|
|
12527
|
+
}
|
|
12528
|
+
if (/\s/u.test(char)) {
|
|
12529
|
+
if (started)
|
|
12530
|
+
argv.push(current);
|
|
12531
|
+
current = "";
|
|
12532
|
+
started = false;
|
|
12533
|
+
continue;
|
|
12534
|
+
}
|
|
12535
|
+
if (SHELL_META.test(char))
|
|
12536
|
+
return new Error(`"${command}" uses shell syntax (${char}); configure an argv array instead`);
|
|
12537
|
+
current += char;
|
|
12538
|
+
started = true;
|
|
12539
|
+
}
|
|
12540
|
+
if (quote)
|
|
12541
|
+
return new Error(`"${command}" has an unterminated quote`);
|
|
12542
|
+
if (started)
|
|
12543
|
+
argv.push(current);
|
|
12544
|
+
return argv.length ? argv : new Error("empty command");
|
|
12545
|
+
}
|
|
12546
|
+
var git = (cwd, args) => {
|
|
12547
|
+
const run = spawnSync3("git", args, {
|
|
12548
|
+
cwd,
|
|
12549
|
+
encoding: "utf8",
|
|
12550
|
+
timeout: 20000,
|
|
12551
|
+
killSignal: "SIGKILL",
|
|
12552
|
+
windowsHide: true
|
|
12553
|
+
});
|
|
12554
|
+
const value = run.status === 0 ? (run.stdout ?? "").trim() : "";
|
|
12555
|
+
return value || null;
|
|
12556
|
+
};
|
|
12557
|
+
function checkRoot(cwd) {
|
|
12558
|
+
return path10.resolve(git(cwd, ["rev-parse", "--show-toplevel"]) ?? cwd);
|
|
12559
|
+
}
|
|
12560
|
+
var packageManager = (root) => {
|
|
12561
|
+
const has = (file) => fs8.existsSync(path10.join(root, file));
|
|
12562
|
+
if (has("bun.lock") || has("bun.lockb"))
|
|
12563
|
+
return "bun";
|
|
12564
|
+
if (has("pnpm-lock.yaml"))
|
|
12565
|
+
return "pnpm";
|
|
12566
|
+
if (has("yarn.lock"))
|
|
12567
|
+
return "yarn";
|
|
12568
|
+
return "npm";
|
|
12569
|
+
};
|
|
12570
|
+
var scriptCheck = (pm, name) => {
|
|
12571
|
+
const argv = [pm, "run", name];
|
|
12572
|
+
const accepts = [argv];
|
|
12573
|
+
if (pm === "pnpm" || pm === "yarn" || pm === "npm" && name === "test")
|
|
12574
|
+
accepts.push([pm, name]);
|
|
12575
|
+
return { name, argv, accepts };
|
|
12576
|
+
};
|
|
12577
|
+
var CHECK_NAME = /^[a-z0-9][a-z0-9:._-]{0,63}$/u;
|
|
12578
|
+
function detectedChecks(root) {
|
|
12579
|
+
const read = (file) => {
|
|
12580
|
+
try {
|
|
12581
|
+
return fs8.readFileSync(path10.join(root, file), "utf8");
|
|
12582
|
+
} catch {
|
|
12583
|
+
return null;
|
|
12584
|
+
}
|
|
12585
|
+
};
|
|
12586
|
+
const checks = [];
|
|
12587
|
+
const add = (check) => {
|
|
12588
|
+
if (!checks.some((item) => item.name === check.name))
|
|
12589
|
+
checks.push(check);
|
|
12590
|
+
};
|
|
12591
|
+
const manifestText = read("package.json");
|
|
12592
|
+
if (manifestText !== null) {
|
|
12593
|
+
let manifest = null;
|
|
12594
|
+
try {
|
|
12595
|
+
manifest = JSON.parse(manifestText);
|
|
12596
|
+
} catch {}
|
|
12597
|
+
const scripts = isRecord2(manifest) && isRecord2(manifest.scripts) ? manifest.scripts : {};
|
|
12598
|
+
const pm = packageManager(root);
|
|
12599
|
+
for (const name of SCRIPT_CHECKS) {
|
|
12600
|
+
const script = scripts[name];
|
|
12601
|
+
if (typeof script === "string" && script.trim() !== "")
|
|
12602
|
+
add(scriptCheck(pm, name));
|
|
12603
|
+
}
|
|
12604
|
+
}
|
|
12605
|
+
const plain = (name, argv) => ({ name, argv, accepts: [argv] });
|
|
12606
|
+
if (read("go.mod") !== null)
|
|
12607
|
+
add(plain("test", ["go", "test", "./..."]));
|
|
12608
|
+
if (read("Cargo.toml") !== null)
|
|
12609
|
+
add(plain("test", ["cargo", "test"]));
|
|
12610
|
+
if (read("pytest.ini") !== null || /\bpytest\b/u.test(read("pyproject.toml") ?? ""))
|
|
12611
|
+
add(plain("test", ["pytest"]));
|
|
12612
|
+
const makefile = read("Makefile") ?? read("makefile") ?? read("GNUmakefile");
|
|
12613
|
+
if (makefile !== null && /^test\s*:(?!=)/mu.test(makefile))
|
|
12614
|
+
add(plain("test", ["make", "test"]));
|
|
12615
|
+
return checks;
|
|
12616
|
+
}
|
|
12617
|
+
var GATES = ["testing", "verification"];
|
|
12618
|
+
function loadCheckConfig(cwd) {
|
|
12619
|
+
const root = checkRoot(cwd);
|
|
12620
|
+
const file = path10.join(root, CHECKS_FILE);
|
|
12621
|
+
const none = (error = null) => ({
|
|
12622
|
+
source: error ? CHECKS_FILE : "none",
|
|
12623
|
+
root,
|
|
12624
|
+
checks: [],
|
|
12625
|
+
gates: {},
|
|
12626
|
+
error
|
|
12627
|
+
});
|
|
12628
|
+
let text = null;
|
|
12629
|
+
try {
|
|
12630
|
+
text = fs8.readFileSync(file, "utf8");
|
|
12631
|
+
} catch (error) {
|
|
12632
|
+
if (error.code !== "ENOENT")
|
|
12633
|
+
return none(`cannot read ${CHECKS_FILE}: ${error.message}`);
|
|
12634
|
+
}
|
|
12635
|
+
if (text === null) {
|
|
12636
|
+
const checks = detectedChecks(root);
|
|
12637
|
+
return checks.length ? { source: "defaults", root, checks, gates: {}, error: null } : none();
|
|
12638
|
+
}
|
|
12639
|
+
let parsed;
|
|
12640
|
+
try {
|
|
12641
|
+
parsed = JSON.parse(text);
|
|
12642
|
+
} catch {
|
|
12643
|
+
return none(`${CHECKS_FILE} is not valid JSON`);
|
|
12644
|
+
}
|
|
12645
|
+
if (!isRecord2(parsed) || !isRecord2(parsed.checks))
|
|
12646
|
+
return none(`${CHECKS_FILE} must be {"checks":{"<name>":"<command>"|["argv",…]}}`);
|
|
12647
|
+
const checks = [];
|
|
12648
|
+
for (const [name, value] of Object.entries(parsed.checks)) {
|
|
12649
|
+
if (!CHECK_NAME.test(name))
|
|
12650
|
+
return none(`${CHECKS_FILE}: check name "${name}" must match ${CHECK_NAME.source}`);
|
|
12651
|
+
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");
|
|
12652
|
+
if (argv instanceof Error)
|
|
12653
|
+
return none(`${CHECKS_FILE}: check "${name}": ${argv.message}`);
|
|
12654
|
+
checks.push({ name, argv, accepts: [argv] });
|
|
12655
|
+
}
|
|
12656
|
+
const gates = {};
|
|
12657
|
+
if (parsed.gates !== undefined) {
|
|
12658
|
+
if (!isRecord2(parsed.gates))
|
|
12659
|
+
return none(`${CHECKS_FILE}: gates must be {"testing":"<name>"|["<name>",…]}`);
|
|
12660
|
+
for (const [gate, value] of Object.entries(parsed.gates)) {
|
|
12661
|
+
if (!GATES.includes(gate))
|
|
12662
|
+
return none(`${CHECKS_FILE}: unknown gate "${gate}" (use ${GATES.join(" or ")})`);
|
|
12663
|
+
const names = typeof value === "string" ? [value] : value;
|
|
12664
|
+
if (!Array.isArray(names) || names.length === 0 || !names.every((name) => typeof name === "string" && checks.some((c) => c.name === name)))
|
|
12665
|
+
return none(`${CHECKS_FILE}: gates.${gate} must name configured checks`);
|
|
12666
|
+
gates[gate] = names;
|
|
12667
|
+
}
|
|
12668
|
+
}
|
|
12669
|
+
return { source: CHECKS_FILE, root, checks, gates, error: null };
|
|
12670
|
+
}
|
|
12671
|
+
var sameArgv = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
|
|
12672
|
+
function matchesNamedCheck(config, name, argv, cwd) {
|
|
12673
|
+
if (config.error || name === null || cwd !== ".")
|
|
12674
|
+
return false;
|
|
12675
|
+
const check = config.checks.find((item) => item.name === name);
|
|
12676
|
+
return Boolean(check?.accepts.some((accepted) => sameArgv(accepted, argv)));
|
|
12677
|
+
}
|
|
12678
|
+
function gateCheckNames(config, dimension) {
|
|
12679
|
+
const explicit = config.gates[dimension];
|
|
12680
|
+
if (explicit)
|
|
12681
|
+
return explicit;
|
|
12682
|
+
const names = config.checks.map((check) => check.name);
|
|
12683
|
+
if (dimension === "testing")
|
|
12684
|
+
return names.includes("test") ? ["test"] : [];
|
|
12685
|
+
return names;
|
|
12686
|
+
}
|
|
12687
|
+
var workitOnPath;
|
|
12688
|
+
function workitAvailable(env = process.env) {
|
|
12689
|
+
if (workitOnPath !== undefined)
|
|
12690
|
+
return workitOnPath;
|
|
12691
|
+
const windows = process.platform === "win32";
|
|
12692
|
+
const exts = windows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
12693
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path10.delimiter).filter(Boolean);
|
|
12694
|
+
workitOnPath = dirs.some((dir) => exts.some((ext) => {
|
|
12695
|
+
try {
|
|
12696
|
+
return fs8.statSync(path10.join(dir, `workit${ext}`)).isFile();
|
|
12697
|
+
} catch {
|
|
12698
|
+
return false;
|
|
12699
|
+
}
|
|
12700
|
+
}));
|
|
12701
|
+
return workitOnPath;
|
|
12702
|
+
}
|
|
12703
|
+
function checkCommand(args, version, onPath = workitAvailable()) {
|
|
12704
|
+
return onPath ? `workit check ${args}` : `npx -y @brainervirus/workit-cli@${version} check ${args}`;
|
|
12705
|
+
}
|
|
12706
|
+
|
|
12707
|
+
// packages/workit-core/src/git/rev.ts
|
|
12708
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
12709
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
12710
|
+
import fs9 from "node:fs";
|
|
12711
|
+
import os2 from "node:os";
|
|
12712
|
+
import path11 from "node:path";
|
|
12713
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
12714
|
+
var GIT_TIMEOUTS = { local: 20000, worktree: 120000, network: 15000 };
|
|
12715
|
+
var git2 = (cwd, args, options = {}) => {
|
|
12716
|
+
const result = spawnSync4("git", args, {
|
|
12717
|
+
cwd,
|
|
12718
|
+
encoding: "utf8",
|
|
12719
|
+
env: options.env ?? process.env,
|
|
12720
|
+
input: options.input,
|
|
12721
|
+
maxBuffer: MAX_BUFFER,
|
|
12722
|
+
timeout: options.timeoutMs ?? GIT_TIMEOUTS.local,
|
|
12723
|
+
killSignal: "SIGKILL",
|
|
12724
|
+
windowsHide: true
|
|
12725
|
+
});
|
|
12726
|
+
const error = result.error;
|
|
12727
|
+
return {
|
|
12728
|
+
ok: result.status === 0,
|
|
12729
|
+
stdout: result.stdout ?? "",
|
|
12730
|
+
stderr: result.stderr ?? "",
|
|
12731
|
+
timedOut: error?.code === "ETIMEDOUT"
|
|
12732
|
+
};
|
|
12733
|
+
};
|
|
12734
|
+
var line = (run) => {
|
|
12735
|
+
if (!run.ok)
|
|
12736
|
+
return null;
|
|
12737
|
+
const value = run.stdout.trim();
|
|
12738
|
+
return value ? value : null;
|
|
12739
|
+
};
|
|
12740
|
+
function headSha(cwd) {
|
|
12741
|
+
return line(git2(cwd, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]));
|
|
12742
|
+
}
|
|
12743
|
+
var emptyTree = (cwd) => line(git2(cwd, ["mktree"], { input: "" }));
|
|
12744
|
+
var MAX_UNTRACKED_BYTES = 5 * 1024 * 1024;
|
|
12745
|
+
var seedIndex = (from, to) => {
|
|
12746
|
+
fs9.copyFileSync(from, to);
|
|
12747
|
+
const { atimeNs, mtimeNs } = fs9.statSync(from, { bigint: true });
|
|
12748
|
+
const seconds = (ns) => (Number(ns / 1000n) - 1) / 1e6;
|
|
12749
|
+
fs9.utimesSync(to, seconds(atimeNs), seconds(mtimeNs));
|
|
12750
|
+
};
|
|
12751
|
+
function worktreeTree(cwd, options = {}) {
|
|
12752
|
+
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
12753
|
+
const indexPath = line(git2(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"]));
|
|
12754
|
+
if (!top || !indexPath)
|
|
12755
|
+
return null;
|
|
12756
|
+
const cap = options.maxUntrackedBytes ?? MAX_UNTRACKED_BYTES;
|
|
12757
|
+
const timeoutMs = options.timeoutMs ?? GIT_TIMEOUTS.worktree;
|
|
12758
|
+
const scratch = fs9.mkdtempSync(path11.join(os2.tmpdir(), "workit-tree-"));
|
|
12759
|
+
try {
|
|
12760
|
+
const tempIndex = path11.join(scratch, "index");
|
|
12761
|
+
if (fs9.existsSync(indexPath))
|
|
12762
|
+
seedIndex(indexPath, tempIndex);
|
|
12763
|
+
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
12764
|
+
const untracked = git2(top, ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
12765
|
+
env,
|
|
12766
|
+
timeoutMs
|
|
12767
|
+
});
|
|
12768
|
+
if (!untracked.ok)
|
|
12769
|
+
return null;
|
|
12770
|
+
const skipped = [];
|
|
12771
|
+
for (const file of untracked.stdout.split("\x00").filter(Boolean)) {
|
|
12772
|
+
try {
|
|
12773
|
+
const stat = fs9.statSync(path11.join(top, file));
|
|
12774
|
+
if (stat.isFile() && stat.size > cap)
|
|
12775
|
+
skipped.push({ path: file, size: stat.size, mtimeMs: Math.trunc(stat.mtimeMs) });
|
|
12776
|
+
} catch {}
|
|
12777
|
+
}
|
|
12778
|
+
skipped.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
12779
|
+
const added = skipped.length ? git2(top, ["add", "-A", "--pathspec-from-file=-", "--pathspec-file-nul"], {
|
|
12780
|
+
env,
|
|
12781
|
+
timeoutMs,
|
|
12782
|
+
input: [".", ...skipped.map((file) => `:(exclude,literal)${file.path}`)].join("\x00")
|
|
12783
|
+
}) : git2(top, ["add", "-A"], { env, timeoutMs });
|
|
12784
|
+
if (!added.ok)
|
|
12785
|
+
return null;
|
|
12786
|
+
const tree = line(git2(top, ["write-tree"], { env, timeoutMs }));
|
|
12787
|
+
if (!tree)
|
|
12788
|
+
return null;
|
|
12789
|
+
const base = line(git2(top, ["rev-parse", "--verify", "-q", "HEAD^{tree}"])) ?? emptyTree(top);
|
|
12790
|
+
const key = skipped.length ? `sha256:${createHash7("sha256").update(JSON.stringify({ tree, skipped })).digest("hex")}` : tree;
|
|
12791
|
+
return { tree, key, dirty: tree !== base || skipped.length > 0, skipped };
|
|
12792
|
+
} finally {
|
|
12793
|
+
fs9.rmSync(scratch, { recursive: true, force: true });
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12796
|
+
function worktreeSignal(cwd, options = {}) {
|
|
12797
|
+
const top = line(git2(cwd, ["rev-parse", "--show-toplevel"]));
|
|
12798
|
+
if (!top)
|
|
12799
|
+
return null;
|
|
12800
|
+
const status = git2(top, [
|
|
12801
|
+
"status",
|
|
12802
|
+
"--porcelain=v1",
|
|
12803
|
+
"-z",
|
|
12804
|
+
"--untracked-files=all",
|
|
12805
|
+
"--no-renames",
|
|
12806
|
+
"--ignore-submodules=dirty"
|
|
12807
|
+
], {
|
|
12808
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
12809
|
+
timeoutMs: options.timeoutMs ?? GIT_TIMEOUTS.worktree
|
|
12810
|
+
});
|
|
12811
|
+
if (!status.ok)
|
|
12812
|
+
return null;
|
|
12813
|
+
const hash = createHash7("sha256");
|
|
12814
|
+
hash.update(`${headSha(top) ?? "unborn"}\x00`);
|
|
12815
|
+
for (const entry of status.stdout.split("\x00")) {
|
|
12816
|
+
if (entry.length < 4)
|
|
12817
|
+
continue;
|
|
12818
|
+
const file = entry.slice(3);
|
|
12819
|
+
const absolute = path11.join(top, file);
|
|
12820
|
+
let stat = "-";
|
|
12821
|
+
try {
|
|
12822
|
+
const info = fs9.lstatSync(absolute, { bigint: true });
|
|
12823
|
+
stat = `${info.mode.toString(8)}:${info.size}:${info.mtimeNs}`;
|
|
12824
|
+
if (info.isDirectory())
|
|
12825
|
+
stat += `:${headSha(absolute) ?? "-"}`;
|
|
12826
|
+
} catch {}
|
|
12827
|
+
hash.update(`${entry.slice(0, 2)}\x00${file}\x00${stat}\x00`);
|
|
12828
|
+
}
|
|
12829
|
+
return `sig:${hash.digest("hex")}`;
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
// packages/workit-core/src/core/task-evaluation.ts
|
|
12833
|
+
var digestBytes3 = (value) => createHash8("sha256").update(value).digest("hex");
|
|
12834
|
+
var inside = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path12.sep}`);
|
|
12835
|
+
var relative = (root, candidate) => path12.relative(root, candidate).split(path12.sep).join("/") || ".";
|
|
12472
12836
|
var canonicalPath = (value) => {
|
|
12473
12837
|
if (typeof value !== "string" || !value)
|
|
12474
12838
|
return null;
|
|
12475
12839
|
if (value.includes("\\") || value.split("/").includes(".."))
|
|
12476
12840
|
return null;
|
|
12477
|
-
const normalized =
|
|
12841
|
+
const normalized = path12.posix.normalize(value);
|
|
12478
12842
|
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/"))
|
|
12479
12843
|
return null;
|
|
12480
12844
|
return normalized === "." ? "." : normalized.replace(/\/$/, "");
|
|
@@ -12510,7 +12874,7 @@ var compareCodeUnits3 = (left, right) => {
|
|
|
12510
12874
|
return left.length - right.length;
|
|
12511
12875
|
};
|
|
12512
12876
|
var gitPaths = (root) => {
|
|
12513
|
-
const result =
|
|
12877
|
+
const result = spawnSync5("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
12514
12878
|
cwd: root,
|
|
12515
12879
|
encoding: "buffer"
|
|
12516
12880
|
});
|
|
@@ -12522,21 +12886,21 @@ var gitPaths = (root) => {
|
|
|
12522
12886
|
git: !/not a git repository/i.test(stderr)
|
|
12523
12887
|
};
|
|
12524
12888
|
}
|
|
12525
|
-
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
12526
|
-
const stagedDeleted =
|
|
12889
|
+
const paths = result.stdout ? result.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")) : [];
|
|
12890
|
+
const stagedDeleted = spawnSync5("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], { cwd: root, encoding: "buffer" });
|
|
12527
12891
|
if (stagedDeleted.status !== 0) {
|
|
12528
12892
|
const stderr = stagedDeleted.stderr?.toString("utf8") ?? "";
|
|
12529
12893
|
if (!/not a git repository/i.test(stderr))
|
|
12530
12894
|
return { paths, uncertain: true, git: true };
|
|
12531
12895
|
} else if (stagedDeleted.stdout) {
|
|
12532
|
-
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(
|
|
12896
|
+
paths.push(...stagedDeleted.stdout.toString("utf8").split("\x00").filter(Boolean).map((item) => item.split(path12.sep).join("/")));
|
|
12533
12897
|
}
|
|
12534
12898
|
return { paths: [...new Set(paths)], uncertain: false, git: true };
|
|
12535
12899
|
};
|
|
12536
12900
|
var walk = (root, directory, output) => {
|
|
12537
12901
|
let names;
|
|
12538
12902
|
try {
|
|
12539
|
-
names =
|
|
12903
|
+
names = fs10.readdirSync(directory);
|
|
12540
12904
|
} catch {
|
|
12541
12905
|
return true;
|
|
12542
12906
|
}
|
|
@@ -12544,10 +12908,10 @@ var walk = (root, directory, output) => {
|
|
|
12544
12908
|
for (const name of names) {
|
|
12545
12909
|
if (name === ".git" || name === ".workit")
|
|
12546
12910
|
continue;
|
|
12547
|
-
const target =
|
|
12911
|
+
const target = path12.join(directory, name);
|
|
12548
12912
|
let stat;
|
|
12549
12913
|
try {
|
|
12550
|
-
stat =
|
|
12914
|
+
stat = fs10.lstatSync(target);
|
|
12551
12915
|
} catch {
|
|
12552
12916
|
uncertain = true;
|
|
12553
12917
|
continue;
|
|
@@ -12562,14 +12926,14 @@ var walk = (root, directory, output) => {
|
|
|
12562
12926
|
var scopeRoots = (root, scope) => {
|
|
12563
12927
|
const roots = [];
|
|
12564
12928
|
for (const item of scope.paths.length ? scope.paths : ["."]) {
|
|
12565
|
-
const target =
|
|
12929
|
+
const target = path12.resolve(root, item);
|
|
12566
12930
|
if (!inside(root, target))
|
|
12567
12931
|
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` }] });
|
|
12568
12932
|
let ancestor = target;
|
|
12569
|
-
while (!
|
|
12570
|
-
ancestor =
|
|
12933
|
+
while (!fs10.existsSync(ancestor) && ancestor !== root)
|
|
12934
|
+
ancestor = path12.dirname(ancestor);
|
|
12571
12935
|
try {
|
|
12572
|
-
if (!inside(root,
|
|
12936
|
+
if (!inside(root, fs10.realpathSync(ancestor)))
|
|
12573
12937
|
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` }] });
|
|
12574
12938
|
} catch {
|
|
12575
12939
|
return failure2("invalid_input", `candidate scope cannot be inspected: ${item}`);
|
|
@@ -12581,7 +12945,7 @@ var scopeRoots = (root, scope) => {
|
|
|
12581
12945
|
function captureCandidate(root, scope, environment = []) {
|
|
12582
12946
|
let checkout;
|
|
12583
12947
|
try {
|
|
12584
|
-
checkout =
|
|
12948
|
+
checkout = fs10.realpathSync(root);
|
|
12585
12949
|
} catch {
|
|
12586
12950
|
return failure2("invalid_input", "candidate root does not exist");
|
|
12587
12951
|
}
|
|
@@ -12599,11 +12963,11 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12599
12963
|
if (item === ".git" || item === ".workit")
|
|
12600
12964
|
continue;
|
|
12601
12965
|
names.add(item);
|
|
12602
|
-
if (!
|
|
12966
|
+
if (!fs10.existsSync(target))
|
|
12603
12967
|
continue;
|
|
12604
12968
|
let stat;
|
|
12605
12969
|
try {
|
|
12606
|
-
stat =
|
|
12970
|
+
stat = fs10.lstatSync(target);
|
|
12607
12971
|
} catch {
|
|
12608
12972
|
uncertain = true;
|
|
12609
12973
|
continue;
|
|
@@ -12619,17 +12983,17 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12619
12983
|
for (const item of [...names].toSorted()) {
|
|
12620
12984
|
if (!scopeMatches(item, normalizedScope))
|
|
12621
12985
|
continue;
|
|
12622
|
-
const target =
|
|
12986
|
+
const target = path12.join(checkout, item);
|
|
12623
12987
|
try {
|
|
12624
|
-
const stat =
|
|
12988
|
+
const stat = fs10.lstatSync(target);
|
|
12625
12989
|
if (stat.isSymbolicLink()) {
|
|
12626
|
-
const link =
|
|
12990
|
+
const link = fs10.readlinkSync(target);
|
|
12627
12991
|
files.push({ path: item, kind: "symlink", digest: digestBytes3(link), executable: null });
|
|
12628
12992
|
} else if (stat.isFile()) {
|
|
12629
12993
|
files.push({
|
|
12630
12994
|
path: item,
|
|
12631
12995
|
kind: "file",
|
|
12632
|
-
digest: digestBytes3(
|
|
12996
|
+
digest: digestBytes3(fs10.readFileSync(target)),
|
|
12633
12997
|
executable: (stat.mode & 73) !== 0
|
|
12634
12998
|
});
|
|
12635
12999
|
} else if (!stat.isDirectory()) {
|
|
@@ -12654,7 +13018,7 @@ function captureCandidate(root, scope, environment = []) {
|
|
|
12654
13018
|
const values = supplied.toSorted((left, right) => compareCodeUnits3(left.name, right.name)).map(({ name, value }) => ({ name, value, refs: [] }));
|
|
12655
13019
|
if (values.some(({ value }) => value === null))
|
|
12656
13020
|
completeness = "uncertain";
|
|
12657
|
-
const headResult =
|
|
13021
|
+
const headResult = spawnSync5("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
|
|
12658
13022
|
if (headResult.status !== 0 && !/not a git repository/i.test(headResult.stderr ?? ""))
|
|
12659
13023
|
completeness = "uncertain";
|
|
12660
13024
|
const head = headResult.status === 0 ? headResult.stdout.trim() : null;
|
|
@@ -12692,7 +13056,24 @@ var sameSession = (left, right) => {
|
|
|
12692
13056
|
return left !== null && other !== null && left.host === other.host && left.handle === other.handle;
|
|
12693
13057
|
};
|
|
12694
13058
|
var evidenceScopes = (task, ids) => (task.policy?.requirements ?? []).filter((requirement) => ids.includes(requirement.id)).map((requirement) => requirement.scope);
|
|
12695
|
-
|
|
13059
|
+
var memo2 = (compute) => {
|
|
13060
|
+
let value;
|
|
13061
|
+
return () => {
|
|
13062
|
+
if (value === undefined)
|
|
13063
|
+
value = compute();
|
|
13064
|
+
return value;
|
|
13065
|
+
};
|
|
13066
|
+
};
|
|
13067
|
+
var treeFreshness = (root) => ({
|
|
13068
|
+
mode: "tree",
|
|
13069
|
+
current: memo2(() => worktreeTree(root)?.key ?? null)
|
|
13070
|
+
});
|
|
13071
|
+
var signalFreshness = (root) => ({
|
|
13072
|
+
mode: "signal",
|
|
13073
|
+
current: memo2(() => worktreeSignal(root))
|
|
13074
|
+
});
|
|
13075
|
+
var isObservedCheck = (entry) => entry.data.kind === "check" && entry.data.observation?.observer === "workit_cli" && entry.provenance?.kind === "host_observed" && entry.provenance.host === "workit_cli";
|
|
13076
|
+
function evaluateEvidence(task, candidate, freshness) {
|
|
12696
13077
|
const current = candidate ?? task.candidates.at(-1) ?? null;
|
|
12697
13078
|
return task.evidence.map((entry) => {
|
|
12698
13079
|
const evidence = entry.data;
|
|
@@ -12702,6 +13083,25 @@ function evaluateEvidence(task, candidate) {
|
|
|
12702
13083
|
status: "failed",
|
|
12703
13084
|
reason: "failed evidence remains historical"
|
|
12704
13085
|
};
|
|
13086
|
+
const observation = isObservedCheck(entry) ? evidence.observation : undefined;
|
|
13087
|
+
if (observation && observation.tree !== null) {
|
|
13088
|
+
if (task.status === "closed")
|
|
13089
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "recorded before close" };
|
|
13090
|
+
if (observation.modifiedWorktree)
|
|
13091
|
+
return {
|
|
13092
|
+
evidenceId: entry.id,
|
|
13093
|
+
status: "stale",
|
|
13094
|
+
reason: "the check changed the worktree while it ran; ignore or clean its outputs"
|
|
13095
|
+
};
|
|
13096
|
+
const recorded = freshness?.mode === "signal" ? observation.signal : observation.tree;
|
|
13097
|
+
if (recorded !== null && freshness && recorded === freshness.current())
|
|
13098
|
+
return { evidenceId: entry.id, status: evidence.result, reason: "worktree is unchanged" };
|
|
13099
|
+
return {
|
|
13100
|
+
evidenceId: entry.id,
|
|
13101
|
+
status: "stale",
|
|
13102
|
+
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"
|
|
13103
|
+
};
|
|
13104
|
+
}
|
|
12705
13105
|
if (evidence.kind === "check" || evidence.kind === "review") {
|
|
12706
13106
|
if (!evidence.beforeCandidateId || !evidence.candidateId)
|
|
12707
13107
|
return {
|
|
@@ -12785,8 +13185,45 @@ var expectedKinds = (dimension, ruleId) => {
|
|
|
12785
13185
|
return "decision";
|
|
12786
13186
|
return "worker report";
|
|
12787
13187
|
};
|
|
12788
|
-
|
|
12789
|
-
|
|
13188
|
+
var observedGate = (requirement) => (requirement.dimension === "testing" || requirement.dimension === "verification") && requirement.before === "close";
|
|
13189
|
+
function evaluateObservedGate(task, requirement, statuses, config) {
|
|
13190
|
+
const names = gateCheckNames(config, requirement.dimension);
|
|
13191
|
+
const observed = task.evidence.filter(isObservedCheck);
|
|
13192
|
+
const fresh = observed.filter((entry) => statuses.get(entry.id) === "passed" && entry.data.observation?.exitCode === 0);
|
|
13193
|
+
const named = (entry) => {
|
|
13194
|
+
const observation = entry.data.observation;
|
|
13195
|
+
return observation.configured && !observation.shell && names.includes(observation.name ?? "") && matchesNamedCheck(config, observation.name, observation.argv, observation.cwd);
|
|
13196
|
+
};
|
|
13197
|
+
const considered = observed.map((entry) => entry.id);
|
|
13198
|
+
const run = checkCommand(names[0] ?? "<name>", runtimeVersion());
|
|
13199
|
+
if (config.error)
|
|
13200
|
+
return {
|
|
13201
|
+
satisfied: [],
|
|
13202
|
+
considered,
|
|
13203
|
+
reason: `check config is invalid (${config.error}); fix it, then run ${run}`
|
|
13204
|
+
};
|
|
13205
|
+
if (!names.length)
|
|
13206
|
+
return {
|
|
13207
|
+
satisfied: [],
|
|
13208
|
+
considered,
|
|
13209
|
+
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())}`
|
|
13210
|
+
};
|
|
13211
|
+
const satisfied = fresh.filter(named);
|
|
13212
|
+
if (satisfied.length)
|
|
13213
|
+
return {
|
|
13214
|
+
satisfied: satisfied.map((entry) => entry.id),
|
|
13215
|
+
considered,
|
|
13216
|
+
reason: `a fresh passing configured check (${names.join("|")}) was observed by workit check`
|
|
13217
|
+
};
|
|
13218
|
+
const reported = task.evidence.some((entry) => entry.data.kind === "check" && !isObservedCheck(entry) && entry.data.requirementIds.includes(requirement.id));
|
|
13219
|
+
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}`;
|
|
13220
|
+
return { satisfied: [], considered, reason };
|
|
13221
|
+
}
|
|
13222
|
+
function evaluateRequirements(task, workspace, capabilities, candidate, checkoutRoot, _caller, freshness = treeFreshness(checkoutRoot)) {
|
|
13223
|
+
const evidence = evaluateEvidence(task, candidate, freshness);
|
|
13224
|
+
const statuses = new Map(evidence.map((item) => [item.evidenceId, item.status]));
|
|
13225
|
+
let checkConfig;
|
|
13226
|
+
const configFor = () => checkConfig ??= loadCheckConfig(checkoutRoot);
|
|
12790
13227
|
if (task.policy && task.policy.policyVersion !== POLICY_VERSION)
|
|
12791
13228
|
return task.policy.requirements.map((requirement) => ({
|
|
12792
13229
|
requirementId: requirement.id,
|
|
@@ -12796,8 +13233,19 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
12796
13233
|
reason: "stored policy version is unsupported; reassessment is required"
|
|
12797
13234
|
}));
|
|
12798
13235
|
return (task.policy?.requirements ?? []).map((requirement) => {
|
|
13236
|
+
const gate = observedGate(requirement) ? evaluateObservedGate(task, requirement, statuses, configFor()) : null;
|
|
13237
|
+
if (gate?.satisfied.length)
|
|
13238
|
+
return {
|
|
13239
|
+
requirementId: requirement.id,
|
|
13240
|
+
status: "satisfied",
|
|
13241
|
+
evidenceIds: gate.satisfied,
|
|
13242
|
+
decisionIds: [],
|
|
13243
|
+
reason: gate.reason
|
|
13244
|
+
};
|
|
12799
13245
|
const related = task.evidence.map((entry, index) => ({ entry, evaluation: evidence[index] })).filter(({ entry }) => entry.data.requirementIds.includes(requirement.id));
|
|
12800
13246
|
const passed = related.filter(({ entry, evaluation }) => {
|
|
13247
|
+
if (gate)
|
|
13248
|
+
return false;
|
|
12801
13249
|
if (!evidenceMatchesRequirement(evaluation.status, entry.data.kind, requirement.dimension, requirement.ruleId))
|
|
12802
13250
|
return false;
|
|
12803
13251
|
if ((entry.data.kind === "check" || entry.data.kind === "review") && (!entry.data.beforeCandidateId || !entry.data.candidateId || entry.data.beforeCandidateId !== entry.data.candidateId))
|
|
@@ -12860,6 +13308,14 @@ function evaluateRequirements(task, workspace, capabilities, candidate, checkout
|
|
|
12860
13308
|
reason: "an applicable approved limitation permits the missing evidence"
|
|
12861
13309
|
};
|
|
12862
13310
|
const unavailable = capabilities.some((capability) => capability.assurance === "unavailable" && (capability.name === requirement.dimension || capability.surface === requirement.dimension));
|
|
13311
|
+
if (gate)
|
|
13312
|
+
return {
|
|
13313
|
+
requirementId: requirement.id,
|
|
13314
|
+
status: unavailable ? "unavailable" : "unsatisfied",
|
|
13315
|
+
evidenceIds: [...new Set([...gate.considered, ...related.map(({ entry }) => entry.id)])],
|
|
13316
|
+
decisionIds: [],
|
|
13317
|
+
reason: unavailable ? "required capability is unavailable" : gate.reason
|
|
13318
|
+
};
|
|
12863
13319
|
if (!unavailable && related.length > 0 && !passed.length)
|
|
12864
13320
|
return {
|
|
12865
13321
|
requirementId: requirement.id,
|
|
@@ -13036,8 +13492,11 @@ policy, and reapplies the call, returning busy under persistent contention. A
|
|
|
13036
13492
|
revision_conflict means a revision you passed is stale: re-read the record
|
|
13037
13493
|
before deciding whether to retry.
|
|
13038
13494
|
A solo edit does not need writer acquisition; use it when concurrent checkout
|
|
13039
|
-
writers need coordination. Record only observed facts and checks.
|
|
13040
|
-
|
|
13495
|
+
writers need coordination. Record only observed facts and checks. Close-time
|
|
13496
|
+
testing and verification gates accept only a configured check the CLI ran:
|
|
13497
|
+
\`workit check <name>\` (\`npx -y @brainervirus/workit-cli check <name>\` when
|
|
13498
|
+
\`workit\` is not on PATH); a recorded check result is a note and an ad-hoc
|
|
13499
|
+
\`workit check -- <cmd>\` never satisfies a gate. Evidence can become stale when its bound candidate or tree changes; reconcile findings against the
|
|
13041
13500
|
current candidate before recording completion.
|
|
13042
13501
|
|
|
13043
13502
|
Workit validates domain policy against the actual action target, configured
|
|
@@ -13098,7 +13557,7 @@ function reconcileResume(view, observations = []) {
|
|
|
13098
13557
|
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
13099
13558
|
if (!candidate.ok)
|
|
13100
13559
|
return candidate;
|
|
13101
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13560
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data, treeFreshness(view.workspace.root)).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13102
13561
|
const blockers = [...view.task.progress.blockers];
|
|
13103
13562
|
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13104
13563
|
blockers.push({
|
|
@@ -13996,10 +14455,7 @@ class WorkitCore {
|
|
|
13996
14455
|
if (finding.data.disposition !== "fixed")
|
|
13997
14456
|
return finding;
|
|
13998
14457
|
const withEntry = { ...current, candidates, evidence: [...current.evidence, entry] };
|
|
13999
|
-
const statuses = new Map(evaluateEvidence(withEntry, currentCandidate.data).map((item) => [
|
|
14000
|
-
item.evidenceId,
|
|
14001
|
-
item.status
|
|
14002
|
-
]));
|
|
14458
|
+
const statuses = new Map(evaluateEvidence(withEntry, currentCandidate.data, treeFreshness(this.store.root)).map((item) => [item.evidenceId, item.status]));
|
|
14003
14459
|
const verified = withEntry.evidence.some((item) => findingVerificationPasses(finding.data.candidateId, {
|
|
14004
14460
|
kind: item.data.kind,
|
|
14005
14461
|
candidateId: item.data.candidateId,
|
|
@@ -14014,6 +14470,71 @@ class WorkitCore {
|
|
|
14014
14470
|
return changed;
|
|
14015
14471
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14016
14472
|
}
|
|
14473
|
+
observeCheck(request) {
|
|
14474
|
+
return this.retryOmittedRevisions(request, () => this.observeCheckOnce(request));
|
|
14475
|
+
}
|
|
14476
|
+
observeCheckOnce(request) {
|
|
14477
|
+
const root = this.contextRootError();
|
|
14478
|
+
if (!root.ok)
|
|
14479
|
+
return root;
|
|
14480
|
+
if (this.context.caller.host !== "workit_cli" || (this.context.workerId ?? null) !== null)
|
|
14481
|
+
return failure2("permission_denied", "only the workit CLI records observed checks");
|
|
14482
|
+
const observation = checkObservationSchema.safeParse(request?.observation);
|
|
14483
|
+
if (!observation.success)
|
|
14484
|
+
return failure2("invalid_input", "check observation is invalid", {
|
|
14485
|
+
fields: observation.error.issues.map((issue) => ({
|
|
14486
|
+
path: issue.path.join("."),
|
|
14487
|
+
reason: issue.message
|
|
14488
|
+
}))
|
|
14489
|
+
});
|
|
14490
|
+
const task = this.store.readTask(request.taskId);
|
|
14491
|
+
if (!task.ok)
|
|
14492
|
+
return task;
|
|
14493
|
+
if (task.data.status === "closed")
|
|
14494
|
+
return failure2("invalid_transition", "closed task cannot record evidence");
|
|
14495
|
+
const observed = observation.data;
|
|
14496
|
+
let candidate = null;
|
|
14497
|
+
if (observed.tree === null) {
|
|
14498
|
+
const captured = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14499
|
+
if (!captured.ok)
|
|
14500
|
+
return captured;
|
|
14501
|
+
candidate = captured.data;
|
|
14502
|
+
}
|
|
14503
|
+
const label = observed.name ?? observed.argv.join(" ");
|
|
14504
|
+
const data = {
|
|
14505
|
+
kind: "check",
|
|
14506
|
+
claim: `workit check ${label}`.slice(0, 500),
|
|
14507
|
+
requirementIds: [],
|
|
14508
|
+
...candidate ? { beforeCandidateId: candidate.id, candidateId: candidate.id } : {},
|
|
14509
|
+
result: observed.exitCode === 0 ? "passed" : "failed",
|
|
14510
|
+
summary: `${observed.configured ? "configured" : "ad-hoc"} check exited ${observed.exitCode}${observed.timedOut ? " (timed out)" : ""} in ${observed.durationMs} ms`,
|
|
14511
|
+
refs: [],
|
|
14512
|
+
exitCode: observed.exitCode,
|
|
14513
|
+
reviewContext: null,
|
|
14514
|
+
observation: observed
|
|
14515
|
+
};
|
|
14516
|
+
const changed = this.store.mutateTask(task.data.id, request.expectedRevision ?? task.data.revision, (current, mutation) => {
|
|
14517
|
+
const candidates = candidate === null || current.candidates.some((item) => item.id === candidate.id) ? current.candidates : [...current.candidates, candidate];
|
|
14518
|
+
const critical = [...new Set([...current.critical ?? [], CHECK_OBSERVATION_PATH])];
|
|
14519
|
+
return success(mutation.revision, null, {
|
|
14520
|
+
...current,
|
|
14521
|
+
critical,
|
|
14522
|
+
candidates,
|
|
14523
|
+
evidence: [
|
|
14524
|
+
...current.evidence,
|
|
14525
|
+
{
|
|
14526
|
+
id: newId(),
|
|
14527
|
+
recordedAt: mutation.now,
|
|
14528
|
+
provenance: provenance(this.context, "host_observed"),
|
|
14529
|
+
data
|
|
14530
|
+
}
|
|
14531
|
+
]
|
|
14532
|
+
});
|
|
14533
|
+
}, trustedNow(this.context));
|
|
14534
|
+
if (!changed.ok)
|
|
14535
|
+
return changed;
|
|
14536
|
+
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14537
|
+
}
|
|
14017
14538
|
decision(request) {
|
|
14018
14539
|
return this.recordDecision(request);
|
|
14019
14540
|
}
|
|
@@ -14249,7 +14770,7 @@ class WorkitCore {
|
|
|
14249
14770
|
const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14250
14771
|
if (!current.ok)
|
|
14251
14772
|
return current;
|
|
14252
|
-
const evaluations = evaluateEvidence(task.data, current.data);
|
|
14773
|
+
const evaluations = evaluateEvidence(task.data, current.data, treeFreshness(this.store.root));
|
|
14253
14774
|
const verified = evidence.some((entry) => {
|
|
14254
14775
|
const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
|
|
14255
14776
|
return entry.recordedAt >= finding.recordedAt && findingVerificationPasses(finding.data.candidateId, {
|
|
@@ -14953,7 +15474,8 @@ class WorkitCore {
|
|
|
14953
15474
|
if (!current.ok)
|
|
14954
15475
|
return current;
|
|
14955
15476
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
14956
|
-
const
|
|
15477
|
+
const tree = treeFreshness(this.store.root);
|
|
15478
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
14957
15479
|
return success(task.revision, workspace.data.revision, {
|
|
14958
15480
|
id: task.id,
|
|
14959
15481
|
revision: task.revision,
|
|
@@ -14970,7 +15492,7 @@ class WorkitCore {
|
|
|
14970
15492
|
writer: evaluationWorkspace.writer
|
|
14971
15493
|
});
|
|
14972
15494
|
}
|
|
14973
|
-
compactContext(taskId) {
|
|
15495
|
+
compactContext(taskId, freshness) {
|
|
14974
15496
|
const root = this.contextRootError();
|
|
14975
15497
|
if (!root.ok)
|
|
14976
15498
|
return root;
|
|
@@ -14980,12 +15502,12 @@ class WorkitCore {
|
|
|
14980
15502
|
const helper = this.helperTaskGuard(task.data, true);
|
|
14981
15503
|
if (!helper.ok)
|
|
14982
15504
|
return helper;
|
|
14983
|
-
const view = this.view(task.data, false);
|
|
15505
|
+
const view = this.view(task.data, false, freshness ?? signalFreshness(this.store.root));
|
|
14984
15506
|
if (!view.ok)
|
|
14985
15507
|
return view;
|
|
14986
15508
|
return success(view.revision, view.workspaceRevision, compactTaskContext(view.data));
|
|
14987
15509
|
}
|
|
14988
|
-
view(task, capture = true) {
|
|
15510
|
+
view(task, capture = true, freshness = treeFreshness(this.store.root)) {
|
|
14989
15511
|
const workspace = this.store.readWorkspace();
|
|
14990
15512
|
if (!workspace.ok)
|
|
14991
15513
|
return workspace;
|
|
@@ -14996,13 +15518,14 @@ class WorkitCore {
|
|
|
14996
15518
|
if (!current.ok)
|
|
14997
15519
|
return current;
|
|
14998
15520
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
14999
|
-
const
|
|
15521
|
+
const tree = freshness;
|
|
15522
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
15000
15523
|
return success(task.revision, workspace.data.revision, {
|
|
15001
15524
|
task,
|
|
15002
15525
|
workspace: evaluationWorkspace,
|
|
15003
15526
|
capabilities: this.context.capabilities,
|
|
15004
15527
|
requirements,
|
|
15005
|
-
evidence: evaluateEvidence(task, current.data)
|
|
15528
|
+
evidence: evaluateEvidence(task, current.data, tree)
|
|
15006
15529
|
});
|
|
15007
15530
|
}
|
|
15008
15531
|
reconcileResume(view, observations = []) {
|
|
@@ -15229,13 +15752,14 @@ class WorkitCore {
|
|
|
15229
15752
|
return current;
|
|
15230
15753
|
const withCandidate = current.data;
|
|
15231
15754
|
const taskForView = withCandidate.id === task.data.candidates.at(-1)?.id ? task.data : { ...task.data, candidates: [...task.data.candidates, withCandidate] };
|
|
15232
|
-
const
|
|
15755
|
+
const tree = treeFreshness(this.store.root);
|
|
15756
|
+
const requirements = evaluateRequirements(taskForView, workspace.data, this.context.capabilities, withCandidate, this.store.root, this.context.caller, tree);
|
|
15233
15757
|
const view = {
|
|
15234
15758
|
task: taskForView,
|
|
15235
15759
|
workspace: workspace.data,
|
|
15236
15760
|
capabilities: this.context.capabilities,
|
|
15237
15761
|
requirements,
|
|
15238
|
-
evidence: evaluateEvidence(taskForView, withCandidate)
|
|
15762
|
+
evidence: evaluateEvidence(taskForView, withCandidate, tree)
|
|
15239
15763
|
};
|
|
15240
15764
|
const closure = evaluateClosure(input.outcome, view);
|
|
15241
15765
|
if (!closure.ok)
|
|
@@ -15289,7 +15813,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
15289
15813
|
};
|
|
15290
15814
|
});
|
|
15291
15815
|
// packages/workit-core/src/hooks/context.ts
|
|
15292
|
-
import
|
|
15816
|
+
import path13 from "node:path";
|
|
15293
15817
|
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
|
|
15294
15818
|
var newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt);
|
|
15295
15819
|
var sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null;
|
|
@@ -15314,6 +15838,19 @@ var filesKey = (files) => {
|
|
|
15314
15838
|
return signatures.some((item) => item !== "-" && racySignature(item)) ? null : signatures.join("\x00");
|
|
15315
15839
|
};
|
|
15316
15840
|
var CACHE_LIMIT = 64;
|
|
15841
|
+
var SIGNAL_TTL_MS = 1500;
|
|
15842
|
+
var signals = new Map;
|
|
15843
|
+
var recentSignal = (root) => {
|
|
15844
|
+
const now = Date.now();
|
|
15845
|
+
const cached = signals.get(root);
|
|
15846
|
+
if (cached && now - cached.at < SIGNAL_TTL_MS)
|
|
15847
|
+
return cached.value;
|
|
15848
|
+
const value = worktreeSignal(root);
|
|
15849
|
+
signals.set(root, { at: now, value });
|
|
15850
|
+
if (signals.size > CACHE_LIMIT)
|
|
15851
|
+
signals.delete(signals.keys().next().value);
|
|
15852
|
+
return value;
|
|
15853
|
+
};
|
|
15317
15854
|
var cache = new Map;
|
|
15318
15855
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15319
15856
|
const listed = store.listTaskIndex();
|
|
@@ -15337,18 +15874,26 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15337
15874
|
capabilities: context.capabilities
|
|
15338
15875
|
});
|
|
15339
15876
|
const hit = cache.get(slot);
|
|
15340
|
-
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files))
|
|
15877
|
+
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files) && (hit.signal === null || hit.signal === recentSignal(store.root)))
|
|
15341
15878
|
return hit.value;
|
|
15342
15879
|
const task = store.readTask(entry.id);
|
|
15343
15880
|
if (!task.ok)
|
|
15344
15881
|
return null;
|
|
15345
|
-
const
|
|
15346
|
-
const observed =
|
|
15347
|
-
const
|
|
15882
|
+
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15883
|
+
const observed = task.data.evidence.some(isObservedCheck);
|
|
15884
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path13.resolve(store.root, ref.path)] : []));
|
|
15885
|
+
const observedFiles = filesKey(files);
|
|
15886
|
+
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15348
15887
|
if (!compact.ok)
|
|
15349
15888
|
return null;
|
|
15350
15889
|
cache.delete(slot);
|
|
15351
|
-
cache.set(slot, {
|
|
15890
|
+
cache.set(slot, {
|
|
15891
|
+
key,
|
|
15892
|
+
files,
|
|
15893
|
+
filesKey: observedFiles,
|
|
15894
|
+
signal: observed ? freshness.current() : null,
|
|
15895
|
+
value: compact.data
|
|
15896
|
+
});
|
|
15352
15897
|
if (cache.size > CACHE_LIMIT)
|
|
15353
15898
|
cache.delete(cache.keys().next().value);
|
|
15354
15899
|
return compact.data;
|
|
@@ -15421,6 +15966,8 @@ var shellPolicy = (cwd, command) => {
|
|
|
15421
15966
|
var usable2 = (value) => value === "native" || value === "partial";
|
|
15422
15967
|
var NONE2 = { kind: "none" };
|
|
15423
15968
|
var offered = new Set;
|
|
15969
|
+
var CLAUDE_WORKTREE_IMPLEMENTER = "workit:implementer";
|
|
15970
|
+
var subagentStartText = (host, descriptor, event) => host === "claude_code" && event.agentType === CLAUDE_WORKTREE_IMPLEMENTER ? `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) working in its own git worktree: it may edit and commit there, within its brief's scope. Before the first commit, switch to a policy-compliant branch (\`git switch -c <type>/<slug>\`, e.g. feature/<slug>); branch policy hooks still deny protected or non-compliant branches. Never push, open a PR, or merge unless the brief asks for it.` : `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`;
|
|
15424
15971
|
function handleHook(input, deps) {
|
|
15425
15972
|
const { descriptor } = deps;
|
|
15426
15973
|
const event = input.event;
|
|
@@ -15446,10 +15993,7 @@ function handleHook(input, deps) {
|
|
|
15446
15993
|
case "shell.pre":
|
|
15447
15994
|
return usable2(descriptor.shellPolicy.deny) ? shellPolicy(input.cwd, event.command) : NONE2;
|
|
15448
15995
|
case "subagent.start":
|
|
15449
|
-
return {
|
|
15450
|
-
kind: "context",
|
|
15451
|
-
text: `Workit observed ${descriptor.label} subagent ${event.agentId} (${event.agentType}) as read-only/agent-guided; writer delegation is unavailable.`
|
|
15452
|
-
};
|
|
15996
|
+
return { kind: "context", text: subagentStartText(input.host, descriptor, event) };
|
|
15453
15997
|
case "compact.pre":
|
|
15454
15998
|
return {
|
|
15455
15999
|
kind: "notice",
|
|
@@ -15494,12 +16038,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
15494
16038
|
};
|
|
15495
16039
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
15496
16040
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
15497
|
-
import
|
|
15498
|
-
var
|
|
16041
|
+
import path14 from "node:path";
|
|
16042
|
+
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15499
16043
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
15500
16044
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
15501
16045
|
var existingDirectory = (value) => {
|
|
15502
|
-
if (!nonEmpty2(value) || !
|
|
16046
|
+
if (!nonEmpty2(value) || !path14.isAbsolute(value) || !existsSync5(value))
|
|
15503
16047
|
return null;
|
|
15504
16048
|
try {
|
|
15505
16049
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -15636,7 +16180,7 @@ var EVENTS = {
|
|
|
15636
16180
|
};
|
|
15637
16181
|
var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
|
|
15638
16182
|
var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
|
|
15639
|
-
var toolCommand = (value) =>
|
|
16183
|
+
var toolCommand = (value) => isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15640
16184
|
var eventOf = (name, value) => {
|
|
15641
16185
|
const toolUseId = optionalText(value.tool_use_id);
|
|
15642
16186
|
switch (name) {
|
|
@@ -15656,7 +16200,7 @@ var eventOf = (name, value) => {
|
|
|
15656
16200
|
const command = toolCommand(value);
|
|
15657
16201
|
if (!SHELL_TOOLS.has(String(value.tool_name)) || !command)
|
|
15658
16202
|
return { ok: false, error: "only shell PostToolUse is mapped" };
|
|
15659
|
-
const response =
|
|
16203
|
+
const response = isRecord3(value.tool_response) ? value.tool_response : {};
|
|
15660
16204
|
return {
|
|
15661
16205
|
ok: true,
|
|
15662
16206
|
event: {
|
|
@@ -15732,14 +16276,14 @@ var render = (decision, native) => {
|
|
|
15732
16276
|
var claudeCodeAdapter = {
|
|
15733
16277
|
descriptor: CLAUDE_CODE_DESCRIPTOR,
|
|
15734
16278
|
parse(raw) {
|
|
15735
|
-
const native =
|
|
16279
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15736
16280
|
const fail = (error) => ({
|
|
15737
16281
|
ok: false,
|
|
15738
16282
|
error,
|
|
15739
16283
|
native,
|
|
15740
16284
|
event: native ? EVENTS[native] : null
|
|
15741
16285
|
});
|
|
15742
|
-
if (!
|
|
16286
|
+
if (!isRecord3(raw) || !native)
|
|
15743
16287
|
return fail("hook_event_name is required");
|
|
15744
16288
|
if (!nonEmpty2(raw.session_id))
|
|
15745
16289
|
return fail("session_id is required");
|
|
@@ -15888,7 +16432,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
|
|
|
15888
16432
|
var isShellTool = (name) => SHELL_TOOLS2.has(String(name).toLowerCase());
|
|
15889
16433
|
var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
|
|
15890
16434
|
var parseCodexHookInput = (value) => {
|
|
15891
|
-
if (!
|
|
16435
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS2, String(value.hook_event_name)))
|
|
15892
16436
|
return { ok: false, error: "hook_event_name is required" };
|
|
15893
16437
|
const event = value.hook_event_name;
|
|
15894
16438
|
const cwd = existingDirectory(value.cwd);
|
|
@@ -15896,7 +16440,7 @@ var parseCodexHookInput = (value) => {
|
|
|
15896
16440
|
return { ok: false, error: "cwd must be an existing absolute directory" };
|
|
15897
16441
|
if (event === "PreToolUse" && !nonEmpty2(value.tool_name))
|
|
15898
16442
|
return { ok: false, error: "tool_name is required" };
|
|
15899
|
-
const command =
|
|
16443
|
+
const command = isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15900
16444
|
if (event === "PreToolUse" && isShellTool(value.tool_name) && !command)
|
|
15901
16445
|
return { ok: false, error: "tool_input.command is required for shell tools" };
|
|
15902
16446
|
if (event === "SubagentStart" && !nonEmpty2(value.agent_id))
|
|
@@ -15979,7 +16523,7 @@ var codexAdapter = {
|
|
|
15979
16523
|
const host = detectCodexSurface(env);
|
|
15980
16524
|
const parsed = parseCodexHookInput(raw);
|
|
15981
16525
|
if (!parsed.ok) {
|
|
15982
|
-
const native =
|
|
16526
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS2, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15983
16527
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS2[native] : null };
|
|
15984
16528
|
}
|
|
15985
16529
|
const input = parsed.data;
|
|
@@ -16006,7 +16550,7 @@ var codexAdapter = {
|
|
|
16006
16550
|
};
|
|
16007
16551
|
|
|
16008
16552
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16009
|
-
import
|
|
16553
|
+
import path15 from "node:path";
|
|
16010
16554
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16011
16555
|
var none = { support: "none", native: null };
|
|
16012
16556
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16125,7 +16669,7 @@ var EVENTS3 = {
|
|
|
16125
16669
|
preCompact: "compact.pre"
|
|
16126
16670
|
};
|
|
16127
16671
|
var parseCursorHookInput = (value) => {
|
|
16128
|
-
if (!
|
|
16672
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
|
|
16129
16673
|
return { ok: false, error: "hook_event_name is required" };
|
|
16130
16674
|
const roots = value.workspace_roots;
|
|
16131
16675
|
if (!Array.isArray(roots) || roots.length === 0 || !roots.every(nonEmpty2))
|
|
@@ -16189,7 +16733,7 @@ var protocolEvent2 = (input) => {
|
|
|
16189
16733
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16190
16734
|
}
|
|
16191
16735
|
};
|
|
16192
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
16736
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path15.sep) ? root : `${root}${path15.sep}`);
|
|
16193
16737
|
var hookCwd = (input) => {
|
|
16194
16738
|
const cwd = existingDirectory(input.cwd);
|
|
16195
16739
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|
|
@@ -16226,7 +16770,7 @@ var cursorAdapter = {
|
|
|
16226
16770
|
parse(raw) {
|
|
16227
16771
|
const parsed = parseCursorHookInput(raw);
|
|
16228
16772
|
if (!parsed.ok) {
|
|
16229
|
-
const native =
|
|
16773
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS3, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
16230
16774
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS3[native] : null };
|
|
16231
16775
|
}
|
|
16232
16776
|
const input = parsed.data;
|