@reneturcios/greenlight 0.10.1 → 0.11.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/README.md +3 -0
- package/dist/autopilot-fixes.d.ts +50 -0
- package/dist/autopilot-fixes.js +216 -0
- package/dist/autopilot-fixes.js.map +1 -0
- package/dist/autopilot-git.d.ts +83 -0
- package/dist/autopilot-git.js +164 -0
- package/dist/autopilot-git.js.map +1 -0
- package/dist/autopilot-triage.d.ts +93 -0
- package/dist/autopilot-triage.js +222 -0
- package/dist/autopilot-triage.js.map +1 -0
- package/dist/autopilot.d.ts +207 -0
- package/dist/autopilot.js +903 -0
- package/dist/autopilot.js.map +1 -0
- package/dist/cli.js +44 -0
- package/dist/cli.js.map +1 -1
- package/dist/help.js +18 -0
- package/dist/help.js.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +6 -3
package/README.md
CHANGED
|
@@ -276,6 +276,9 @@ The Agent Policy Index field work kept finding agent bans Greenlight itself miss
|
|
|
276
276
|
- **Scoped bans dual-class instead of collapsing.** A ban aimed at one kind of agent operation (TypeScript's "bulk, agent-driven contributions", ppt-master's "purely AI-generated PRs submitted without human review") now surfaces as both `agents_banned` for autonomous operation and `agents_disclosure` for the human-led path. An explicit prohibition (nuxt, ripgrep, zed) still stands on its own, and explicit welcome or waiver language (hyperframes welcomes AI help and waives disclosure) never reads as a ban.
|
|
277
277
|
- **Friction JSON states its units.** `friction --json` now carries `outsider_merge_share` (0 to 1 fraction) and `outsider_merge_share_pct` (0 to 100), plus `signals.externalPRMergeRate_pct` next to the existing fraction field. The human-readable output is unchanged.
|
|
278
278
|
|
|
279
|
+
### v0.11.0: Autopilot
|
|
280
|
+
|
|
281
|
+
Open PRs were dying between babysit sweeps: a bot review sitting unanswered, a CI failure on the PR's own code, a drifted branch, a missing DCO trailer. Autopilot closes that loop. `greenlight autopilot` (report-only by default, `--apply` to fix) collects reviews, comments, and check results per open PR, verifies bot findings against the code at head (REAL, FALSE_POSITIVE with quoted counter-evidence, or NEEDS_HUMAN), and triages CI failures as introduced vs pre-existing against the base branch. The mechanical items fix themselves: DCO gaps get message-only surgery with byte-identical trees, merge conflicts resolve only when provably mechanical (lockfiles regenerated, changelogs unioned, version or counter bumps), and formatting pushes only when the diff is whitespace-only. Everything else becomes a plan, never a speculative edit. Fixes land on your fork branch with Rene's sign-off, respect the capability ledger's blocked repos, and replies queue as drafts behind the comment block instead of posting. Also exposed as the read-only `autopilot_report` MCP tool.
|
|
279
282
|
### v0.10.1: never again hardening
|
|
280
283
|
|
|
281
284
|
Three failures on 2026-10-18, each paid for in full, each now structurally impossible to repeat:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type ConflictStrategy = "lockfile" | "changelog" | "version-bump" | "manual";
|
|
2
|
+
/** Decide how a conflicted file may be resolved: mechanical, or manual only. */
|
|
3
|
+
export declare function conflictFileStrategy(path: string): ConflictStrategy;
|
|
4
|
+
export interface ConflictHunk {
|
|
5
|
+
base: string[];
|
|
6
|
+
ours: string[];
|
|
7
|
+
theirs: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse git conflict markers into hunks. Returns null when the text has no
|
|
11
|
+
* markers or a marker is malformed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseConflictHunks(text: string): ConflictHunk[] | null;
|
|
14
|
+
/**
|
|
15
|
+
* Mechanical single-line bump resolution (reticle's guard 13->14 class):
|
|
16
|
+
* when a hunk changed exactly one token on each side, one side kept the
|
|
17
|
+
* base value, and the other side is a strict increase (semver or integer
|
|
18
|
+
* counter), the increase wins. Anything else is not mechanical: null.
|
|
19
|
+
*
|
|
20
|
+
* The hunk's base lines are passed explicitly (git markers do not carry them).
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveVersionBumpHunk(hunk: ConflictHunk, baseLines: string[]): {
|
|
23
|
+
resolved: string[];
|
|
24
|
+
taken: "ours" | "theirs";
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Changelog merge: union of added lines from both sides, base lines kept in
|
|
28
|
+
* order, each side's new lines appended in the order they appear. Exact
|
|
29
|
+
* duplicate lines are not duplicated.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveChangelogMerge(base: string, ours: string, theirs: string): string;
|
|
32
|
+
/** Detect the package manager for lockfile regeneration from lockfile name. */
|
|
33
|
+
export declare function lockfileManager(lockfileName: string): string | null;
|
|
34
|
+
export interface GitRunner {
|
|
35
|
+
run: (cwd: string, args: string[], stdin?: string) => Promise<{
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export declare function defaultGitRunner(): GitRunner;
|
|
41
|
+
/**
|
|
42
|
+
* Formatting-only verification: after running a formatter, the diff must be
|
|
43
|
+
* whitespace-only. `git diff -w` empty means no non-whitespace changes.
|
|
44
|
+
*/
|
|
45
|
+
export declare function diffIsFormattingOnly(git: GitRunner, cwd: string): Promise<boolean>;
|
|
46
|
+
/** Detect a formatter path in a repo dir: npm scripts or known configs. */
|
|
47
|
+
export declare function detectFormatter(files: string[], scripts: Record<string, string>): {
|
|
48
|
+
name: string;
|
|
49
|
+
cmd: string[];
|
|
50
|
+
} | null;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanical fix resolvers for Autopilot (v0.11.0).
|
|
3
|
+
*
|
|
4
|
+
* The rule is strict: Autopilot may only resolve conflicts that are
|
|
5
|
+
* provably mechanical. Anything that touches source logic in a
|
|
6
|
+
* non-trivial way aborts the fix and reports NEEDS_HUMAN with the
|
|
7
|
+
* conflicted files. Every resolver below is a pure function, tested
|
|
8
|
+
* against fixtures, so the merge decision is reviewable before it runs.
|
|
9
|
+
*/
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
const LOCKFILE_NAMES = new Set([
|
|
12
|
+
"package-lock.json",
|
|
13
|
+
"pnpm-lock.yaml",
|
|
14
|
+
"yarn.lock",
|
|
15
|
+
"bun.lockb",
|
|
16
|
+
"Cargo.lock",
|
|
17
|
+
"go.sum",
|
|
18
|
+
"poetry.lock",
|
|
19
|
+
"Pipfile.lock",
|
|
20
|
+
]);
|
|
21
|
+
/** Decide how a conflicted file may be resolved: mechanical, or manual only. */
|
|
22
|
+
export function conflictFileStrategy(path) {
|
|
23
|
+
const base = path.split("/").pop() ?? path;
|
|
24
|
+
if (LOCKFILE_NAMES.has(base))
|
|
25
|
+
return "lockfile";
|
|
26
|
+
if (/^changelog/i.test(base) || /^changes\.md$/i.test(base))
|
|
27
|
+
return "changelog";
|
|
28
|
+
return "manual";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse git conflict markers into hunks. Returns null when the text has no
|
|
32
|
+
* markers or a marker is malformed.
|
|
33
|
+
*/
|
|
34
|
+
export function parseConflictHunks(text) {
|
|
35
|
+
const hunks = [];
|
|
36
|
+
const lines = text.split("\n");
|
|
37
|
+
let i = 0;
|
|
38
|
+
let inConflict = false;
|
|
39
|
+
let ours = [];
|
|
40
|
+
let theirs = [];
|
|
41
|
+
let mid = false;
|
|
42
|
+
for (; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i];
|
|
44
|
+
if (line.startsWith("<<<<<<<")) {
|
|
45
|
+
if (inConflict)
|
|
46
|
+
return null;
|
|
47
|
+
inConflict = true;
|
|
48
|
+
mid = false;
|
|
49
|
+
ours = [];
|
|
50
|
+
theirs = [];
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!inConflict)
|
|
54
|
+
continue;
|
|
55
|
+
if (line.startsWith("=======")) {
|
|
56
|
+
mid = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith(">>>>>>>")) {
|
|
60
|
+
// base is unknown at this level; the caller supplies it per hunk.
|
|
61
|
+
hunks.push({ base: [], ours, theirs });
|
|
62
|
+
inConflict = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
(mid ? theirs : ours).push(line);
|
|
66
|
+
}
|
|
67
|
+
if (inConflict)
|
|
68
|
+
return null;
|
|
69
|
+
return hunks.length > 0 ? hunks : null;
|
|
70
|
+
}
|
|
71
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
72
|
+
function compareSemver(a, b) {
|
|
73
|
+
if (!SEMVER.test(a) || !SEMVER.test(b))
|
|
74
|
+
return null;
|
|
75
|
+
const pa = a.split(".").map((x) => Number(x));
|
|
76
|
+
const pb = b.split(".").map((x) => Number(x));
|
|
77
|
+
for (let k = 0; k < 3; k++) {
|
|
78
|
+
if (pa[k] !== pb[k])
|
|
79
|
+
return pa[k] < pb[k] ? -1 : 1;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
function compareCounter(a, b) {
|
|
84
|
+
if (!/^\d+$/.test(a) || !/^\d+$/.test(b))
|
|
85
|
+
return null;
|
|
86
|
+
const na = Number(a);
|
|
87
|
+
const nb = Number(b);
|
|
88
|
+
return na === nb ? 0 : na < nb ? -1 : 1;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Mechanical single-line bump resolution (reticle's guard 13->14 class):
|
|
92
|
+
* when a hunk changed exactly one token on each side, one side kept the
|
|
93
|
+
* base value, and the other side is a strict increase (semver or integer
|
|
94
|
+
* counter), the increase wins. Anything else is not mechanical: null.
|
|
95
|
+
*
|
|
96
|
+
* The hunk's base lines are passed explicitly (git markers do not carry them).
|
|
97
|
+
*/
|
|
98
|
+
export function resolveVersionBumpHunk(hunk, baseLines) {
|
|
99
|
+
if (hunk.ours.length !== 1 || hunk.theirs.length !== 1 || baseLines.length !== 1)
|
|
100
|
+
return null;
|
|
101
|
+
const [oursLine] = hunk.ours;
|
|
102
|
+
const [theirsLine] = hunk.theirs;
|
|
103
|
+
const [baseLine] = baseLines;
|
|
104
|
+
// Find the single changed token: walk from both ends.
|
|
105
|
+
const clean = (t) => t.replace(/[;,)]+$/, "");
|
|
106
|
+
const tokenOf = (oursTok, theirsTok, baseTok) => {
|
|
107
|
+
if (oursTok === theirsTok)
|
|
108
|
+
return null;
|
|
109
|
+
const oursKept = oursTok === baseTok;
|
|
110
|
+
const theirsKept = theirsTok === baseTok;
|
|
111
|
+
if (oursKept === theirsKept)
|
|
112
|
+
return null;
|
|
113
|
+
const bumped = clean(oursKept ? theirsTok : oursTok);
|
|
114
|
+
const kept = clean(oursKept ? oursTok : theirsTok);
|
|
115
|
+
const cmp = compareSemver(kept, bumped) ?? compareCounter(kept, bumped);
|
|
116
|
+
if (cmp === null || cmp >= 0)
|
|
117
|
+
return null;
|
|
118
|
+
return oursKept ? "theirs" : "ours";
|
|
119
|
+
};
|
|
120
|
+
// Tokenize on whitespace; the single differing token decides.
|
|
121
|
+
const ot = oursLine.split(/(\s+)/);
|
|
122
|
+
const tt = theirsLine.split(/(\s+)/);
|
|
123
|
+
const bt = baseLine.split(/(\s+)/);
|
|
124
|
+
if (ot.length !== tt.length || ot.length !== bt.length)
|
|
125
|
+
return null;
|
|
126
|
+
let decided = null;
|
|
127
|
+
for (let i = 0; i < ot.length; i++) {
|
|
128
|
+
const winner = tokenOf(ot[i], tt[i], bt[i]);
|
|
129
|
+
if (winner === null)
|
|
130
|
+
continue;
|
|
131
|
+
if (decided !== null && decided !== winner)
|
|
132
|
+
return null;
|
|
133
|
+
decided = winner;
|
|
134
|
+
}
|
|
135
|
+
if (decided === null)
|
|
136
|
+
return null;
|
|
137
|
+
return { resolved: decided === "ours" ? hunk.ours : hunk.theirs, taken: decided };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Changelog merge: union of added lines from both sides, base lines kept in
|
|
141
|
+
* order, each side's new lines appended in the order they appear. Exact
|
|
142
|
+
* duplicate lines are not duplicated.
|
|
143
|
+
*/
|
|
144
|
+
export function resolveChangelogMerge(base, ours, theirs) {
|
|
145
|
+
const baseLines = base.split("\n");
|
|
146
|
+
const ourLines = ours.split("\n");
|
|
147
|
+
const theirLines = theirs.split("\n");
|
|
148
|
+
const baseSet = new Set(baseLines);
|
|
149
|
+
const out = [...baseLines];
|
|
150
|
+
const seen = new Set(baseLines);
|
|
151
|
+
for (const line of [...ourLines, ...theirLines]) {
|
|
152
|
+
if (baseSet.has(line) || seen.has(line))
|
|
153
|
+
continue;
|
|
154
|
+
seen.add(line);
|
|
155
|
+
out.push(line);
|
|
156
|
+
}
|
|
157
|
+
return out.join("\n");
|
|
158
|
+
}
|
|
159
|
+
/** Detect the package manager for lockfile regeneration from lockfile name. */
|
|
160
|
+
export function lockfileManager(lockfileName) {
|
|
161
|
+
const base = lockfileName.split("/").pop() ?? lockfileName;
|
|
162
|
+
switch (base) {
|
|
163
|
+
case "package-lock.json":
|
|
164
|
+
return "npm";
|
|
165
|
+
case "pnpm-lock.yaml":
|
|
166
|
+
return "pnpm";
|
|
167
|
+
case "yarn.lock":
|
|
168
|
+
return "yarn";
|
|
169
|
+
case "bun.lockb":
|
|
170
|
+
return "bun";
|
|
171
|
+
case "Cargo.lock":
|
|
172
|
+
return "cargo";
|
|
173
|
+
case "go.sum":
|
|
174
|
+
return "go";
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export function defaultGitRunner() {
|
|
180
|
+
return {
|
|
181
|
+
run: (cwd, args) => new Promise((resolve, reject) => {
|
|
182
|
+
execFile("git", args, { cwd, timeout: 120_000 }, (err, stdout, stderr) => {
|
|
183
|
+
if (err)
|
|
184
|
+
reject(new Error(`git ${args.join(" ")} failed: ${stderr || err.message}`));
|
|
185
|
+
else
|
|
186
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
187
|
+
});
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Formatting-only verification: after running a formatter, the diff must be
|
|
193
|
+
* whitespace-only. `git diff -w` empty means no non-whitespace changes.
|
|
194
|
+
*/
|
|
195
|
+
export async function diffIsFormattingOnly(git, cwd) {
|
|
196
|
+
const { stdout } = await git.run(cwd, ["diff", "-w", "--numstat"]);
|
|
197
|
+
return stdout.trim().length === 0;
|
|
198
|
+
}
|
|
199
|
+
/** Detect a formatter path in a repo dir: npm scripts or known configs. */
|
|
200
|
+
export function detectFormatter(files, scripts) {
|
|
201
|
+
if (scripts["format"])
|
|
202
|
+
return { name: "npm run format", cmd: ["npm", "run", "format"] };
|
|
203
|
+
if (scripts["lint:fix"])
|
|
204
|
+
return { name: "npm run lint:fix", cmd: ["npm", "run", "lint:fix"] };
|
|
205
|
+
if (scripts["lint"] && /--fix/.test(scripts["lint"]))
|
|
206
|
+
return { name: "npm run lint", cmd: ["npm", "run", "lint"] };
|
|
207
|
+
const has = (n) => files.includes(n);
|
|
208
|
+
if (has(".prettierrc") ||
|
|
209
|
+
has(".prettierrc.json") ||
|
|
210
|
+
has("prettier.config.js") ||
|
|
211
|
+
has("prettier.config.cjs")) {
|
|
212
|
+
return { name: "prettier", cmd: ["npx", "--no-install", "prettier", "--write", "."] };
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=autopilot-fixes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autopilot-fixes.js","sourceRoot":"","sources":["../src/autopilot-fixes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Rene's standing DCO identity: he authorized Tree to sign non-legal attestations in his name. */
|
|
2
|
+
export declare const DCO_SIGNOFF_LINE = "Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>";
|
|
3
|
+
export interface CommitObject {
|
|
4
|
+
sha: string;
|
|
5
|
+
treeSha: string;
|
|
6
|
+
message: string;
|
|
7
|
+
parents: string[];
|
|
8
|
+
authorName: string;
|
|
9
|
+
authorEmail: string;
|
|
10
|
+
committerName: string;
|
|
11
|
+
committerEmail: string;
|
|
12
|
+
}
|
|
13
|
+
/** True when the message carries a DCO Signed-off-by trailer on any line. */
|
|
14
|
+
export declare function hasSignoff(message: string): boolean;
|
|
15
|
+
/** Append the sign-off line to a message that lacks it; leave signed messages untouched. */
|
|
16
|
+
export declare function withSignoff(message: string, line?: string): string;
|
|
17
|
+
export interface GitOpsDeps {
|
|
18
|
+
getCommit?: (owner: string, repo: string, sha: string) => Promise<CommitObject>;
|
|
19
|
+
createCommit?: (owner: string, repo: string, args: {
|
|
20
|
+
message: string;
|
|
21
|
+
tree: string;
|
|
22
|
+
parents: string[];
|
|
23
|
+
author: {
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
};
|
|
27
|
+
committer: {
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
};
|
|
31
|
+
}) => Promise<string>;
|
|
32
|
+
updateRef?: (owner: string, repo: string, branch: string, sha: string, force: boolean) => Promise<void>;
|
|
33
|
+
getRef?: (owner: string, repo: string, branch: string) => Promise<string | null>;
|
|
34
|
+
createBlob?: (owner: string, repo: string, content: string) => Promise<string>;
|
|
35
|
+
createTree?: (owner: string, repo: string, baseTree: string, entries: Array<{
|
|
36
|
+
path: string;
|
|
37
|
+
sha: string;
|
|
38
|
+
mode: string;
|
|
39
|
+
}>) => Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pure planner for DCO surgery: given the PR's commit chain (oldest first),
|
|
43
|
+
* decide which commits need a rewritten message. Signed commits anchor the
|
|
44
|
+
* chain and keep their SHAs; unsigned commits get a replacement mapped
|
|
45
|
+
* old->new parent. Trees are never touched, so every replacement commit
|
|
46
|
+
* carries the exact tree of its original.
|
|
47
|
+
*/
|
|
48
|
+
export interface DcoPlan {
|
|
49
|
+
/** Commits that need rewriting, in chain order (oldest first). */
|
|
50
|
+
rewrites: Array<{
|
|
51
|
+
sha: string;
|
|
52
|
+
treeSha: string;
|
|
53
|
+
message: string;
|
|
54
|
+
newMessage: string;
|
|
55
|
+
}>;
|
|
56
|
+
/** Commits already signed: anchors, untouched. */
|
|
57
|
+
anchors: string[];
|
|
58
|
+
}
|
|
59
|
+
/** The plan for one PR's commit list, given the full commit objects. */
|
|
60
|
+
export declare function planDcoSurgery(commits: CommitObject[]): DcoPlan;
|
|
61
|
+
export interface DcoSurgeryResult {
|
|
62
|
+
oldHead: string;
|
|
63
|
+
newHead: string;
|
|
64
|
+
fixed: number;
|
|
65
|
+
anchored: number;
|
|
66
|
+
/** Every replacement tree matched its original: the diff is byte-identical. */
|
|
67
|
+
treesIdentical: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Apply DCO surgery on a fork branch: message-only commit rewrites with
|
|
71
|
+
* byte-identical trees, then force-move the branch ref. Throws before the
|
|
72
|
+
* ref move if any tree would change.
|
|
73
|
+
*/
|
|
74
|
+
export declare function applyDcoSurgery(forkOwner: string, forkRepo: string, branch: string, commits: CommitObject[], deps?: GitOpsDeps): Promise<DcoSurgeryResult>;
|
|
75
|
+
export interface GitOps {
|
|
76
|
+
getCommit: NonNullable<GitOpsDeps["getCommit"]>;
|
|
77
|
+
createCommit: NonNullable<GitOpsDeps["createCommit"]>;
|
|
78
|
+
updateRef: NonNullable<GitOpsDeps["updateRef"]>;
|
|
79
|
+
getRef: NonNullable<GitOpsDeps["getRef"]>;
|
|
80
|
+
createBlob: NonNullable<GitOpsDeps["createBlob"]>;
|
|
81
|
+
createTree: NonNullable<GitOpsDeps["createTree"]>;
|
|
82
|
+
}
|
|
83
|
+
export declare function defaultGitOps(): GitOps;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for Autopilot, through the GitHub Git Data API only.
|
|
3
|
+
*
|
|
4
|
+
* Autopilot fixes must never push arbitrary code, so every mutation goes
|
|
5
|
+
* through these primitives: create a blob, a tree, a commit, and move a
|
|
6
|
+
* ref. The DCO surgery (message-only commit rewrites) and the format fix
|
|
7
|
+
* (computed merge trees) both reduce to "commit with an exact tree, then
|
|
8
|
+
* move the branch ref", which this module verifies step by step.
|
|
9
|
+
*/
|
|
10
|
+
import { apiGet, apiPost } from "./github.js";
|
|
11
|
+
/** Rene's standing DCO identity: he authorized Tree to sign non-legal attestations in his name. */
|
|
12
|
+
export const DCO_SIGNOFF_LINE = "Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>";
|
|
13
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
14
|
+
/** True when the message carries a DCO Signed-off-by trailer on any line. */
|
|
15
|
+
export function hasSignoff(message) {
|
|
16
|
+
return message
|
|
17
|
+
.split("\n")
|
|
18
|
+
.some((line) => /^Signed-off-by:\s*\S/i.test(line.trim()));
|
|
19
|
+
}
|
|
20
|
+
/** Append the sign-off line to a message that lacks it; leave signed messages untouched. */
|
|
21
|
+
export function withSignoff(message, line = DCO_SIGNOFF_LINE) {
|
|
22
|
+
if (hasSignoff(message))
|
|
23
|
+
return message;
|
|
24
|
+
return `${message.replace(/\n+$/, "")}\n\n${line}`;
|
|
25
|
+
}
|
|
26
|
+
async function defaultGetCommit(owner, repo, sha) {
|
|
27
|
+
const c = (await apiGet(`/repos/${owner}/${repo}/git/commits/${sha}`));
|
|
28
|
+
return {
|
|
29
|
+
sha,
|
|
30
|
+
treeSha: str(c.tree?.sha),
|
|
31
|
+
message: str(c.message),
|
|
32
|
+
parents: (c.parents ?? []).map((p) => str(p.sha)).filter(Boolean),
|
|
33
|
+
authorName: str(c.author?.name),
|
|
34
|
+
authorEmail: str(c.author?.email),
|
|
35
|
+
committerName: str(c.committer?.name),
|
|
36
|
+
committerEmail: str(c.committer?.email),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function defaultCreateCommit(owner, repo, args) {
|
|
40
|
+
const c = (await apiPost(`/repos/${owner}/${repo}/git/commits`, args));
|
|
41
|
+
const sha = str(c.sha);
|
|
42
|
+
if (!sha)
|
|
43
|
+
throw new Error("Git Data API did not return a commit sha");
|
|
44
|
+
return sha;
|
|
45
|
+
}
|
|
46
|
+
async function defaultUpdateRef(owner, repo, branch, sha, force) {
|
|
47
|
+
await apiPost(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, { sha, force });
|
|
48
|
+
}
|
|
49
|
+
async function defaultGetRef(owner, repo, branch) {
|
|
50
|
+
try {
|
|
51
|
+
const r = (await apiGet(`/repos/${owner}/${repo}/git/ref/heads/${branch}`));
|
|
52
|
+
const sha = str(r.object?.sha);
|
|
53
|
+
return sha || null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function defaultCreateBlob(owner, repo, content) {
|
|
60
|
+
const b = (await apiPost(`/repos/${owner}/${repo}/git/blobs`, {
|
|
61
|
+
content,
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
}));
|
|
64
|
+
return str(b.sha);
|
|
65
|
+
}
|
|
66
|
+
async function defaultCreateTree(owner, repo, baseTree, entries) {
|
|
67
|
+
const t = (await apiPost(`/repos/${owner}/${repo}/git/trees`, {
|
|
68
|
+
base_tree: baseTree,
|
|
69
|
+
tree: entries.map((e) => ({ path: e.path, mode: e.mode, type: "blob", sha: e.sha })),
|
|
70
|
+
}));
|
|
71
|
+
return str(t.sha);
|
|
72
|
+
}
|
|
73
|
+
/** The plan for one PR's commit list, given the full commit objects. */
|
|
74
|
+
export function planDcoSurgery(commits) {
|
|
75
|
+
const rewrites = [];
|
|
76
|
+
const anchors = [];
|
|
77
|
+
for (const c of commits) {
|
|
78
|
+
if (hasSignoff(c.message)) {
|
|
79
|
+
anchors.push(c.sha);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
rewrites.push({
|
|
83
|
+
sha: c.sha,
|
|
84
|
+
treeSha: c.treeSha,
|
|
85
|
+
message: c.message,
|
|
86
|
+
newMessage: withSignoff(c.message),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { rewrites, anchors };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Apply DCO surgery on a fork branch: message-only commit rewrites with
|
|
94
|
+
* byte-identical trees, then force-move the branch ref. Throws before the
|
|
95
|
+
* ref move if any tree would change.
|
|
96
|
+
*/
|
|
97
|
+
export async function applyDcoSurgery(forkOwner, forkRepo, branch, commits, deps = {}) {
|
|
98
|
+
const getCommit = deps.getCommit ?? defaultGetCommit;
|
|
99
|
+
const createCommit = deps.createCommit ?? defaultCreateCommit;
|
|
100
|
+
const updateRef = deps.updateRef ?? defaultUpdateRef;
|
|
101
|
+
const plan = planDcoSurgery(commits);
|
|
102
|
+
if (plan.rewrites.length === 0) {
|
|
103
|
+
const head = commits.length > 0 ? commits[commits.length - 1].sha : "";
|
|
104
|
+
return { oldHead: head, newHead: head, fixed: 0, anchored: plan.anchors.length, treesIdentical: true };
|
|
105
|
+
}
|
|
106
|
+
const full = new Map(commits.map((c) => [c.sha, c]));
|
|
107
|
+
const shaToNew = new Map();
|
|
108
|
+
let lastNewParent = "";
|
|
109
|
+
for (const c of commits) {
|
|
110
|
+
if (hasSignoff(c.message)) {
|
|
111
|
+
shaToNew.set(c.sha, c.sha);
|
|
112
|
+
lastNewParent = c.sha;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const orig = full.get(c.sha) ?? (await getCommit(forkOwner, forkRepo, c.sha));
|
|
116
|
+
const newParents = orig.parents.map((p) => shaToNew.get(p) ?? p);
|
|
117
|
+
// The very first unsigned commit parents a signed anchor; keep going.
|
|
118
|
+
const newSha = await createCommit(forkOwner, forkRepo, {
|
|
119
|
+
message: withSignoff(orig.message),
|
|
120
|
+
tree: orig.treeSha,
|
|
121
|
+
parents: newParents.length > 0 ? newParents : [],
|
|
122
|
+
author: { name: orig.authorName, email: orig.authorEmail },
|
|
123
|
+
committer: { name: orig.committerName, email: orig.committerEmail },
|
|
124
|
+
});
|
|
125
|
+
shaToNew.set(c.sha, newSha);
|
|
126
|
+
lastNewParent = newSha;
|
|
127
|
+
}
|
|
128
|
+
// Verify: every replacement tree equals its original tree.
|
|
129
|
+
let treesIdentical = true;
|
|
130
|
+
for (const c of commits) {
|
|
131
|
+
if (hasSignoff(c.message))
|
|
132
|
+
continue;
|
|
133
|
+
const replacement = await getCommit(forkOwner, forkRepo, shaToNew.get(c.sha) ?? "");
|
|
134
|
+
const original = full.get(c.sha) ?? (await getCommit(forkOwner, forkRepo, c.sha));
|
|
135
|
+
if (replacement.treeSha !== original.treeSha) {
|
|
136
|
+
treesIdentical = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!treesIdentical) {
|
|
141
|
+
throw new Error("DCO surgery aborted: a replacement tree differs from its original");
|
|
142
|
+
}
|
|
143
|
+
const oldHead = commits[commits.length - 1].sha;
|
|
144
|
+
const newHead = shaToNew.get(oldHead) ?? oldHead;
|
|
145
|
+
await updateRef(forkOwner, forkRepo, branch, newHead, true);
|
|
146
|
+
return {
|
|
147
|
+
oldHead,
|
|
148
|
+
newHead,
|
|
149
|
+
fixed: plan.rewrites.length,
|
|
150
|
+
anchored: plan.anchors.length,
|
|
151
|
+
treesIdentical: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function defaultGitOps() {
|
|
155
|
+
return {
|
|
156
|
+
getCommit: defaultGetCommit,
|
|
157
|
+
createCommit: defaultCreateCommit,
|
|
158
|
+
updateRef: defaultUpdateRef,
|
|
159
|
+
getRef: defaultGetRef,
|
|
160
|
+
createBlob: defaultCreateBlob,
|
|
161
|
+
createTree: defaultCreateTree,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=autopilot-git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autopilot-git.js","sourceRoot":"","sources":["../src/autopilot-git.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autopilot feedback classification (v0.11.0).
|
|
3
|
+
*
|
|
4
|
+
* Every actionable item on a PR gets a kind, and every bot finding gets a
|
|
5
|
+
* validity verdict that is VERIFIED against the code at the PR head before
|
|
6
|
+
* Autopilot touches anything:
|
|
7
|
+
*
|
|
8
|
+
* REAL the flagged code or pattern exists at head as described
|
|
9
|
+
* FALSE_POSITIVE the flagged code or pattern does not exist at head,
|
|
10
|
+
* with the counter-evidence quoted
|
|
11
|
+
* NEEDS_HUMAN the finding cannot be resolved deterministically
|
|
12
|
+
*
|
|
13
|
+
* Deterministic verification comes first. The optional LLM layer is only
|
|
14
|
+
* consulted where the codebase already wires it, and stays off by default.
|
|
15
|
+
*/
|
|
16
|
+
import type { AuthorKind } from "./review-triage.js";
|
|
17
|
+
import { authorKindOf, type ClassifiedFinding } from "./review-triage.js";
|
|
18
|
+
import type { ClassifiedCheck } from "./ci-triage.js";
|
|
19
|
+
export type FeedbackKind = "ci_failure" | "review_change_request" | "bot_finding" | "merge_conflict" | "dco_gap" | "maintainer_question" | "approval" | "other";
|
|
20
|
+
export type FindingVerdict = "REAL" | "FALSE_POSITIVE" | "NEEDS_HUMAN";
|
|
21
|
+
export interface FeedbackItem {
|
|
22
|
+
/** Stable identity for watermarks: e.g. "review:12345", "check:build". */
|
|
23
|
+
id: string;
|
|
24
|
+
kind: FeedbackKind;
|
|
25
|
+
title: string;
|
|
26
|
+
body: string;
|
|
27
|
+
author: string;
|
|
28
|
+
authorKind: AuthorKind;
|
|
29
|
+
url?: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
verdict?: FindingVerdict;
|
|
33
|
+
/** The evidence for the verdict: quoted code, or why it cannot be resolved. */
|
|
34
|
+
evidence?: string;
|
|
35
|
+
/** Draft reply for a human to post. Autopilot never posts comments. */
|
|
36
|
+
draftReply?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface VerifyDeps {
|
|
39
|
+
/** File contents at the PR head. Returns null when the file is absent. */
|
|
40
|
+
fetchFileAtHead?: (path: string) => Promise<string | null>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pull candidate file paths and short code quotes out of a bot finding body.
|
|
44
|
+
* Review comments usually name the file explicitly (backticked paths or
|
|
45
|
+
* file names) or ship a diff hunk; both are usable for verification.
|
|
46
|
+
*/
|
|
47
|
+
export declare function extractCandidates(body: string, anchoredPath?: string): {
|
|
48
|
+
paths: string[];
|
|
49
|
+
quotes: string[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Deterministically verify one bot finding against the code at head.
|
|
53
|
+
* REAL when the flagged pattern exists as described; FALSE_POSITIVE when
|
|
54
|
+
* it demonstrably does not (counter-evidence quoted); NEEDS_HUMAN when the
|
|
55
|
+
* files cannot be resolved or the claim is not checkable this way.
|
|
56
|
+
*/
|
|
57
|
+
export declare function verifyBotFinding(item: {
|
|
58
|
+
body: string;
|
|
59
|
+
path?: string;
|
|
60
|
+
}, deps?: VerifyDeps): Promise<{
|
|
61
|
+
verdict: FindingVerdict;
|
|
62
|
+
evidence: string;
|
|
63
|
+
}>;
|
|
64
|
+
/** Kind-only classification for review items, before any verification. */
|
|
65
|
+
export declare function classifyReviewItem(input: {
|
|
66
|
+
body: string;
|
|
67
|
+
author: string;
|
|
68
|
+
authorKind: AuthorKind;
|
|
69
|
+
reviewState?: string;
|
|
70
|
+
}): FeedbackKind;
|
|
71
|
+
/**
|
|
72
|
+
* Build Autopilot feedback items from review-triage findings, keeping the
|
|
73
|
+
* triage class and the ready reply draft. Bot findings keep kind
|
|
74
|
+
* "bot_finding" so the verifier can adjudicate them.
|
|
75
|
+
*/
|
|
76
|
+
export declare function feedbackFromReviewFindings(findings: ClassifiedFinding[], prAuthor: string): FeedbackItem[];
|
|
77
|
+
/**
|
|
78
|
+
* Build feedback items from CI triage. Only failures the classifier leans
|
|
79
|
+
* "yours" or cannot place (indeterminate) become fix candidates; pre-existing
|
|
80
|
+
* and external blockers are reported but never touched.
|
|
81
|
+
*/
|
|
82
|
+
export declare function feedbackFromCiChecks(checks: ClassifiedCheck[]): FeedbackItem[];
|
|
83
|
+
export interface TriageSummary {
|
|
84
|
+
total: number;
|
|
85
|
+
byKind: Record<FeedbackKind, number>;
|
|
86
|
+
botFindingsReal: number;
|
|
87
|
+
botFindingsFalsePositive: number;
|
|
88
|
+
botFindingsNeedsHuman: number;
|
|
89
|
+
ciIntroduced: number;
|
|
90
|
+
ciPreExistingOrExternal: number;
|
|
91
|
+
}
|
|
92
|
+
export declare function summarizeFeedback(items: FeedbackItem[]): TriageSummary;
|
|
93
|
+
export { authorKindOf };
|