@brainervirus/workit-cursor 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor-plugin/plugin.json +1 -1
- package/commands/wk-bdd.md +7 -0
- package/commands/wk-test-audit.md +7 -0
- package/dist/cursor-session-start.js +608 -72
- package/dist/mcp-server.js +628 -112
- package/dist/workit-hook.js +615 -69
- package/package.json +3 -3
- package/skills/workit-bdd/SKILL.md +42 -0
- package/skills/workit-behavioral-tdd/SKILL.md +17 -9
- package/skills/workit-debug/SKILL.md +2 -1
- package/skills/workit-test-audit/SKILL.md +43 -0
package/dist/workit-hook.js
CHANGED
|
@@ -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
|
|
@@ -13086,8 +13545,9 @@ target result; a local commit alone is not evidence of a requested remote push.
|
|
|
13086
13545
|
Skill routing: slash aliases /wk-* load on demand. Use workit-steer for a
|
|
13087
13546
|
substantial interruption or change of direction, workit-deslop when relevant
|
|
13088
13547
|
to a PR-ready endpoint, workit-green-run for failing CI, workit-blast-radius
|
|
13089
|
-
when impact is uncertain,
|
|
13090
|
-
choices
|
|
13548
|
+
when impact is uncertain, workit-challenge for genuinely open consequential
|
|
13549
|
+
choices, workit-bdd to turn acceptance criteria into Given/When/Then tests, and
|
|
13550
|
+
workit-test-audit to check tests for tautologies. Load workit-plan when dependencies or handoff need durable next actions.
|
|
13091
13551
|
Load the skill; never act from memory of it.
|
|
13092
13552
|
`.trim();
|
|
13093
13553
|
|
|
@@ -13098,7 +13558,7 @@ function reconcileResume(view, observations = []) {
|
|
|
13098
13558
|
const candidate = captureCandidate(view.workspace.root, view.task.intent.data.scope, []);
|
|
13099
13559
|
if (!candidate.ok)
|
|
13100
13560
|
return candidate;
|
|
13101
|
-
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13561
|
+
const staleEvidenceIds = evaluateEvidence(view.task, candidate.data, treeFreshness(view.workspace.root)).filter((entry) => entry.status === "stale").map((entry) => entry.evidenceId);
|
|
13102
13562
|
const blockers = [...view.task.progress.blockers];
|
|
13103
13563
|
if (view.task.workers.some((entry) => isUncertainWorker(entry.data.state)))
|
|
13104
13564
|
blockers.push({
|
|
@@ -13996,10 +14456,7 @@ class WorkitCore {
|
|
|
13996
14456
|
if (finding.data.disposition !== "fixed")
|
|
13997
14457
|
return finding;
|
|
13998
14458
|
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
|
-
]));
|
|
14459
|
+
const statuses = new Map(evaluateEvidence(withEntry, currentCandidate.data, treeFreshness(this.store.root)).map((item) => [item.evidenceId, item.status]));
|
|
14003
14460
|
const verified = withEntry.evidence.some((item) => findingVerificationPasses(finding.data.candidateId, {
|
|
14004
14461
|
kind: item.data.kind,
|
|
14005
14462
|
candidateId: item.data.candidateId,
|
|
@@ -14014,6 +14471,71 @@ class WorkitCore {
|
|
|
14014
14471
|
return changed;
|
|
14015
14472
|
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14016
14473
|
}
|
|
14474
|
+
observeCheck(request) {
|
|
14475
|
+
return this.retryOmittedRevisions(request, () => this.observeCheckOnce(request));
|
|
14476
|
+
}
|
|
14477
|
+
observeCheckOnce(request) {
|
|
14478
|
+
const root = this.contextRootError();
|
|
14479
|
+
if (!root.ok)
|
|
14480
|
+
return root;
|
|
14481
|
+
if (this.context.caller.host !== "workit_cli" || (this.context.workerId ?? null) !== null)
|
|
14482
|
+
return failure2("permission_denied", "only the workit CLI records observed checks");
|
|
14483
|
+
const observation = checkObservationSchema.safeParse(request?.observation);
|
|
14484
|
+
if (!observation.success)
|
|
14485
|
+
return failure2("invalid_input", "check observation is invalid", {
|
|
14486
|
+
fields: observation.error.issues.map((issue) => ({
|
|
14487
|
+
path: issue.path.join("."),
|
|
14488
|
+
reason: issue.message
|
|
14489
|
+
}))
|
|
14490
|
+
});
|
|
14491
|
+
const task = this.store.readTask(request.taskId);
|
|
14492
|
+
if (!task.ok)
|
|
14493
|
+
return task;
|
|
14494
|
+
if (task.data.status === "closed")
|
|
14495
|
+
return failure2("invalid_transition", "closed task cannot record evidence");
|
|
14496
|
+
const observed = observation.data;
|
|
14497
|
+
let candidate = null;
|
|
14498
|
+
if (observed.tree === null) {
|
|
14499
|
+
const captured = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14500
|
+
if (!captured.ok)
|
|
14501
|
+
return captured;
|
|
14502
|
+
candidate = captured.data;
|
|
14503
|
+
}
|
|
14504
|
+
const label = observed.name ?? observed.argv.join(" ");
|
|
14505
|
+
const data = {
|
|
14506
|
+
kind: "check",
|
|
14507
|
+
claim: `workit check ${label}`.slice(0, 500),
|
|
14508
|
+
requirementIds: [],
|
|
14509
|
+
...candidate ? { beforeCandidateId: candidate.id, candidateId: candidate.id } : {},
|
|
14510
|
+
result: observed.exitCode === 0 ? "passed" : "failed",
|
|
14511
|
+
summary: `${observed.configured ? "configured" : "ad-hoc"} check exited ${observed.exitCode}${observed.timedOut ? " (timed out)" : ""} in ${observed.durationMs} ms`,
|
|
14512
|
+
refs: [],
|
|
14513
|
+
exitCode: observed.exitCode,
|
|
14514
|
+
reviewContext: null,
|
|
14515
|
+
observation: observed
|
|
14516
|
+
};
|
|
14517
|
+
const changed = this.store.mutateTask(task.data.id, request.expectedRevision ?? task.data.revision, (current, mutation) => {
|
|
14518
|
+
const candidates = candidate === null || current.candidates.some((item) => item.id === candidate.id) ? current.candidates : [...current.candidates, candidate];
|
|
14519
|
+
const critical = [...new Set([...current.critical ?? [], CHECK_OBSERVATION_PATH])];
|
|
14520
|
+
return success(mutation.revision, null, {
|
|
14521
|
+
...current,
|
|
14522
|
+
critical,
|
|
14523
|
+
candidates,
|
|
14524
|
+
evidence: [
|
|
14525
|
+
...current.evidence,
|
|
14526
|
+
{
|
|
14527
|
+
id: newId(),
|
|
14528
|
+
recordedAt: mutation.now,
|
|
14529
|
+
provenance: provenance(this.context, "host_observed"),
|
|
14530
|
+
data
|
|
14531
|
+
}
|
|
14532
|
+
]
|
|
14533
|
+
});
|
|
14534
|
+
}, trustedNow(this.context));
|
|
14535
|
+
if (!changed.ok)
|
|
14536
|
+
return changed;
|
|
14537
|
+
return success(changed.data.revision, null, changed.data.evidence.at(-1));
|
|
14538
|
+
}
|
|
14017
14539
|
decision(request) {
|
|
14018
14540
|
return this.recordDecision(request);
|
|
14019
14541
|
}
|
|
@@ -14249,7 +14771,7 @@ class WorkitCore {
|
|
|
14249
14771
|
const current = captureCandidate(this.store.root, task.data.intent.data.scope, environment());
|
|
14250
14772
|
if (!current.ok)
|
|
14251
14773
|
return current;
|
|
14252
|
-
const evaluations = evaluateEvidence(task.data, current.data);
|
|
14774
|
+
const evaluations = evaluateEvidence(task.data, current.data, treeFreshness(this.store.root));
|
|
14253
14775
|
const verified = evidence.some((entry) => {
|
|
14254
14776
|
const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
|
|
14255
14777
|
return entry.recordedAt >= finding.recordedAt && findingVerificationPasses(finding.data.candidateId, {
|
|
@@ -14953,7 +15475,8 @@ class WorkitCore {
|
|
|
14953
15475
|
if (!current.ok)
|
|
14954
15476
|
return current;
|
|
14955
15477
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
14956
|
-
const
|
|
15478
|
+
const tree = treeFreshness(this.store.root);
|
|
15479
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
14957
15480
|
return success(task.revision, workspace.data.revision, {
|
|
14958
15481
|
id: task.id,
|
|
14959
15482
|
revision: task.revision,
|
|
@@ -14970,7 +15493,7 @@ class WorkitCore {
|
|
|
14970
15493
|
writer: evaluationWorkspace.writer
|
|
14971
15494
|
});
|
|
14972
15495
|
}
|
|
14973
|
-
compactContext(taskId) {
|
|
15496
|
+
compactContext(taskId, freshness) {
|
|
14974
15497
|
const root = this.contextRootError();
|
|
14975
15498
|
if (!root.ok)
|
|
14976
15499
|
return root;
|
|
@@ -14980,12 +15503,12 @@ class WorkitCore {
|
|
|
14980
15503
|
const helper = this.helperTaskGuard(task.data, true);
|
|
14981
15504
|
if (!helper.ok)
|
|
14982
15505
|
return helper;
|
|
14983
|
-
const view = this.view(task.data, false);
|
|
15506
|
+
const view = this.view(task.data, false, freshness ?? signalFreshness(this.store.root));
|
|
14984
15507
|
if (!view.ok)
|
|
14985
15508
|
return view;
|
|
14986
15509
|
return success(view.revision, view.workspaceRevision, compactTaskContext(view.data));
|
|
14987
15510
|
}
|
|
14988
|
-
view(task, capture = true) {
|
|
15511
|
+
view(task, capture = true, freshness = treeFreshness(this.store.root)) {
|
|
14989
15512
|
const workspace = this.store.readWorkspace();
|
|
14990
15513
|
if (!workspace.ok)
|
|
14991
15514
|
return workspace;
|
|
@@ -14996,13 +15519,14 @@ class WorkitCore {
|
|
|
14996
15519
|
if (!current.ok)
|
|
14997
15520
|
return current;
|
|
14998
15521
|
const evaluationWorkspace = task.status === "closed" ? { ...workspace.data, writer: null } : workspace.data;
|
|
14999
|
-
const
|
|
15522
|
+
const tree = freshness;
|
|
15523
|
+
const requirements = evaluateRequirements(task, evaluationWorkspace, this.context.capabilities, current.data, this.store.root, this.context.caller, tree);
|
|
15000
15524
|
return success(task.revision, workspace.data.revision, {
|
|
15001
15525
|
task,
|
|
15002
15526
|
workspace: evaluationWorkspace,
|
|
15003
15527
|
capabilities: this.context.capabilities,
|
|
15004
15528
|
requirements,
|
|
15005
|
-
evidence: evaluateEvidence(task, current.data)
|
|
15529
|
+
evidence: evaluateEvidence(task, current.data, tree)
|
|
15006
15530
|
});
|
|
15007
15531
|
}
|
|
15008
15532
|
reconcileResume(view, observations = []) {
|
|
@@ -15229,13 +15753,14 @@ class WorkitCore {
|
|
|
15229
15753
|
return current;
|
|
15230
15754
|
const withCandidate = current.data;
|
|
15231
15755
|
const taskForView = withCandidate.id === task.data.candidates.at(-1)?.id ? task.data : { ...task.data, candidates: [...task.data.candidates, withCandidate] };
|
|
15232
|
-
const
|
|
15756
|
+
const tree = treeFreshness(this.store.root);
|
|
15757
|
+
const requirements = evaluateRequirements(taskForView, workspace.data, this.context.capabilities, withCandidate, this.store.root, this.context.caller, tree);
|
|
15233
15758
|
const view = {
|
|
15234
15759
|
task: taskForView,
|
|
15235
15760
|
workspace: workspace.data,
|
|
15236
15761
|
capabilities: this.context.capabilities,
|
|
15237
15762
|
requirements,
|
|
15238
|
-
evidence: evaluateEvidence(taskForView, withCandidate)
|
|
15763
|
+
evidence: evaluateEvidence(taskForView, withCandidate, tree)
|
|
15239
15764
|
};
|
|
15240
15765
|
const closure = evaluateClosure(input.outcome, view);
|
|
15241
15766
|
if (!closure.ok)
|
|
@@ -15289,7 +15814,7 @@ var capabilitiesFor = (descriptor, observed = {}) => descriptor.capabilities.map
|
|
|
15289
15814
|
};
|
|
15290
15815
|
});
|
|
15291
15816
|
// packages/workit-core/src/hooks/context.ts
|
|
15292
|
-
import
|
|
15817
|
+
import path13 from "node:path";
|
|
15293
15818
|
var boundTo = (entry, session) => entry.sessions.some((item) => item.host === session.host && item.handle === session.handle);
|
|
15294
15819
|
var newestFirst = (left, right) => right.updatedAt.localeCompare(left.updatedAt);
|
|
15295
15820
|
var sessionTaskEntry = (entries, session) => entries.filter((entry) => entry.status !== "closed" && boundTo(entry, session)).toSorted(newestFirst)[0] ?? null;
|
|
@@ -15314,6 +15839,19 @@ var filesKey = (files) => {
|
|
|
15314
15839
|
return signatures.some((item) => item !== "-" && racySignature(item)) ? null : signatures.join("\x00");
|
|
15315
15840
|
};
|
|
15316
15841
|
var CACHE_LIMIT = 64;
|
|
15842
|
+
var SIGNAL_TTL_MS = 1500;
|
|
15843
|
+
var signals = new Map;
|
|
15844
|
+
var recentSignal = (root) => {
|
|
15845
|
+
const now = Date.now();
|
|
15846
|
+
const cached = signals.get(root);
|
|
15847
|
+
if (cached && now - cached.at < SIGNAL_TTL_MS)
|
|
15848
|
+
return cached.value;
|
|
15849
|
+
const value = worktreeSignal(root);
|
|
15850
|
+
signals.set(root, { at: now, value });
|
|
15851
|
+
if (signals.size > CACHE_LIMIT)
|
|
15852
|
+
signals.delete(signals.keys().next().value);
|
|
15853
|
+
return value;
|
|
15854
|
+
};
|
|
15317
15855
|
var cache = new Map;
|
|
15318
15856
|
function sessionCompactContext(store, session, context, selection = "session-bound") {
|
|
15319
15857
|
const listed = store.listTaskIndex();
|
|
@@ -15337,18 +15875,26 @@ function entryCompactContext(store, entry, session, context) {
|
|
|
15337
15875
|
capabilities: context.capabilities
|
|
15338
15876
|
});
|
|
15339
15877
|
const hit = cache.get(slot);
|
|
15340
|
-
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files))
|
|
15878
|
+
if (hit && hit.key === key && hit.filesKey !== null && hit.filesKey === filesKey(hit.files) && (hit.signal === null || hit.signal === recentSignal(store.root)))
|
|
15341
15879
|
return hit.value;
|
|
15342
15880
|
const task = store.readTask(entry.id);
|
|
15343
15881
|
if (!task.ok)
|
|
15344
15882
|
return null;
|
|
15345
|
-
const
|
|
15346
|
-
const observed =
|
|
15347
|
-
const
|
|
15883
|
+
const freshness = { mode: "signal", current: () => recentSignal(store.root) };
|
|
15884
|
+
const observed = task.data.evidence.some(isObservedCheck);
|
|
15885
|
+
const files = task.data.decisions.flatMap((decision) => decision.data.binding.contentRefs.flatMap((ref) => ref.kind === "file" ? [path13.resolve(store.root, ref.path)] : []));
|
|
15886
|
+
const observedFiles = filesKey(files);
|
|
15887
|
+
const compact = new WorkitCore(store, context).compactContext(entry.id, freshness);
|
|
15348
15888
|
if (!compact.ok)
|
|
15349
15889
|
return null;
|
|
15350
15890
|
cache.delete(slot);
|
|
15351
|
-
cache.set(slot, {
|
|
15891
|
+
cache.set(slot, {
|
|
15892
|
+
key,
|
|
15893
|
+
files,
|
|
15894
|
+
filesKey: observedFiles,
|
|
15895
|
+
signal: observed ? freshness.current() : null,
|
|
15896
|
+
value: compact.data
|
|
15897
|
+
});
|
|
15352
15898
|
if (cache.size > CACHE_LIMIT)
|
|
15353
15899
|
cache.delete(cache.keys().next().value);
|
|
15354
15900
|
return compact.data;
|
|
@@ -15493,12 +16039,12 @@ var dispatchHook = (adapter, raw, env = process.env) => {
|
|
|
15493
16039
|
};
|
|
15494
16040
|
// packages/workit-core/src/hooks/hosts/fields.ts
|
|
15495
16041
|
import { existsSync as existsSync5, realpathSync as realpathSync7, statSync as statSync2 } from "node:fs";
|
|
15496
|
-
import
|
|
15497
|
-
var
|
|
16042
|
+
import path14 from "node:path";
|
|
16043
|
+
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15498
16044
|
var nonEmpty2 = (value) => typeof value === "string" && value.trim() !== "";
|
|
15499
16045
|
var optionalText = (value) => typeof value === "string" ? value : null;
|
|
15500
16046
|
var existingDirectory = (value) => {
|
|
15501
|
-
if (!nonEmpty2(value) || !
|
|
16047
|
+
if (!nonEmpty2(value) || !path14.isAbsolute(value) || !existsSync5(value))
|
|
15502
16048
|
return null;
|
|
15503
16049
|
try {
|
|
15504
16050
|
return statSync2(value).isDirectory() ? realpathSync7(value) : null;
|
|
@@ -15635,7 +16181,7 @@ var EVENTS = {
|
|
|
15635
16181
|
};
|
|
15636
16182
|
var SOURCES = new Set(["startup", "resume", "clear", "compact", "fork"]);
|
|
15637
16183
|
var SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
|
|
15638
|
-
var toolCommand = (value) =>
|
|
16184
|
+
var toolCommand = (value) => isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15639
16185
|
var eventOf = (name, value) => {
|
|
15640
16186
|
const toolUseId = optionalText(value.tool_use_id);
|
|
15641
16187
|
switch (name) {
|
|
@@ -15655,7 +16201,7 @@ var eventOf = (name, value) => {
|
|
|
15655
16201
|
const command = toolCommand(value);
|
|
15656
16202
|
if (!SHELL_TOOLS.has(String(value.tool_name)) || !command)
|
|
15657
16203
|
return { ok: false, error: "only shell PostToolUse is mapped" };
|
|
15658
|
-
const response =
|
|
16204
|
+
const response = isRecord3(value.tool_response) ? value.tool_response : {};
|
|
15659
16205
|
return {
|
|
15660
16206
|
ok: true,
|
|
15661
16207
|
event: {
|
|
@@ -15731,14 +16277,14 @@ var render = (decision, native) => {
|
|
|
15731
16277
|
var claudeCodeAdapter = {
|
|
15732
16278
|
descriptor: CLAUDE_CODE_DESCRIPTOR,
|
|
15733
16279
|
parse(raw) {
|
|
15734
|
-
const native =
|
|
16280
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15735
16281
|
const fail = (error) => ({
|
|
15736
16282
|
ok: false,
|
|
15737
16283
|
error,
|
|
15738
16284
|
native,
|
|
15739
16285
|
event: native ? EVENTS[native] : null
|
|
15740
16286
|
});
|
|
15741
|
-
if (!
|
|
16287
|
+
if (!isRecord3(raw) || !native)
|
|
15742
16288
|
return fail("hook_event_name is required");
|
|
15743
16289
|
if (!nonEmpty2(raw.session_id))
|
|
15744
16290
|
return fail("session_id is required");
|
|
@@ -15887,7 +16433,7 @@ var SHELL_TOOLS2 = new Set(["bash", "unified-exec"]);
|
|
|
15887
16433
|
var isShellTool = (name) => SHELL_TOOLS2.has(String(name).toLowerCase());
|
|
15888
16434
|
var SOURCES2 = new Set(["startup", "resume", "clear", "compact"]);
|
|
15889
16435
|
var parseCodexHookInput = (value) => {
|
|
15890
|
-
if (!
|
|
16436
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS2, String(value.hook_event_name)))
|
|
15891
16437
|
return { ok: false, error: "hook_event_name is required" };
|
|
15892
16438
|
const event = value.hook_event_name;
|
|
15893
16439
|
const cwd = existingDirectory(value.cwd);
|
|
@@ -15895,7 +16441,7 @@ var parseCodexHookInput = (value) => {
|
|
|
15895
16441
|
return { ok: false, error: "cwd must be an existing absolute directory" };
|
|
15896
16442
|
if (event === "PreToolUse" && !nonEmpty2(value.tool_name))
|
|
15897
16443
|
return { ok: false, error: "tool_name is required" };
|
|
15898
|
-
const command =
|
|
16444
|
+
const command = isRecord3(value.tool_input) ? commandText(value.tool_input.command) : null;
|
|
15899
16445
|
if (event === "PreToolUse" && isShellTool(value.tool_name) && !command)
|
|
15900
16446
|
return { ok: false, error: "tool_input.command is required for shell tools" };
|
|
15901
16447
|
if (event === "SubagentStart" && !nonEmpty2(value.agent_id))
|
|
@@ -15978,7 +16524,7 @@ var codexAdapter = {
|
|
|
15978
16524
|
const host = detectCodexSurface(env);
|
|
15979
16525
|
const parsed = parseCodexHookInput(raw);
|
|
15980
16526
|
if (!parsed.ok) {
|
|
15981
|
-
const native =
|
|
16527
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS2, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
15982
16528
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS2[native] : null };
|
|
15983
16529
|
}
|
|
15984
16530
|
const input = parsed.data;
|
|
@@ -16005,7 +16551,7 @@ var codexAdapter = {
|
|
|
16005
16551
|
};
|
|
16006
16552
|
|
|
16007
16553
|
// packages/workit-core/src/hooks/hosts/cursor.ts
|
|
16008
|
-
import
|
|
16554
|
+
import path15 from "node:path";
|
|
16009
16555
|
var undocumented2 = { support: "undocumented", native: null };
|
|
16010
16556
|
var none = { support: "none", native: null };
|
|
16011
16557
|
var CURSOR_DESCRIPTOR = {
|
|
@@ -16124,7 +16670,7 @@ var EVENTS3 = {
|
|
|
16124
16670
|
preCompact: "compact.pre"
|
|
16125
16671
|
};
|
|
16126
16672
|
var parseCursorHookInput = (value) => {
|
|
16127
|
-
if (!
|
|
16673
|
+
if (!isRecord3(value) || !Object.hasOwn(EVENTS3, String(value.hook_event_name)))
|
|
16128
16674
|
return { ok: false, error: "hook_event_name is required" };
|
|
16129
16675
|
const roots = value.workspace_roots;
|
|
16130
16676
|
if (!Array.isArray(roots) || roots.length === 0 || !roots.every(nonEmpty2))
|
|
@@ -16188,7 +16734,7 @@ var protocolEvent2 = (input) => {
|
|
|
16188
16734
|
return { kind: "compact.pre", trigger: "auto" };
|
|
16189
16735
|
}
|
|
16190
16736
|
};
|
|
16191
|
-
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(
|
|
16737
|
+
var within = (root, dir) => dir === root || dir.startsWith(root.endsWith(path15.sep) ? root : `${root}${path15.sep}`);
|
|
16192
16738
|
var hookCwd = (input) => {
|
|
16193
16739
|
const cwd = existingDirectory(input.cwd);
|
|
16194
16740
|
if (cwd && input.hook_event_name === "beforeShellExecution")
|
|
@@ -16225,7 +16771,7 @@ var cursorAdapter = {
|
|
|
16225
16771
|
parse(raw) {
|
|
16226
16772
|
const parsed = parseCursorHookInput(raw);
|
|
16227
16773
|
if (!parsed.ok) {
|
|
16228
|
-
const native =
|
|
16774
|
+
const native = isRecord3(raw) && Object.hasOwn(EVENTS3, String(raw.hook_event_name)) ? raw.hook_event_name : null;
|
|
16229
16775
|
return { ok: false, error: parsed.error, native, event: native ? EVENTS3[native] : null };
|
|
16230
16776
|
}
|
|
16231
16777
|
const input = parsed.data;
|