@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "8.23.0",
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": [
@@ -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
@@ -36,11 +36,11 @@ 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:
@@ -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
  }