@brainervirus/workit-core 8.22.1 → 8.22.3
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/package.json
CHANGED
|
@@ -1068,10 +1068,6 @@ export const advertisedOperationSchemas = {
|
|
|
1068
1068
|
} as const;
|
|
1069
1069
|
export type TaskStartRequest = z.infer<typeof taskOperations.start>;
|
|
1070
1070
|
|
|
1071
|
-
const compiledOperationSchemas = Object.fromEntries(
|
|
1072
|
-
OPERATION_FAMILIES.map((family) => [family, z.compile(operationSchemas[family])]),
|
|
1073
|
-
) as typeof operationSchemas;
|
|
1074
|
-
|
|
1075
1071
|
export type Id = string;
|
|
1076
1072
|
export type Revision = string;
|
|
1077
1073
|
export type Digest = string;
|
|
@@ -1165,8 +1161,7 @@ export function parseOperation(family: OperationFamily, input: unknown): Result<
|
|
|
1165
1161
|
(input as { schemaVersion?: unknown }).schemaVersion !== SCHEMA_VERSION
|
|
1166
1162
|
)
|
|
1167
1163
|
return failure("unsupported_version", "unsupported schema version", { operation: family });
|
|
1168
|
-
const
|
|
1169
|
-
const parsed = compiled.success ? operationSchemas[family].safeParse(input) : compiled;
|
|
1164
|
+
const parsed = operationSchemas[family].safeParse(input);
|
|
1170
1165
|
if (parsed.success) return success(null, null, parsed.data);
|
|
1171
1166
|
const fields = parsed.error.issues.map((issue) => ({
|
|
1172
1167
|
path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
|
|
@@ -408,6 +408,6 @@ export const cursorAdapter: HostAdapter = {
|
|
|
408
408
|
// only), so the agent is told the id its `workit` calls should act as.
|
|
409
409
|
addendum: (input) =>
|
|
410
410
|
input.session.id
|
|
411
|
-
? `<workit-session>Cursor puts no session id in the shell:
|
|
411
|
+
? `<workit-session>Cursor puts no session id in the shell: start workit commands that record authorship with the global flag \`--session ${input.session.id}\` (e.g. \`workit --session ${input.session.id} git commit -m …\`; a leading env assignment can miss the \`Shell(workit)\` allowlist). A verifier uses its own id, never this one.</workit-session>`
|
|
412
412
|
: null,
|
|
413
413
|
};
|
package/src/hooks/raw-git.ts
CHANGED
|
@@ -1623,10 +1623,10 @@ const nudgeFor = (action: RawAction, host: HookInput["host"], session: string):
|
|
|
1623
1623
|
switch (action.kind) {
|
|
1624
1624
|
case "commit": {
|
|
1625
1625
|
// Cursor's shell carries no conversation id: name it explicitly.
|
|
1626
|
-
const
|
|
1626
|
+
const workit = host === "cursor" && session ? `workit --session ${session}` : "workit";
|
|
1627
1627
|
return action.amend
|
|
1628
|
-
? `workit: raw ${quote("git commit --amend")} is recorded for this session; for new commits prefer ${quote(`${
|
|
1629
|
-
: `workit: prefer ${quote(`${
|
|
1628
|
+
? `workit: raw ${quote("git commit --amend")} is recorded for this session; for new commits prefer ${quote(`${workit} git commit -m '<msg>'`)} (adds the Workit-Session trailer and checks branch and commit policy).`
|
|
1629
|
+
: `workit: prefer ${quote(`${workit} git commit -m '<msg>' [--all | -- <paths>]`)}: it adds the Workit-Session trailer and checks branch and commit policy.`;
|
|
1630
1630
|
}
|
|
1631
1631
|
case "push":
|
|
1632
1632
|
return action.blindForce
|
|
@@ -42,9 +42,19 @@ export const sessionMarkerFile = (
|
|
|
42
42
|
const list = (value: unknown): string[] =>
|
|
43
43
|
Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const EMPTY: SessionMarker = { nudged: [], loaded: [], raw: [], stops: [], judges: [] };
|
|
46
|
+
|
|
47
|
+
const readRaw = (file: string): string => {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(file, "utf8");
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const parseMarker = (text: string): SessionMarker => {
|
|
46
56
|
try {
|
|
47
|
-
const value = JSON.parse(
|
|
57
|
+
const value = JSON.parse(text) as Record<string, unknown>;
|
|
48
58
|
return {
|
|
49
59
|
nudged: list(value.nudged),
|
|
50
60
|
loaded: list(value.loaded),
|
|
@@ -53,14 +63,51 @@ export const readSessionMarker = (file: string): SessionMarker => {
|
|
|
53
63
|
judges: list(value.judges),
|
|
54
64
|
};
|
|
55
65
|
} catch {
|
|
56
|
-
return {
|
|
66
|
+
return { ...EMPTY };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const readSessionMarker = (file: string): SessionMarker => parseMarker(readRaw(file));
|
|
71
|
+
|
|
72
|
+
/** Write through a temp file in the same directory and rename: a reader never sees half a marker. */
|
|
73
|
+
const writeAtomic = (file: string, marker: SessionMarker): void => {
|
|
74
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
75
|
+
const temp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
76
|
+
try {
|
|
77
|
+
fs.writeFileSync(temp, JSON.stringify(marker));
|
|
78
|
+
fs.renameSync(temp, file);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
fs.rmSync(temp, { force: true });
|
|
81
|
+
throw error;
|
|
57
82
|
}
|
|
58
83
|
};
|
|
59
84
|
|
|
60
85
|
export const writeSessionMarker = (file: string, marker: SessionMarker): void => {
|
|
61
86
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
writeAtomic(file, marker);
|
|
88
|
+
} catch {
|
|
89
|
+
// Fail open: without a marker a later nudge repeats.
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read-modify-write: `change` maps the marker as read to the marker to keep.
|
|
95
|
+
* Another hook's rename between the read and ours is detected (the file's
|
|
96
|
+
* text changed) and `change` runs once more on the fresh marker, so an entry
|
|
97
|
+
* of a concurrent hook is not lost. Fails open like the rest.
|
|
98
|
+
*/
|
|
99
|
+
export const updateSessionMarker = (
|
|
100
|
+
file: string,
|
|
101
|
+
change: (marker: SessionMarker) => SessionMarker,
|
|
102
|
+
): void => {
|
|
103
|
+
try {
|
|
104
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
105
|
+
const before = readRaw(file);
|
|
106
|
+
const next = change(parseMarker(before));
|
|
107
|
+
if (attempt === 0 && readRaw(file) !== before) continue;
|
|
108
|
+
writeAtomic(file, next);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
64
111
|
} catch {
|
|
65
112
|
// Fail open: without a marker a later nudge repeats.
|
|
66
113
|
}
|
|
@@ -71,7 +118,8 @@ export const trackJudge = (input: HookInput, agentId: string | null, running: bo
|
|
|
71
118
|
const session = input.session.id;
|
|
72
119
|
const file = session && agentId ? sessionMarkerFile(input, session) : null;
|
|
73
120
|
if (!file || !agentId) return;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
updateSessionMarker(file, (marker) => {
|
|
122
|
+
const rest = marker.judges.filter((id) => id !== agentId);
|
|
123
|
+
return { ...marker, judges: running ? [...rest, agentId] : rest };
|
|
124
|
+
});
|
|
77
125
|
};
|
package/src/hooks/stop.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// green or merged (a PR opens without one): the author's own `--self`
|
|
15
15
|
// verdict at normal risk under `verification: self`, an accepted
|
|
16
16
|
// non-author verdict at high risk or under `verification: independent`.
|
|
17
|
-
// Not raised
|
|
17
|
+
// Not raised when the task's latest judgment records no behavior
|
|
18
|
+
// change at normal risk (policy/derive.ts asks for none either), nor while a verifier or reviewer this lead started still runs.
|
|
18
19
|
// Each obligation blocks once per head and session (a marker keyed by kind
|
|
19
20
|
// and head), not once per turn.
|
|
20
21
|
// A branch that landed owes nothing: a recorded merge (`workit pr merge`, or
|
|
@@ -30,13 +31,14 @@
|
|
|
30
31
|
import { spawnSync } from "node:child_process";
|
|
31
32
|
import { resolveAutonomy } from "../autonomy";
|
|
32
33
|
import { latestJudgment } from "../core/policy/derive";
|
|
34
|
+
import type { Judgment } from "../core/task-contract";
|
|
33
35
|
import { TaskStore } from "../core/task-store";
|
|
34
36
|
import { currentBranch, pushRemoteName } from "../git/rev";
|
|
35
37
|
import { checkVerdicts, readLedger, type ReadRow } from "../ledger";
|
|
36
38
|
import type { HostDescriptor } from "./descriptor";
|
|
37
39
|
import type { HookDecision, HookInput } from "./protocol";
|
|
38
40
|
import { guardedBranch, inWorkitWorkspace } from "./raw-git";
|
|
39
|
-
import { readSessionMarker, sessionMarkerFile,
|
|
41
|
+
import { readSessionMarker, sessionMarkerFile, updateSessionMarker } from "./session-marker";
|
|
40
42
|
|
|
41
43
|
const NONE: HookDecision = { kind: "none" };
|
|
42
44
|
|
|
@@ -235,20 +237,20 @@ const redChecks = (
|
|
|
235
237
|
};
|
|
236
238
|
};
|
|
237
239
|
|
|
238
|
-
/**
|
|
239
|
-
const
|
|
240
|
+
/** The open task's latest judgment (the one policy/derive.ts reads), or null when none is recorded. */
|
|
241
|
+
const taskJudgment = (cwd: string): Judgment | null => {
|
|
240
242
|
try {
|
|
241
243
|
const store = new TaskStore(cwd);
|
|
242
244
|
const listed = store.listTaskIndex();
|
|
243
245
|
const key = store.currentKey();
|
|
244
|
-
if (!listed.ok || !key.ok) return
|
|
246
|
+
if (!listed.ok || !key.ok) return null;
|
|
245
247
|
const entry = listed.data
|
|
246
248
|
.filter((item) => item.key === key.data.key && item.status !== "closed")
|
|
247
249
|
.toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
|
|
248
250
|
const task = entry ? store.readTask(entry.id) : null;
|
|
249
|
-
return task?.ok === true
|
|
251
|
+
return task?.ok === true ? latestJudgment(task.data) : null;
|
|
250
252
|
} catch {
|
|
251
|
-
return
|
|
253
|
+
return null;
|
|
252
254
|
}
|
|
253
255
|
};
|
|
254
256
|
|
|
@@ -266,7 +268,12 @@ const unverified = (
|
|
|
266
268
|
): StopObligation | null => {
|
|
267
269
|
const check = checkVerdicts(cwd, branch, all);
|
|
268
270
|
if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict")) return null;
|
|
269
|
-
const
|
|
271
|
+
const judgment = taskJudgment(cwd);
|
|
272
|
+
const high = judgment?.riskTier === "high";
|
|
273
|
+
// Like policy/derive.ts: a verdict is asked for a behavior change or high
|
|
274
|
+
// risk. A recorded judgment with neither (docs only) owes none.
|
|
275
|
+
if (judgment && !judgment.behaviorChange && !high) return null;
|
|
276
|
+
const independent = verification === "independent" || high;
|
|
270
277
|
if (!independent && check.review === "self-reviewed") return null;
|
|
271
278
|
const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
|
|
272
279
|
const verifier = `${descriptor.subagents.agentPrefix}verifier`;
|
|
@@ -353,7 +360,7 @@ export function stopDecision(input: HookInput, descriptor: HostDescriptor): Hook
|
|
|
353
360
|
const key = `${left.kind}:${left.head ?? ""}`;
|
|
354
361
|
if (file && marker) {
|
|
355
362
|
if (marker.stops.includes(key)) return NONE;
|
|
356
|
-
|
|
363
|
+
updateSessionMarker(file, (fresh) => ({ ...fresh, stops: [...fresh.stops, key] }));
|
|
357
364
|
}
|
|
358
365
|
return {
|
|
359
366
|
kind: "continue",
|