@brainervirus/workit-core 8.23.0 → 8.24.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/package.json +1 -1
- package/skills/workit-fanout/SKILL.md +1 -1
- package/skills/workit-implement/SKILL.md +5 -5
- package/skills/workit-review/SKILL.md +3 -0
- package/skills/workit-ship/SKILL.md +1 -2
- package/src/autonomy.ts +22 -11
- package/src/check-config.ts +9 -2
- package/src/core/methods.ts +3 -3
- package/src/core/policy/attributes.ts +97 -0
- package/src/core/policy/current-judgment.ts +20 -0
- package/src/core/policy/derive.ts +57 -9
- package/src/core/policy/lane.ts +513 -0
- package/src/core/policy/pr-lane.ts +32 -0
- package/src/core/policy/substance.ts +365 -0
- package/src/core/workspaces.ts +1 -1
- package/src/forge/pr-ops.ts +54 -12
- package/src/forge/report.ts +4 -2
- package/src/git/rev.ts +133 -0
- package/src/hooks/stop.ts +50 -33
- package/src/ledger.ts +7 -2
- package/src/stack.ts +54 -12
package/src/hooks/stop.ts
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
// or merged;
|
|
12
12
|
// (c) verdict: the session opened the PR (or verified delivery) but the
|
|
13
13
|
// head lacks the verdict the policy asks for, while the endpoint is
|
|
14
|
-
// green or merged (a PR opens without one)
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
14
|
+
// green or merged (a PR opens without one). Under `verification:
|
|
15
|
+
// proportional` (the default) the review lane of the head decides
|
|
16
|
+
// (core/policy/lane.ts): none owes nothing, self the author's own
|
|
17
|
+
// `--self` verdict, focused and full an accepted non-author verdict.
|
|
18
|
+
// Under `self`: the author's own at normal risk; at high risk or under
|
|
19
|
+
// `independent`: a non-author one, and not raised when the task's
|
|
20
|
+
// latest judgment records no behavior change at normal risk
|
|
21
|
+
// (policy/derive.ts asks for none either). Never raised while a
|
|
22
|
+
// verifier or reviewer this lead started still runs.
|
|
19
23
|
// Each obligation blocks once per head and session (a marker keyed by kind
|
|
20
24
|
// and head), not once per turn.
|
|
21
25
|
// A branch that landed owes nothing: a recorded merge (`workit pr merge`, or
|
|
@@ -29,10 +33,9 @@
|
|
|
29
33
|
// the ledger and local git only (no forge call), and every failure allows
|
|
30
34
|
// the stop.
|
|
31
35
|
import { spawnSync } from "node:child_process";
|
|
32
|
-
import { resolveAutonomy } from "../autonomy";
|
|
33
|
-
import {
|
|
34
|
-
import
|
|
35
|
-
import { TaskStore } from "../core/task-store";
|
|
36
|
+
import { resolveAutonomy, type VerificationMode } from "../autonomy";
|
|
37
|
+
import { currentJudgment } from "../core/policy/current-judgment";
|
|
38
|
+
import { branchLane, laneNeed } from "../core/policy/lane";
|
|
36
39
|
import { currentBranch, pushRemoteName } from "../git/rev";
|
|
37
40
|
import { checkVerdicts, readLedger, type ReadRow } from "../ledger";
|
|
38
41
|
import type { HostDescriptor } from "./descriptor";
|
|
@@ -237,43 +240,57 @@ const redChecks = (
|
|
|
237
240
|
};
|
|
238
241
|
};
|
|
239
242
|
|
|
240
|
-
/**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
243
|
+
/**
|
|
244
|
+
* The base the branch's PR targets: the newest the forge reported (a
|
|
245
|
+
* `pr.status` row, so a PR retargeted on the forge is read against its
|
|
246
|
+
* current base), else the one it was opened with (`pr.created`), else null
|
|
247
|
+
* (the default base). Read from the ledger: the hook never calls the forge.
|
|
248
|
+
*/
|
|
249
|
+
const prBase = (rows: readonly ReadRow[]): string | null => {
|
|
250
|
+
const seen = rows.findLast(
|
|
251
|
+
(row) =>
|
|
252
|
+
(row.type === "pr.status" || row.type === "pr.created") &&
|
|
253
|
+
row.observer === "workit_cli" &&
|
|
254
|
+
typeof row.base === "string",
|
|
255
|
+
);
|
|
256
|
+
return typeof seen?.base === "string" ? seen.base : null;
|
|
255
257
|
};
|
|
256
258
|
|
|
257
259
|
/**
|
|
258
260
|
* (c) Delivered by this session, yet the head lacks the verdict the policy
|
|
259
|
-
* asks for
|
|
260
|
-
*
|
|
261
|
+
* asks for. Under `verification: proportional` the review lane of the head
|
|
262
|
+
* decides (core/policy/lane.ts): none owes nothing, self the author's own,
|
|
263
|
+
* focused and full a non-author one. Under `self`, the author's own at
|
|
264
|
+
* normal risk; under `independent` or at high risk, a non-author one.
|
|
261
265
|
*/
|
|
262
266
|
const unverified = (
|
|
263
267
|
cwd: string,
|
|
264
268
|
branch: string,
|
|
265
269
|
all: readonly ReadRow[],
|
|
266
270
|
descriptor: HostDescriptor,
|
|
267
|
-
verification:
|
|
271
|
+
verification: VerificationMode,
|
|
272
|
+
head: string | null,
|
|
268
273
|
): StopObligation | null => {
|
|
269
274
|
const check = checkVerdicts(cwd, branch, all);
|
|
270
275
|
if (check.accepted.accepted || check.accepted.reasons.includes("failing_verdict")) return null;
|
|
271
|
-
const judgment =
|
|
276
|
+
const judgment = currentJudgment(cwd);
|
|
272
277
|
const high = judgment?.riskTier === "high";
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
let independent: boolean;
|
|
279
|
+
if (verification === "proportional") {
|
|
280
|
+
const lane = branchLane(cwd, {
|
|
281
|
+
base: prBase(all.filter((row) => row.branch === branch)),
|
|
282
|
+
head,
|
|
283
|
+
judgment,
|
|
284
|
+
});
|
|
285
|
+
const need = laneNeed(lane.lane);
|
|
286
|
+
if (need === "none") return null;
|
|
287
|
+
independent = need === "non-author";
|
|
288
|
+
} else {
|
|
289
|
+
// Like policy/derive.ts: a verdict is asked for a behavior change or high
|
|
290
|
+
// risk. A recorded judgment with neither (docs only) owes none.
|
|
291
|
+
if (judgment && !judgment.behaviorChange && !high) return null;
|
|
292
|
+
independent = verification === "independent" || high;
|
|
293
|
+
}
|
|
277
294
|
if (!independent && check.review === "self-reviewed") return null;
|
|
278
295
|
const has = check.review === "self-reviewed" ? "only a self verdict" : "no current verdict";
|
|
279
296
|
const verifier = `${descriptor.subagents.agentPrefix}verifier`;
|
|
@@ -332,7 +349,7 @@ export function stopObligation(
|
|
|
332
349
|
}
|
|
333
350
|
// A PR opens without a verdict: only green and merged need one.
|
|
334
351
|
if (!delivered || (endpoint !== "green" && endpoint !== "merged")) return null;
|
|
335
|
-
const left = unverified(input.cwd, branch, all, descriptor, autonomy.verification);
|
|
352
|
+
const left = unverified(input.cwd, branch, all, descriptor, autonomy.verification, head);
|
|
336
353
|
return left ? { ...left, head } : null;
|
|
337
354
|
} catch {
|
|
338
355
|
return null;
|
package/src/ledger.ts
CHANGED
|
@@ -1622,15 +1622,20 @@ export function checkVerdicts(
|
|
|
1622
1622
|
? newestCurrent.reasons
|
|
1623
1623
|
: (["type_check_only"] as RejectReason[]),
|
|
1624
1624
|
};
|
|
1625
|
-
|
|
1625
|
+
// The author's latest pass or failure on the current code wins, across
|
|
1626
|
+
// kinds: a newer failure cancels an older pass (review finding mvkcoxgb).
|
|
1627
|
+
const latestSelf = failing
|
|
1626
1628
|
? undefined
|
|
1627
1629
|
: verdicts.findLast(
|
|
1628
1630
|
(entry) =>
|
|
1629
1631
|
entry.current &&
|
|
1630
1632
|
!entry.independent &&
|
|
1631
1633
|
!untrusted(entry.reasons) &&
|
|
1632
|
-
STRONG_RESULTS.has(String(entry.verdict.result))
|
|
1634
|
+
(STRONG_RESULTS.has(String(entry.verdict.result)) ||
|
|
1635
|
+
FAILING.has(String(entry.verdict.result))),
|
|
1633
1636
|
);
|
|
1637
|
+
const selfEntry =
|
|
1638
|
+
latestSelf && STRONG_RESULTS.has(String(latestSelf.verdict.result)) ? latestSelf : undefined;
|
|
1634
1639
|
return {
|
|
1635
1640
|
branch,
|
|
1636
1641
|
head,
|
package/src/stack.ts
CHANGED
|
@@ -73,6 +73,8 @@ import {
|
|
|
73
73
|
type ReadRow,
|
|
74
74
|
type VerdictBasis,
|
|
75
75
|
} from "./ledger";
|
|
76
|
+
import type { LaneNeed, ReviewLane } from "./core/policy/lane";
|
|
77
|
+
import { prLaneGate } from "./core/policy/pr-lane";
|
|
76
78
|
|
|
77
79
|
export const STACK_VERSION = 1;
|
|
78
80
|
|
|
@@ -1016,7 +1018,20 @@ export type BranchStatus = {
|
|
|
1016
1018
|
next: NextAction | null;
|
|
1017
1019
|
checks: ChecksState | null;
|
|
1018
1020
|
blockers: string[];
|
|
1019
|
-
|
|
1021
|
+
/**
|
|
1022
|
+
* `accepted`: an accepted independent verdict; `satisfied`: what the land
|
|
1023
|
+
* gate reads, the verdict the PR's review lane asks for (an accepted
|
|
1024
|
+
* independent one outside `verification: proportional`).
|
|
1025
|
+
*/
|
|
1026
|
+
verdict: {
|
|
1027
|
+
accepted: boolean;
|
|
1028
|
+
satisfied: boolean;
|
|
1029
|
+
basis: VerdictBasis;
|
|
1030
|
+
reasons: string[];
|
|
1031
|
+
verdictId: string | null;
|
|
1032
|
+
lane: ReviewLane | null;
|
|
1033
|
+
need: LaneNeed;
|
|
1034
|
+
};
|
|
1020
1035
|
};
|
|
1021
1036
|
|
|
1022
1037
|
export type Qualification =
|
|
@@ -1081,16 +1096,27 @@ export function repoMismatch(ctx: Pick<Ctx, "resolved">, stack: StackFile): Stac
|
|
|
1081
1096
|
return null;
|
|
1082
1097
|
}
|
|
1083
1098
|
|
|
1084
|
-
const verdictOf = (
|
|
1099
|
+
const verdictOf = (
|
|
1100
|
+
cwd: string,
|
|
1101
|
+
branch: string,
|
|
1102
|
+
rows: readonly ReadRow[],
|
|
1103
|
+
doc: PrStatusDoc | null,
|
|
1104
|
+
): BranchStatus["verdict"] => {
|
|
1085
1105
|
const check = checkVerdicts(cwd, branch, rows);
|
|
1106
|
+
// Without a PR there is no base to diff against: the gate stays independent.
|
|
1107
|
+
const gate = doc ? prLaneGate(cwd, check, { branch, base: doc.base, head: doc.head.sha }) : null;
|
|
1086
1108
|
return {
|
|
1087
1109
|
accepted: check.accepted.accepted,
|
|
1110
|
+
satisfied: gate ? gate.satisfied : check.accepted.accepted,
|
|
1088
1111
|
basis: check.current.basis,
|
|
1089
|
-
reasons: check.accepted.reasons
|
|
1090
|
-
verdictId:
|
|
1091
|
-
|
|
1112
|
+
reasons: check.accepted.reasons,
|
|
1113
|
+
verdictId: gate
|
|
1114
|
+
? gate.verdictId
|
|
1115
|
+
: typeof check.accepted.verdict?.id === "string" && check.accepted.accepted
|
|
1092
1116
|
? check.accepted.verdict.id
|
|
1093
1117
|
: null,
|
|
1118
|
+
lane: gate?.lane.lane ?? null,
|
|
1119
|
+
need: gate?.need ?? "non-author",
|
|
1094
1120
|
};
|
|
1095
1121
|
};
|
|
1096
1122
|
|
|
@@ -1161,12 +1187,20 @@ export function qualify(
|
|
|
1161
1187
|
`${label} has a current independent failed verdict for ${doc.head.sha.slice(0, 12)}; --unverified never lands over a rejection`,
|
|
1162
1188
|
"a new independent verdict on the head supersedes it",
|
|
1163
1189
|
);
|
|
1164
|
-
if (!(bypass && allowUnverified) && !verdict.
|
|
1190
|
+
if (!(bypass && allowUnverified) && !verdict.satisfied) {
|
|
1191
|
+
const bypassHint = allowUnverified
|
|
1192
|
+
? `; or, only if the user asks to land unverified: workit stack land --unverified --reason "<why>"`
|
|
1193
|
+
: "";
|
|
1165
1194
|
return stop(
|
|
1166
1195
|
verdict.reasons.includes("no_verdict") ? "no_verdict" : "needs_verdict",
|
|
1167
|
-
|
|
1168
|
-
|
|
1196
|
+
verdict.need === "self"
|
|
1197
|
+
? `${label} has no verdict for ${doc.head.sha.slice(0, 12)} (lane self: the author's own --self verdict is enough; ${verdict.reasons.join(", ") || "none"})`
|
|
1198
|
+
: `${label} has no accepted independent verdict for ${doc.head.sha.slice(0, 12)}${verdict.lane && verdict.lane !== "none" ? ` (lane ${verdict.lane})` : ""} (${verdict.reasons.join(", ") || "none"})`,
|
|
1199
|
+
verdict.need === "self"
|
|
1200
|
+
? `after an observed passing \`workit check test\`, the author runs: workit ledger verdict tests-verified --self --how "<what you ran>" --branch ${entry.branch}${bypassHint}`
|
|
1201
|
+
: `an independent session verifies ${entry.branch} and runs: workit ledger verdict verified --how "<what was exercised>" --branch ${entry.branch}${bypassHint}`,
|
|
1169
1202
|
);
|
|
1203
|
+
}
|
|
1170
1204
|
if (!grant.allowed)
|
|
1171
1205
|
return stop(
|
|
1172
1206
|
"grant_required",
|
|
@@ -1215,8 +1249,16 @@ export function stackStatus(ctx: Ctx, stack: StackFile): StackResult<StatusOutco
|
|
|
1215
1249
|
checks: doc?.checks.state ?? null,
|
|
1216
1250
|
blockers: doc?.blockers ?? [],
|
|
1217
1251
|
verdict: merged
|
|
1218
|
-
? {
|
|
1219
|
-
|
|
1252
|
+
? {
|
|
1253
|
+
accepted: true,
|
|
1254
|
+
satisfied: true,
|
|
1255
|
+
basis: "none",
|
|
1256
|
+
reasons: [],
|
|
1257
|
+
verdictId: null,
|
|
1258
|
+
lane: null,
|
|
1259
|
+
need: "non-author",
|
|
1260
|
+
}
|
|
1261
|
+
: verdictOf(ctx.cwd, entry.branch, ledger.value.rows, doc),
|
|
1220
1262
|
};
|
|
1221
1263
|
branches.push(row);
|
|
1222
1264
|
if (merged) {
|
|
@@ -1866,7 +1908,7 @@ export async function landStack(
|
|
|
1866
1908
|
}
|
|
1867
1909
|
}
|
|
1868
1910
|
const grant = requireGrant(ctx.cwd, "merge");
|
|
1869
|
-
const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows());
|
|
1911
|
+
const verdict = verdictOf(ctx.cwd, entry.branch, ledgerRows(), doc);
|
|
1870
1912
|
// A dry run predicts the retarget the real run would do first.
|
|
1871
1913
|
const effective =
|
|
1872
1914
|
options.dryRun && simulated.size > 0 && doc ? { ...doc, base: stack.trunk } : doc;
|
|
@@ -1906,7 +1948,7 @@ export async function landStack(
|
|
|
1906
1948
|
deleteBranch: false,
|
|
1907
1949
|
actor: ctx.actor,
|
|
1908
1950
|
expect: { base: stack.trunk, branch: entry.branch },
|
|
1909
|
-
...(bypass && !verdict.
|
|
1951
|
+
...(bypass && !verdict.satisfied ? { unverified: bypass } : {}),
|
|
1910
1952
|
},
|
|
1911
1953
|
ctx.sleep,
|
|
1912
1954
|
);
|