@brainervirus/workit-core 8.22.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.22.3",
3
+ "version": "8.24.0",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -20,14 +20,14 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
20
20
  PR (`references/orchestration.md`).
21
21
  2. **Record standing orders** (`workit ledger standing add "<order>"`): every
22
22
  user directive all workers share. Directives decay across resumes.
23
- 3. **Brief each worker verbatim** with `workit fanout brief <slice>`: plan
24
- fields, standing orders, SCRATCH, session id, fan-in and heavy-lease rules.
23
+ 3. **Brief each worker verbatim**: `workit fanout worktree create <slice>`
24
+ (every host), then `workit fanout brief <slice>` (WORKTREE, SCRATCH, rules).
25
25
  Never hand-edit it; change the plan or the standing orders and render again.
26
26
  4. **Spawn in a rolling window**, in the background, each in its own worktree:
27
27
  keep 4-6 in flight and refill from `spawnable` in `workit fanout status` as
28
28
  each one finishes. Model: the brief's `model:` line, else TIER
29
- (`references/orchestration.md`). Claude Code: the `implementer` agent;
30
- elsewhere first `workit fanout worktree create <slice>`.
29
+ (`references/orchestration.md`). Claude Code: the `implementer` agent, never
30
+ with `isolation: "worktree"`; Workit's hooks confine it to its WORKTREE.
31
31
  5. **Judge liveness by side effects only:** `workit fanout status` (head age,
32
32
  PR, CI, verdict, landed; STUCK past the TIMEBOX). Stop a stuck worker and
33
33
  observe that it exited (a timeout is not proof). Removing a worktree drops
@@ -40,7 +40,7 @@ units of ~100+ lines each. Coupled work stays with one owner. Workit never spawn
40
40
  chain resumes. A fix round and recheck that both fail: a decider, not a
41
41
  third round; product choices go to the user (`references/orchestration.md`).
42
42
  7. **Verify by the workspace `verification` setting** (`workit grant show`).
43
- A report is a pointer, never evidence. `self`: a verifier that wrote none
43
+ A report is a pointer, never evidence. `proportional` (default): each slice's lane (`workit pr status`) decides; none needs no verdict, self the author's `--self`, focused (cheap model: Claude Code `model: sonnet`) and full a verifier after the last push. `self`: a verifier that wrote none
44
44
  of the slices, or you if you wrote none. `independent` or high risk: a
45
45
  separate verifier session, never yours (doctrine: the ledger only refuses
46
46
  authors). One verifier may take a batch of slices, one verdict per branch
@@ -25,6 +25,7 @@ REPORT: branch, head SHA, files changed, each VERIFY command with its exit code,
25
25
  each ACCEPTANCE line met / not met, rulings you made (`workit ledger ruling`),
26
26
  anything out of scope as a follow-up, not a diff.
27
27
  STANDING: <the standing orders, verbatim: `workit ledger standing add` records each one>
28
+ WORKTREE: <the path `workit fanout worktree create <slice>` made; the worker runs `workit --cwd <it> …` there>
28
29
  <identity line, per the lead's host: see "Identity and git rules per host" below>
29
30
  ```
30
31
 
@@ -48,6 +49,7 @@ FORBIDDEN: no edits outside SCOPE; no schema migration; no new packages
48
49
  FAN-IN: one PR per slice. Never rebase, retarget, merge or force-push: the lead owns topology.
49
50
  REPORT: as in the template
50
51
  STANDING: conventional commits; no comments that restate code; ask nothing, record rulings instead
52
+ WORKTREE: ../app-wt/usage-endpoint (made by `workit fanout worktree create usage-endpoint`; yours alone)
51
53
  ```
52
54
 
53
55
  ## Plan file
@@ -106,25 +108,29 @@ lead's host (`WORKIT_HOST`, or the host's own session variable):
106
108
 
107
109
  | Lead's host | Identity line | RULES 1 and 3 |
108
110
  | --- | --- | --- |
109
- | Claude Code | `Session: your env already has WORKIT_SESSION_ID (the lead's id)`; no `export`, no hand-minted id (SubagentStart issues `<lead>:<agent id>` only to verifiers and reviewers, never to an implementer, so the worker's commits carry the lead's id) | plain git: `git fetch origin && git switch -c <branch> origin/<base>` and `git commit -m <msg> --trailer "Workit-Session: <the env WORKIT_SESSION_ID>" --trailer "<host attribution>" -- <paths in SCOPE>`, because worktree isolation refuses every `workit git` form |
111
+ | Claude Code | `Session: your env already has WORKIT_SESSION_ID (the lead's id)`; no `export`, no hand-minted id (SubagentStart issues `<lead>:<agent id>` only to verifiers and reviewers, never to an implementer, so the worker's commits carry the lead's id) | `workit git branch <branch> --base <base>`, `workit git commit -m <msg> -- <paths in SCOPE>`: the implementer runs without Claude Code's worktree isolation, which refused every `workit git` form |
110
112
  | OpenCode, Pi, Codex | `Session: your host already sets WORKIT_SESSION_ID in your shell`; export nothing (the host derives the id and an export cannot replace it) | `workit git branch <branch> --base <base>`, `workit git commit -m <msg> -- <paths in SCOPE>` |
111
113
  | Cursor, CI, a plain terminal | `export WORKIT_SESSION_ID=<lead>-w-<slice>` (`+try<n>` for a race attempt): declared, not checked | the same `workit git` forms |
112
114
 
113
- `MODE: resume` is `git switch <branch>` on every host (the branch exists; the
114
- lead removed the dead worker's worktree after its exit, so the switch
115
- succeeds), then continue from its head. A race attempt (`--attempt <n>`)
116
- follows its host's row with the branch `<branch>-try<n>`.
115
+ Once `workit fanout worktree create <slice>` made the worktree, the brief's
116
+ WORKTREE line names it and RULES 1 and 3 run against it on every host:
117
+ `workit --cwd <worktree> git commit …`, `workit --cwd <worktree> check <name>`,
118
+ one verb per call. The worktree is already on the branch (MODE new or resume),
119
+ so no branch command comes first. Without one, `MODE: resume` is
120
+ `git switch <branch>` (the branch exists), then continue from its head. A race
121
+ attempt (`--attempt <n>`) follows its host's row with the branch
122
+ `<branch>-try<n>`.
117
123
 
118
124
  ## Worker rules (`workit fanout brief` appends them as RULES)
119
125
 
120
- 1. First command: create the branch (`MODE: new`; the worktree may start on a
121
- name that breaks branch policy) or `git switch` to it (`MODE: resume`),
122
- as the table above says for your host.
126
+ 1. Work only in WORKTREE, already on the branch, one verb per call against it
127
+ (`workit --cwd <worktree> …`); Workit's hooks deny an implementer's change
128
+ outside it. With no worktree made yet: create the branch (`MODE: new`) or
129
+ `git switch` to it (`MODE: resume`), as the table above says.
123
130
  2. Decide ambiguities yourself and record them:
124
131
  `workit ledger ruling "<what>" --why "<why>" --cost-if-wrong "<cost>"`.
125
132
  Stop only for an irreversible action, a security-sensitive one, or a side
126
133
  effect outside the worktree.
127
- 3. Commit as the table says (`workit git commit -m "<msg>" -- <paths in SCOPE>`,
128
- or plain git with the trailers on Claude Code); push only if the brief says
129
- so.
134
+ 3. Commit with `workit --cwd <worktree> git commit -m "<msg>" -- <paths in SCOPE>`;
135
+ push only if the brief says so.
130
136
  4. Never record a verdict on your own work.
@@ -35,8 +35,8 @@ parent is verified, appears there by itself.
35
35
 
36
36
  A slice is retried when it is STUCK, its worker exits without meeting
37
37
  ACCEPTANCE, or its verdict fails. Stop the worker and observe that it exited.
38
- Release its worktree: `workit fanout worktree release <slice>` (native
39
- worktrees: record `git -C <wt> status --short` first). Then spawn one fresh
38
+ Release its worktree: `workit fanout worktree release <slice>` (it records
39
+ `git status` first), and create it again for the retry. Then spawn one fresh
40
40
  worker with `workit fanout brief <slice> --mode resume`: same branch, and the
41
41
  brief points at its head and its ledger rows (last report, verdicts,
42
42
  rulings). If the retry fails too, escalate instead of a third worker:
@@ -69,9 +69,9 @@ list. The fixer applies that list once, then one focused recheck follows.
69
69
  When a hard slice has an uncertain approach and a cheap VERIFY, run 2-3
70
70
  attempts at once instead of retrying in series. `workit fanout brief <slice>
71
71
  --attempt <n>` gives each attempt its own branch (`<branch>-try<n>`), session
72
- and scratch dir. Each attempt needs its own worktree: Claude Code's
73
- `implementer` isolates natively; on other hosts `fanout worktree create` makes
74
- only the slice's own worktree, so run the attempts one after another there.
72
+ and scratch dir. Each attempt needs its own worktree, and `fanout worktree
73
+ create` makes only the slice's own one, so run the attempts one after another
74
+ there on every host.
75
75
  Verify every attempt, keep the one with the best verdict (accepted first,
76
76
  then the fewest changed files, then the earliest), point the slice branch at
77
77
  it (`git branch <branch> <winner>`), have the verifier record the verdict on
@@ -109,7 +109,7 @@ Which session id a verdict carries, per host:
109
109
  `WORKIT_SESSION_ID=<lead>:verifier-<random hex>`, a fresh suffix per
110
110
  verifier. Cursor cannot check it: it is declared, not derived.
111
111
  - A worker's commits: on Claude Code the lead's `WORKIT_SESSION_ID` already in
112
- its env (SubagentStart issues no implementer id; plain git, no export, so the
112
+ its env (SubagentStart issues no implementer id; no export, so the
113
113
  lead authored those commits: a verifier or reviewer records the verdict, not
114
114
  the lead); on OpenCode, Pi and Codex the id the host already
115
115
  puts in its shell; on Cursor, CI and a plain terminal the brief's
@@ -36,17 +36,19 @@ description: Build a requested change in small verified steps - follow local pat
36
36
  when that was requested, or the request implies delivery and the effective
37
37
  endpoint in `workit grant show` is `pr`, `green` or `merged`; `green` and
38
38
  `merged` keep babysitting. Otherwise it is `commit`.
39
- 7. Verify, as far as the policy asks. Normal risk: after `workit check test`
40
- passes, record your own `workit ledger verdict tests-verified --self --how
41
- "<what you ran>"`; it reads self-reviewed, never verified, and is enough.
42
- Ask a non-author only for high risk, a workspace with `verification:
43
- independent` (`workit grant show`) or a verified merge: a fresh verifier with its own host-derived session (Claude Code and Codex:
39
+ 7. Verify as far as the review lane asks (`workit policy assess`, `workit pr
40
+ status`). Lane none: no verdict. Lane self: after `workit check test` passes,
41
+ record `workit ledger verdict tests-verified --self --how "<what you ran>"`;
42
+ it reads self-reviewed, never verified, and is enough. Focused, full, high
43
+ risk or `verification: self|independent` merges: one fresh verifier per PR after the last push, its own host-derived session (Claude Code: the `verifier` agent, `model: sonnet` when focused; Codex:
44
44
  the `verifier` agent; OpenCode: a child session; Pi: a supervised worker),
45
45
  never a hand-minted id. Before saying done, reconcile
46
46
  every named deliverable against the target checkout and observe it (for a push:
47
47
  `workit verify-delivery push`).
48
48
 
49
49
  Independent slices that could run in parallel: fan them out (workit-fanout).
50
+ One `implementer` slice: `workit fanout worktree create <slice>` before spawning,
51
+ then `workit fanout brief <slice>`; no host isolation (Workit's hooks confine it).
50
52
 
51
53
  Stay inline by default (workspace `delegation: inline-first`). Hand work off
52
54
  only past the thresholds: an explorer after about 3 tool calls or 10k tokens
@@ -13,6 +13,9 @@ OpenCode, a child session; Pi, a supervised worker. Never hand-mint one such
13
13
  as `<lead>-v1`: it is refused as `forged_session`.
14
14
  An author's `--self` verdict reads self-reviewed and never counts as
15
15
  independent; `type-check-only` never proves a behavior change.
16
+ Depth follows the review lane in `workit pr status`: focused reads the diff and
17
+ runs its tests; full also drives the real surface (verify-<app>). Review once
18
+ per PR after its last push; a second pass on an unchanged head is waste.
16
19
 
17
20
  1. Pin the candidate: `git rev-parse HEAD`, the base, `git diff <base>...HEAD`;
18
21
  for a PR, `workit pr status --json` (checks, unresolved threads).
@@ -51,8 +51,7 @@ the same check. Fix round and recheck both failed: a decider (workit-fanout).
51
51
  allows; no sleep loop. Red: read `logTail`, classify. Flake or infra: one
52
52
  `workit ci rerun --failed --reason flake|infra` per head. Real: reproduce
53
53
  with `workit check`, fix the root cause, batch every fix in one push.
54
- 6. **Verified.** High risk, `verification: independent` or a verified merge: a
55
- non-author verdict (workit-review); else a self verdict stands. Land only when
54
+ 6. **Verified.** The lane in `workit pr status` decides: none, no verdict; self, your `--self` verdict; focused or full (or `verification: self|independent`), one non-author verifier per PR after the last push, never per push (workit-review), on Claude Code `model: sonnet` when focused. Land only when
56
55
  granted: `workit stack land` or `workit pr merge`; `--unverified --reason` if user-asked.
57
56
  7. **Observe it landed:** `workit verify-delivery pr` or `merge`. After a failed
58
57
  verdict or a check red 3+ times, at any endpoint: offer `/wk-retro` in one
package/src/autonomy.ts CHANGED
@@ -185,17 +185,28 @@ function endpointView(entry: unknown, context: EndpointContext): EndpointView {
185
185
  }
186
186
 
187
187
  /**
188
- * How a normal-risk behavior change is verified (S17). `self` (default): an
189
- * observed passing `workit check test` plus the author's own `--self`
190
- * verdict, labelled self-reviewed. `independent`: a verdict from a session
191
- * that did not author the branch. User config only, like grants.
188
+ * How a behavior change is verified (S17, #333). `proportional` (default):
189
+ * the review lane computed from the diff decides (core/policy/lane.ts): none
190
+ * needs no verdict, self the author's own `--self` verdict, focused and full
191
+ * a non-author verdict, at every gate including merge. `self`: an observed
192
+ * passing `workit check test` plus the author's own `--self` verdict at
193
+ * normal risk, labelled self-reviewed; merge still needs a non-author
194
+ * verdict. `independent`: a verdict from a session that did not author the
195
+ * branch. User config only, like grants.
192
196
  */
193
- export type VerificationMode = "self" | "independent";
194
- export const DEFAULT_VERIFICATION: VerificationMode = "self";
195
- export const verificationOf = (entry: unknown): VerificationMode =>
196
- (entry as { verification?: unknown } | null)?.verification === "independent"
197
- ? "independent"
198
- : "self";
197
+ export type VerificationMode = "self" | "independent" | "proportional";
198
+ export const VERIFICATION_MODES: readonly VerificationMode[] = [
199
+ "proportional",
200
+ "self",
201
+ "independent",
202
+ ];
203
+ export const DEFAULT_VERIFICATION: VerificationMode = "proportional";
204
+ export const verificationOf = (entry: unknown): VerificationMode => {
205
+ const value = (entry as { verification?: unknown } | null)?.verification;
206
+ return VERIFICATION_MODES.includes(value as VerificationMode)
207
+ ? (value as VerificationMode)
208
+ : DEFAULT_VERIFICATION;
209
+ };
199
210
 
200
211
  export type AutonomySource = "autonomy" | "autoApprove" | "default";
201
212
 
@@ -415,7 +426,7 @@ export function writeGrants(
415
426
  dir: string = grantsDir(),
416
427
  /** A new default endpoint; `null` removes it (back to `commit`). */
417
428
  endpoint?: DefaultEndpoint | null,
418
- /** A new verification mode; `null` removes it (back to `self`). */
429
+ /** A new verification mode; `null` removes it (back to `proportional`). */
419
430
  verification?: VerificationMode | null,
420
431
  ): GrantWrite {
421
432
  const current = readWorkspacesResult(dir);
@@ -42,12 +42,19 @@ export function checksConfigFile(root: string): string {
42
42
  return !fs.existsSync(current) && fs.existsSync(legacy) ? legacy : current;
43
43
  }
44
44
 
45
- /** Create `<root>/.workit/` with its self-ignoring `.gitignore` (idempotent) and return the dir. */
45
+ const WORKIT_GITIGNORE = "*\n!risk-paths\n";
46
+
47
+ /**
48
+ * Create `<root>/.workit/` with its `.gitignore` (idempotent) and return the
49
+ * dir. It ignores everything but `risk-paths`, which a repo commits; the old
50
+ * `*` alone is rewritten.
51
+ */
46
52
  export function ensureWorkitDir(root: string): string {
47
53
  const dir = path.join(root, ".workit");
48
54
  fs.mkdirSync(dir, { recursive: true });
49
55
  const ignore = path.join(dir, ".gitignore");
50
- if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, "*\n");
56
+ if (!fs.existsSync(ignore) || fs.readFileSync(ignore, "utf8") === "*\n")
57
+ fs.writeFileSync(ignore, WORKIT_GITIGNORE);
51
58
  return dir;
52
59
  }
53
60
 
@@ -115,9 +115,9 @@ Autonomy contract:
115
115
  Prose is a note: only a configured check the CLI ran (\`workit check <name>\`)
116
116
  satisfies a gate; an ad-hoc \`workit check -- <cmd>\` never does.
117
117
  - Author is not verifier: your own verdict is \`--self\`, shown as
118
- self-reviewed, never verified. High risk, \`verification: independent\` and
119
- merge need a verifier with its own host-derived session (a subagent's
120
- \`<lead>:<child>\`); never hand-mint one (\`<lead>-v1\`).
118
+ self-reviewed, never verified. High risk, a focused or full lane and
119
+ merge under \`verification: self|independent\` need a verifier with its
120
+ own host-derived session (a subagent's \`<lead>:<child>\`); never hand-mint one (\`<lead>-v1\`).
121
121
  - A local-commit endpoint does not imply PR readiness, and a local commit alone
122
122
  is not evidence of a requested remote push. Before reporting delivery,
123
123
  reconcile every requested item and observe it (\`workit verify-delivery\`).
@@ -0,0 +1,97 @@
1
+ // `linguist-generated` from the `.gitattributes` files of one tree (#333,
2
+ // review finding mvkcoxgb). The review lane reads them from the base
3
+ // revision only: never `.git/info/attributes`, a global attributes file or
4
+ // the working tree, so a change cannot mark its own code generated. What this
5
+ // parser does not understand (quoted patterns, macros, directory patterns)
6
+ // marks nothing generated, which only raises a lane.
7
+
8
+ type Rule = { regex: RegExp; generated: boolean };
9
+
10
+ /** A gitattributes pattern relative to `dir` ("" for the root) as a regex over repo paths. */
11
+ function patternRegex(dir: string, pattern: string): RegExp | null {
12
+ if (!pattern || pattern.endsWith("/") || pattern.startsWith('"') || pattern.startsWith("!"))
13
+ return null;
14
+ const anchored = pattern.includes("/");
15
+ const body = pattern.replace(/^\//u, "");
16
+ let source = "";
17
+ for (let index = 0; index < body.length; index += 1) {
18
+ const char = body[index];
19
+ if (char === "*" && body[index + 1] === "*") {
20
+ const slash = body[index + 2] === "/";
21
+ source += slash ? "(?:.*/)?" : ".*";
22
+ index += slash ? 2 : 1;
23
+ } else if (char === "*") source += "[^/]*";
24
+ else if (char === "?") source += "[^/]";
25
+ else if (char === "[") {
26
+ const end = body.indexOf("]", index + 2);
27
+ if (end < 0) return null;
28
+ const set = body
29
+ .slice(index + 1, end)
30
+ .replace(/^!/u, "^")
31
+ .replace(/\\/gu, "\\\\");
32
+ source += `[${set}]`;
33
+ index = end;
34
+ } else if (char === "\\" && index + 1 < body.length) {
35
+ source += body[index + 1].replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
36
+ index += 1;
37
+ } else source += char.replace(/[.*+?^${}()|[\]\\/]/gu, "\\$&");
38
+ }
39
+ const prefix = dir ? `${dir.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/` : "";
40
+ try {
41
+ return new RegExp(anchored ? `^${prefix}${source}$` : `^${prefix}(?:.*/)?${source}$`, "u");
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** The `linguist-generated` rules of one `.gitattributes` file in `dir`. */
48
+ function rulesOf(dir: string, text: string): Rule[] {
49
+ const out: Rule[] = [];
50
+ for (const raw of text.split("\n")) {
51
+ const line = raw.trim();
52
+ if (!line || line.startsWith("#") || line.startsWith("[attr]")) continue;
53
+ const [pattern, ...attributes] = line.split(/\s+/u);
54
+ let generated: boolean | null = null;
55
+ for (const attribute of attributes) {
56
+ if (attribute === "linguist-generated" || attribute === "linguist-generated=true")
57
+ generated = true;
58
+ else if (
59
+ attribute === "-linguist-generated" ||
60
+ attribute === "!linguist-generated" ||
61
+ attribute.startsWith("linguist-generated=")
62
+ )
63
+ generated = false;
64
+ }
65
+ if (generated === null) continue;
66
+ const regex = patternRegex(dir, pattern);
67
+ if (regex) out.push({ regex, generated });
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Of `paths`, those the `.gitattributes` files of one tree mark
74
+ * `linguist-generated` (`files`: each file's repo path and text). A deeper
75
+ * file overrides a shallower one, and a later line an earlier one.
76
+ */
77
+ export function generatedByAttributes(
78
+ files: ReadonlyMap<string, string>,
79
+ paths: readonly string[],
80
+ ): Set<string> {
81
+ const rules = [...files]
82
+ .map(([file, text]) => {
83
+ const dir = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : "";
84
+ return {
85
+ depth: dir ? dir.split("/").length : 0,
86
+ rules: rulesOf(dir, text),
87
+ };
88
+ })
89
+ .toSorted((left, right) => left.depth - right.depth)
90
+ .flatMap((entry) => entry.rules);
91
+ const out = new Set<string>();
92
+ for (const file of paths) {
93
+ const last = rules.findLast((rule) => rule.regex.test(file));
94
+ if (last?.generated) out.add(file);
95
+ }
96
+ return out;
97
+ }
@@ -0,0 +1,20 @@
1
+ import type { Judgment } from "../task-contract";
2
+ import { TaskStore } from "../task-store";
3
+ import { latestJudgment } from "./derive";
4
+
5
+ /** The open task's latest judgment (the one policy/derive.ts reads), or null when none is recorded. Never throws. */
6
+ export function currentJudgment(cwd: string): Judgment | null {
7
+ try {
8
+ const store = new TaskStore(cwd);
9
+ const listed = store.listTaskIndex();
10
+ const key = store.currentKey();
11
+ if (!listed.ok || !key.ok) return null;
12
+ const entry = listed.data
13
+ .filter((item) => item.key === key.data.key && item.status !== "closed")
14
+ .toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0];
15
+ const task = entry ? store.readTask(entry.id) : null;
16
+ return task?.ok === true ? latestJudgment(task.data) : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { currentBranch } from "../../git/rev";
7
7
  import { STRONG_RESULTS, checkVerdicts, readLedger, type ReadRow } from "../../ledger";
8
8
  import type { VerificationMode } from "../../autonomy";
9
+ import { branchLane, laneNeed, type BranchLane } from "./lane";
9
10
  import {
10
11
  requirementId,
11
12
  sha256,
@@ -21,6 +22,7 @@ export const RULES = {
21
22
  self: "verdict:self",
22
23
  nonAuthor: "verdict:non-author",
23
24
  verified: "verdict:verified",
25
+ lane: "verdict:lane",
24
26
  product: "decision:product",
25
27
  plan: "plan",
26
28
  } as const;
@@ -46,8 +48,11 @@ const normalizedScope = (scope: Scope): Scope => ({
46
48
  /**
47
49
  * | judgment | requirement |
48
50
  * | behaviorChange | check:test (configured, fresh tree), close |
49
- * | behaviorChange && risk = normal | verdict:self (default) or verdict:non-author |
50
- * | | (workspace verification: independent), close |
51
+ * | risk = normal, | verdict:lane: the review lane from the diff |
52
+ * | verification: proportional | decides (core/policy/lane.ts; met under lane |
53
+ * | (whatever behaviorChange says) | none), close |
54
+ * | behaviorChange && risk = normal | verdict:self (verification: self) or |
55
+ * | | verdict:non-author (independent), close |
51
56
  * | risk = high | verdict:verified (ledger), close |
52
57
  * | productChoiceOpen | decision:product, before write |
53
58
  * | needsPlan or risk = high | plan (doc ref, soft), before write |
@@ -62,6 +67,7 @@ export function deriveRequirements(
62
67
  ): Requirement[] {
63
68
  const out: Requirement[] = [];
64
69
  const base = { scope, dependentAction: null };
70
+ const proportional = verification === "proportional";
65
71
  if (judgment.behaviorChange)
66
72
  out.push(
67
73
  derived({
@@ -74,7 +80,25 @@ export function deriveRequirements(
74
80
  acceptanceAllowed: false,
75
81
  }),
76
82
  );
77
- if (judgment.behaviorChange && judgment.riskTier === "normal")
83
+ // The diff, not the behavior-change judgment, decides: the requirement is
84
+ // derived at normal risk whatever behaviorChange says and is met under lane
85
+ // none (review finding mvkcoxgb). Trivial work keeps needing nothing at
86
+ // close; the merge gate still reads its lane.
87
+ if (proportional && judgment.riskTier === "normal")
88
+ out.push(
89
+ derived({
90
+ ...base,
91
+ ruleId: RULES.lane,
92
+ dimension: "review",
93
+ reason:
94
+ "The review lane computed from the diff decides the verdict: none needs none, self the author's own (shown as self-reviewed), focused and full a non-author one.",
95
+ satisfaction:
96
+ "Read the lane in `workit policy assess` or `workit pr status`; record what it asks: `workit ledger verdict tests-verified --self --how <what you ran>` for self, or hand focused and full to a verifier.",
97
+ before: "close",
98
+ acceptanceAllowed: false,
99
+ }),
100
+ );
101
+ if (!proportional && judgment.behaviorChange && judgment.riskTier === "normal")
78
102
  out.push(
79
103
  verification === "independent"
80
104
  ? derived({
@@ -264,7 +288,29 @@ const planRecorded = (task: TaskRecord, root: string): RuleStatus => {
264
288
  * an accepted independent one; `verdict:verified` an accepted independent
265
289
  * `verified` verdict of kind `live`.
266
290
  */
267
- const verdict = (requirement: Requirement, root: string, ledger: LedgerView): RuleStatus => {
291
+ /** The review lane of the checked-out work (the working tree against the default base). */
292
+ export const workingLane = (task: TaskRecord, root: string): BranchLane =>
293
+ branchLane(root, { head: null, workingTree: true, judgment: latestJudgment(task) });
294
+
295
+ /** `verdict:lane`: the rule the lane asks for, or met under lane none. */
296
+ const laneVerdict = (task: TaskRecord, root: string, ledger: LedgerView): RuleStatus => {
297
+ // With no branch (outside git) there is no diff to read and nothing could
298
+ // hold a verdict: a judgment of no behavior change owes none, as before lanes.
299
+ if (!ledger.branch() && latestJudgment(task)?.behaviorChange === false)
300
+ return {
301
+ met: true,
302
+ reason: "no branch to read a lane from, and the judgment records no behavior change",
303
+ decisionIds: [],
304
+ };
305
+ const lane = workingLane(task, root);
306
+ const label = `lane ${lane.lane} (${lane.reasons.join("; ")})`;
307
+ const need = laneNeed(lane.lane);
308
+ if (need === "none") return { met: true, reason: `${label}: no verdict needed`, decisionIds: [] };
309
+ const status = verdict(need === "self" ? RULES.self : RULES.nonAuthor, root, ledger);
310
+ return { ...status, reason: `${label}: ${status.reason}` };
311
+ };
312
+
313
+ const verdict = (ruleId: string, root: string, ledger: LedgerView): RuleStatus => {
268
314
  const branch = ledger.branch();
269
315
  if (!branch)
270
316
  return { met: false, reason: "no branch is checked out to hold a verdict", decisionIds: [] };
@@ -274,12 +320,12 @@ const verdict = (requirement: Requirement, root: string, ledger: LedgerView): Ru
274
320
  STRONG_RESULTS.has(String(entry.verdict.result));
275
321
  const met =
276
322
  !failing &&
277
- (requirement.ruleId === RULES.verified
323
+ (ruleId === RULES.verified
278
324
  ? check.verdicts.find(
279
325
  (entry) =>
280
326
  entry.accepted && entry.kind === "live" && String(entry.verdict.result) === "verified",
281
327
  )
282
- : requirement.ruleId === RULES.nonAuthor
328
+ : ruleId === RULES.nonAuthor
283
329
  ? check.verdicts.find((entry) => entry.accepted && strong(entry))
284
330
  : check.verdicts.find((entry) => entry.current && strong(entry)));
285
331
  if (met)
@@ -294,9 +340,9 @@ const verdict = (requirement: Requirement, root: string, ledger: LedgerView): Ru
294
340
  ? "type-check-only does not prove a behavior change"
295
341
  : (check.accepted.reasons.join(", ") || "no_verdict").replace(/_/g, " ");
296
342
  const unblock =
297
- requirement.ruleId === RULES.self
343
+ ruleId === RULES.self
298
344
  ? "record your own verdict after an observed `workit check test`: `workit ledger verdict tests-verified --self --how <what you ran>` (shown as self-reviewed), or have a verifier record one"
299
- : `an independent session records \`workit ledger verdict verified${requirement.ruleId === RULES.verified ? " --kind live" : ""} --how <what it exercised>\``;
345
+ : `an independent session records \`workit ledger verdict verified${ruleId === RULES.verified ? " --kind live" : ""} --how <what it exercised>\``;
300
346
  return { met: false, reason: `${why}: ${unblock}`, decisionIds: [] };
301
347
  };
302
348
 
@@ -315,7 +361,9 @@ export function derivedRuleStatus(
315
361
  case RULES.self:
316
362
  case RULES.nonAuthor:
317
363
  case RULES.verified:
318
- return verdict(requirement, root, ledger);
364
+ return verdict(requirement.ruleId, root, ledger);
365
+ case RULES.lane:
366
+ return laneVerdict(task, root, ledger);
319
367
  default:
320
368
  return null;
321
369
  }