@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.
@@ -9,9 +9,19 @@ import { join } from "node:path";
9
9
  import { createAssistantMessageEventStream, InMemoryCredentialStore, } from "@earendil-works/pi-ai";
10
10
  import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, prepareComplianceDispatch, } from "./compliance-transport.js";
11
11
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
12
- import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
13
12
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
13
+ import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.js";
14
+ import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
14
15
  import { hasUpstreamErrorTestimony, isNonSuccessHttpStatus, projectConfirmedRemotePayload, } from "./upstream-error-testimony.js";
16
+ /** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
17
+ function buildEvidenceChildSystemPrompt() {
18
+ return [
19
+ "Work only in the supplied workspace.",
20
+ "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
21
+ REVIEWER_VERIFICATION_BOUNDARY,
22
+ "Return one substantive non-blank report.",
23
+ ].join("\n");
24
+ }
15
25
  // ── shared constants / types ──────────────────────────────────────────────
16
26
  export const AUDITOR_TURN_LIMIT = 32;
17
27
  export const DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES = 2;
@@ -450,11 +460,7 @@ export async function executeEvidenceChild(workspace, prompt, context, options =
450
460
  model: inherited.model,
451
461
  thinkingLevel: context.thinkingLevel ?? "off",
452
462
  modelRuntime: inherited.runtime,
453
- systemPrompt: [
454
- "Work only in the supplied workspace.",
455
- "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
456
- "Return one substantive non-blank report.",
457
- ].join("\n"),
463
+ systemPrompt: buildEvidenceChildSystemPrompt(),
458
464
  sessionManager: createRecordSession({
459
465
  cwd: workspace,
460
466
  kind: "evidence-children",
@@ -68,6 +68,22 @@ export function validateRuntimeReviewerReceipt(output) {
68
68
  if (status === "failed" && report !== undefined)
69
69
  throw new Error("Failed Reviewer outcome cannot bind a report");
70
70
  }
71
+ // One read inside the existing accepted-leg consistency cycle — not a second validator.
72
+ // launched ⇔ Standards+Spec; skipped-missing ⇔ Standards only.
73
+ const specDisposition = read(output, "specDisposition");
74
+ if (specDisposition === "launched") {
75
+ if (expectedAxes.length !== 2 || expectedAxes[0] !== "standards" || expectedAxes[1] !== "spec") {
76
+ throw new Error("Reviewer specDisposition launched requires Standards+Spec accepted legs");
77
+ }
78
+ }
79
+ else if (specDisposition === "skipped-missing") {
80
+ if (expectedAxes.length !== 1 || expectedAxes[0] !== "standards") {
81
+ throw new Error("Reviewer specDisposition skipped-missing requires Standards-only accepted legs");
82
+ }
83
+ }
84
+ else if (specDisposition !== undefined) {
85
+ throw new Error("Invalid Reviewer specDisposition");
86
+ }
71
87
  }
72
88
  return output;
73
89
  }
@@ -147,6 +147,18 @@ function validateRuntimeReviewerReceipt(output) {
147
147
  throw new Error("Successful Reviewer outcome lacks report");
148
148
  if (status === "failed" && report !== void 0) throw new Error("Failed Reviewer outcome cannot bind a report");
149
149
  }
150
+ const specDisposition = read(output, "specDisposition");
151
+ if (specDisposition === "launched") {
152
+ if (expectedAxes.length !== 2 || expectedAxes[0] !== "standards" || expectedAxes[1] !== "spec") {
153
+ throw new Error("Reviewer specDisposition launched requires Standards+Spec accepted legs");
154
+ }
155
+ } else if (specDisposition === "skipped-missing") {
156
+ if (expectedAxes.length !== 1 || expectedAxes[0] !== "standards") {
157
+ throw new Error("Reviewer specDisposition skipped-missing requires Standards-only accepted legs");
158
+ }
159
+ } else if (specDisposition !== void 0) {
160
+ throw new Error("Invalid Reviewer specDisposition");
161
+ }
150
162
  }
151
163
  return output;
152
164
  }
@@ -15464,6 +15476,17 @@ function requireOptionPath(flag, value) {
15464
15476
  }
15465
15477
  return value;
15466
15478
  }
15479
+ function requireAuthorityRef(value) {
15480
+ if (value === void 0 || value.trim() === "") {
15481
+ throw new CliUsageError("--authority-ref requires a nonempty durable reference");
15482
+ }
15483
+ if (/\s/.test(value)) {
15484
+ throw new CliUsageError(
15485
+ "--authority-ref requires a durable reference, not inline Spec prose"
15486
+ );
15487
+ }
15488
+ return value;
15489
+ }
15467
15490
  function parseJudgeArgv(args) {
15468
15491
  const attachmentPaths = [];
15469
15492
  let project;
@@ -16426,6 +16449,7 @@ function buildDoctorTransportPrompt(admitted) {
16426
16449
  }
16427
16450
  function parseReviewerArgv(args) {
16428
16451
  const attachmentPaths = [];
16452
+ const authorityRefs = [];
16429
16453
  let project;
16430
16454
  let baseRevision;
16431
16455
  const positional = [];
@@ -16452,6 +16476,14 @@ function parseReviewerArgv(args) {
16452
16476
  baseRevision = requireOptionPath("--base", token.slice("--base=".length));
16453
16477
  continue;
16454
16478
  }
16479
+ if (token === "--authority-ref") {
16480
+ authorityRefs.push(requireAuthorityRef(tokens.shift()));
16481
+ continue;
16482
+ }
16483
+ if (token.startsWith("--authority-ref=")) {
16484
+ authorityRefs.push(requireAuthorityRef(token.slice("--authority-ref=".length)));
16485
+ continue;
16486
+ }
16455
16487
  if (token.startsWith("-") && token !== "-") {
16456
16488
  throw new CliUsageError(`unknown reviewer option: ${token}`);
16457
16489
  }
@@ -16464,6 +16496,7 @@ function parseReviewerArgv(args) {
16464
16496
  instruction: positional.join(" "),
16465
16497
  attachmentPaths,
16466
16498
  baseRevision,
16499
+ authorityRefs,
16467
16500
  ...project === void 0 ? {} : { project }
16468
16501
  };
16469
16502
  }
@@ -16474,6 +16507,9 @@ async function admitReviewerInvocation(options) {
16474
16507
  if (options.baseRevision.trim() === "") {
16475
16508
  throw new CliUsageError("--base requires a nonempty revision");
16476
16509
  }
16510
+ const authorityRefs = Object.freeze(
16511
+ (options.authorityRefs ?? []).map((ref) => requireAuthorityRef(ref))
16512
+ );
16477
16513
  const projectRoot = resolve4(options.project ?? options.cwd);
16478
16514
  const runId = (options.createRunId ?? uuidv7)();
16479
16515
  const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } = roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "reviewer", home: options.home });
@@ -16499,6 +16535,7 @@ async function admitReviewerInvocation(options) {
16499
16535
  instruction,
16500
16536
  instructionEmpty,
16501
16537
  baseRevision: options.baseRevision,
16538
+ authorityRefs: [...authorityRefs],
16502
16539
  attachments: attachments.map((a) => ({
16503
16540
  provenancePath: a.provenancePath,
16504
16541
  frozenPath: a.frozenPath,
@@ -16528,6 +16565,7 @@ async function admitReviewerInvocation(options) {
16528
16565
  sessionFile,
16529
16566
  admittedRequestPath,
16530
16567
  baseRevision: options.baseRevision,
16568
+ authorityRefs,
16531
16569
  ...ticketFields
16532
16570
  };
16533
16571
  }
@@ -16804,7 +16842,7 @@ var init_reviewer_scope_prompt = __esm({
16804
16842
  });
16805
16843
 
16806
16844
  // src/reviewer-construction.ts
16807
- var REVIEWER_CONSTRUCTION_RECIPE, REVIEWER_AXIS_OUTPUT_ADAPTER, REVIEWER_STANDARDS_CONCLUSION_KEYS, REVIEWER_STANDARDS_CONCLUSION_LABELS;
16845
+ var REVIEWER_CONSTRUCTION_RECIPE, REVIEWER_AXIS_OUTPUT_ADAPTER, REVIEWER_VERIFICATION_BOUNDARY, REVIEWER_STANDARDS_CONCLUSION_KEYS, REVIEWER_STANDARDS_CONCLUSION_LABELS;
16808
16846
  var init_reviewer_construction = __esm({
16809
16847
  "src/reviewer-construction.ts"() {
16810
16848
  "use strict";
@@ -16821,6 +16859,14 @@ var init_reviewer_construction = __esm({
16821
16859
  version: 1,
16822
16860
  implementationSha256: sha256Hex("reviewer-axis-output:v1:single-axis-verbatim-report+standards-three-priorities")
16823
16861
  });
16862
+ REVIEWER_VERIFICATION_BOUNDARY = [
16863
+ "Verification-Boundary: you may run focused product tests during this review turn when independent verification needs them.",
16864
+ "A full repository test suite is not forbidden, but do not re-run it every review round;",
16865
+ "prefer once at family wrap-up unless this review specifically requires a broader run.",
16866
+ "Slice and review work should not trigger frequent full-suite reruns.",
16867
+ "Independently discover test facts (including existing coder/fixer receipts and any tests you run);",
16868
+ "do not treat caller prose as the source of those facts."
16869
+ ].join(" ");
16824
16870
  REVIEWER_STANDARDS_CONCLUSION_KEYS = Object.freeze([
16825
16871
  "constitutionality",
16826
16872
  "minimum-necessary-test-cost",
@@ -16845,6 +16891,7 @@ var init_reviewer_dispatch = __esm({
16845
16891
  init_reviewer_prompt_identity();
16846
16892
  init_sha256();
16847
16893
  init_reviewer_construction();
16894
+ init_reviewer_construction();
16848
16895
  init_reviewer_preflight_error();
16849
16896
  init_sha256();
16850
16897
  init_reviewer_prompt_identity();
@@ -17640,6 +17687,7 @@ async function loadResumableRunRecord(home, runId) {
17640
17687
  let prerequisitesPath;
17641
17688
  let prerequisites;
17642
17689
  let baseRevision;
17690
+ let authorityRefs;
17643
17691
  let mergerInputPath;
17644
17692
  let derived;
17645
17693
  let correlationId;
@@ -17677,6 +17725,18 @@ async function loadResumableRunRecord(home, runId) {
17677
17725
  if (typeof record4.baseRevision === "string" && record4.baseRevision.trim() !== "") {
17678
17726
  baseRevision = record4.baseRevision;
17679
17727
  }
17728
+ if (Array.isArray(record4.authorityRefs)) {
17729
+ authorityRefs = Object.freeze(
17730
+ record4.authorityRefs.map((ref) => {
17731
+ if (typeof ref !== "string") {
17732
+ throw new CliUsageError(
17733
+ "role run admitted authority refs must be durable reference strings"
17734
+ );
17735
+ }
17736
+ return requireAuthorityRef(ref);
17737
+ })
17738
+ );
17739
+ }
17680
17740
  if (typeof record4.mergerInputPath === "string" && record4.mergerInputPath.trim() !== "") {
17681
17741
  mergerInputPath = record4.mergerInputPath;
17682
17742
  }
@@ -17696,9 +17756,11 @@ async function loadResumableRunRecord(home, runId) {
17696
17756
  correlationId = fromAdmitted.correlationId;
17697
17757
  ticketNumber = fromAdmitted.ticketNumber;
17698
17758
  }
17699
- } catch {
17759
+ } catch (error) {
17760
+ if (error instanceof CliUsageError) throw error;
17700
17761
  throw new CliUsageError(
17701
- `role run admitted request is unreadable: ${runId}`
17762
+ `role run admitted request is unreadable: ${runId}`,
17763
+ { cause: error }
17702
17764
  );
17703
17765
  }
17704
17766
  if (correlationId === void 0 || ticketNumber === void 0) {
@@ -17735,6 +17797,7 @@ async function loadResumableRunRecord(home, runId) {
17735
17797
  ...prerequisitesPath === void 0 ? {} : { prerequisitesPath },
17736
17798
  ...prerequisites === void 0 ? {} : { prerequisites },
17737
17799
  ...baseRevision === void 0 ? {} : { baseRevision },
17800
+ ...authorityRefs === void 0 ? {} : { authorityRefs },
17738
17801
  ...mergerInputPath === void 0 ? {} : { mergerInputPath },
17739
17802
  ...derived === void 0 ? {} : { derived },
17740
17803
  ...correlationId === void 0 ? {} : { correlationId },
@@ -17890,6 +17953,7 @@ async function loadResumableReviewerRun(home, runId) {
17890
17953
  sessionFile: loaded.run.sessionFile,
17891
17954
  admittedRequestPath: loaded.run.admittedRequestPath,
17892
17955
  baseRevision,
17956
+ authorityRefs: Object.freeze([...loaded.admittedFields.authorityRefs ?? []]),
17893
17957
  ...restoredTicketFields(loaded.admittedFields)
17894
17958
  };
17895
17959
  return {
@@ -18093,8 +18157,9 @@ var init_evidence_child_executor = __esm({
18093
18157
  "use strict";
18094
18158
  init_compliance_transport();
18095
18159
  init_package_owned_tool_idle();
18096
- init_stream_idle_guard();
18097
18160
  init_receipt_delivery_policy();
18161
+ init_reviewer_construction();
18162
+ init_stream_idle_guard();
18098
18163
  init_upstream_error_testimony();
18099
18164
  }
18100
18165
  });
@@ -19352,6 +19417,7 @@ function reviewerDecisiveFacts(output) {
19352
19417
  const axes = reviewerAxes(outcomes.readable ? outcomes.value : void 0);
19353
19418
  const reportAxes = reviewerAxes(reports.readable ? reports.value : void 0);
19354
19419
  const acceptedBatch = safelyRead(candidate, "acceptedBatch");
19420
+ const specDisposition = safelyRead(candidate, "specDisposition");
19355
19421
  const facts = {
19356
19422
  axes,
19357
19423
  reportAxes,
@@ -19359,6 +19425,9 @@ function reviewerDecisiveFacts(output) {
19359
19425
  ...auditNoReceiptDecisiveFact(candidate)
19360
19426
  };
19361
19427
  if (status.readable && typeof status.value === "string") facts.reviewerStatus = status.value;
19428
+ if (specDisposition.readable && (specDisposition.value === "launched" || specDisposition.value === "skipped-missing")) {
19429
+ facts.specDisposition = specDisposition.value;
19430
+ }
19362
19431
  const diagnostic = safelyRead(candidate, "diagnostic");
19363
19432
  if (status.readable && status.value === "refused" && diagnostic.readable) {
19364
19433
  facts.diagnosticPresent = typeof diagnostic.value === "string" && diagnostic.value.trim().length > 0;
@@ -20649,6 +20718,7 @@ async function publishReviewerArtifacts(admitted, roleOutcome, sessionDirectory,
20649
20718
  sessionFile: admitted.sessionFile,
20650
20719
  admittedRequestPath: admitted.admittedRequestPath,
20651
20720
  baseRevision: admitted.baseRevision,
20721
+ authorityRefs: [...admitted.authorityRefs],
20652
20722
  ...admitted.instructionEmpty ? {} : { callerProvenance: admitted.instruction },
20653
20723
  attachments: admitted.attachments.map((a) => ({
20654
20724
  provenancePath: a.provenancePath,
@@ -23335,6 +23405,7 @@ function buildReviewerActivationExtraArgs(admitted, options) {
23335
23405
  options.packageRoot,
23336
23406
  "code-review"
23337
23407
  );
23408
+ const authorityRefArgs = admitted.authorityRefs.length === 0 ? [] : ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
23338
23409
  return [
23339
23410
  "--no-skills",
23340
23411
  "--skill",
@@ -23351,6 +23422,7 @@ function buildReviewerActivationExtraArgs(admitted, options) {
23351
23422
  "reviewer",
23352
23423
  "--ak-review-base",
23353
23424
  admitted.baseRevision,
23425
+ ...authorityRefArgs,
23354
23426
  "--mode",
23355
23427
  "json",
23356
23428
  ...buildModelArgs7(options.model),
@@ -23362,6 +23434,7 @@ function buildReviewerResumeActivationExtraArgs(admitted, options) {
23362
23434
  options.packageRoot,
23363
23435
  "code-review"
23364
23436
  );
23437
+ const authorityRefArgs = admitted.authorityRefs.length === 0 ? [] : ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
23365
23438
  return [
23366
23439
  "--no-skills",
23367
23440
  "--skill",
@@ -23378,6 +23451,7 @@ function buildReviewerResumeActivationExtraArgs(admitted, options) {
23378
23451
  "reviewer",
23379
23452
  "--ak-review-base",
23380
23453
  admitted.baseRevision,
23454
+ ...authorityRefArgs,
23381
23455
  "--mode",
23382
23456
  "json",
23383
23457
  ...buildModelArgs7(options.model),
@@ -23571,6 +23645,7 @@ async function runPublicReviewer(argv, env, io, parseReviewerArgv2) {
23571
23645
  instruction: parsed.instruction,
23572
23646
  attachmentPaths: parsed.attachmentPaths,
23573
23647
  baseRevision: parsed.baseRevision,
23648
+ authorityRefs: parsed.authorityRefs,
23574
23649
  ...parsed.project === void 0 ? {} : { project: parsed.project },
23575
23650
  ...env.createRunId === void 0 ? {} : { createRunId: env.createRunId }
23576
23651
  });
@@ -11,6 +11,21 @@ export const REVIEWER_AXIS_OUTPUT_ADAPTER = Object.freeze({
11
11
  version: 1,
12
12
  implementationSha256: sha256Hex("reviewer-axis-output:v1:single-axis-verbatim-report+standards-three-priorities"),
13
13
  });
14
+ /**
15
+ * Package-owned #1185 review verification cadence.
16
+ * Single true source consumed by two real actor carriers: parent Reviewer system-prompt injection
17
+ * and evidence-child system prompt. Not part of axis-adapter identity or axis leg prompts.
18
+ * Graded guidance only: focused tests allowed; full suite not forbidden but avoid frequent every-round reruns.
19
+ * Does not narrow ADR 0064 tools and adds no command ban, allowlist, or runtime block.
20
+ */
21
+ export const REVIEWER_VERIFICATION_BOUNDARY = [
22
+ "Verification-Boundary: you may run focused product tests during this review turn when independent verification needs them.",
23
+ "A full repository test suite is not forbidden, but do not re-run it every review round;",
24
+ "prefer once at family wrap-up unless this review specifically requires a broader run.",
25
+ "Slice and review work should not trigger frequent full-suite reruns.",
26
+ "Independently discover test facts (including existing coder/fixer receipts and any tests you run);",
27
+ "do not treat caller prose as the source of those facts.",
28
+ ].join(" ");
14
29
  /** Typed Standards conclusion keys owned by reviewer construction (presentation labels are not the contract). */
15
30
  export const REVIEWER_STANDARDS_CONCLUSION_KEYS = Object.freeze([
16
31
  "constitutionality",
@@ -63,8 +78,22 @@ export function reviewerAxisMethodAdapter(axis) {
63
78
  "The returned report is the complete output envelope and its UTF-8 bytes are preserved verbatim; no heading parser, sanitizer, section splitter, rewrite, aggregation, or replacement leg follows.",
64
79
  ].join("\n");
65
80
  }
66
- /** Deterministic compiler: fixed target/range plus packaged Skill in, dispatch text out. */
81
+ /**
82
+ * Spec-only evidence-child material carrier for durable authority references.
83
+ * Exact values preserved; no prose extraction and no Standards/parent injection.
84
+ */
85
+ export function reviewerAuthorityRefsMaterial(authorityRefs) {
86
+ return [
87
+ "Authority-Refs:",
88
+ JSON.stringify(Object.freeze([...authorityRefs])),
89
+ "These are durable authority references only. Read them as Spec grounding materials; do not invent Spec prose from caller instruction.",
90
+ ].join("\n");
91
+ }
92
+ /** Deterministic compiler: fixed target/range plus discovery product in, dispatch text out. */
67
93
  export function constructReviewerDispatch(input) {
94
+ const launchSpec = input.specAuthority.status === "available";
95
+ const authorityRefs = Object.freeze(input.specAuthority.status === "available" ? [...input.specAuthority.refs] : []);
96
+ const specDisposition = launchSpec ? "launched" : "skipped-missing";
68
97
  const common = [
69
98
  `Target: ${input.range.target}`,
70
99
  `Base: ${input.range.base}`,
@@ -76,11 +105,20 @@ export function constructReviewerDispatch(input) {
76
105
  "Fixed-Range:",
77
106
  JSON.stringify(input.range, null, 2),
78
107
  ].join("\n");
79
- const axes = [{ axis: "standards" }, { axis: "spec" }];
80
- const legs = axes.map((x) => Object.freeze({
81
- axis: x.axis,
82
- prompt: `${common}\n${reviewerAxisMethodAdapter(x.axis)}\n`,
83
- }));
108
+ const axes = launchSpec
109
+ ? [{ axis: "standards" }, { axis: "spec" }]
110
+ : [{ axis: "standards" }];
111
+ const legs = axes.map((x) => {
112
+ const parts = [common, reviewerAxisMethodAdapter(x.axis)];
113
+ // Spec evidence-child only — never Standards or a parent replacement Spec leg.
114
+ if (x.axis === "spec" && authorityRefs.length > 0) {
115
+ parts.push(reviewerAuthorityRefsMaterial(authorityRefs));
116
+ }
117
+ return Object.freeze({
118
+ axis: x.axis,
119
+ prompt: `${parts.join("\n")}\n`,
120
+ });
121
+ });
84
122
  return Object.freeze({
85
123
  identity: input.identity,
86
124
  recipe: "reviewer-common-bundle-v1",
@@ -90,6 +128,8 @@ export function constructReviewerDispatch(input) {
90
128
  }),
91
129
  targetSnapshot: input.target,
92
130
  range: input.range,
131
+ authorityRefs,
132
+ specDisposition,
93
133
  legs: Object.freeze(legs),
94
134
  });
95
135
  }
@@ -3,10 +3,67 @@ import { immutableReviewerPin } from "./reviewer-pinned-git.js";
3
3
  export { createReviewerPinnedGitReader, immutableReviewerPin } from "./reviewer-pinned-git.js";
4
4
  import { isReviewerPromptText, sameReviewerPromptText } from "./reviewer-prompt-identity.js";
5
5
  import { sha256Hex } from "./sha256.js";
6
- import { constructReviewerDispatch } from "./reviewer-construction.js";
6
+ import { constructReviewerDispatch, } from "./reviewer-construction.js";
7
+ export {} from "./reviewer-construction.js";
7
8
  import { ReviewerCorrectablePreflightError } from "./reviewer-preflight-error.js";
8
9
  export { sha256Hex } from "./sha256.js";
9
10
  export { isReviewerPromptText as isReviewerPromptIdentity, sameReviewerPromptText as sameReviewerPromptIdentity } from "./reviewer-prompt-identity.js";
11
+ const GENERIC_FEATURE_TOKENS = new Set(["", "head", "main", "master", "trunk", "develop", "development"]);
12
+ /** Conventional branch shells that must not hide the feature token (feat/login → login). */
13
+ const BRANCH_SHELL_PREFIX = /^(?:feat|feature|fix|bugfix|hotfix|chore|docs|refactor)-/;
14
+ function normalizeFeatureToken(value) {
15
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
16
+ }
17
+ /** Expand one branch/ref name into matchable tokens, stripping conventional shells. */
18
+ function expandFeatureTokens(raw) {
19
+ const normalized = normalizeFeatureToken(raw);
20
+ if (normalized.length === 0)
21
+ return Object.freeze([]);
22
+ const tokens = new Set([normalized]);
23
+ const stripped = normalized.replace(BRANCH_SHELL_PREFIX, "");
24
+ if (stripped.length > 0 && stripped !== normalized)
25
+ tokens.add(stripped);
26
+ return Object.freeze([...tokens]);
27
+ }
28
+ /**
29
+ * Unique production owner of code-review Skill step 2 Spec discovery.
30
+ * Directly yields durable refs Spec child can read, or confirmed missing.
31
+ * - Supplied authorityRefs ⇒ available with those refs as material.
32
+ * - Matching pinned-target docs/specs/.scratch paths ⇒ available with those paths as material.
33
+ * - Commit message bare #N without durable source ⇒ missing (not available).
34
+ * Only confirmed absence yields missing; other Git/I-O failures keep true cause for preflight.
35
+ * Construction builds Standards/Spec solely from this product.
36
+ */
37
+ export async function discoverReviewerSpecAuthority(input) {
38
+ if (input.authorityRefs.length > 0) {
39
+ return Object.freeze({
40
+ status: "available",
41
+ refs: Object.freeze([...input.authorityRefs]),
42
+ });
43
+ }
44
+ const featureTokens = await input.reader.featureTokens();
45
+ const tokens = [
46
+ ...new Set(featureTokens
47
+ .flatMap((raw) => expandFeatureTokens(raw))
48
+ .filter((token) => token.length >= 3 && !GENERIC_FEATURE_TOKENS.has(token))),
49
+ ];
50
+ if (tokens.length === 0) {
51
+ return Object.freeze({ status: "missing" });
52
+ }
53
+ // Pinned target tree only — Spec child cannot read live-worktree or gitignored paths.
54
+ const candidates = await input.reader.listSpecCandidatePaths();
55
+ const matched = candidates.filter((relativePath) => {
56
+ const normalizedPath = normalizeFeatureToken(relativePath);
57
+ return tokens.some((token) => normalizedPath.includes(token));
58
+ });
59
+ if (matched.length === 0) {
60
+ return Object.freeze({ status: "missing" });
61
+ }
62
+ return Object.freeze({
63
+ status: "available",
64
+ refs: Object.freeze(matched),
65
+ });
66
+ }
10
67
  export const REVIEWER_PREFLIGHT_VIOLATIONS = ["base-invalid", "range-invalid", "prompt-identity-invalid", "target-drift"];
11
68
  export class ReviewerPreflightError extends Error {
12
69
  code;
@@ -49,12 +106,18 @@ export function createReviewerDispatcher(d) {
49
106
  try {
50
107
  const base = await d.reader.resolve(baseRevision);
51
108
  const range = await d.reader.range(base);
109
+ const authorityRefs = Object.freeze([...(d.authorityRefs ?? [])]);
110
+ const specAuthority = await discoverReviewerSpecAuthority({
111
+ authorityRefs,
112
+ reader: d.reader,
113
+ });
52
114
  dispatch = constructReviewerDispatch({
53
115
  identity,
54
116
  canonicalSkill: d.canonicalSkill,
55
117
  target,
56
118
  range,
57
119
  ...(d.reviewScopeKeys === undefined ? {} : { reviewScopeKeys: d.reviewScopeKeys }),
120
+ specAuthority,
58
121
  });
59
122
  if (!sameReviewerPinnedTarget(await d.reader.snapshot(), target)) {
60
123
  throw new ReviewerPreflightError("target-drift", "pinned target snapshot changed before child execution");
@@ -5,7 +5,8 @@ export function projectAcceptedDispatch(dispatch) {
5
5
  return {
6
6
  source: "reviewer-dispatch", type: "accepted", identity: dispatch.identity,
7
7
  recipe: dispatch.recipe, input: dispatch.input, target: dispatch.targetSnapshot,
8
- range: dispatch.range, legs: dispatch.legs,
8
+ range: dispatch.range, authorityRefs: dispatch.authorityRefs,
9
+ specDisposition: dispatch.specDisposition, legs: dispatch.legs,
9
10
  };
10
11
  }
11
12
  export function projectReviewerDispatchOutcome(ledger, dispatch, result) {
@@ -142,5 +142,38 @@ export async function createReviewerPinnedGitReader(root = process.cwd()) {
142
142
  invalid("range-invalid", "review range must contain a non-empty diff between base and pinned target");
143
143
  return Object.freeze({ base: mergeBase, target: targetHead, diffCommand, diffSha256: sha256Hex(Uint8Array.from(diff)), commits: Object.freeze(commitsText ? commitsText.split("\n") : []) });
144
144
  },
145
+ async featureTokens() {
146
+ // Pinned ref snapshot is the target-tree fact — no live branch/symbolic-ref walk,
147
+ // no catch-to-empty. Detached/remote-only tips surface via refs/remotes/* entries.
148
+ const names = new Set();
149
+ for (const [refName, entry] of Object.entries(pin.refs)) {
150
+ if (entry.peeledCommitId !== targetHead)
151
+ continue;
152
+ const short = refName.startsWith("refs/heads/")
153
+ ? refName.slice("refs/heads/".length)
154
+ : refName.startsWith("refs/tags/")
155
+ ? refName.slice("refs/tags/".length)
156
+ : refName.startsWith("refs/remotes/")
157
+ ? refName.slice("refs/remotes/".length).replace(/^[^/]+\//, "")
158
+ : refName;
159
+ if (short.trim() !== "")
160
+ names.add(short.trim());
161
+ }
162
+ return Object.freeze([...names]);
163
+ },
164
+ async listSpecCandidatePaths() {
165
+ const roots = ["docs", "specs", ".scratch"];
166
+ // git ls-tree exits 0 with empty stdout when none of the roots exist at targetHead.
167
+ // Other Git/I-O failures keep their true cause for the dispatch preflight path.
168
+ const text = await gitText(repositoryRoot, [
169
+ "ls-tree",
170
+ "-r",
171
+ "--name-only",
172
+ targetHead,
173
+ "--",
174
+ ...roots,
175
+ ]);
176
+ return Object.freeze(text === "" ? [] : text.split("\n").filter((line) => line.length > 0));
177
+ },
145
178
  });
146
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.1918",
3
+ "version": "0.1.1941",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -30,15 +30,26 @@ import {
30
30
  type AuditorParentAttemptBinding,
31
31
  } from "./compliance-transport.ts";
32
32
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
33
+ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
34
+ import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
33
35
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
34
36
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
35
- import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
36
37
  import {
37
38
  hasUpstreamErrorTestimony,
38
39
  isNonSuccessHttpStatus,
39
40
  projectConfirmedRemotePayload,
40
41
  } from "./upstream-error-testimony.ts";
41
42
 
43
+ /** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
44
+ function buildEvidenceChildSystemPrompt(): string {
45
+ return [
46
+ "Work only in the supplied workspace.",
47
+ "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
48
+ REVIEWER_VERIFICATION_BOUNDARY,
49
+ "Return one substantive non-blank report.",
50
+ ].join("\n");
51
+ }
52
+
42
53
  // ── shared constants / types ──────────────────────────────────────────────
43
54
 
44
55
  export const AUDITOR_TURN_LIMIT = 32;
@@ -561,11 +572,7 @@ export async function executeEvidenceChild(
561
572
  model: inherited.model,
562
573
  thinkingLevel: context.thinkingLevel ?? "off",
563
574
  modelRuntime: inherited.runtime,
564
- systemPrompt: [
565
- "Work only in the supplied workspace.",
566
- "Use the available evidence tools to investigate. Do not commit, push, or mutate remotes.",
567
- "Return one substantive non-blank report.",
568
- ].join("\n"),
575
+ systemPrompt: buildEvidenceChildSystemPrompt(),
569
576
  sessionManager: createRecordSession({
570
577
  cwd: workspace,
571
578
  kind: "evidence-children",
@@ -26,11 +26,15 @@ export type RuntimeReviewerAcceptedBatch = Readonly<{
26
26
  identity: string;
27
27
  legs: readonly Readonly<{ axis: "standards" | "spec"; prompt: ReviewerReceiptPrompt }>[];
28
28
  }>;
29
+ /** Honest Spec-child disposition on the receipt face. */
30
+ export type RuntimeReviewerSpecDisposition = "launched" | "skipped-missing";
29
31
  export type RuntimeReviewerReceiptV2 = Readonly<{
30
32
  version: 2;
31
33
  status: "completed" | "refused";
32
34
  diagnostic?: string;
33
35
  acceptedBatch?: RuntimeReviewerAcceptedBatch;
36
+ /** Present on accepted batches: launched Spec child, or skipped after confirmed missing Spec. */
37
+ specDisposition?: RuntimeReviewerSpecDisposition;
34
38
  reports: Readonly<Partial<Record<"standards" | "spec", VerbatimChildReport>>>;
35
39
  outcomes: Readonly<Partial<Record<"standards" | "spec", RuntimeReviewerOutcome>>>;
36
40
  identities: Readonly<{
@@ -100,6 +104,21 @@ export function validateRuntimeReviewerReceipt(output: unknown): RuntimeReviewer
100
104
  throw new Error("Successful Reviewer outcome lacks report");
101
105
  if (status === "failed" && report !== undefined) throw new Error("Failed Reviewer outcome cannot bind a report");
102
106
  }
107
+
108
+ // One read inside the existing accepted-leg consistency cycle — not a second validator.
109
+ // launched ⇔ Standards+Spec; skipped-missing ⇔ Standards only.
110
+ const specDisposition = read(output, "specDisposition");
111
+ if (specDisposition === "launched") {
112
+ if (expectedAxes.length !== 2 || expectedAxes[0] !== "standards" || expectedAxes[1] !== "spec") {
113
+ throw new Error("Reviewer specDisposition launched requires Standards+Spec accepted legs");
114
+ }
115
+ } else if (specDisposition === "skipped-missing") {
116
+ if (expectedAxes.length !== 1 || expectedAxes[0] !== "standards") {
117
+ throw new Error("Reviewer specDisposition skipped-missing requires Standards-only accepted legs");
118
+ }
119
+ } else if (specDisposition !== undefined) {
120
+ throw new Error("Invalid Reviewer specDisposition");
121
+ }
103
122
  }
104
123
  return output as RuntimeReviewerReceiptV2;
105
124
  }