@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.
Files changed (60) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +5 -4
  3. package/THIRD_PARTY_NOTICES.md +8 -6
  4. package/dist/acp-host/production-host.js +249 -624
  5. package/dist/headless-host/production-host.js +251 -626
  6. package/dist/migrate-book-topology.js +23 -6
  7. package/dist/package-resources/method-skill.js +6 -1
  8. package/dist/pi/role-turn-host.js +3 -1
  9. package/dist/public-cli/invocation.js +83 -32
  10. package/dist/public-cli/main.js +240 -306
  11. package/dist/public-cli/option-definitions.js +23 -6
  12. package/dist/public-cli/post-admission.js +0 -4
  13. package/dist/public-cli/run-lifecycle.js +25 -4
  14. package/dist/public-cli/settlement.js +6 -77
  15. package/dist/role-activation-flags.js +1 -0
  16. package/extensions/role-runtime.ts +3 -5
  17. package/package.json +1 -1
  18. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  19. package/resources/methods/ak-cross-m-review/CONTEXT.md +48 -0
  20. package/resources/methods/ak-cross-m-review/LICENSE +21 -0
  21. package/resources/methods/ak-cross-m-review/SKILL.md +170 -0
  22. package/resources/methods/ak-cross-m-review/prompts/cmr-completeness.md +118 -0
  23. package/resources/methods/ak-cross-m-review/prompts/cmr-reviewer.md +128 -0
  24. package/resources/methods/ak-cross-m-review/provenance.json +41 -0
  25. package/souls/judge.md +1 -1
  26. package/souls/reviewer.md +1 -1
  27. package/src/archivist-record-entry.ts +1 -1
  28. package/src/canonical-skill-binding.ts +2 -2
  29. package/src/host-contracts.ts +2 -0
  30. package/src/package-contracts/reviewer-output.ts +4 -2
  31. package/src/package-resources/method-skill.ts +7 -2
  32. package/src/pi/role-turn-host.ts +3 -1
  33. package/src/public-cli/invocation.ts +108 -39
  34. package/src/public-cli/option-definitions.ts +23 -6
  35. package/src/public-cli/post-admission.ts +0 -4
  36. package/src/public-cli/reviewer-run.ts +39 -16
  37. package/src/public-cli/run-lifecycle.ts +33 -3
  38. package/src/public-cli/settlement.ts +8 -73
  39. package/src/reviewer-role.ts +27 -16
  40. package/src/role-activation-flags.ts +1 -0
  41. package/src/role-runtime-dependencies.ts +2 -4
  42. package/src/role-runtime.ts +9 -26
  43. package/src/worker-role.ts +1 -1
  44. package/dist/adr-path-refs.js +0 -23
  45. package/dist/public-cli/reviewer-dispatch-rejection.js +0 -77
  46. package/dist/reviewer-dispatch.js +0 -14
  47. package/dist/reviewer-git-snapshot.js +0 -32
  48. package/dist/reviewer-pinned-git.js +0 -308
  49. package/dist/reviewer-preflight-error.js +0 -15
  50. package/dist/reviewer-prompt-identity.js +0 -10
  51. package/resources/methods/code-review/SKILL.md +0 -92
  52. package/resources/methods/code-review/agents/openai.yaml +0 -3
  53. package/resources/methods/code-review/provenance.json +0 -26
  54. package/src/adr-path-refs.ts +0 -25
  55. package/src/public-cli/reviewer-dispatch-rejection.ts +0 -106
  56. package/src/reviewer-dispatch.ts +0 -39
  57. package/src/reviewer-git-snapshot.ts +0 -45
  58. package/src/reviewer-pinned-git.ts +0 -330
  59. package/src/reviewer-preflight-error.ts +0 -14
  60. package/src/reviewer-prompt-identity.ts +0 -17
@@ -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-scope-keys",
158
+ name: "ak-review-lens",
160
159
  definition: Object.freeze({
161
- description: "Optional comma-separated exact class keys limiting Reviewer scope",
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 for Spec-axis material only",
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
- ...(reviewScopeKeys === undefined ? {} : { reviewScopeKeys }),
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" | "code-review",
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");
@@ -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 code-review for tdd",
393
+ "Canonical Skill binding loader returned ak-cross-m-review for tdd",
394
394
  );
395
395
  }
396
396
  binding = loaded;
@@ -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
- }