@akagilnc/pi-workflow-roles 0.1.1918 → 0.1.1941
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/dist/evidence-child-executor.js +12 -6
- package/dist/package-contracts/reviewer-output.js +16 -0
- package/dist/public-cli/main.js +79 -4
- package/dist/reviewer-construction.js +46 -6
- package/dist/reviewer-dispatch.js +64 -1
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/reviewer-pinned-git.js +33 -0
- package/package.json +1 -1
- package/src/evidence-child-executor.ts +13 -6
- package/src/package-contracts/reviewer-output.ts +19 -0
- package/src/public-cli/invocation.ts +46 -1
- package/src/public-cli/reviewer-run.ts +12 -0
- package/src/public-cli/run-lifecycle.ts +22 -1
- package/src/public-cli/settlement.ts +8 -0
- package/src/reviewer-construction.ts +64 -7
- package/src/reviewer-dispatch.ts +81 -2
- package/src/reviewer-execution-ledger.ts +2 -1
- package/src/reviewer-pinned-git.ts +43 -0
- package/src/reviewer-role.ts +143 -132
- package/src/reviewer-settlement.ts +4 -0
- package/src/role-runtime.ts +186 -3
|
@@ -134,6 +134,11 @@ export type AdmittedReviewerInvocation = AdmittedRoleInvocationBase & {
|
|
|
134
134
|
readonly role: "reviewer";
|
|
135
135
|
/** Required fixed base revision for the pinned review target (ADR 0037). */
|
|
136
136
|
readonly baseRevision: string;
|
|
137
|
+
/**
|
|
138
|
+
* Optional durable authority references/URLs frozen at admission.
|
|
139
|
+
* Spec evidence-child material only — never Standards, never invocation prose promotion.
|
|
140
|
+
*/
|
|
141
|
+
readonly authorityRefs: readonly string[];
|
|
137
142
|
};
|
|
138
143
|
|
|
139
144
|
/** Mechanical envelope derived from the active ordinary two-parent merge. */
|
|
@@ -318,6 +323,8 @@ export type ParseReviewerArgvResult = {
|
|
|
318
323
|
attachmentPaths: string[];
|
|
319
324
|
/** Required fixed base revision for the pinned review target. */
|
|
320
325
|
baseRevision: string;
|
|
326
|
+
/** Repeatable durable authority references/URLs (exact order preserved). */
|
|
327
|
+
authorityRefs: string[];
|
|
321
328
|
project?: string;
|
|
322
329
|
};
|
|
323
330
|
|
|
@@ -353,6 +360,25 @@ function requireOptionPath(
|
|
|
353
360
|
return value;
|
|
354
361
|
}
|
|
355
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Public --authority-ref admission grammar (refs-only).
|
|
365
|
+
* Unique owner for fresh argv and durable resume restore — no string-only parallel.
|
|
366
|
+
* Accepts durable reference tokens as-is; rejects blank and inline Spec prose
|
|
367
|
+
* (whitespace-bearing sentences). Does not fetch, normalize, or judge content.
|
|
368
|
+
*/
|
|
369
|
+
export function requireAuthorityRef(value: string | undefined): string {
|
|
370
|
+
if (value === undefined || value.trim() === "") {
|
|
371
|
+
throw new CliUsageError("--authority-ref requires a nonempty durable reference");
|
|
372
|
+
}
|
|
373
|
+
// Spec prose sentences contain whitespace; durable public refs are single tokens.
|
|
374
|
+
if (/\s/.test(value)) {
|
|
375
|
+
throw new CliUsageError(
|
|
376
|
+
"--authority-ref requires a durable reference, not inline Spec prose",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
|
|
356
382
|
/**
|
|
357
383
|
* Parse Judge-specific argv after the `judge` token.
|
|
358
384
|
* Rejects any public burden selector/hint and unknown flags.
|
|
@@ -1581,13 +1607,15 @@ export function buildDoctorTransportPrompt(
|
|
|
1581
1607
|
|
|
1582
1608
|
/**
|
|
1583
1609
|
* Parse Reviewer-specific argv after the `reviewer` token.
|
|
1584
|
-
* Public flags: --project
|
|
1610
|
+
* Public flags: --project, required --base, optional repeatable --authority-ref.
|
|
1585
1611
|
* Reviewer gathers its own evidence; users submit neither attachments nor capability packets.
|
|
1612
|
+
* Caller instruction remains scope/procedure provenance — not Spec authority.
|
|
1586
1613
|
*/
|
|
1587
1614
|
export function parseReviewerArgv(
|
|
1588
1615
|
args: readonly string[],
|
|
1589
1616
|
): ParseReviewerArgvResult {
|
|
1590
1617
|
const attachmentPaths: string[] = [];
|
|
1618
|
+
const authorityRefs: string[] = [];
|
|
1591
1619
|
let project: string | undefined;
|
|
1592
1620
|
let baseRevision: string | undefined;
|
|
1593
1621
|
const positional: string[] = [];
|
|
@@ -1615,6 +1643,14 @@ export function parseReviewerArgv(
|
|
|
1615
1643
|
baseRevision = requireOptionPath("--base", token.slice("--base=".length));
|
|
1616
1644
|
continue;
|
|
1617
1645
|
}
|
|
1646
|
+
if (token === "--authority-ref") {
|
|
1647
|
+
authorityRefs.push(requireAuthorityRef(tokens.shift()));
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (token.startsWith("--authority-ref=")) {
|
|
1651
|
+
authorityRefs.push(requireAuthorityRef(token.slice("--authority-ref=".length)));
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1618
1654
|
if (token.startsWith("-") && token !== "-") {
|
|
1619
1655
|
throw new CliUsageError(`unknown reviewer option: ${token}`);
|
|
1620
1656
|
}
|
|
@@ -1628,6 +1664,7 @@ export function parseReviewerArgv(
|
|
|
1628
1664
|
instruction: positional.join(" "),
|
|
1629
1665
|
attachmentPaths,
|
|
1630
1666
|
baseRevision,
|
|
1667
|
+
authorityRefs,
|
|
1631
1668
|
...(project === undefined ? {} : { project }),
|
|
1632
1669
|
};
|
|
1633
1670
|
}
|
|
@@ -1639,6 +1676,8 @@ export type AdmitReviewerInvocationOptions = {
|
|
|
1639
1676
|
instruction: string;
|
|
1640
1677
|
attachmentPaths: readonly string[];
|
|
1641
1678
|
baseRevision: string;
|
|
1679
|
+
/** Optional durable authority references/URLs; frozen unchanged at admission. */
|
|
1680
|
+
authorityRefs?: readonly string[];
|
|
1642
1681
|
project?: string;
|
|
1643
1682
|
createRunId?: () => string;
|
|
1644
1683
|
};
|
|
@@ -1646,6 +1685,7 @@ export type AdmitReviewerInvocationOptions = {
|
|
|
1646
1685
|
/**
|
|
1647
1686
|
* Admit a Reviewer Role run on the fixed base only.
|
|
1648
1687
|
* Caller instruction is optional provenance; Reviewer acquires issue/authority independently.
|
|
1688
|
+
* Optional authorityRefs are frozen as durable references only — not Spec prose.
|
|
1649
1689
|
*/
|
|
1650
1690
|
export async function admitReviewerInvocation(
|
|
1651
1691
|
options: AdmitReviewerInvocationOptions,
|
|
@@ -1656,6 +1696,9 @@ export async function admitReviewerInvocation(
|
|
|
1656
1696
|
if (options.baseRevision.trim() === "") {
|
|
1657
1697
|
throw new CliUsageError("--base requires a nonempty revision");
|
|
1658
1698
|
}
|
|
1699
|
+
const authorityRefs = Object.freeze(
|
|
1700
|
+
(options.authorityRefs ?? []).map((ref) => requireAuthorityRef(ref)),
|
|
1701
|
+
);
|
|
1659
1702
|
|
|
1660
1703
|
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1661
1704
|
const runId = (options.createRunId ?? uuidv7)();
|
|
@@ -1686,6 +1729,7 @@ export async function admitReviewerInvocation(
|
|
|
1686
1729
|
instruction,
|
|
1687
1730
|
instructionEmpty,
|
|
1688
1731
|
baseRevision: options.baseRevision,
|
|
1732
|
+
authorityRefs: [...authorityRefs],
|
|
1689
1733
|
attachments: attachments.map((a) => ({
|
|
1690
1734
|
provenancePath: a.provenancePath,
|
|
1691
1735
|
frozenPath: a.frozenPath,
|
|
@@ -1715,6 +1759,7 @@ export async function admitReviewerInvocation(
|
|
|
1715
1759
|
sessionFile,
|
|
1716
1760
|
admittedRequestPath,
|
|
1717
1761
|
baseRevision: options.baseRevision,
|
|
1762
|
+
authorityRefs,
|
|
1718
1763
|
...ticketFields,
|
|
1719
1764
|
};
|
|
1720
1765
|
}
|
|
@@ -118,6 +118,10 @@ export function buildReviewerActivationExtraArgs(
|
|
|
118
118
|
options.packageRoot,
|
|
119
119
|
"code-review",
|
|
120
120
|
);
|
|
121
|
+
const authorityRefArgs =
|
|
122
|
+
admitted.authorityRefs.length === 0
|
|
123
|
+
? []
|
|
124
|
+
: ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
|
|
121
125
|
return [
|
|
122
126
|
"--no-skills",
|
|
123
127
|
"--skill",
|
|
@@ -134,6 +138,7 @@ export function buildReviewerActivationExtraArgs(
|
|
|
134
138
|
"reviewer",
|
|
135
139
|
"--ak-review-base",
|
|
136
140
|
admitted.baseRevision,
|
|
141
|
+
...authorityRefArgs,
|
|
137
142
|
"--mode",
|
|
138
143
|
"json",
|
|
139
144
|
...buildModelArgs(options.model),
|
|
@@ -157,6 +162,10 @@ export function buildReviewerResumeActivationExtraArgs(
|
|
|
157
162
|
options.packageRoot,
|
|
158
163
|
"code-review",
|
|
159
164
|
);
|
|
165
|
+
const authorityRefArgs =
|
|
166
|
+
admitted.authorityRefs.length === 0
|
|
167
|
+
? []
|
|
168
|
+
: ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
|
|
160
169
|
return [
|
|
161
170
|
"--no-skills",
|
|
162
171
|
"--skill",
|
|
@@ -173,6 +182,7 @@ export function buildReviewerResumeActivationExtraArgs(
|
|
|
173
182
|
"reviewer",
|
|
174
183
|
"--ak-review-base",
|
|
175
184
|
admitted.baseRevision,
|
|
185
|
+
...authorityRefArgs,
|
|
176
186
|
"--mode",
|
|
177
187
|
"json",
|
|
178
188
|
...buildModelArgs(options.model),
|
|
@@ -420,6 +430,7 @@ export async function runPublicReviewer(
|
|
|
420
430
|
instruction: string;
|
|
421
431
|
attachmentPaths: string[];
|
|
422
432
|
baseRevision: string;
|
|
433
|
+
authorityRefs: string[];
|
|
423
434
|
project?: string;
|
|
424
435
|
},
|
|
425
436
|
): Promise<{
|
|
@@ -436,6 +447,7 @@ export async function runPublicReviewer(
|
|
|
436
447
|
instruction: parsed.instruction,
|
|
437
448
|
attachmentPaths: parsed.attachmentPaths,
|
|
438
449
|
baseRevision: parsed.baseRevision,
|
|
450
|
+
authorityRefs: parsed.authorityRefs,
|
|
439
451
|
...(parsed.project === undefined ? {} : { project: parsed.project }),
|
|
440
452
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
441
453
|
});
|
|
@@ -24,6 +24,7 @@ export {
|
|
|
24
24
|
import type { FixerPhase } from "../package-contracts/fixer-output.ts";
|
|
25
25
|
import type { FixerPrerequisite } from "../package-contracts/fixer-packet.ts";
|
|
26
26
|
import {
|
|
27
|
+
requireAuthorityRef,
|
|
27
28
|
type AdmittedCoderInvocation,
|
|
28
29
|
type AdmittedFixerInvocation,
|
|
29
30
|
type AdmittedJudgeInvocation,
|
|
@@ -389,6 +390,7 @@ type LoadedAdmittedRequestFields = {
|
|
|
389
390
|
readonly prerequisitesPath?: string;
|
|
390
391
|
readonly prerequisites?: readonly FixerPrerequisite[];
|
|
391
392
|
readonly baseRevision?: string;
|
|
393
|
+
readonly authorityRefs?: readonly string[];
|
|
392
394
|
readonly mergerInputPath?: string;
|
|
393
395
|
readonly derived?: DerivedMergerEnvelope;
|
|
394
396
|
readonly correlationId?: string;
|
|
@@ -463,6 +465,7 @@ async function loadResumableRunRecord(
|
|
|
463
465
|
let prerequisitesPath: string | undefined;
|
|
464
466
|
let prerequisites: readonly FixerPrerequisite[] | undefined;
|
|
465
467
|
let baseRevision: string | undefined;
|
|
468
|
+
let authorityRefs: readonly string[] | undefined;
|
|
466
469
|
let mergerInputPath: string | undefined;
|
|
467
470
|
let derived: DerivedMergerEnvelope | undefined;
|
|
468
471
|
let correlationId: string | undefined;
|
|
@@ -506,6 +509,19 @@ async function loadResumableRunRecord(
|
|
|
506
509
|
) {
|
|
507
510
|
baseRevision = record.baseRevision;
|
|
508
511
|
}
|
|
512
|
+
if (Array.isArray(record.authorityRefs)) {
|
|
513
|
+
// Reuse unique --authority-ref grammar; blank/inline prose must not resume as authority.
|
|
514
|
+
authorityRefs = Object.freeze(
|
|
515
|
+
record.authorityRefs.map((ref) => {
|
|
516
|
+
if (typeof ref !== "string") {
|
|
517
|
+
throw new CliUsageError(
|
|
518
|
+
"role run admitted authority refs must be durable reference strings",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return requireAuthorityRef(ref);
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
509
525
|
if (
|
|
510
526
|
typeof record.mergerInputPath === "string" &&
|
|
511
527
|
record.mergerInputPath.trim() !== ""
|
|
@@ -540,9 +556,12 @@ async function loadResumableRunRecord(
|
|
|
540
556
|
correlationId = fromAdmitted.correlationId;
|
|
541
557
|
ticketNumber = fromAdmitted.ticketNumber;
|
|
542
558
|
}
|
|
543
|
-
} catch {
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Preserve unique --authority-ref grammar failures; do not collapse to unreadable.
|
|
561
|
+
if (error instanceof CliUsageError) throw error;
|
|
544
562
|
throw new CliUsageError(
|
|
545
563
|
`role run admitted request is unreadable: ${runId}`,
|
|
564
|
+
{ cause: error },
|
|
546
565
|
);
|
|
547
566
|
}
|
|
548
567
|
if (correlationId === undefined || ticketNumber === undefined) {
|
|
@@ -589,6 +608,7 @@ async function loadResumableRunRecord(
|
|
|
589
608
|
...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
|
|
590
609
|
...(prerequisites === undefined ? {} : { prerequisites }),
|
|
591
610
|
...(baseRevision === undefined ? {} : { baseRevision }),
|
|
611
|
+
...(authorityRefs === undefined ? {} : { authorityRefs }),
|
|
592
612
|
...(mergerInputPath === undefined ? {} : { mergerInputPath }),
|
|
593
613
|
...(derived === undefined ? {} : { derived }),
|
|
594
614
|
...(correlationId === undefined ? {} : { correlationId }),
|
|
@@ -806,6 +826,7 @@ export async function loadResumableReviewerRun(
|
|
|
806
826
|
sessionFile: loaded.run.sessionFile,
|
|
807
827
|
admittedRequestPath: loaded.run.admittedRequestPath,
|
|
808
828
|
baseRevision,
|
|
829
|
+
authorityRefs: Object.freeze([...(loaded.admittedFields.authorityRefs ?? [])]),
|
|
809
830
|
...restoredTicketFields(loaded.admittedFields),
|
|
810
831
|
};
|
|
811
832
|
return {
|
|
@@ -1255,6 +1255,7 @@ function reviewerDecisiveFacts(
|
|
|
1255
1255
|
const axes = reviewerAxes(outcomes.readable ? outcomes.value : undefined);
|
|
1256
1256
|
const reportAxes = reviewerAxes(reports.readable ? reports.value : undefined);
|
|
1257
1257
|
const acceptedBatch = safelyRead(candidate, "acceptedBatch");
|
|
1258
|
+
const specDisposition = safelyRead(candidate, "specDisposition");
|
|
1258
1259
|
const facts: Record<string, unknown> = {
|
|
1259
1260
|
axes,
|
|
1260
1261
|
reportAxes,
|
|
@@ -1262,6 +1263,12 @@ function reviewerDecisiveFacts(
|
|
|
1262
1263
|
...auditNoReceiptDecisiveFact(candidate),
|
|
1263
1264
|
};
|
|
1264
1265
|
if (status.readable && typeof status.value === "string") facts.reviewerStatus = status.value;
|
|
1266
|
+
if (
|
|
1267
|
+
specDisposition.readable &&
|
|
1268
|
+
(specDisposition.value === "launched" || specDisposition.value === "skipped-missing")
|
|
1269
|
+
) {
|
|
1270
|
+
facts.specDisposition = specDisposition.value;
|
|
1271
|
+
}
|
|
1265
1272
|
const diagnostic = safelyRead(candidate, "diagnostic");
|
|
1266
1273
|
if (status.readable && status.value === "refused" && diagnostic.readable) {
|
|
1267
1274
|
facts.diagnosticPresent = typeof diagnostic.value === "string" && diagnostic.value.trim().length > 0;
|
|
@@ -3165,6 +3172,7 @@ export async function publishReviewerArtifacts(
|
|
|
3165
3172
|
sessionFile: admitted.sessionFile,
|
|
3166
3173
|
admittedRequestPath: admitted.admittedRequestPath,
|
|
3167
3174
|
baseRevision: admitted.baseRevision,
|
|
3175
|
+
authorityRefs: [...admitted.authorityRefs],
|
|
3168
3176
|
...(admitted.instructionEmpty
|
|
3169
3177
|
? {}
|
|
3170
3178
|
: { callerProvenance: admitted.instruction }),
|
|
@@ -18,6 +18,22 @@ export const REVIEWER_AXIS_OUTPUT_ADAPTER = Object.freeze({
|
|
|
18
18
|
implementationSha256: sha256Hex("reviewer-axis-output:v1:single-axis-verbatim-report+standards-three-priorities"),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Package-owned #1185 review verification cadence.
|
|
23
|
+
* Single true source consumed by two real actor carriers: parent Reviewer system-prompt injection
|
|
24
|
+
* and evidence-child system prompt. Not part of axis-adapter identity or axis leg prompts.
|
|
25
|
+
* Graded guidance only: focused tests allowed; full suite not forbidden but avoid frequent every-round reruns.
|
|
26
|
+
* Does not narrow ADR 0064 tools and adds no command ban, allowlist, or runtime block.
|
|
27
|
+
*/
|
|
28
|
+
export const REVIEWER_VERIFICATION_BOUNDARY = [
|
|
29
|
+
"Verification-Boundary: you may run focused product tests during this review turn when independent verification needs them.",
|
|
30
|
+
"A full repository test suite is not forbidden, but do not re-run it every review round;",
|
|
31
|
+
"prefer once at family wrap-up unless this review specifically requires a broader run.",
|
|
32
|
+
"Slice and review work should not trigger frequent full-suite reruns.",
|
|
33
|
+
"Independently discover test facts (including existing coder/fixer receipts and any tests you run);",
|
|
34
|
+
"do not treat caller prose as the source of those facts.",
|
|
35
|
+
].join(" ");
|
|
36
|
+
|
|
21
37
|
/** Typed Standards conclusion keys owned by reviewer construction (presentation labels are not the contract). */
|
|
22
38
|
export const REVIEWER_STANDARDS_CONCLUSION_KEYS = Object.freeze([
|
|
23
39
|
"constitutionality",
|
|
@@ -85,6 +101,15 @@ export function reviewerAxisMethodAdapter(axis: ReviewerAxis): string {
|
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
export type ConstructedReviewerLeg = Readonly<{ axis: "standards" | "spec"; prompt: ReviewerPromptText }>;
|
|
104
|
+
/**
|
|
105
|
+
* Unique discovery product for Skill step 2: durable refs Spec child can read, or confirmed missing.
|
|
106
|
+
* Construction builds Standards/Spec solely from this product — no secondary launch decision.
|
|
107
|
+
*/
|
|
108
|
+
export type ReviewerSpecAuthorityDiscovery =
|
|
109
|
+
| Readonly<{ status: "available"; refs: readonly string[] }>
|
|
110
|
+
| Readonly<{ status: "missing" }>;
|
|
111
|
+
/** Spec-child cardinality decision recorded on the accepted dispatch. */
|
|
112
|
+
export type ReviewerSpecDisposition = "launched" | "skipped-missing";
|
|
88
113
|
export type ConstructedReviewerDispatch = Readonly<{
|
|
89
114
|
identity: string;
|
|
90
115
|
recipe: "reviewer-common-bundle-v1";
|
|
@@ -94,17 +119,40 @@ export type ConstructedReviewerDispatch = Readonly<{
|
|
|
94
119
|
}>;
|
|
95
120
|
targetSnapshot: ReviewerPinnedTarget;
|
|
96
121
|
range: ReviewerRange;
|
|
122
|
+
/** Frozen durable authority references carried into Spec evidence-child material only. */
|
|
123
|
+
authorityRefs: readonly string[];
|
|
124
|
+
/** Honest Spec-child disposition: launched, or skipped after confirmed missing Spec. */
|
|
125
|
+
specDisposition: ReviewerSpecDisposition;
|
|
97
126
|
legs: readonly ConstructedReviewerLeg[];
|
|
98
127
|
}>;
|
|
99
128
|
|
|
100
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Spec-only evidence-child material carrier for durable authority references.
|
|
131
|
+
* Exact values preserved; no prose extraction and no Standards/parent injection.
|
|
132
|
+
*/
|
|
133
|
+
export function reviewerAuthorityRefsMaterial(authorityRefs: readonly string[]): string {
|
|
134
|
+
return [
|
|
135
|
+
"Authority-Refs:",
|
|
136
|
+
JSON.stringify(Object.freeze([...authorityRefs])),
|
|
137
|
+
"These are durable authority references only. Read them as Spec grounding materials; do not invent Spec prose from caller instruction.",
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Deterministic compiler: fixed target/range plus discovery product in, dispatch text out. */
|
|
101
142
|
export function constructReviewerDispatch(input: {
|
|
102
143
|
identity: string;
|
|
103
144
|
canonicalSkill: string;
|
|
104
145
|
target: ReviewerPinnedTarget;
|
|
105
146
|
range: ReviewerRange;
|
|
106
147
|
reviewScopeKeys?: readonly string[];
|
|
148
|
+
/** Unique discovery product (available+refs material, or missing). */
|
|
149
|
+
specAuthority: ReviewerSpecAuthorityDiscovery;
|
|
107
150
|
}): ConstructedReviewerDispatch {
|
|
151
|
+
const launchSpec = input.specAuthority.status === "available";
|
|
152
|
+
const authorityRefs = Object.freeze(
|
|
153
|
+
input.specAuthority.status === "available" ? [...input.specAuthority.refs] : [],
|
|
154
|
+
);
|
|
155
|
+
const specDisposition: ReviewerSpecDisposition = launchSpec ? "launched" : "skipped-missing";
|
|
108
156
|
const common = [
|
|
109
157
|
`Target: ${input.range.target}`,
|
|
110
158
|
`Base: ${input.range.base}`,
|
|
@@ -116,13 +164,20 @@ export function constructReviewerDispatch(input: {
|
|
|
116
164
|
"Fixed-Range:",
|
|
117
165
|
JSON.stringify(input.range, null, 2),
|
|
118
166
|
].join("\n");
|
|
119
|
-
const axes
|
|
120
|
-
|
|
121
|
-
|
|
167
|
+
const axes: readonly { axis: "standards" | "spec" }[] = launchSpec
|
|
168
|
+
? [{ axis: "standards" }, { axis: "spec" }]
|
|
169
|
+
: [{ axis: "standards" }];
|
|
170
|
+
const legs = axes.map((x) => {
|
|
171
|
+
const parts = [common, reviewerAxisMethodAdapter(x.axis)];
|
|
172
|
+
// Spec evidence-child only — never Standards or a parent replacement Spec leg.
|
|
173
|
+
if (x.axis === "spec" && authorityRefs.length > 0) {
|
|
174
|
+
parts.push(reviewerAuthorityRefsMaterial(authorityRefs));
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze({
|
|
122
177
|
axis: x.axis,
|
|
123
|
-
prompt: `${
|
|
124
|
-
})
|
|
125
|
-
);
|
|
178
|
+
prompt: `${parts.join("\n")}\n`,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
126
181
|
return Object.freeze({
|
|
127
182
|
identity: input.identity,
|
|
128
183
|
recipe: "reviewer-common-bundle-v1",
|
|
@@ -132,6 +187,8 @@ export function constructReviewerDispatch(input: {
|
|
|
132
187
|
}),
|
|
133
188
|
targetSnapshot: input.target,
|
|
134
189
|
range: input.range,
|
|
190
|
+
authorityRefs,
|
|
191
|
+
specDisposition,
|
|
135
192
|
legs: Object.freeze(legs),
|
|
136
193
|
});
|
|
137
194
|
}
|
package/src/reviewer-dispatch.ts
CHANGED
|
@@ -1,13 +1,84 @@
|
|
|
1
1
|
import { sameReviewerPinnedTarget } from "./reviewer-git-snapshot.ts";
|
|
2
|
-
import { immutableReviewerPin, type ReviewerPinnedGitReader, type ReviewerPinnedTarget } from "./reviewer-pinned-git.ts";
|
|
2
|
+
import { immutableReviewerPin, type ReviewerPinnedGitReader, type ReviewerPinnedTarget, type ReviewerRange } from "./reviewer-pinned-git.ts";
|
|
3
3
|
export { createReviewerPinnedGitReader, immutableReviewerPin, type ReviewerPinnedGitReader, type ReviewerPinnedTarget, type ReviewerRange } from "./reviewer-pinned-git.ts";
|
|
4
4
|
import { isReviewerPromptText, sameReviewerPromptText, type ReviewerPromptText } from "./reviewer-prompt-identity.ts";
|
|
5
5
|
import { sha256Hex } from "./sha256.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
constructReviewerDispatch,
|
|
8
|
+
type ConstructedReviewerDispatch,
|
|
9
|
+
type ReviewerSpecAuthorityDiscovery,
|
|
10
|
+
} from "./reviewer-construction.ts";
|
|
11
|
+
export {
|
|
12
|
+
type ReviewerSpecAuthorityDiscovery,
|
|
13
|
+
type ReviewerSpecDisposition,
|
|
14
|
+
} from "./reviewer-construction.ts";
|
|
7
15
|
import { ReviewerCorrectablePreflightError } from "./reviewer-preflight-error.ts";
|
|
8
16
|
export { sha256Hex } from "./sha256.ts";
|
|
9
17
|
export { isReviewerPromptText as isReviewerPromptIdentity, sameReviewerPromptText as sameReviewerPromptIdentity, type ReviewerPromptText as ReviewerPromptIdentity } from "./reviewer-prompt-identity.ts";
|
|
10
18
|
|
|
19
|
+
const GENERIC_FEATURE_TOKENS = new Set(["", "head", "main", "master", "trunk", "develop", "development"]);
|
|
20
|
+
/** Conventional branch shells that must not hide the feature token (feat/login → login). */
|
|
21
|
+
const BRANCH_SHELL_PREFIX = /^(?:feat|feature|fix|bugfix|hotfix|chore|docs|refactor)-/;
|
|
22
|
+
|
|
23
|
+
function normalizeFeatureToken(value: string): string {
|
|
24
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Expand one branch/ref name into matchable tokens, stripping conventional shells. */
|
|
28
|
+
function expandFeatureTokens(raw: string): readonly string[] {
|
|
29
|
+
const normalized = normalizeFeatureToken(raw);
|
|
30
|
+
if (normalized.length === 0) return Object.freeze([]);
|
|
31
|
+
const tokens = new Set<string>([normalized]);
|
|
32
|
+
const stripped = normalized.replace(BRANCH_SHELL_PREFIX, "");
|
|
33
|
+
if (stripped.length > 0 && stripped !== normalized) tokens.add(stripped);
|
|
34
|
+
return Object.freeze([...tokens]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Unique production owner of code-review Skill step 2 Spec discovery.
|
|
39
|
+
* Directly yields durable refs Spec child can read, or confirmed missing.
|
|
40
|
+
* - Supplied authorityRefs ⇒ available with those refs as material.
|
|
41
|
+
* - Matching pinned-target docs/specs/.scratch paths ⇒ available with those paths as material.
|
|
42
|
+
* - Commit message bare #N without durable source ⇒ missing (not available).
|
|
43
|
+
* Only confirmed absence yields missing; other Git/I-O failures keep true cause for preflight.
|
|
44
|
+
* Construction builds Standards/Spec solely from this product.
|
|
45
|
+
*/
|
|
46
|
+
export async function discoverReviewerSpecAuthority(input: {
|
|
47
|
+
authorityRefs: readonly string[];
|
|
48
|
+
reader: ReviewerPinnedGitReader;
|
|
49
|
+
}): Promise<ReviewerSpecAuthorityDiscovery> {
|
|
50
|
+
if (input.authorityRefs.length > 0) {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
status: "available" as const,
|
|
53
|
+
refs: Object.freeze([...input.authorityRefs]),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const featureTokens = await input.reader.featureTokens();
|
|
57
|
+
const tokens = [
|
|
58
|
+
...new Set(
|
|
59
|
+
featureTokens
|
|
60
|
+
.flatMap((raw) => expandFeatureTokens(raw))
|
|
61
|
+
.filter((token) => token.length >= 3 && !GENERIC_FEATURE_TOKENS.has(token)),
|
|
62
|
+
),
|
|
63
|
+
];
|
|
64
|
+
if (tokens.length === 0) {
|
|
65
|
+
return Object.freeze({ status: "missing" as const });
|
|
66
|
+
}
|
|
67
|
+
// Pinned target tree only — Spec child cannot read live-worktree or gitignored paths.
|
|
68
|
+
const candidates = await input.reader.listSpecCandidatePaths();
|
|
69
|
+
const matched = candidates.filter((relativePath) => {
|
|
70
|
+
const normalizedPath = normalizeFeatureToken(relativePath);
|
|
71
|
+
return tokens.some((token) => normalizedPath.includes(token));
|
|
72
|
+
});
|
|
73
|
+
if (matched.length === 0) {
|
|
74
|
+
return Object.freeze({ status: "missing" as const });
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
status: "available" as const,
|
|
78
|
+
refs: Object.freeze(matched),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
11
82
|
export type AcceptedReviewerLeg = ConstructedReviewerDispatch["legs"][number];
|
|
12
83
|
export type AcceptedReviewerDispatch = ConstructedReviewerDispatch;
|
|
13
84
|
export type AcceptedReviewerExecution = Readonly<{
|
|
@@ -28,6 +99,8 @@ type DispatcherDependencies = Readonly<{
|
|
|
28
99
|
canonicalSkill: string;
|
|
29
100
|
reader: ReviewerPinnedGitReader;
|
|
30
101
|
reviewScopeKeys?: readonly string[];
|
|
102
|
+
/** Durable authority references preserved unchanged into Spec-leg construction only. */
|
|
103
|
+
authorityRefs?: readonly string[];
|
|
31
104
|
run(execution: AcceptedReviewerExecution, invocation: unknown): Promise<unknown>;
|
|
32
105
|
decisionEvidence?(decision: ReviewerDecisionEvidence): void;
|
|
33
106
|
}>;
|
|
@@ -66,12 +139,18 @@ export function createReviewerDispatcher(d: DispatcherDependencies) {
|
|
|
66
139
|
try {
|
|
67
140
|
const base = await d.reader.resolve(baseRevision);
|
|
68
141
|
const range = await d.reader.range(base);
|
|
142
|
+
const authorityRefs = Object.freeze([...(d.authorityRefs ?? [])]);
|
|
143
|
+
const specAuthority = await discoverReviewerSpecAuthority({
|
|
144
|
+
authorityRefs,
|
|
145
|
+
reader: d.reader,
|
|
146
|
+
});
|
|
69
147
|
dispatch = constructReviewerDispatch({
|
|
70
148
|
identity,
|
|
71
149
|
canonicalSkill: d.canonicalSkill,
|
|
72
150
|
target,
|
|
73
151
|
range,
|
|
74
152
|
...(d.reviewScopeKeys === undefined ? {} : { reviewScopeKeys: d.reviewScopeKeys }),
|
|
153
|
+
specAuthority,
|
|
75
154
|
});
|
|
76
155
|
if (!sameReviewerPinnedTarget(await d.reader.snapshot(), target)) {
|
|
77
156
|
throw new ReviewerPreflightError("target-drift", "pinned target snapshot changed before child execution");
|
|
@@ -27,7 +27,8 @@ export function projectAcceptedDispatch(dispatch: AcceptedReviewerDispatch): Rev
|
|
|
27
27
|
return {
|
|
28
28
|
source: "reviewer-dispatch", type: "accepted", identity: dispatch.identity,
|
|
29
29
|
recipe: dispatch.recipe, input: dispatch.input, target: dispatch.targetSnapshot,
|
|
30
|
-
range: dispatch.range,
|
|
30
|
+
range: dispatch.range, authorityRefs: dispatch.authorityRefs,
|
|
31
|
+
specDisposition: dispatch.specDisposition, legs: dispatch.legs,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -25,6 +25,18 @@ export type ReviewerPinnedGitReader = {
|
|
|
25
25
|
snapshot(): Promise<ReviewerPinnedTarget>;
|
|
26
26
|
resolve(base: string): Promise<string>;
|
|
27
27
|
range(base: string): Promise<ReviewerRange>;
|
|
28
|
+
/**
|
|
29
|
+
* Branch/feature name tokens at the pinned target for Spec path matching.
|
|
30
|
+
* Derived from the pinned ref snapshot (heads/tags/remotes pointing at targetHead);
|
|
31
|
+
* does not depend on current symbolic HEAD, so detached/remote-only tips stay honest.
|
|
32
|
+
*/
|
|
33
|
+
featureTokens(): Promise<readonly string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Durable Spec-candidate paths present in the pinned target tree under docs/specs/.scratch.
|
|
36
|
+
* Spec child clones this target — live working tree is not a source of material facts.
|
|
37
|
+
* Empty list is confirmed absence; other Git/I-O failures propagate with true cause.
|
|
38
|
+
*/
|
|
39
|
+
listSpecCandidatePaths(): Promise<readonly string[]>;
|
|
28
40
|
};
|
|
29
41
|
|
|
30
42
|
const execFileAsync = promisify(execFile);
|
|
@@ -139,6 +151,37 @@ export async function createReviewerPinnedGitReader(root = process.cwd()): Promi
|
|
|
139
151
|
if (diff.length === 0) invalid("range-invalid", "review range must contain a non-empty diff between base and pinned target");
|
|
140
152
|
return Object.freeze({ base: mergeBase, target: targetHead, diffCommand, diffSha256: sha256Hex(Uint8Array.from(diff)), commits: Object.freeze(commitsText ? commitsText.split("\n") : []) });
|
|
141
153
|
},
|
|
154
|
+
async featureTokens() {
|
|
155
|
+
// Pinned ref snapshot is the target-tree fact — no live branch/symbolic-ref walk,
|
|
156
|
+
// no catch-to-empty. Detached/remote-only tips surface via refs/remotes/* entries.
|
|
157
|
+
const names = new Set<string>();
|
|
158
|
+
for (const [refName, entry] of Object.entries(pin.refs)) {
|
|
159
|
+
if (entry.peeledCommitId !== targetHead) continue;
|
|
160
|
+
const short = refName.startsWith("refs/heads/")
|
|
161
|
+
? refName.slice("refs/heads/".length)
|
|
162
|
+
: refName.startsWith("refs/tags/")
|
|
163
|
+
? refName.slice("refs/tags/".length)
|
|
164
|
+
: refName.startsWith("refs/remotes/")
|
|
165
|
+
? refName.slice("refs/remotes/".length).replace(/^[^/]+\//, "")
|
|
166
|
+
: refName;
|
|
167
|
+
if (short.trim() !== "") names.add(short.trim());
|
|
168
|
+
}
|
|
169
|
+
return Object.freeze([...names]);
|
|
170
|
+
},
|
|
171
|
+
async listSpecCandidatePaths() {
|
|
172
|
+
const roots = ["docs", "specs", ".scratch"] as const;
|
|
173
|
+
// git ls-tree exits 0 with empty stdout when none of the roots exist at targetHead.
|
|
174
|
+
// Other Git/I-O failures keep their true cause for the dispatch preflight path.
|
|
175
|
+
const text = await gitText(repositoryRoot, [
|
|
176
|
+
"ls-tree",
|
|
177
|
+
"-r",
|
|
178
|
+
"--name-only",
|
|
179
|
+
targetHead,
|
|
180
|
+
"--",
|
|
181
|
+
...roots,
|
|
182
|
+
]);
|
|
183
|
+
return Object.freeze(text === "" ? [] : text.split("\n").filter((line) => line.length > 0));
|
|
184
|
+
},
|
|
142
185
|
|
|
143
186
|
});
|
|
144
187
|
}
|