@akagilnc/pi-workflow-roles 0.1.4403 → 0.1.4419
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 +4 -3
- package/README.zh-CN.md +5 -4
- package/THIRD_PARTY_NOTICES.md +8 -6
- package/dist/acp-host/production-host.js +249 -624
- package/dist/headless-host/production-host.js +251 -626
- package/dist/migrate-book-topology.js +23 -6
- package/dist/package-resources/method-skill.js +6 -1
- package/dist/pi/role-turn-host.js +3 -1
- package/dist/public-cli/invocation.js +83 -32
- package/dist/public-cli/main.js +240 -306
- package/dist/public-cli/option-definitions.js +23 -6
- package/dist/public-cli/post-admission.js +0 -4
- package/dist/public-cli/run-lifecycle.js +25 -4
- package/dist/public-cli/settlement.js +6 -77
- package/dist/role-activation-flags.js +1 -0
- package/extensions/role-runtime.ts +3 -5
- package/package.json +1 -1
- package/resources/836-deleted-machine-instruction-inventory.md +1 -1
- package/resources/methods/ak-cross-m-review/CONTEXT.md +48 -0
- package/resources/methods/ak-cross-m-review/LICENSE +21 -0
- package/resources/methods/ak-cross-m-review/SKILL.md +170 -0
- package/resources/methods/ak-cross-m-review/prompts/cmr-completeness.md +118 -0
- package/resources/methods/ak-cross-m-review/prompts/cmr-reviewer.md +128 -0
- package/resources/methods/ak-cross-m-review/provenance.json +41 -0
- package/souls/judge.md +1 -1
- package/souls/reviewer.md +1 -1
- package/src/archivist-record-entry.ts +1 -1
- package/src/canonical-skill-binding.ts +2 -2
- package/src/host-contracts.ts +2 -0
- package/src/package-contracts/reviewer-output.ts +4 -2
- package/src/package-resources/method-skill.ts +7 -2
- package/src/pi/role-turn-host.ts +3 -1
- package/src/public-cli/invocation.ts +108 -39
- package/src/public-cli/option-definitions.ts +23 -6
- package/src/public-cli/post-admission.ts +0 -4
- package/src/public-cli/reviewer-run.ts +39 -16
- package/src/public-cli/run-lifecycle.ts +33 -3
- package/src/public-cli/settlement.ts +8 -73
- package/src/reviewer-role.ts +27 -16
- package/src/role-activation-flags.ts +1 -0
- package/src/role-runtime-dependencies.ts +2 -4
- package/src/role-runtime.ts +9 -26
- package/src/worker-role.ts +1 -1
- package/dist/adr-path-refs.js +0 -23
- package/dist/public-cli/reviewer-dispatch-rejection.js +0 -77
- package/dist/reviewer-dispatch.js +0 -14
- package/dist/reviewer-git-snapshot.js +0 -32
- package/dist/reviewer-pinned-git.js +0 -308
- package/dist/reviewer-preflight-error.js +0 -15
- package/dist/reviewer-prompt-identity.js +0 -10
- package/resources/methods/code-review/SKILL.md +0 -92
- package/resources/methods/code-review/agents/openai.yaml +0 -3
- package/resources/methods/code-review/provenance.json +0 -26
- package/src/adr-path-refs.ts +0 -25
- package/src/public-cli/reviewer-dispatch-rejection.ts +0 -106
- package/src/reviewer-dispatch.ts +0 -39
- package/src/reviewer-git-snapshot.ts +0 -45
- package/src/reviewer-pinned-git.ts +0 -330
- package/src/reviewer-preflight-error.ts +0 -14
- package/src/reviewer-prompt-identity.ts +0 -17
package/src/role-runtime.ts
CHANGED
|
@@ -140,7 +140,6 @@ import {
|
|
|
140
140
|
type ReviewerActivation,
|
|
141
141
|
type ReviewerAdmittedInputs,
|
|
142
142
|
} from "./reviewer-role.ts";
|
|
143
|
-
import type { ReviewerPinnedGitReader } from "./reviewer-dispatch.ts";
|
|
144
143
|
import type { GatekeeperNonPassResult } from "./gatekeeper-role.ts";
|
|
145
144
|
|
|
146
145
|
/**
|
|
@@ -156,16 +155,16 @@ const REVIEWER_TRANSPORT_FLAGS = Object.freeze([
|
|
|
156
155
|
}),
|
|
157
156
|
}),
|
|
158
157
|
Object.freeze({
|
|
159
|
-
name: "ak-review-
|
|
158
|
+
name: "ak-review-lens",
|
|
160
159
|
definition: Object.freeze({
|
|
161
|
-
description: "
|
|
160
|
+
description: "Caller-selected single review lens: completeness or correctness",
|
|
162
161
|
type: "string" as const,
|
|
163
162
|
}),
|
|
164
163
|
}),
|
|
165
164
|
Object.freeze({
|
|
166
165
|
name: "ak-review-authority-refs",
|
|
167
166
|
definition: Object.freeze({
|
|
168
|
-
description: "JSON array of durable authority references
|
|
167
|
+
description: "JSON array of durable authority references projected as Skill --authority inputs",
|
|
169
168
|
type: "string" as const,
|
|
170
169
|
}),
|
|
171
170
|
}),
|
|
@@ -207,19 +206,6 @@ export const STATION_CHILD_FLAG = Object.freeze({
|
|
|
207
206
|
* Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar).
|
|
208
207
|
*/
|
|
209
208
|
function decodeReviewerAdmittedInputs(getFlag: (name: string) => unknown): ReviewerAdmittedInputs {
|
|
210
|
-
let reviewScopeKeys: readonly string[] | undefined;
|
|
211
|
-
const rawScopeKeys = getFlag("ak-review-scope-keys");
|
|
212
|
-
if (rawScopeKeys !== undefined) {
|
|
213
|
-
if (typeof rawScopeKeys !== "string" || rawScopeKeys.length === 0) {
|
|
214
|
-
throw new Error("Reviewer scope keys must be a nonempty comma-separated string");
|
|
215
|
-
}
|
|
216
|
-
const parsed = rawScopeKeys.split(",");
|
|
217
|
-
if (parsed.some((key) => key.trim().length === 0) || new Set(parsed).size !== parsed.length) {
|
|
218
|
-
throw new Error("Reviewer scope keys contain a blank or exact duplicate key");
|
|
219
|
-
}
|
|
220
|
-
reviewScopeKeys = Object.freeze(parsed);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
209
|
let authorityRefs: readonly string[] | undefined;
|
|
224
210
|
const rawAuthorityRefs = getFlag("ak-review-authority-refs");
|
|
225
211
|
if (rawAuthorityRefs !== undefined) {
|
|
@@ -251,9 +237,13 @@ function decodeReviewerAdmittedInputs(getFlag: (name: string) => unknown): Revie
|
|
|
251
237
|
if (typeof baseRevision !== "string" || !baseRevision.trim()) {
|
|
252
238
|
throw new Error("Reviewer role requires --ak-review-base");
|
|
253
239
|
}
|
|
240
|
+
const rawLens = getFlag("ak-review-lens");
|
|
241
|
+
if (rawLens !== "completeness" && rawLens !== "correctness") {
|
|
242
|
+
throw new Error("Reviewer role requires --ak-review-lens completeness|correctness");
|
|
243
|
+
}
|
|
254
244
|
return Object.freeze({
|
|
255
245
|
baseRevision,
|
|
256
|
-
|
|
246
|
+
lens: rawLens,
|
|
257
247
|
...(authorityRefs === undefined ? {} : { authorityRefs }),
|
|
258
248
|
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
259
249
|
});
|
|
@@ -383,7 +373,6 @@ export {
|
|
|
383
373
|
} from "./judge-role.ts";
|
|
384
374
|
export { ENGINE_DETOUR_TOOL_NAME, AK_ROLE_ENGINE_ENV } from "./engine-detour.ts";
|
|
385
375
|
export {
|
|
386
|
-
AGENT_TOOL_NAME,
|
|
387
376
|
REVIEWER_OUTPUT_TOOL_NAME,
|
|
388
377
|
type ReviewerIntent,
|
|
389
378
|
} from "./reviewer-role.ts";
|
|
@@ -423,7 +412,6 @@ export {
|
|
|
423
412
|
COLLECTOR_REQUEST_TOOL,
|
|
424
413
|
COLLECTOR_WAIT_TOOL,
|
|
425
414
|
} from "./collector-role.ts";
|
|
426
|
-
export type { ReviewerPinnedGitReader } from "./reviewer-dispatch.ts";
|
|
427
415
|
export type { CollectorReceipt } from "./package-contracts/collector-output.ts";
|
|
428
416
|
export type { CollectorGitHubTransport } from "./collector-github.ts";
|
|
429
417
|
export type { CollectorClock } from "./collector-evidence.ts";
|
|
@@ -599,7 +587,6 @@ export type RoleRuntimeDependencies = {
|
|
|
599
587
|
loadCoderSoul?(): Promise<string>;
|
|
600
588
|
loadCoderTask?(path: string): Promise<string>;
|
|
601
589
|
loadReviewerSoul?(): Promise<string>;
|
|
602
|
-
createReviewerPinnedGitReader?(): Promise<ReviewerPinnedGitReader>;
|
|
603
590
|
loadCollectorSoul?(): Promise<string>;
|
|
604
591
|
/** #677: optional packaged seed for first-use general bot handbook. */
|
|
605
592
|
loadCollectorHandbookSeed?(): Promise<string>;
|
|
@@ -623,7 +610,7 @@ export type RoleRuntimeDependencies = {
|
|
|
623
610
|
createNavigatorAttendance?(options: { context: HostContext; role: string; phase: NavigatorPhase; subjectKey: string; subject: string; authority: string; contextError?: unknown; invocationId: string; onEvent: (event: import("./navigator-attendance.ts").NavigatorEvent, report: import("./navigator-attendance.ts").NavigatorReport) => void | Promise<void> }): NavigatorAttendanceDependency | Promise<NavigatorAttendanceDependency>;
|
|
624
611
|
loadNavigatorWorkContext?(options: { context: HostContext; role: string; phase: NavigatorPhase; getFlag?: (name: string) => unknown }): Promise<NavigatorWorkContext>;
|
|
625
612
|
loadCanonicalSkillBinding?(
|
|
626
|
-
name: "tdd" | "
|
|
613
|
+
name: "tdd" | "ak-cross-m-review",
|
|
627
614
|
): Promise<AnyCanonicalSkillBinding>;
|
|
628
615
|
activationClock?(): string;
|
|
629
616
|
activationTraceWriter?: (record: ActivationTraceRecord) => void | Promise<void>;
|
|
@@ -1782,10 +1769,6 @@ export function createRoleRuntimeExtension(
|
|
|
1782
1769
|
}
|
|
1783
1770
|
return dependencies.loadReviewerSoul();
|
|
1784
1771
|
},
|
|
1785
|
-
async createPinnedGitReader() {
|
|
1786
|
-
if (dependencies.createReviewerPinnedGitReader === undefined) throw new Error("Reviewer runtime dependencies are not configured");
|
|
1787
|
-
return dependencies.createReviewerPinnedGitReader();
|
|
1788
|
-
},
|
|
1789
1772
|
async loadCanonicalSkillBinding(name) {
|
|
1790
1773
|
if (dependencies.loadCanonicalSkillBinding === undefined) {
|
|
1791
1774
|
throw new Error("Reviewer runtime dependencies are not configured");
|
package/src/worker-role.ts
CHANGED
|
@@ -390,7 +390,7 @@ export function createCoderRoleRuntime(
|
|
|
390
390
|
const loaded = await dependencies.loadCanonicalSkillBinding("tdd");
|
|
391
391
|
if (loaded.name !== "tdd") {
|
|
392
392
|
throw new Error(
|
|
393
|
-
"Canonical Skill binding loader returned
|
|
393
|
+
"Canonical Skill binding loader returned ak-cross-m-review for tdd",
|
|
394
394
|
);
|
|
395
395
|
}
|
|
396
396
|
binding = loaded;
|
package/dist/adr-path-refs.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared docs/adr path extraction from free text (issue body / ticket face).
|
|
3
|
-
* Single authority for reviewer Spec discovery.
|
|
4
|
-
*
|
|
5
|
-
* Shape only: segments under docs/adr/ without `..` or empty parts. Real IO
|
|
6
|
-
* confinement still lives at the read seam (ADR 0038).
|
|
7
|
-
*/
|
|
8
|
-
/** docs/adr paths referenced inside free text (order of first appearance). */
|
|
9
|
-
// Each segment must start with alnum so `.` / `..` traversal claims never match.
|
|
10
|
-
const ADR_PATH_IN_BODY = /docs\/adr\/(?:[A-Za-z0-9][A-Za-z0-9._-]*\/)*[A-Za-z0-9][A-Za-z0-9._-]*\.md/g;
|
|
11
|
-
/** Extract unique docs/adr/*.md paths; preserve first-seen order. */
|
|
12
|
-
export function extractReferencedAdrPaths(text) {
|
|
13
|
-
const seen = new Set();
|
|
14
|
-
const paths = [];
|
|
15
|
-
for (const match of text.matchAll(ADR_PATH_IN_BODY)) {
|
|
16
|
-
const path = match[0];
|
|
17
|
-
if (seen.has(path))
|
|
18
|
-
continue;
|
|
19
|
-
seen.add(path);
|
|
20
|
-
paths.push(path);
|
|
21
|
-
}
|
|
22
|
-
return Object.freeze(paths);
|
|
23
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable Reviewer-rejection child→parent page under AK_ROLE_RUN_DIR.
|
|
3
|
-
* Stays in public-cli (AK artifact owner); role-runtime imports this neighbor.
|
|
4
|
-
*/
|
|
5
|
-
import { writeFileSync } from "node:fs";
|
|
6
|
-
import { readFile, unlink } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { REVIEWER_PREFLIGHT_VIOLATIONS, } from "../reviewer-dispatch.js";
|
|
9
|
-
const REVIEWER_DISPATCH_REJECTION_FILE = "typed-known-failure.json";
|
|
10
|
-
function isReviewerPreflightViolation(value) {
|
|
11
|
-
return (typeof value === "string" &&
|
|
12
|
-
REVIEWER_PREFLIGHT_VIOLATIONS.includes(value));
|
|
13
|
-
}
|
|
14
|
-
function reviewerDispatchRejectionPath(runDirectory) {
|
|
15
|
-
return join(runDirectory, REVIEWER_DISPATCH_REJECTION_FILE);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Clear any prior attempt's Reviewer rejection page so resume/retry cannot
|
|
19
|
-
* inherit a stale knownFailure.details.
|
|
20
|
-
*/
|
|
21
|
-
export async function clearReviewerDispatchRejection(runDirectory) {
|
|
22
|
-
try {
|
|
23
|
-
await unlink(reviewerDispatchRejectionPath(runDirectory));
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (error instanceof Error &&
|
|
27
|
-
"code" in error &&
|
|
28
|
-
error.code === "ENOENT") {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Synchronous durable write for Reviewer dispatch rejection (child process exit is sync).
|
|
36
|
-
* Parent public CLI recovers via readReviewerDispatchRejection into knownFailure.
|
|
37
|
-
*/
|
|
38
|
-
export function recordReviewerDispatchRejectionSync(runDirectory, rejection) {
|
|
39
|
-
writeFileSync(reviewerDispatchRejectionPath(runDirectory), `${JSON.stringify(rejection)}\n`, "utf8");
|
|
40
|
-
}
|
|
41
|
-
/** Recover a child-written Reviewer dispatch rejection through a fixed mapping. */
|
|
42
|
-
export async function readReviewerDispatchRejection(runDirectory) {
|
|
43
|
-
let raw;
|
|
44
|
-
try {
|
|
45
|
-
raw = await readFile(reviewerDispatchRejectionPath(runDirectory), "utf8");
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (error instanceof Error &&
|
|
49
|
-
"code" in error &&
|
|
50
|
-
error.code === "ENOENT") {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
const parsed = JSON.parse(raw);
|
|
56
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
57
|
-
const error = new Error("Reviewer dispatch rejection page must be a JSON object");
|
|
58
|
-
error.name = "ReviewerDispatchRejectionContractError";
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
const record = parsed;
|
|
62
|
-
if (typeof record.diagnostic !== "string" ||
|
|
63
|
-
record.diagnostic.trim() === "" ||
|
|
64
|
-
!Array.isArray(record.violations) ||
|
|
65
|
-
record.violations.length === 0 ||
|
|
66
|
-
record.violations.some((value) => !isReviewerPreflightViolation(value))) {
|
|
67
|
-
const error = new Error("Reviewer dispatch rejection page has unusable required fields");
|
|
68
|
-
error.name = "ReviewerDispatchRejectionContractError";
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
cause: "activation",
|
|
73
|
-
diagnostic: record.diagnostic,
|
|
74
|
-
identity: { name: "ReviewerDispatchRejectionError" },
|
|
75
|
-
details: { violations: Object.freeze([...record.violations]) },
|
|
76
|
-
};
|
|
77
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #836 删 8: no code-compiled axis dispatch.
|
|
3
|
-
* Parent seat invokes packaged code-review skill; this module does not construct or run children.
|
|
4
|
-
*/
|
|
5
|
-
export { branchNamesAtPinnedHead, createReviewerPinnedGitReader, immutableReviewerPin, } from "./reviewer-pinned-git.js";
|
|
6
|
-
export { extractReferencedAdrPaths } from "./adr-path-refs.js";
|
|
7
|
-
export { sha256Hex } from "./sha256.js";
|
|
8
|
-
export { isReviewerPromptText as isReviewerPromptIdentity, sameReviewerPromptText as sameReviewerPromptIdentity, } from "./reviewer-prompt-identity.js";
|
|
9
|
-
export const REVIEWER_PREFLIGHT_VIOLATIONS = [
|
|
10
|
-
"base-invalid",
|
|
11
|
-
"range-invalid",
|
|
12
|
-
"prompt-identity-invalid",
|
|
13
|
-
"target-drift",
|
|
14
|
-
];
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export const REVIEW_REF_PREFIXES = ["refs/heads", "refs/tags", "refs/remotes"];
|
|
2
|
-
export function parseReviewerRefSnapshot(raw) {
|
|
3
|
-
const refs = {};
|
|
4
|
-
for (const line of raw.split("\n")) {
|
|
5
|
-
if (!line)
|
|
6
|
-
continue;
|
|
7
|
-
const fields = line.split("\0");
|
|
8
|
-
if (fields.length !== 5 || !fields[0] || !fields[1] || !fields[3]) {
|
|
9
|
-
throw new Error(`Malformed Git ref snapshot line: ${line}`);
|
|
10
|
-
}
|
|
11
|
-
const peeledCommitId = fields[3] === "commit"
|
|
12
|
-
? fields[1]
|
|
13
|
-
: fields[4] === "commit" && fields[2] ? fields[2] : null;
|
|
14
|
-
refs[fields[0]] = Object.freeze({
|
|
15
|
-
objectId: fields[1],
|
|
16
|
-
peeledCommitId,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
return refs;
|
|
20
|
-
}
|
|
21
|
-
export function reviewerRefSnapshotArgs() {
|
|
22
|
-
return ["for-each-ref", "--format=%(refname)%00%(objectname)%00%(*objectname)%00%(objecttype)%00%(*objecttype)", ...REVIEW_REF_PREFIXES];
|
|
23
|
-
}
|
|
24
|
-
export function immutableReviewerRefs(refs) {
|
|
25
|
-
return Object.freeze(Object.fromEntries(Object.entries(refs)
|
|
26
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
27
|
-
.map(([name, entry]) => [name, Object.freeze({ objectId: entry.objectId, peeledCommitId: entry.peeledCommitId })])));
|
|
28
|
-
}
|
|
29
|
-
export function sameReviewerPinnedTarget(actual, expected) {
|
|
30
|
-
return actual.repositoryRoot === expected.repositoryRoot && actual.objectFormat === expected.objectFormat &&
|
|
31
|
-
actual.targetHead === expected.targetHead;
|
|
32
|
-
}
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { access, realpath } from "node:fs/promises";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
|
-
import { immutableReviewerRefs, parseReviewerRefSnapshot, reviewerRefSnapshotArgs } from "./reviewer-git-snapshot.js";
|
|
5
|
-
import { sha256Hex } from "./sha256.js";
|
|
6
|
-
import { ReviewerCorrectablePreflightError } from "./reviewer-preflight-error.js";
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
|
-
async function execGit(args, options) {
|
|
9
|
-
// Pin C locale at the sole Git exec seam so English diagnostic classifiers stay honest under translated gettext installs.
|
|
10
|
-
try {
|
|
11
|
-
return await execFileAsync("git", args, {
|
|
12
|
-
...options,
|
|
13
|
-
env: { ...process.env, LC_ALL: "C" },
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
const source = error;
|
|
18
|
-
const wrapped = new Error("git process failed", { cause: error });
|
|
19
|
-
Object.assign(wrapped, { code: source.code ?? null, signal: source.signal ?? null, timedOut: source.killed === true && source.signal === "SIGTERM", aborted: source.name === "AbortError", stderr: String(source.stderr ?? ""), stdout: String(source.stdout ?? "") });
|
|
20
|
-
throw wrapped;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function exitCode(error) { const code = typeof error === "object" && error !== null ? error.code : undefined; return typeof code === "number" ? code : undefined; }
|
|
24
|
-
function gitStderr(error) {
|
|
25
|
-
if (typeof error !== "object" || error === null)
|
|
26
|
-
return "";
|
|
27
|
-
const stderr = error.stderr;
|
|
28
|
-
return typeof stderr === "string" ? stderr : "";
|
|
29
|
-
}
|
|
30
|
-
/** Confirmed `origin` remote absence only (`git remote get-url origin`). */
|
|
31
|
-
function isConfirmedMissingOriginRemote(error) {
|
|
32
|
-
return /No such remote ['"]origin['"]/.test(gitStderr(error));
|
|
33
|
-
}
|
|
34
|
-
/** Confirmed path-at-pinned-tree absence only — exit 128 alone is not enough. */
|
|
35
|
-
function isConfirmedPinnedPathAbsent(error, path) {
|
|
36
|
-
const stderr = gitStderr(error);
|
|
37
|
-
const quoted = `'${path}'`;
|
|
38
|
-
return (stderr.includes(`path ${quoted} does not exist in `) ||
|
|
39
|
-
stderr.includes(`path ${quoted} exists on disk, but not in `));
|
|
40
|
-
}
|
|
41
|
-
async function repositoryIsAvailable(root) { try {
|
|
42
|
-
await access(`${root}/.git`);
|
|
43
|
-
return { available: true };
|
|
44
|
-
}
|
|
45
|
-
catch (cause) {
|
|
46
|
-
return { available: false, cause };
|
|
47
|
-
} }
|
|
48
|
-
export const immutableReviewerPin = (pin) => Object.freeze({
|
|
49
|
-
repositoryRoot: pin.repositoryRoot, objectFormat: pin.objectFormat, targetHead: pin.targetHead, refs: immutableReviewerRefs(pin.refs),
|
|
50
|
-
});
|
|
51
|
-
/** Short name from a full ref, stripping heads/tags/remotes namespaces (and remote remote-name). */
|
|
52
|
-
function shortNameFromPinnedRef(refName) {
|
|
53
|
-
const short = refName.startsWith("refs/heads/")
|
|
54
|
-
? refName.slice("refs/heads/".length)
|
|
55
|
-
: refName.startsWith("refs/tags/")
|
|
56
|
-
? refName.slice("refs/tags/".length)
|
|
57
|
-
: refName.startsWith("refs/remotes/")
|
|
58
|
-
? refName.slice("refs/remotes/".length).replace(/^[^/]+\//, "")
|
|
59
|
-
: refName;
|
|
60
|
-
const trimmed = short.trim();
|
|
61
|
-
return trimmed === "" ? undefined : trimmed;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Branch-only short names at pinned targetHead for ticket-number provenance (#343).
|
|
65
|
-
* Heads and remotes only — tags never supply branch-token ticket candidates.
|
|
66
|
-
*/
|
|
67
|
-
export function branchNamesAtPinnedHead(pin) {
|
|
68
|
-
const names = new Set();
|
|
69
|
-
for (const [refName, entry] of Object.entries(pin.refs)) {
|
|
70
|
-
if (entry.peeledCommitId !== pin.targetHead)
|
|
71
|
-
continue;
|
|
72
|
-
if (!refName.startsWith("refs/heads/") && !refName.startsWith("refs/remotes/"))
|
|
73
|
-
continue;
|
|
74
|
-
const short = shortNameFromPinnedRef(refName);
|
|
75
|
-
if (short !== undefined)
|
|
76
|
-
names.add(short);
|
|
77
|
-
}
|
|
78
|
-
return Object.freeze([...names]);
|
|
79
|
-
}
|
|
80
|
-
async function gitText(root, args) {
|
|
81
|
-
const { stdout } = await execGit(["-C", root, ...args], { encoding: "utf8" });
|
|
82
|
-
return stdout.trim();
|
|
83
|
-
}
|
|
84
|
-
/** Concrete fixed-point Git I/O; dispatch owns all policy applied to these reads. */
|
|
85
|
-
export async function createReviewerPinnedGitReader(root = process.cwd()) {
|
|
86
|
-
const discoveredRoot = await gitText(root, ["rev-parse", "--show-toplevel"]);
|
|
87
|
-
const repositoryRoot = await realpath(discoveredRoot);
|
|
88
|
-
const objectFormat = await gitText(repositoryRoot, ["rev-parse", "--show-object-format"]);
|
|
89
|
-
if (objectFormat !== "sha1" && objectFormat !== "sha256")
|
|
90
|
-
throw new Error("Unsupported Git object format");
|
|
91
|
-
const oidWidth = objectFormat === "sha1" ? 40 : 64;
|
|
92
|
-
const targetHead = await gitText(repositoryRoot, ["rev-parse", "HEAD^{commit}"]);
|
|
93
|
-
const reachableCommitIds = Object.freeze((await gitText(repositoryRoot, ["rev-list", targetHead])).split("\n").filter(Boolean));
|
|
94
|
-
const refs = parseReviewerRefSnapshot(await gitText(repositoryRoot, reviewerRefSnapshotArgs()));
|
|
95
|
-
const pin = immutableReviewerPin({ repositoryRoot, objectFormat, targetHead, refs });
|
|
96
|
-
const invalid = (code, diagnostic, cause) => {
|
|
97
|
-
throw new ReviewerCorrectablePreflightError(code, diagnostic, cause === undefined ? undefined : { cause });
|
|
98
|
-
};
|
|
99
|
-
const symbolic = (base) => {
|
|
100
|
-
const selected = Object.hasOwn(refs, base) ? base : (() => {
|
|
101
|
-
const candidates = [`refs/heads/${base}`, `refs/tags/${base}`, `refs/remotes/${base}`].filter((name) => Object.hasOwn(refs, name));
|
|
102
|
-
if (candidates.length > 1)
|
|
103
|
-
invalid("base-invalid", "base revision is ambiguous across pinned refs");
|
|
104
|
-
return candidates[0];
|
|
105
|
-
})();
|
|
106
|
-
if (selected === undefined)
|
|
107
|
-
return undefined;
|
|
108
|
-
const commit = refs[selected]?.peeledCommitId;
|
|
109
|
-
if (commit === null)
|
|
110
|
-
invalid("base-invalid", "base revision ref must resolve to a commit");
|
|
111
|
-
return commit ?? undefined;
|
|
112
|
-
};
|
|
113
|
-
return Object.freeze({
|
|
114
|
-
pin,
|
|
115
|
-
async snapshot() {
|
|
116
|
-
const liveObjectFormat = await gitText(repositoryRoot, ["rev-parse", "--show-object-format"]);
|
|
117
|
-
if (liveObjectFormat !== "sha1" && liveObjectFormat !== "sha256")
|
|
118
|
-
throw new Error("Unsupported Git object format");
|
|
119
|
-
return immutableReviewerPin({ repositoryRoot, objectFormat: liveObjectFormat, targetHead: await gitText(repositoryRoot, ["rev-parse", "HEAD^{commit}"]), refs: parseReviewerRefSnapshot(await gitText(repositoryRoot, reviewerRefSnapshotArgs())) });
|
|
120
|
-
},
|
|
121
|
-
async resolve(base) {
|
|
122
|
-
if (!/^[A-Za-z0-9._/~^+-]+$/.test(base) || base.startsWith("-") || base.includes("..") || base.includes("@{")) {
|
|
123
|
-
invalid("base-invalid", "base revision syntax is invalid or uses a forbidden revision form");
|
|
124
|
-
}
|
|
125
|
-
let commit;
|
|
126
|
-
const headExpression = /^HEAD((?:~[0-9]+|\^[0-9]+)*)$/.exec(base);
|
|
127
|
-
if (headExpression) {
|
|
128
|
-
try {
|
|
129
|
-
commit = await gitText(repositoryRoot, ["rev-parse", "--verify", `${targetHead}${headExpression[1]}^{commit}`]);
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (exitCode(error) === 128) {
|
|
133
|
-
const repository = await repositoryIsAvailable(repositoryRoot);
|
|
134
|
-
if (repository.available)
|
|
135
|
-
invalid("base-invalid", "base revision HEAD ancestry expression must resolve to a reachable commit", error);
|
|
136
|
-
}
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else if (new RegExp(`^[0-9a-f]{${oidWidth}}$`).test(base))
|
|
141
|
-
commit = base;
|
|
142
|
-
else if (new RegExp(`^[0-9a-f]{4,${oidWidth - 1}}$`).test(base) && !(objectFormat === "sha256" && base.length === 40)) {
|
|
143
|
-
const matches = reachableCommitIds.filter((candidate) => candidate.startsWith(base));
|
|
144
|
-
if (matches.length !== 1)
|
|
145
|
-
invalid("base-invalid", "base revision abbreviation must identify exactly one reachable commit");
|
|
146
|
-
commit = matches[0];
|
|
147
|
-
}
|
|
148
|
-
else
|
|
149
|
-
commit = symbolic(base);
|
|
150
|
-
if (commit === undefined)
|
|
151
|
-
invalid("base-invalid", "base revision must name an existing pinned ref or reachable commit");
|
|
152
|
-
try {
|
|
153
|
-
commit = await gitText(repositoryRoot, ["rev-parse", "--verify", `${commit}^{commit}`]);
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
if (exitCode(error) === 128) {
|
|
157
|
-
const repository = await repositoryIsAvailable(repositoryRoot);
|
|
158
|
-
if (repository.available)
|
|
159
|
-
invalid("base-invalid", "base revision must resolve to an existing commit", error);
|
|
160
|
-
}
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
try {
|
|
164
|
-
await gitText(repositoryRoot, ["merge-base", "--is-ancestor", commit, targetHead]);
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
if (exitCode(error) === 1)
|
|
168
|
-
invalid("base-invalid", "base revision must be an ancestor of the pinned target", error);
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
return commit;
|
|
172
|
-
},
|
|
173
|
-
async range(base) {
|
|
174
|
-
let mergeBase;
|
|
175
|
-
try {
|
|
176
|
-
mergeBase = await gitText(repositoryRoot, ["merge-base", base, targetHead]);
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
if (exitCode(error) === 1) {
|
|
180
|
-
invalid("range-invalid", "review range requires a common ancestor for base and pinned target", error);
|
|
181
|
-
}
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
if (!mergeBase)
|
|
185
|
-
invalid("range-invalid", "review range requires a common ancestor for base and pinned target");
|
|
186
|
-
const diffCommand = `git diff ${mergeBase}...${targetHead}`;
|
|
187
|
-
const [{ stdout: diff }, commitsText] = await Promise.all([
|
|
188
|
-
execGit(["-C", repositoryRoot, "diff", `${mergeBase}...${targetHead}`], { encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }),
|
|
189
|
-
gitText(repositoryRoot, ["rev-list", "--reverse", `${mergeBase}..${targetHead}`]),
|
|
190
|
-
]);
|
|
191
|
-
if (diff.length === 0)
|
|
192
|
-
invalid("range-invalid", "review range must contain a non-empty diff between base and pinned target");
|
|
193
|
-
return Object.freeze({ base: mergeBase, target: targetHead, diffCommand, diffSha256: sha256Hex(Uint8Array.from(diff)), commits: Object.freeze(commitsText ? commitsText.split("\n") : []) });
|
|
194
|
-
},
|
|
195
|
-
async featureTokens() {
|
|
196
|
-
// Pinned ref snapshot is the target-tree fact — no live branch/symbolic-ref walk,
|
|
197
|
-
// no catch-to-empty. Detached/remote-only tips surface via refs/remotes/* entries.
|
|
198
|
-
// Includes tags for local Spec-path matching only; ticket branch source is separate.
|
|
199
|
-
const names = new Set();
|
|
200
|
-
for (const [refName, entry] of Object.entries(pin.refs)) {
|
|
201
|
-
if (entry.peeledCommitId !== targetHead)
|
|
202
|
-
continue;
|
|
203
|
-
const short = shortNameFromPinnedRef(refName);
|
|
204
|
-
if (short !== undefined)
|
|
205
|
-
names.add(short);
|
|
206
|
-
}
|
|
207
|
-
return Object.freeze([...names]);
|
|
208
|
-
},
|
|
209
|
-
async listSpecCandidatePaths() {
|
|
210
|
-
const roots = ["docs", "specs", ".scratch"];
|
|
211
|
-
// git ls-tree exits 0 with empty stdout when none of the roots exist at targetHead.
|
|
212
|
-
// Other Git/I-O failures keep their true cause for the dispatch preflight path.
|
|
213
|
-
const text = await gitText(repositoryRoot, [
|
|
214
|
-
"ls-tree",
|
|
215
|
-
"-r",
|
|
216
|
-
"--name-only",
|
|
217
|
-
targetHead,
|
|
218
|
-
"--",
|
|
219
|
-
...roots,
|
|
220
|
-
]);
|
|
221
|
-
return Object.freeze(text === "" ? [] : text.split("\n").filter((line) => line.length > 0));
|
|
222
|
-
},
|
|
223
|
-
async originRepository() {
|
|
224
|
-
let remoteUrl;
|
|
225
|
-
try {
|
|
226
|
-
remoteUrl = await gitText(repositoryRoot, ["remote", "get-url", "origin"]);
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
// Only confirmed origin absence softens to unavailable; other Git failures keep true cause.
|
|
230
|
-
if (isConfirmedMissingOriginRemote(error))
|
|
231
|
-
return undefined;
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
// Non-github / unparseable remote URL = self-fetch unavailable (soft degrade).
|
|
235
|
-
return parseGitHubOriginRemote(remoteUrl);
|
|
236
|
-
},
|
|
237
|
-
async commitMessagesNewestFirst(base) {
|
|
238
|
-
const text = await gitText(repositoryRoot, [
|
|
239
|
-
"log",
|
|
240
|
-
"--format=%s",
|
|
241
|
-
`${base}..${targetHead}`,
|
|
242
|
-
]);
|
|
243
|
-
return Object.freeze(text === "" ? [] : text.split("\n"));
|
|
244
|
-
},
|
|
245
|
-
async readPinnedText(path) {
|
|
246
|
-
// Reject path traversal / absolute paths — Spec material is relative tree paths only.
|
|
247
|
-
if (path.length === 0 ||
|
|
248
|
-
path.startsWith("/") ||
|
|
249
|
-
path.includes("\0") ||
|
|
250
|
-
path.split("/").some((part) => part === ".." || part === "")) {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
const { stdout } = await execGit(["-C", repositoryRoot, "show", `${targetHead}:${path}`], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
|
|
255
|
-
return stdout;
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
// Only confirmed path-at-pinned-tree absence softens to missing; exit 128 is not a blanket.
|
|
259
|
-
if (isConfirmedPinnedPathAbsent(error, path))
|
|
260
|
-
return undefined;
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Parse github.com owner/repo from a git remote URL.
|
|
268
|
-
* Supports scp-like SSH, ssh://, https://, and git:// shapes. Soft: undefined when not github.
|
|
269
|
-
*/
|
|
270
|
-
export function parseGitHubOriginRemote(remoteUrl) {
|
|
271
|
-
const trimmed = remoteUrl.trim();
|
|
272
|
-
if (trimmed.length === 0)
|
|
273
|
-
return undefined;
|
|
274
|
-
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i.exec(trimmed);
|
|
275
|
-
if (scp)
|
|
276
|
-
return normalizeOrigin(scp[1], scp[2]);
|
|
277
|
-
const ssh = /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(trimmed);
|
|
278
|
-
if (ssh)
|
|
279
|
-
return normalizeOrigin(ssh[1], ssh[2]);
|
|
280
|
-
let parsed;
|
|
281
|
-
try {
|
|
282
|
-
parsed = new URL(trimmed);
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
return undefined;
|
|
286
|
-
}
|
|
287
|
-
if (!/^github\.com$/i.test(parsed.hostname))
|
|
288
|
-
return undefined;
|
|
289
|
-
if (parsed.search !== "" || parsed.hash !== "")
|
|
290
|
-
return undefined;
|
|
291
|
-
const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
|
|
292
|
-
if (parts.length !== 2)
|
|
293
|
-
return undefined;
|
|
294
|
-
return normalizeOrigin(parts[0], parts[1]);
|
|
295
|
-
}
|
|
296
|
-
function normalizeOrigin(ownerRaw, repoRaw) {
|
|
297
|
-
const owner = ownerRaw.trim();
|
|
298
|
-
const repo = stripGitSuffix(repoRaw.trim());
|
|
299
|
-
if (owner.length === 0 || repo.length === 0)
|
|
300
|
-
return undefined;
|
|
301
|
-
// Conservative identity: no path separators or URL material inside segments.
|
|
302
|
-
if (/[/?#@\\]/.test(owner) || /[/?#@\\]/.test(repo))
|
|
303
|
-
return undefined;
|
|
304
|
-
return Object.freeze({ owner, repo });
|
|
305
|
-
}
|
|
306
|
-
function stripGitSuffix(name) {
|
|
307
|
-
return name.toLowerCase().endsWith(".git") ? name.slice(0, -4) : name;
|
|
308
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export const REVIEWER_CORRECTABLE_PREFLIGHT_CODES = [
|
|
2
|
-
"base-invalid",
|
|
3
|
-
"range-invalid",
|
|
4
|
-
];
|
|
5
|
-
/** Closed policy error shared by concrete Git reads and dispatch compilation. */
|
|
6
|
-
export class ReviewerCorrectablePreflightError extends Error {
|
|
7
|
-
code;
|
|
8
|
-
diagnostic;
|
|
9
|
-
constructor(code, diagnostic = `${code} constraint failed`, options) {
|
|
10
|
-
super(`${code}: ${diagnostic}`, options);
|
|
11
|
-
this.code = code;
|
|
12
|
-
this.diagnostic = diagnostic;
|
|
13
|
-
this.name = "ReviewerCorrectablePreflightError";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ADR 0031: Reviewer model-visible text remains plain text.
|
|
3
|
-
* No length/digest identity shell; callers compare strings directly when needed.
|
|
4
|
-
*/
|
|
5
|
-
export function isReviewerPromptText(value) {
|
|
6
|
-
return typeof value === "string";
|
|
7
|
-
}
|
|
8
|
-
export function sameReviewerPromptText(first, second) {
|
|
9
|
-
return first === second;
|
|
10
|
-
}
|