@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 +1 -1
- package/skills/workit-fanout/SKILL.md +5 -5
- package/skills/workit-fanout/references/brief.md +17 -11
- package/skills/workit-fanout/references/orchestration.md +6 -6
- package/skills/workit-implement/SKILL.md +7 -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/fanout-brief.ts +23 -15
- package/src/fanout-worktree.ts +20 -6
- 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/handle.ts +29 -8
- package/src/hooks/raw-git.ts +19 -0
- package/src/hooks/stop.ts +50 -33
- package/src/hooks/worktree-confine.ts +363 -0
- package/src/hooks/write-gate.ts +189 -35
- package/src/ledger.ts +7 -2
- package/src/stack.ts +54 -12
- package/src/worktree-deps.ts +142 -0
package/package.json
CHANGED
|
@@ -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
|
|
24
|
-
|
|
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
|
-
|
|
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) |
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
128
|
-
|
|
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>` (
|
|
39
|
-
|
|
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
|
|
73
|
-
`
|
|
74
|
-
|
|
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;
|
|
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
|
|
40
|
-
|
|
41
|
-
"<what you ran>"`;
|
|
42
|
-
|
|
43
|
-
|
|
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.**
|
|
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
|
|
189
|
-
*
|
|
190
|
-
* verdict,
|
|
191
|
-
*
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 `
|
|
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);
|
package/src/check-config.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
56
|
+
if (!fs.existsSync(ignore) || fs.readFileSync(ignore, "utf8") === "*\n")
|
|
57
|
+
fs.writeFileSync(ignore, WORKIT_GITIGNORE);
|
|
51
58
|
return dir;
|
|
52
59
|
}
|
|
53
60
|
|
package/src/core/methods.ts
CHANGED
|
@@ -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,
|
|
119
|
-
merge need a verifier with its
|
|
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
|
-
* |
|
|
50
|
-
* |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
:
|
|
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
|
-
|
|
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${
|
|
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
|
}
|