@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/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
|
);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Dependencies for a fresh worktree (issue #310): a linked worktree has no
|
|
2
|
+
// node_modules, so a resolver walks up to whatever node_modules is above it
|
|
3
|
+
// (the main checkout's, for a worktree nested inside it) and a workspace
|
|
4
|
+
// package resolves to the main checkout's source: tests then run the wrong
|
|
5
|
+
// code. Instead of a package-manager install, the worktree gets its own
|
|
6
|
+
// node_modules that mirrors the main checkout's one level deep:
|
|
7
|
+
//
|
|
8
|
+
// - a symlink is copied as it is: a relative one (bun, npm and yarn link
|
|
9
|
+
// workspace packages as `../../packages/<name>`, and `.bin` entries as
|
|
10
|
+
// `../<pkg>/bin/x`) then resolves inside the worktree, which has the same
|
|
11
|
+
// layout; an absolute one into the main checkout (a Windows junction) is
|
|
12
|
+
// re-pointed at the worktree's counterpart;
|
|
13
|
+
// - `@scope` directories and `.bin` are mirrored the same way, one level;
|
|
14
|
+
// - anything else (a third-party package) is linked to the main checkout's,
|
|
15
|
+
// so installed packages are shared, never copied.
|
|
16
|
+
//
|
|
17
|
+
// It runs only where the worktree has no node_modules of its own and git
|
|
18
|
+
// ignores node_modules there (else the links would show as changes and block
|
|
19
|
+
// release). `git worktree remove` unlinks the links and never follows them.
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { gitRun } from "./fanout";
|
|
23
|
+
|
|
24
|
+
const MODULES = "node_modules";
|
|
25
|
+
/** Directories searched for node_modules below the checkout root (packages/<name>). */
|
|
26
|
+
const MAX_DEPTH = 2;
|
|
27
|
+
|
|
28
|
+
export type LinkedDependencies = {
|
|
29
|
+
/** The main checkout the links point at. */
|
|
30
|
+
from: string;
|
|
31
|
+
/** The node_modules directories made, relative to the worktree. */
|
|
32
|
+
dirs: string[];
|
|
33
|
+
/** Links made across them. */
|
|
34
|
+
entries: number;
|
|
35
|
+
ms: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const within = (child: string, parent: string): boolean => {
|
|
39
|
+
const relative = path.relative(parent, child);
|
|
40
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** node_modules directories of the main checkout, as paths relative to it. */
|
|
44
|
+
function moduleDirs(main: string): string[] {
|
|
45
|
+
const out: string[] = [];
|
|
46
|
+
const walk = (relative: string, depth: number) => {
|
|
47
|
+
let entries: fs.Dirent[];
|
|
48
|
+
try {
|
|
49
|
+
entries = fs.readdirSync(path.join(main, relative), { withFileTypes: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isDirectory()) continue;
|
|
55
|
+
const child = path.join(relative, entry.name);
|
|
56
|
+
if (entry.name === MODULES) out.push(child);
|
|
57
|
+
else if (depth < MAX_DEPTH && !entry.name.startsWith(".")) walk(child, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk("", 0);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function linkEntry(main: string, worktree: string, source: string, target: string): void {
|
|
65
|
+
const stat = fs.lstatSync(source);
|
|
66
|
+
if (stat.isSymbolicLink()) {
|
|
67
|
+
const link = fs.readlinkSync(source);
|
|
68
|
+
// An absolute link into the main checkout (outside its node_modules) is a
|
|
69
|
+
// workspace package: point it at the worktree's copy.
|
|
70
|
+
const outside =
|
|
71
|
+
path.isAbsolute(link) &&
|
|
72
|
+
within(link, main) &&
|
|
73
|
+
!path.relative(main, link).split(path.sep).includes(MODULES);
|
|
74
|
+
const to = outside ? path.join(worktree, path.relative(main, link)) : link;
|
|
75
|
+
fs.symlinkSync(to, target, process.platform === "win32" ? "junction" : undefined);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fs.symlinkSync(
|
|
79
|
+
source,
|
|
80
|
+
target,
|
|
81
|
+
process.platform === "win32" ? (stat.isDirectory() ? "junction" : "file") : undefined,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Mirror `source` into a new directory `target`; returns the links made. */
|
|
86
|
+
function mirror(main: string, worktree: string, source: string, target: string, top: boolean) {
|
|
87
|
+
fs.mkdirSync(target);
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
90
|
+
const from = path.join(source, entry.name);
|
|
91
|
+
const to = path.join(target, entry.name);
|
|
92
|
+
if (top && entry.isDirectory() && (entry.name.startsWith("@") || entry.name === ".bin"))
|
|
93
|
+
count += mirror(main, worktree, from, to, false);
|
|
94
|
+
else {
|
|
95
|
+
linkEntry(main, worktree, from, to);
|
|
96
|
+
count += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Give `worktree` node_modules that resolve its own workspace packages and
|
|
104
|
+
* share the main checkout's installed ones. Null when there is nothing to
|
|
105
|
+
* link (no node_modules in the main checkout), with a note when it was
|
|
106
|
+
* skipped; never throws.
|
|
107
|
+
*/
|
|
108
|
+
export function linkWorktreeDependencies(
|
|
109
|
+
main: string,
|
|
110
|
+
worktree: string,
|
|
111
|
+
): { linked: LinkedDependencies | null; notes: string[] } {
|
|
112
|
+
const started = performance.now();
|
|
113
|
+
const notes: string[] = [];
|
|
114
|
+
const dirs: string[] = [];
|
|
115
|
+
let entries = 0;
|
|
116
|
+
for (const relative of moduleDirs(main)) {
|
|
117
|
+
const target = path.join(worktree, relative);
|
|
118
|
+
if (!fs.existsSync(path.dirname(target)) || fs.existsSync(target)) continue;
|
|
119
|
+
// The trailing slash: the directory does not exist yet, and `node_modules/` only matches one.
|
|
120
|
+
if (!gitRun(worktree, ["check-ignore", "-q", `${relative.split(path.sep).join("/")}/`]).ok) {
|
|
121
|
+
notes.push(
|
|
122
|
+
`${relative.split(path.sep).join("/")} is not ignored by git, so it was not linked from ${main}; install dependencies in the worktree yourself`,
|
|
123
|
+
);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
entries += mirror(main, worktree, path.join(main, relative), target, true);
|
|
128
|
+
dirs.push(relative.split(path.sep).join("/"));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
131
|
+
notes.push(
|
|
132
|
+
`could not link ${relative} from ${main} (${(error as Error).message}); install dependencies in the worktree yourself`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (dirs.length === 0) return { linked: null, notes };
|
|
137
|
+
const ms = Math.round(performance.now() - started);
|
|
138
|
+
notes.push(
|
|
139
|
+
`linked ${dirs.join(", ")} from ${main} (${entries} entries, ${ms} ms): workspace packages resolve to this worktree, installed packages are shared`,
|
|
140
|
+
);
|
|
141
|
+
return { linked: { from: main, dirs, entries, ms }, notes };
|
|
142
|
+
}
|