@akagilnc/pi-workflow-roles 0.1.1893 → 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 +447 -173
- 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/factory-board.ts +60 -3
- package/src/package-contracts/reviewer-output.ts +19 -0
- package/src/public-cli/cli.ts +96 -39
- package/src/public-cli/coder-run.ts +11 -4
- package/src/public-cli/collector-run.ts +7 -3
- package/src/public-cli/fixer-run.ts +11 -4
- package/src/public-cli/invocation.ts +183 -102
- package/src/public-cli/judge-run.ts +11 -4
- package/src/public-cli/merger-run.ts +11 -4
- package/src/public-cli/reviewer-run.ts +23 -4
- package/src/public-cli/run-lifecycle.ts +97 -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
- package/src/ticket-frontmatter.ts +94 -0
- package/src/ticket-trajectory.ts +145 -5
|
@@ -308,8 +308,9 @@ async function dispatchAdmittedMerger(input: {
|
|
|
308
308
|
PI_CODING_AGENT_DIR: env.agentDir,
|
|
309
309
|
AK_ROLE_RUN_DIR: admitted.runDirectory,
|
|
310
310
|
};
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
const correlationId = admitted.correlationId ?? env.correlationId;
|
|
312
|
+
if (correlationId !== undefined && correlationId.trim() !== "") {
|
|
313
|
+
childEnv.AK_CORRELATION_ID = correlationId;
|
|
313
314
|
}
|
|
314
315
|
|
|
315
316
|
let result: ExplicitInternalPiResult;
|
|
@@ -589,7 +590,10 @@ export async function runPublicMerger(
|
|
|
589
590
|
|
|
590
591
|
return await dispatchAdmittedMerger({
|
|
591
592
|
admitted,
|
|
592
|
-
env
|
|
593
|
+
env: {
|
|
594
|
+
...env,
|
|
595
|
+
...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
|
|
596
|
+
},
|
|
593
597
|
io,
|
|
594
598
|
extraArgs,
|
|
595
599
|
lease,
|
|
@@ -676,7 +680,10 @@ export async function runPublicMergerResume(
|
|
|
676
680
|
|
|
677
681
|
return await dispatchAdmittedMerger({
|
|
678
682
|
admitted,
|
|
679
|
-
env
|
|
683
|
+
env: {
|
|
684
|
+
...env,
|
|
685
|
+
...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
|
|
686
|
+
},
|
|
680
687
|
io,
|
|
681
688
|
extraArgs,
|
|
682
689
|
lease,
|
|
@@ -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),
|
|
@@ -293,8 +303,9 @@ async function dispatchAdmittedReviewer(input: {
|
|
|
293
303
|
PI_CODING_AGENT_DIR: env.agentDir,
|
|
294
304
|
AK_ROLE_RUN_DIR: admitted.runDirectory,
|
|
295
305
|
};
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
const correlationId = admitted.correlationId ?? env.correlationId;
|
|
307
|
+
if (correlationId !== undefined && correlationId.trim() !== "") {
|
|
308
|
+
childEnv.AK_CORRELATION_ID = correlationId;
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
let result: ExplicitInternalPiResult;
|
|
@@ -419,6 +430,7 @@ export async function runPublicReviewer(
|
|
|
419
430
|
instruction: string;
|
|
420
431
|
attachmentPaths: string[];
|
|
421
432
|
baseRevision: string;
|
|
433
|
+
authorityRefs: string[];
|
|
422
434
|
project?: string;
|
|
423
435
|
},
|
|
424
436
|
): Promise<{
|
|
@@ -435,6 +447,7 @@ export async function runPublicReviewer(
|
|
|
435
447
|
instruction: parsed.instruction,
|
|
436
448
|
attachmentPaths: parsed.attachmentPaths,
|
|
437
449
|
baseRevision: parsed.baseRevision,
|
|
450
|
+
authorityRefs: parsed.authorityRefs,
|
|
438
451
|
...(parsed.project === undefined ? {} : { project: parsed.project }),
|
|
439
452
|
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
440
453
|
});
|
|
@@ -484,7 +497,10 @@ export async function runPublicReviewer(
|
|
|
484
497
|
|
|
485
498
|
return await dispatchAdmittedReviewer({
|
|
486
499
|
admitted,
|
|
487
|
-
env
|
|
500
|
+
env: {
|
|
501
|
+
...env,
|
|
502
|
+
...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
|
|
503
|
+
},
|
|
488
504
|
io,
|
|
489
505
|
extraArgs,
|
|
490
506
|
lease,
|
|
@@ -570,7 +586,10 @@ export async function runPublicReviewerResume(
|
|
|
570
586
|
|
|
571
587
|
return await dispatchAdmittedReviewer({
|
|
572
588
|
admitted,
|
|
573
|
-
env
|
|
589
|
+
env: {
|
|
590
|
+
...env,
|
|
591
|
+
...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
|
|
592
|
+
},
|
|
574
593
|
io,
|
|
575
594
|
extraArgs,
|
|
576
595
|
lease,
|
|
@@ -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,10 +390,43 @@ 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;
|
|
396
|
+
readonly correlationId?: string;
|
|
397
|
+
readonly ticketNumber?: number;
|
|
394
398
|
};
|
|
395
399
|
|
|
400
|
+
/** Restore optional correlation + typed ticketNumber from a durable admitted page. */
|
|
401
|
+
function parsePersistedTicketIdentity(
|
|
402
|
+
record: Record<string, unknown>,
|
|
403
|
+
): { correlationId?: string; ticketNumber?: number } {
|
|
404
|
+
const correlationId =
|
|
405
|
+
typeof record.correlationId === "string" && record.correlationId.trim() !== ""
|
|
406
|
+
? record.correlationId
|
|
407
|
+
: undefined;
|
|
408
|
+
const ticketNumber =
|
|
409
|
+
typeof record.ticketNumber === "number" &&
|
|
410
|
+
Number.isInteger(record.ticketNumber) &&
|
|
411
|
+
record.ticketNumber >= 1
|
|
412
|
+
? record.ticketNumber
|
|
413
|
+
: undefined;
|
|
414
|
+
return {
|
|
415
|
+
...(correlationId === undefined ? {} : { correlationId }),
|
|
416
|
+
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function restoredTicketFields(fields: LoadedAdmittedRequestFields): {
|
|
421
|
+
correlationId?: string;
|
|
422
|
+
ticketNumber?: number;
|
|
423
|
+
} {
|
|
424
|
+
return {
|
|
425
|
+
...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
|
|
426
|
+
...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
396
430
|
async function loadResumableRunRecord(
|
|
397
431
|
home: string,
|
|
398
432
|
runId: string,
|
|
@@ -431,8 +465,11 @@ async function loadResumableRunRecord(
|
|
|
431
465
|
let prerequisitesPath: string | undefined;
|
|
432
466
|
let prerequisites: readonly FixerPrerequisite[] | undefined;
|
|
433
467
|
let baseRevision: string | undefined;
|
|
468
|
+
let authorityRefs: readonly string[] | undefined;
|
|
434
469
|
let mergerInputPath: string | undefined;
|
|
435
470
|
let derived: DerivedMergerEnvelope | undefined;
|
|
471
|
+
let correlationId: string | undefined;
|
|
472
|
+
let ticketNumber: number | undefined;
|
|
436
473
|
try {
|
|
437
474
|
const raw: unknown = JSON.parse(
|
|
438
475
|
await readFile(run.admittedRequestPath, "utf8"),
|
|
@@ -472,6 +509,19 @@ async function loadResumableRunRecord(
|
|
|
472
509
|
) {
|
|
473
510
|
baseRevision = record.baseRevision;
|
|
474
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
|
+
}
|
|
475
525
|
if (
|
|
476
526
|
typeof record.mergerInputPath === "string" &&
|
|
477
527
|
record.mergerInputPath.trim() !== ""
|
|
@@ -502,12 +552,49 @@ async function loadResumableRunRecord(
|
|
|
502
552
|
};
|
|
503
553
|
}
|
|
504
554
|
}
|
|
555
|
+
const fromAdmitted = parsePersistedTicketIdentity(record);
|
|
556
|
+
correlationId = fromAdmitted.correlationId;
|
|
557
|
+
ticketNumber = fromAdmitted.ticketNumber;
|
|
505
558
|
}
|
|
506
|
-
} catch {
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Preserve unique --authority-ref grammar failures; do not collapse to unreadable.
|
|
561
|
+
if (error instanceof CliUsageError) throw error;
|
|
507
562
|
throw new CliUsageError(
|
|
508
563
|
`role run admitted request is unreadable: ${runId}`,
|
|
564
|
+
{ cause: error },
|
|
509
565
|
);
|
|
510
566
|
}
|
|
567
|
+
if (correlationId === undefined || ticketNumber === undefined) {
|
|
568
|
+
try {
|
|
569
|
+
const invocationRaw: unknown = JSON.parse(
|
|
570
|
+
await readFile(join(run.runDirectory, "invocation.json"), "utf8"),
|
|
571
|
+
);
|
|
572
|
+
if (
|
|
573
|
+
invocationRaw !== null &&
|
|
574
|
+
typeof invocationRaw === "object" &&
|
|
575
|
+
!Array.isArray(invocationRaw)
|
|
576
|
+
) {
|
|
577
|
+
const fromInvocation = parsePersistedTicketIdentity(
|
|
578
|
+
invocationRaw as Record<string, unknown>,
|
|
579
|
+
);
|
|
580
|
+
if (correlationId === undefined) correlationId = fromInvocation.correlationId;
|
|
581
|
+
if (ticketNumber === undefined) ticketNumber = fromInvocation.ticketNumber;
|
|
582
|
+
}
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (
|
|
585
|
+
!(
|
|
586
|
+
error instanceof Error &&
|
|
587
|
+
"code" in error &&
|
|
588
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
589
|
+
)
|
|
590
|
+
) {
|
|
591
|
+
throw new CliUsageError(
|
|
592
|
+
`role run invocation identity is unreadable: ${runId}`,
|
|
593
|
+
{ cause: error },
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
511
598
|
return {
|
|
512
599
|
run,
|
|
513
600
|
observation: run.resumable,
|
|
@@ -521,8 +608,11 @@ async function loadResumableRunRecord(
|
|
|
521
608
|
...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
|
|
522
609
|
...(prerequisites === undefined ? {} : { prerequisites }),
|
|
523
610
|
...(baseRevision === undefined ? {} : { baseRevision }),
|
|
611
|
+
...(authorityRefs === undefined ? {} : { authorityRefs }),
|
|
524
612
|
...(mergerInputPath === undefined ? {} : { mergerInputPath }),
|
|
525
613
|
...(derived === undefined ? {} : { derived }),
|
|
614
|
+
...(correlationId === undefined ? {} : { correlationId }),
|
|
615
|
+
...(ticketNumber === undefined ? {} : { ticketNumber }),
|
|
526
616
|
},
|
|
527
617
|
};
|
|
528
618
|
}
|
|
@@ -577,6 +667,7 @@ export async function loadResumableJudgeRun(
|
|
|
577
667
|
sessionDirectory: loaded.run.sessionDirectory,
|
|
578
668
|
sessionFile: loaded.run.sessionFile,
|
|
579
669
|
admittedRequestPath: loaded.run.admittedRequestPath,
|
|
670
|
+
...restoredTicketFields(loaded.admittedFields),
|
|
580
671
|
};
|
|
581
672
|
return {
|
|
582
673
|
admitted,
|
|
@@ -630,6 +721,7 @@ export async function loadResumableCoderRun(
|
|
|
630
721
|
sessionFile: loaded.run.sessionFile,
|
|
631
722
|
admittedRequestPath: loaded.run.admittedRequestPath,
|
|
632
723
|
taskPath,
|
|
724
|
+
...restoredTicketFields(loaded.admittedFields),
|
|
633
725
|
};
|
|
634
726
|
return {
|
|
635
727
|
admitted,
|
|
@@ -688,6 +780,7 @@ export async function loadResumableFixerRun(
|
|
|
688
780
|
? {}
|
|
689
781
|
: { prerequisitesPath: loaded.admittedFields.prerequisitesPath }),
|
|
690
782
|
prerequisites,
|
|
783
|
+
...restoredTicketFields(loaded.admittedFields),
|
|
691
784
|
};
|
|
692
785
|
return {
|
|
693
786
|
admitted,
|
|
@@ -733,6 +826,8 @@ export async function loadResumableReviewerRun(
|
|
|
733
826
|
sessionFile: loaded.run.sessionFile,
|
|
734
827
|
admittedRequestPath: loaded.run.admittedRequestPath,
|
|
735
828
|
baseRevision,
|
|
829
|
+
authorityRefs: Object.freeze([...(loaded.admittedFields.authorityRefs ?? [])]),
|
|
830
|
+
...restoredTicketFields(loaded.admittedFields),
|
|
736
831
|
};
|
|
737
832
|
return {
|
|
738
833
|
admitted,
|
|
@@ -792,6 +887,7 @@ export async function loadResumableMergerRun(
|
|
|
792
887
|
admittedRequestPath: loaded.run.admittedRequestPath,
|
|
793
888
|
mergerInputPath,
|
|
794
889
|
derived,
|
|
890
|
+
...restoredTicketFields(loaded.admittedFields),
|
|
795
891
|
};
|
|
796
892
|
return {
|
|
797
893
|
admitted,
|
|
@@ -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
|
}
|