@akagilnc/pi-workflow-roles 0.1.1918 → 0.1.2004

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/evidence-child-executor.js +12 -6
  2. package/dist/package-contracts/reviewer-output.js +16 -0
  3. package/dist/public-cli/main.js +2520 -72
  4. package/dist/reviewer-construction.js +46 -6
  5. package/dist/reviewer-dispatch.js +64 -1
  6. package/dist/reviewer-execution-ledger.js +2 -1
  7. package/dist/reviewer-pinned-git.js +33 -0
  8. package/package.json +1 -1
  9. package/src/atomic-write.ts +26 -0
  10. package/src/evidence-child-executor.ts +13 -6
  11. package/src/ledger-session-read.ts +260 -0
  12. package/src/package-contracts/reviewer-output.ts +19 -0
  13. package/src/public-cli/cli.ts +24 -0
  14. package/src/public-cli/invocation.ts +398 -2
  15. package/src/public-cli/main.ts +21 -2
  16. package/src/public-cli/registry.ts +18 -1
  17. package/src/public-cli/reviewer-run.ts +12 -0
  18. package/src/public-cli/run-lifecycle.ts +22 -1
  19. package/src/public-cli/settlement.ts +17 -0
  20. package/src/public-cli/taishi-run.ts +235 -0
  21. package/src/reviewer-construction.ts +64 -7
  22. package/src/reviewer-dispatch.ts +81 -2
  23. package/src/reviewer-execution-ledger.ts +2 -1
  24. package/src/reviewer-pinned-git.ts +43 -0
  25. package/src/reviewer-role.ts +143 -132
  26. package/src/reviewer-settlement.ts +4 -0
  27. package/src/role-runtime.ts +186 -3
  28. package/src/run-terminal-artifacts.ts +231 -0
  29. package/src/taishi-cohort.ts +232 -0
  30. package/src/taishi-entry.ts +429 -0
  31. package/src/taishi-index.ts +269 -0
  32. package/src/taishi-ledger.ts +466 -0
  33. package/src/taishi-median.ts +15 -0
  34. package/src/taishi-metric-families/acceptance-success-rework.ts +346 -0
  35. package/src/taishi-metric-families/b2-frame-buckets-actions.ts +274 -0
  36. package/src/taishi-metric-families/leg-wall-clock.ts +90 -0
  37. package/src/taishi-metric-families/round-timeline.ts +201 -0
  38. package/src/taishi-metric-families.ts +36 -0
  39. package/src/taishi-metric-family.ts +41 -0
  40. package/src/taishi-model-groups.ts +198 -0
  41. package/src/taishi-page.ts +320 -0
  42. package/src/ticket-trajectory.ts +9 -62
@@ -134,6 +134,11 @@ export type AdmittedReviewerInvocation = AdmittedRoleInvocationBase & {
134
134
  readonly role: "reviewer";
135
135
  /** Required fixed base revision for the pinned review target (ADR 0037). */
136
136
  readonly baseRevision: string;
137
+ /**
138
+ * Optional durable authority references/URLs frozen at admission.
139
+ * Spec evidence-child material only — never Standards, never invocation prose promotion.
140
+ */
141
+ readonly authorityRefs: readonly string[];
137
142
  };
138
143
 
139
144
  /** Mechanical envelope derived from the active ordinary two-parent merge. */
@@ -318,6 +323,8 @@ export type ParseReviewerArgvResult = {
318
323
  attachmentPaths: string[];
319
324
  /** Required fixed base revision for the pinned review target. */
320
325
  baseRevision: string;
326
+ /** Repeatable durable authority references/URLs (exact order preserved). */
327
+ authorityRefs: string[];
321
328
  project?: string;
322
329
  };
323
330
 
@@ -327,6 +334,50 @@ export type ParseMergerArgvResult = {
327
334
  project?: string;
328
335
  };
329
336
 
337
+ /**
338
+ * #336/#337/#338 taishi public argv — four faces on one registration seam.
339
+ * - issue (default): ticket N and/or project-root P
340
+ * - sweep (#337): optional positional `sweep` and/or --attach paths;
341
+ * sweep payload rides exactly one typed JSON attachment (not argv/stdin)
342
+ * - cohort: two labeled issue-number groups
343
+ * - model-groups: one or more project-root scope keys
344
+ */
345
+ export type ParseTaishiIssueArgv = {
346
+ readonly query: "issue";
347
+ /** Caller ticket / issue number face (#176 numbering space). */
348
+ readonly ticket?: number;
349
+ /** Direct projectRoot mechanical key (ADR 0068). */
350
+ readonly projectRoot?: string;
351
+ };
352
+
353
+ export type ParseTaishiSweepArgv = {
354
+ readonly query: "sweep";
355
+ /**
356
+ * Public CLI attachment paths (--attach). Sweep mode only (#337).
357
+ * Cardinality validated on the sweep run path (exactly one).
358
+ */
359
+ readonly attachmentPaths: readonly string[];
360
+ };
361
+
362
+ export type ParseTaishiCohortArgv = {
363
+ readonly query: "cohort";
364
+ readonly groups: readonly [
365
+ { readonly groupLabel: string; readonly issues: readonly number[] },
366
+ { readonly groupLabel: string; readonly issues: readonly number[] },
367
+ ];
368
+ };
369
+
370
+ export type ParseTaishiModelGroupsArgv = {
371
+ readonly query: "model-groups";
372
+ readonly projectRoots: readonly string[];
373
+ };
374
+
375
+ export type ParseTaishiArgvResult =
376
+ | ParseTaishiIssueArgv
377
+ | ParseTaishiSweepArgv
378
+ | ParseTaishiCohortArgv
379
+ | ParseTaishiModelGroupsArgv;
380
+
330
381
  /** Honest activation-class failure while deriving the active-merge envelope. */
331
382
  export class MergerEnvelopeDerivationError extends Error {
332
383
  readonly code = "merger-envelope-derivation" as const;
@@ -340,7 +391,13 @@ export class MergerEnvelopeDerivationError extends Error {
340
391
 
341
392
  /** Reject missing/blank path values so empty overrides cannot silently degrade. */
342
393
  function requireOptionPath(
343
- flag: "--project" | "--attach" | "--prerequisites" | "--request-manifest" | "--base",
394
+ flag:
395
+ | "--project"
396
+ | "--attach"
397
+ | "--prerequisites"
398
+ | "--request-manifest"
399
+ | "--base"
400
+ | "--project-root",
344
401
  value: string | undefined,
345
402
  ): string {
346
403
  if (value === undefined || value.trim() === "") {
@@ -353,6 +410,25 @@ function requireOptionPath(
353
410
  return value;
354
411
  }
355
412
 
413
+ /**
414
+ * Public --authority-ref admission grammar (refs-only).
415
+ * Unique owner for fresh argv and durable resume restore — no string-only parallel.
416
+ * Accepts durable reference tokens as-is; rejects blank and inline Spec prose
417
+ * (whitespace-bearing sentences). Does not fetch, normalize, or judge content.
418
+ */
419
+ export function requireAuthorityRef(value: string | undefined): string {
420
+ if (value === undefined || value.trim() === "") {
421
+ throw new CliUsageError("--authority-ref requires a nonempty durable reference");
422
+ }
423
+ // Spec prose sentences contain whitespace; durable public refs are single tokens.
424
+ if (/\s/.test(value)) {
425
+ throw new CliUsageError(
426
+ "--authority-ref requires a durable reference, not inline Spec prose",
427
+ );
428
+ }
429
+ return value;
430
+ }
431
+
356
432
  /**
357
433
  * Parse Judge-specific argv after the `judge` token.
358
434
  * Rejects any public burden selector/hint and unknown flags.
@@ -1581,13 +1657,15 @@ export function buildDoctorTransportPrompt(
1581
1657
 
1582
1658
  /**
1583
1659
  * Parse Reviewer-specific argv after the `reviewer` token.
1584
- * Public flags: --project and required --base (the non-interactive CLI cannot answer the canonical fixed-point question).
1660
+ * Public flags: --project, required --base, optional repeatable --authority-ref.
1585
1661
  * Reviewer gathers its own evidence; users submit neither attachments nor capability packets.
1662
+ * Caller instruction remains scope/procedure provenance — not Spec authority.
1586
1663
  */
1587
1664
  export function parseReviewerArgv(
1588
1665
  args: readonly string[],
1589
1666
  ): ParseReviewerArgvResult {
1590
1667
  const attachmentPaths: string[] = [];
1668
+ const authorityRefs: string[] = [];
1591
1669
  let project: string | undefined;
1592
1670
  let baseRevision: string | undefined;
1593
1671
  const positional: string[] = [];
@@ -1615,6 +1693,14 @@ export function parseReviewerArgv(
1615
1693
  baseRevision = requireOptionPath("--base", token.slice("--base=".length));
1616
1694
  continue;
1617
1695
  }
1696
+ if (token === "--authority-ref") {
1697
+ authorityRefs.push(requireAuthorityRef(tokens.shift()));
1698
+ continue;
1699
+ }
1700
+ if (token.startsWith("--authority-ref=")) {
1701
+ authorityRefs.push(requireAuthorityRef(token.slice("--authority-ref=".length)));
1702
+ continue;
1703
+ }
1618
1704
  if (token.startsWith("-") && token !== "-") {
1619
1705
  throw new CliUsageError(`unknown reviewer option: ${token}`);
1620
1706
  }
@@ -1628,6 +1714,7 @@ export function parseReviewerArgv(
1628
1714
  instruction: positional.join(" "),
1629
1715
  attachmentPaths,
1630
1716
  baseRevision,
1717
+ authorityRefs,
1631
1718
  ...(project === undefined ? {} : { project }),
1632
1719
  };
1633
1720
  }
@@ -1639,6 +1726,8 @@ export type AdmitReviewerInvocationOptions = {
1639
1726
  instruction: string;
1640
1727
  attachmentPaths: readonly string[];
1641
1728
  baseRevision: string;
1729
+ /** Optional durable authority references/URLs; frozen unchanged at admission. */
1730
+ authorityRefs?: readonly string[];
1642
1731
  project?: string;
1643
1732
  createRunId?: () => string;
1644
1733
  };
@@ -1646,6 +1735,7 @@ export type AdmitReviewerInvocationOptions = {
1646
1735
  /**
1647
1736
  * Admit a Reviewer Role run on the fixed base only.
1648
1737
  * Caller instruction is optional provenance; Reviewer acquires issue/authority independently.
1738
+ * Optional authorityRefs are frozen as durable references only — not Spec prose.
1649
1739
  */
1650
1740
  export async function admitReviewerInvocation(
1651
1741
  options: AdmitReviewerInvocationOptions,
@@ -1656,6 +1746,9 @@ export async function admitReviewerInvocation(
1656
1746
  if (options.baseRevision.trim() === "") {
1657
1747
  throw new CliUsageError("--base requires a nonempty revision");
1658
1748
  }
1749
+ const authorityRefs = Object.freeze(
1750
+ (options.authorityRefs ?? []).map((ref) => requireAuthorityRef(ref)),
1751
+ );
1659
1752
 
1660
1753
  const projectRoot = resolve(options.project ?? options.cwd);
1661
1754
  const runId = (options.createRunId ?? uuidv7)();
@@ -1686,6 +1779,7 @@ export async function admitReviewerInvocation(
1686
1779
  instruction,
1687
1780
  instructionEmpty,
1688
1781
  baseRevision: options.baseRevision,
1782
+ authorityRefs: [...authorityRefs],
1689
1783
  attachments: attachments.map((a) => ({
1690
1784
  provenancePath: a.provenancePath,
1691
1785
  frozenPath: a.frozenPath,
@@ -1715,6 +1809,7 @@ export async function admitReviewerInvocation(
1715
1809
  sessionFile,
1716
1810
  admittedRequestPath,
1717
1811
  baseRevision: options.baseRevision,
1812
+ authorityRefs,
1718
1813
  ...ticketFields,
1719
1814
  };
1720
1815
  }
@@ -1996,3 +2091,304 @@ export function buildMergerTransportPrompt(
1996
2091
  }
1997
2092
  return lines.join("\n");
1998
2093
  }
2094
+
2095
+ const TAISHI_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
2096
+
2097
+ /**
2098
+ * Parse a positive ticket / issue number for public taishi admission.
2099
+ * Leading zeros and non-integers are structural rejects (same face as #176).
2100
+ * `flag` names the actual argv face in diagnostics (cohort group lists reuse this).
2101
+ */
2102
+ export function parseTaishiTicketNumber(
2103
+ raw: string,
2104
+ flag: string = "--ticket",
2105
+ ): number {
2106
+ const trimmed = raw.trim();
2107
+ if (!TAISHI_TICKET_NUMBER_PATTERN.test(trimmed)) {
2108
+ throw new CliUsageError(
2109
+ `taishi ${flag} must be a positive integer, got ${raw}`,
2110
+ );
2111
+ }
2112
+ const value = Number(trimmed);
2113
+ // Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
2114
+ if (!Number.isSafeInteger(value) || value < 1) {
2115
+ throw new CliUsageError(
2116
+ `taishi ${flag} must be a positive integer, got ${raw}`,
2117
+ );
2118
+ }
2119
+ return value;
2120
+ }
2121
+
2122
+ function parseTaishiIssueNumberList(raw: string, flag: string): number[] {
2123
+ const trimmed = raw.trim();
2124
+ if (trimmed === "") {
2125
+ throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
2126
+ }
2127
+ const parts = trimmed.split(",").map((part) => part.trim());
2128
+ if (parts.some((part) => part === "")) {
2129
+ throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
2130
+ }
2131
+ // Same numeric rule as --ticket; diagnostic names the actual group flag.
2132
+ return parts.map((part) => parseTaishiTicketNumber(part, flag));
2133
+ }
2134
+
2135
+ function requireOptionValue(
2136
+ flag: string,
2137
+ value: string | undefined,
2138
+ what: string,
2139
+ ): string {
2140
+ if (value === undefined || value.trim() === "") {
2141
+ throw new CliUsageError(`${flag} requires ${what}`);
2142
+ }
2143
+ return value;
2144
+ }
2145
+
2146
+ /**
2147
+ * Parse taishi-specific argv after the `taishi` token (#336/#337/#338).
2148
+ * Issue: at least one of --ticket / --project-root.
2149
+ * Sweep: positional `sweep` and/or --attach; payload is the attachment body only.
2150
+ * Cohort / model-groups: explicit query flags with their own required faces.
2151
+ * Faces are mutually exclusive.
2152
+ */
2153
+ export function parseTaishiArgv(args: readonly string[]): ParseTaishiArgvResult {
2154
+ let query: "issue" | "cohort" | "model-groups" = "issue";
2155
+ let ticketRaw: string | undefined;
2156
+ const projectRoots: string[] = [];
2157
+ let groupALabel: string | undefined;
2158
+ let groupAIssuesRaw: string | undefined;
2159
+ let groupBLabel: string | undefined;
2160
+ let groupBIssuesRaw: string | undefined;
2161
+ let sweepToken = false;
2162
+ const attachmentPaths: string[] = [];
2163
+ const tokens = [...args];
2164
+
2165
+ while (tokens.length > 0) {
2166
+ const token = tokens.shift()!;
2167
+ if (token === "--") {
2168
+ if (tokens.length > 0) {
2169
+ throw new CliUsageError(`unexpected taishi argument: ${tokens[0]}`);
2170
+ }
2171
+ break;
2172
+ }
2173
+ if (token === "--cohort") {
2174
+ if (query !== "issue") {
2175
+ throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
2176
+ }
2177
+ query = "cohort";
2178
+ continue;
2179
+ }
2180
+ if (token === "--model-groups") {
2181
+ if (query !== "issue") {
2182
+ throw new CliUsageError("taishi accepts only one of --cohort / --model-groups");
2183
+ }
2184
+ query = "model-groups";
2185
+ continue;
2186
+ }
2187
+ if (token === "--ticket") {
2188
+ const value = tokens.shift();
2189
+ if (value === undefined || value.trim() === "") {
2190
+ throw new CliUsageError("taishi --ticket requires a positive integer");
2191
+ }
2192
+ ticketRaw = value;
2193
+ continue;
2194
+ }
2195
+ if (token.startsWith("--ticket=")) {
2196
+ ticketRaw = token.slice("--ticket=".length);
2197
+ if (ticketRaw.trim() === "") {
2198
+ throw new CliUsageError("taishi --ticket requires a positive integer");
2199
+ }
2200
+ continue;
2201
+ }
2202
+ if (token === "--project-root") {
2203
+ projectRoots.push(requireOptionPath("--project-root", tokens.shift()));
2204
+ continue;
2205
+ }
2206
+ if (token.startsWith("--project-root=")) {
2207
+ projectRoots.push(
2208
+ requireOptionPath("--project-root", token.slice("--project-root=".length)),
2209
+ );
2210
+ continue;
2211
+ }
2212
+ if (token === "--group-a-label") {
2213
+ groupALabel = requireOptionValue("--group-a-label", tokens.shift(), "a label");
2214
+ continue;
2215
+ }
2216
+ if (token.startsWith("--group-a-label=")) {
2217
+ groupALabel = requireOptionValue(
2218
+ "--group-a-label",
2219
+ token.slice("--group-a-label=".length),
2220
+ "a label",
2221
+ );
2222
+ continue;
2223
+ }
2224
+ if (token === "--group-a-issues") {
2225
+ groupAIssuesRaw = requireOptionValue(
2226
+ "--group-a-issues",
2227
+ tokens.shift(),
2228
+ "a comma-separated positive integer list",
2229
+ );
2230
+ continue;
2231
+ }
2232
+ if (token.startsWith("--group-a-issues=")) {
2233
+ groupAIssuesRaw = requireOptionValue(
2234
+ "--group-a-issues",
2235
+ token.slice("--group-a-issues=".length),
2236
+ "a comma-separated positive integer list",
2237
+ );
2238
+ continue;
2239
+ }
2240
+ if (token === "--group-b-label") {
2241
+ groupBLabel = requireOptionValue("--group-b-label", tokens.shift(), "a label");
2242
+ continue;
2243
+ }
2244
+ if (token.startsWith("--group-b-label=")) {
2245
+ groupBLabel = requireOptionValue(
2246
+ "--group-b-label",
2247
+ token.slice("--group-b-label=".length),
2248
+ "a label",
2249
+ );
2250
+ continue;
2251
+ }
2252
+ if (token === "--group-b-issues") {
2253
+ groupBIssuesRaw = requireOptionValue(
2254
+ "--group-b-issues",
2255
+ tokens.shift(),
2256
+ "a comma-separated positive integer list",
2257
+ );
2258
+ continue;
2259
+ }
2260
+ if (token.startsWith("--group-b-issues=")) {
2261
+ groupBIssuesRaw = requireOptionValue(
2262
+ "--group-b-issues",
2263
+ token.slice("--group-b-issues=".length),
2264
+ "a comma-separated positive integer list",
2265
+ );
2266
+ continue;
2267
+ }
2268
+ if (token === "--attach") {
2269
+ attachmentPaths.push(requireOptionPath("--attach", tokens.shift()));
2270
+ continue;
2271
+ }
2272
+ if (token.startsWith("--attach=")) {
2273
+ attachmentPaths.push(
2274
+ requireOptionPath("--attach", token.slice("--attach=".length)),
2275
+ );
2276
+ continue;
2277
+ }
2278
+ if (token.startsWith("-") && token !== "-") {
2279
+ throw new CliUsageError(`unknown taishi option: ${token}`);
2280
+ }
2281
+ // Optional sweep mode token (like coder plan/apply); only once, no other positionals.
2282
+ if (token === "sweep") {
2283
+ if (sweepToken) {
2284
+ throw new CliUsageError("unexpected taishi argument: sweep");
2285
+ }
2286
+ sweepToken = true;
2287
+ continue;
2288
+ }
2289
+ throw new CliUsageError(`unexpected taishi argument: ${token}`);
2290
+ }
2291
+
2292
+ const hasSweepFace = sweepToken || attachmentPaths.length > 0;
2293
+ const hasCohortFlags =
2294
+ groupALabel !== undefined
2295
+ || groupAIssuesRaw !== undefined
2296
+ || groupBLabel !== undefined
2297
+ || groupBIssuesRaw !== undefined;
2298
+
2299
+ if (query === "cohort") {
2300
+ if (
2301
+ groupALabel === undefined
2302
+ || groupAIssuesRaw === undefined
2303
+ || groupBLabel === undefined
2304
+ || groupBIssuesRaw === undefined
2305
+ ) {
2306
+ throw new CliUsageError(
2307
+ "usage: ak-role taishi --cohort --group-a-label <L> --group-a-issues <N[,N...]> --group-b-label <L> --group-b-issues <N[,N...]>",
2308
+ );
2309
+ }
2310
+ if (ticketRaw !== undefined || projectRoots.length > 0) {
2311
+ throw new CliUsageError(
2312
+ "taishi --cohort does not accept --ticket or --project-root",
2313
+ );
2314
+ }
2315
+ if (hasSweepFace) {
2316
+ throw new CliUsageError(
2317
+ "taishi --cohort does not accept sweep --attach",
2318
+ );
2319
+ }
2320
+ return {
2321
+ query: "cohort",
2322
+ groups: [
2323
+ {
2324
+ groupLabel: groupALabel,
2325
+ issues: parseTaishiIssueNumberList(groupAIssuesRaw, "--group-a-issues"),
2326
+ },
2327
+ {
2328
+ groupLabel: groupBLabel,
2329
+ issues: parseTaishiIssueNumberList(groupBIssuesRaw, "--group-b-issues"),
2330
+ },
2331
+ ],
2332
+ };
2333
+ }
2334
+
2335
+ if (query === "model-groups") {
2336
+ if (projectRoots.length === 0) {
2337
+ throw new CliUsageError(
2338
+ "usage: ak-role taishi --model-groups --project-root <P> [--project-root <P> ...]",
2339
+ );
2340
+ }
2341
+ if (ticketRaw !== undefined) {
2342
+ throw new CliUsageError("taishi --model-groups does not accept --ticket");
2343
+ }
2344
+ if (hasCohortFlags) {
2345
+ throw new CliUsageError("taishi --model-groups does not accept cohort group flags");
2346
+ }
2347
+ if (hasSweepFace) {
2348
+ throw new CliUsageError(
2349
+ "taishi --model-groups does not accept sweep --attach",
2350
+ );
2351
+ }
2352
+ return {
2353
+ query: "model-groups",
2354
+ projectRoots,
2355
+ };
2356
+ }
2357
+
2358
+ // default issue or sweep (#336/#337 faces)
2359
+ if (hasCohortFlags) {
2360
+ throw new CliUsageError("taishi issue query does not accept cohort group flags");
2361
+ }
2362
+
2363
+ if (hasSweepFace) {
2364
+ if (ticketRaw !== undefined || projectRoots.length > 0) {
2365
+ throw new CliUsageError(
2366
+ "taishi sweep --attach cannot combine with --ticket or --project-root",
2367
+ );
2368
+ }
2369
+ return {
2370
+ query: "sweep",
2371
+ attachmentPaths,
2372
+ };
2373
+ }
2374
+
2375
+ if (projectRoots.length > 1) {
2376
+ throw new CliUsageError(
2377
+ "taishi issue query accepts at most one --project-root (use --model-groups for many)",
2378
+ );
2379
+ }
2380
+ const projectRoot = projectRoots[0];
2381
+ if (ticketRaw === undefined && projectRoot === undefined) {
2382
+ throw new CliUsageError(
2383
+ "usage: ak-role taishi ((--ticket <N> | --project-root <P>) | [sweep] --attach <sweep.json> | --cohort ... | --model-groups ...)",
2384
+ );
2385
+ }
2386
+
2387
+ return {
2388
+ query: "issue",
2389
+ ...(ticketRaw === undefined
2390
+ ? {}
2391
+ : { ticket: parseTaishiTicketNumber(ticketRaw) }),
2392
+ ...(projectRoot === undefined ? {} : { projectRoot }),
2393
+ };
2394
+ }
@@ -1,11 +1,30 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { dirname, join } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
 
4
5
  import { ensureHostPiRuntimeResolvable } from "./host-pi-runtime.ts";
5
6
 
6
7
  const here = dirname(fileURLToPath(import.meta.url));
7
- // dist/public-cli/main.js → package root is ../..
8
- const packageRoot = join(here, "..", "..");
8
+
9
+ /**
10
+ * Resolve install package root from the public bin location.
11
+ *
12
+ * Shipped layout is `<packageRoot>/dist/public-cli/main.js` → two levels up when
13
+ * that ancestor owns package.json. A relocated single-file bundle (no package
14
+ * tree beside the bin) must NOT keep climbing: `join("/tmp/<bin>","..","..")`
15
+ * is `"/"` on Linux CI, and host-pi linking then does
16
+ * `mkdir('/node_modules/@earendil-works')` → EACCES. Fall back to the bin
17
+ * directory so links stay on the ESM ancestor walk and remain writable.
18
+ */
19
+ function resolvePackageRoot(binDir: string): string {
20
+ const canonical = join(binDir, "..", "..");
21
+ if (existsSync(join(canonical, "package.json"))) {
22
+ return canonical;
23
+ }
24
+ return binDir;
25
+ }
26
+
27
+ const packageRoot = resolvePackageRoot(here);
9
28
 
10
29
  // The host-provided runtime must be resolvable before the CLI module graph loads it.
11
30
  ensureHostPiRuntimeResolvable(packageRoot);
@@ -97,6 +97,12 @@ export function publicStartupCandidates(
97
97
  return STARTUP_CANDIDATES[seat];
98
98
  }
99
99
 
100
+ /** Deterministic public commands — discoverable, never LLM-configurable seats. */
101
+ export const PUBLIC_DETERMINISTIC_COMMANDS = ["taishi"] as const;
102
+
103
+ export type PublicDeterministicCommand =
104
+ (typeof PUBLIC_DETERMINISTIC_COMMANDS)[number];
105
+
100
106
  export type HelpCapability =
101
107
  | {
102
108
  kind: "support";
@@ -107,11 +113,16 @@ export type HelpCapability =
107
113
  name: PublicCallableRole;
108
114
  phases: readonly (string | null)[];
109
115
  defaultPhase: string | null;
116
+ }
117
+ | {
118
+ kind: "deterministic";
119
+ name: PublicDeterministicCommand;
110
120
  };
111
121
 
112
122
  /**
113
123
  * Typed help surface. Presentation formats these facts; tests must not assert
114
124
  * exact help prose or layout (锚定宪法 / ADR 0016 / #105 AC).
125
+ * taishi is a deterministic analysis command on the public CLI — not an LLM seat.
115
126
  */
116
127
  export function listHelpCapabilities(): readonly HelpCapability[] {
117
128
  const support: HelpCapability[] = PUBLIC_CLI_SUPPORT_COMMANDS.map((name) => ({
@@ -133,7 +144,13 @@ export function listHelpCapabilities(): readonly HelpCapability[] {
133
144
  defaultPhase,
134
145
  };
135
146
  });
136
- return [...support, ...roles];
147
+ const deterministic: HelpCapability[] = PUBLIC_DETERMINISTIC_COMMANDS.map(
148
+ (name) => ({
149
+ kind: "deterministic" as const,
150
+ name,
151
+ }),
152
+ );
153
+ return [...support, ...roles, ...deterministic];
137
154
  }
138
155
 
139
156
  export function isPublicCallableRole(value: string): value is PublicCallableRole {
@@ -118,6 +118,10 @@ export function buildReviewerActivationExtraArgs(
118
118
  options.packageRoot,
119
119
  "code-review",
120
120
  );
121
+ const authorityRefArgs =
122
+ admitted.authorityRefs.length === 0
123
+ ? []
124
+ : ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
121
125
  return [
122
126
  "--no-skills",
123
127
  "--skill",
@@ -134,6 +138,7 @@ export function buildReviewerActivationExtraArgs(
134
138
  "reviewer",
135
139
  "--ak-review-base",
136
140
  admitted.baseRevision,
141
+ ...authorityRefArgs,
137
142
  "--mode",
138
143
  "json",
139
144
  ...buildModelArgs(options.model),
@@ -157,6 +162,10 @@ export function buildReviewerResumeActivationExtraArgs(
157
162
  options.packageRoot,
158
163
  "code-review",
159
164
  );
165
+ const authorityRefArgs =
166
+ admitted.authorityRefs.length === 0
167
+ ? []
168
+ : ["--ak-review-authority-refs", JSON.stringify([...admitted.authorityRefs])];
160
169
  return [
161
170
  "--no-skills",
162
171
  "--skill",
@@ -173,6 +182,7 @@ export function buildReviewerResumeActivationExtraArgs(
173
182
  "reviewer",
174
183
  "--ak-review-base",
175
184
  admitted.baseRevision,
185
+ ...authorityRefArgs,
176
186
  "--mode",
177
187
  "json",
178
188
  ...buildModelArgs(options.model),
@@ -420,6 +430,7 @@ export async function runPublicReviewer(
420
430
  instruction: string;
421
431
  attachmentPaths: string[];
422
432
  baseRevision: string;
433
+ authorityRefs: string[];
423
434
  project?: string;
424
435
  },
425
436
  ): Promise<{
@@ -436,6 +447,7 @@ export async function runPublicReviewer(
436
447
  instruction: parsed.instruction,
437
448
  attachmentPaths: parsed.attachmentPaths,
438
449
  baseRevision: parsed.baseRevision,
450
+ authorityRefs: parsed.authorityRefs,
439
451
  ...(parsed.project === undefined ? {} : { project: parsed.project }),
440
452
  ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
441
453
  });
@@ -24,6 +24,7 @@ export {
24
24
  import type { FixerPhase } from "../package-contracts/fixer-output.ts";
25
25
  import type { FixerPrerequisite } from "../package-contracts/fixer-packet.ts";
26
26
  import {
27
+ requireAuthorityRef,
27
28
  type AdmittedCoderInvocation,
28
29
  type AdmittedFixerInvocation,
29
30
  type AdmittedJudgeInvocation,
@@ -389,6 +390,7 @@ type LoadedAdmittedRequestFields = {
389
390
  readonly prerequisitesPath?: string;
390
391
  readonly prerequisites?: readonly FixerPrerequisite[];
391
392
  readonly baseRevision?: string;
393
+ readonly authorityRefs?: readonly string[];
392
394
  readonly mergerInputPath?: string;
393
395
  readonly derived?: DerivedMergerEnvelope;
394
396
  readonly correlationId?: string;
@@ -463,6 +465,7 @@ async function loadResumableRunRecord(
463
465
  let prerequisitesPath: string | undefined;
464
466
  let prerequisites: readonly FixerPrerequisite[] | undefined;
465
467
  let baseRevision: string | undefined;
468
+ let authorityRefs: readonly string[] | undefined;
466
469
  let mergerInputPath: string | undefined;
467
470
  let derived: DerivedMergerEnvelope | undefined;
468
471
  let correlationId: string | undefined;
@@ -506,6 +509,19 @@ async function loadResumableRunRecord(
506
509
  ) {
507
510
  baseRevision = record.baseRevision;
508
511
  }
512
+ if (Array.isArray(record.authorityRefs)) {
513
+ // Reuse unique --authority-ref grammar; blank/inline prose must not resume as authority.
514
+ authorityRefs = Object.freeze(
515
+ record.authorityRefs.map((ref) => {
516
+ if (typeof ref !== "string") {
517
+ throw new CliUsageError(
518
+ "role run admitted authority refs must be durable reference strings",
519
+ );
520
+ }
521
+ return requireAuthorityRef(ref);
522
+ }),
523
+ );
524
+ }
509
525
  if (
510
526
  typeof record.mergerInputPath === "string" &&
511
527
  record.mergerInputPath.trim() !== ""
@@ -540,9 +556,12 @@ async function loadResumableRunRecord(
540
556
  correlationId = fromAdmitted.correlationId;
541
557
  ticketNumber = fromAdmitted.ticketNumber;
542
558
  }
543
- } catch {
559
+ } catch (error) {
560
+ // Preserve unique --authority-ref grammar failures; do not collapse to unreadable.
561
+ if (error instanceof CliUsageError) throw error;
544
562
  throw new CliUsageError(
545
563
  `role run admitted request is unreadable: ${runId}`,
564
+ { cause: error },
546
565
  );
547
566
  }
548
567
  if (correlationId === undefined || ticketNumber === undefined) {
@@ -589,6 +608,7 @@ async function loadResumableRunRecord(
589
608
  ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
590
609
  ...(prerequisites === undefined ? {} : { prerequisites }),
591
610
  ...(baseRevision === undefined ? {} : { baseRevision }),
611
+ ...(authorityRefs === undefined ? {} : { authorityRefs }),
592
612
  ...(mergerInputPath === undefined ? {} : { mergerInputPath }),
593
613
  ...(derived === undefined ? {} : { derived }),
594
614
  ...(correlationId === undefined ? {} : { correlationId }),
@@ -806,6 +826,7 @@ export async function loadResumableReviewerRun(
806
826
  sessionFile: loaded.run.sessionFile,
807
827
  admittedRequestPath: loaded.run.admittedRequestPath,
808
828
  baseRevision,
829
+ authorityRefs: Object.freeze([...(loaded.admittedFields.authorityRefs ?? [])]),
809
830
  ...restoredTicketFields(loaded.admittedFields),
810
831
  };
811
832
  return {