@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.
@@ -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
- if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
312
- childEnv.AK_CORRELATION_ID = env.correlationId;
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
- if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
297
- childEnv.AK_CORRELATION_ID = env.correlationId;
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
- /** Deterministic compiler: fixed target/range plus packaged Skill in, dispatch text out. */
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 = [{ axis: "standards" as const }, { axis: "spec" as const }];
120
- const legs = axes.map((x) =>
121
- Object.freeze({
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: `${common}\n${reviewerAxisMethodAdapter(x.axis)}\n`,
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
  }
@@ -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 { constructReviewerDispatch, type ConstructedReviewerDispatch } from "./reviewer-construction.ts";
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, legs: dispatch.legs,
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
  }