@akagilnc/pi-workflow-roles 0.1.2193 → 0.1.2203

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/README.md CHANGED
@@ -170,7 +170,7 @@ Generated from `src/public-cli/option-definitions.ts`. Prefer `ak-role help <com
170
170
  | `--attach` | — | `path` | when:sweep | yes | option | modes=sweep; max=sweep:1 | Sweep-mode attachment path; required exactly once in sweep; payload is the attachment body. |
171
171
  | `--cohort` | — | — | no | no | option | modes=cohort | Select cohort mode. |
172
172
  | `--group-a-label` | — | `label` | when:cohort | no | option | modes=cohort | Cohort group A label (required in cohort mode). |
173
- | `--group-a-issues` | — | `N[,N...]` | when:cohort | no | option | modes=cohort | Cohort group A comma-separated positive issue numbers (required in cohort mode). |
173
+ | `--group-a-issues` | — | `N\|book:N[,...]` | when:cohort | no | option | modes=cohort | Cohort group A issues: bare N joins cwd book; book:N selects another book; escape a literal comma/backslash in a book key as \, / \\ (required in cohort mode). |
174
174
  | `--group-b-label` | — | `label` | when:cohort | no | option | modes=cohort | Cohort group B label (required in cohort mode). |
175
- | `--group-b-issues` | — | `N[,N...]` | when:cohort | no | option | modes=cohort | Cohort group B comma-separated positive issue numbers (required in cohort mode). |
175
+ | `--group-b-issues` | — | `N\|book:N[,...]` | when:cohort | no | option | modes=cohort | Cohort group B issues: bare N joins cwd book; book:N selects another book; escape a literal comma/backslash in a book key as \, / \\ (required in cohort mode). |
176
176
  <!-- END GENERATED: public-cli-options -->
package/README.zh-CN.md CHANGED
@@ -199,7 +199,7 @@ ak-role merger --project /path/to/worktree "Reconcile the active merge."
199
199
  | `--attach` | — | `path` | 条件:sweep | 是 | option | modes=sweep; max=sweep:1 | sweep 模式附件路径;sweep 必填且恰一次;载荷为附件正文。 |
200
200
  | `--cohort` | — | — | 否 | 否 | option | modes=cohort | 选择 cohort 模式。 |
201
201
  | `--group-a-label` | — | `label` | 条件:cohort | 否 | option | modes=cohort | cohort A 组标签(cohort 模式必填)。 |
202
- | `--group-a-issues` | — | `N[,N...]` | 条件:cohort | 否 | option | modes=cohort | cohort A 组逗号分隔正整数 issue 列表(cohort 模式必填)。 |
202
+ | `--group-a-issues` | — | `N\|book:N[,...]` | 条件:cohort | 否 | option | modes=cohort | cohort A issue:裸 N 归属 cwd 簿;book:N 显式跨簿;簿键中的逗号/反斜杠用 \, / \\ 转义(cohort 模式必填)。 |
203
203
  | `--group-b-label` | — | `label` | 条件:cohort | 否 | option | modes=cohort | cohort B 组标签(cohort 模式必填)。 |
204
- | `--group-b-issues` | — | `N[,N...]` | 条件:cohort | 否 | option | modes=cohort | cohort B 组逗号分隔正整数 issue 列表(cohort 模式必填)。 |
204
+ | `--group-b-issues` | — | `N\|book:N[,...]` | 条件:cohort | 否 | option | modes=cohort | cohort B issue:裸 N 归属 cwd 簿;book:N 显式跨簿;簿键中的逗号/反斜杠用 \, / \\ 转义(cohort 模式必填)。 |
205
205
  <!-- END GENERATED: public-cli-options -->
@@ -8,20 +8,26 @@ const GIT_DISCOVERY_ENV_KEYS = [
8
8
  "GIT_DISCOVERY_ACROSS_FILESYSTEM"
9
9
  ];
10
10
  function envWithoutGitDiscovery(base = process.env) {
11
- const env = { ...base };
11
+ const env = { ...base, LC_ALL: "C" };
12
12
  for (const key of GIT_DISCOVERY_ENV_KEYS) {
13
13
  delete env[key];
14
14
  }
15
15
  return env;
16
16
  }
17
+ const CONFIRMED_NON_REPOSITORY_STDERR = /^fatal:\s*not a git repository/i;
18
+ function isConfirmedNonRepositoryStderr(stderr) {
19
+ return CONFIRMED_NON_REPOSITORY_STDERR.test(stderr);
20
+ }
17
21
  class ActivationGitRepositoryRequiredError extends Error {
18
22
  code = "AK_ACTIVATION_GIT_REPOSITORY_REQUIRED";
23
+ confirmedNonRepository;
19
24
  constructor(detail, options) {
20
25
  super(
21
26
  `Workflow role activation requires a git repository cwd (git rev-parse --git-common-dir failed): ${detail || "unknown git error"}`,
22
27
  options?.cause === void 0 ? void 0 : { cause: options.cause }
23
28
  );
24
29
  this.name = "ActivationGitRepositoryRequiredError";
30
+ this.confirmedNonRepository = options?.confirmedNonRepository ?? false;
25
31
  }
26
32
  }
27
33
  function isGitSpawnInfrastructureError(error) {
@@ -49,7 +55,10 @@ function resolveBookKeyFromGit(cwd) {
49
55
  }
50
56
  const err = error;
51
57
  const detail = typeof err.stderr === "string" ? err.stderr.trim() : Buffer.isBuffer(err.stderr) ? err.stderr.toString("utf8").trim() : typeof err.message === "string" ? err.message : "";
52
- throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", { cause: error });
58
+ throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", {
59
+ cause: error,
60
+ confirmedNonRepository: isConfirmedNonRepositoryStderr(detail)
61
+ });
53
62
  }
54
63
  if (commonDir.length === 0) {
55
64
  throw new Error("git rev-parse --git-common-dir returned an empty path");
@@ -64,5 +73,6 @@ function resolveBookKeyFromGit(cwd) {
64
73
  }
65
74
  export {
66
75
  ActivationGitRepositoryRequiredError,
76
+ isConfirmedNonRepositoryStderr,
67
77
  resolveBookKeyFromGit
68
78
  };
@@ -15080,12 +15080,15 @@ var init_activation_ledger_topology = __esm({
15080
15080
  import { execFileSync } from "node:child_process";
15081
15081
  import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
15082
15082
  function envWithoutGitDiscovery(base = process.env) {
15083
- const env = { ...base };
15083
+ const env = { ...base, LC_ALL: "C" };
15084
15084
  for (const key of GIT_DISCOVERY_ENV_KEYS) {
15085
15085
  delete env[key];
15086
15086
  }
15087
15087
  return env;
15088
15088
  }
15089
+ function isConfirmedNonRepositoryStderr(stderr) {
15090
+ return CONFIRMED_NON_REPOSITORY_STDERR.test(stderr);
15091
+ }
15089
15092
  function isGitSpawnInfrastructureError(error) {
15090
15093
  if (error === null || typeof error !== "object" || !("code" in error)) return false;
15091
15094
  const code = error.code;
@@ -15111,7 +15114,10 @@ function resolveBookKeyFromGit(cwd) {
15111
15114
  }
15112
15115
  const err = error;
15113
15116
  const detail = typeof err.stderr === "string" ? err.stderr.trim() : Buffer.isBuffer(err.stderr) ? err.stderr.toString("utf8").trim() : typeof err.message === "string" ? err.message : "";
15114
- throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", { cause: error });
15117
+ throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", {
15118
+ cause: error,
15119
+ confirmedNonRepository: isConfirmedNonRepositoryStderr(detail)
15120
+ });
15115
15121
  }
15116
15122
  if (commonDir.length === 0) {
15117
15123
  throw new Error("git rev-parse --git-common-dir returned an empty path");
@@ -15124,7 +15130,7 @@ function resolveBookKeyFromGit(cwd) {
15124
15130
  }
15125
15131
  return bookKey;
15126
15132
  }
15127
- var GIT_DISCOVERY_ENV_KEYS, ActivationGitRepositoryRequiredError;
15133
+ var GIT_DISCOVERY_ENV_KEYS, CONFIRMED_NON_REPOSITORY_STDERR, ActivationGitRepositoryRequiredError;
15128
15134
  var init_activation_ledger_git = __esm({
15129
15135
  "src/activation-ledger-git.ts"() {
15130
15136
  "use strict";
@@ -15135,14 +15141,17 @@ var init_activation_ledger_git = __esm({
15135
15141
  "GIT_CEILING_DIRECTORIES",
15136
15142
  "GIT_DISCOVERY_ACROSS_FILESYSTEM"
15137
15143
  ];
15144
+ CONFIRMED_NON_REPOSITORY_STDERR = /^fatal:\s*not a git repository/i;
15138
15145
  ActivationGitRepositoryRequiredError = class extends Error {
15139
15146
  code = "AK_ACTIVATION_GIT_REPOSITORY_REQUIRED";
15147
+ confirmedNonRepository;
15140
15148
  constructor(detail, options) {
15141
15149
  super(
15142
15150
  `Workflow role activation requires a git repository cwd (git rev-parse --git-common-dir failed): ${detail || "unknown git error"}`,
15143
15151
  options?.cause === void 0 ? void 0 : { cause: options.cause }
15144
15152
  );
15145
15153
  this.name = "ActivationGitRepositoryRequiredError";
15154
+ this.confirmedNonRepository = options?.confirmedNonRepository ?? false;
15146
15155
  }
15147
15156
  };
15148
15157
  }
@@ -15752,7 +15761,7 @@ function evaluateTaishiModeOptionContract(mode, counts) {
15752
15761
  if (mode === "cohort") {
15753
15762
  return {
15754
15763
  ok: false,
15755
- message: "usage: ak-role taishi --cohort --group-a-label <L> --group-a-issues <N[,N...]> --group-b-label <L> --group-b-issues <N[,N...]"
15764
+ message: "usage: ak-role taishi --cohort --group-a-label <L> --group-a-issues <N|book:N[,...]> --group-b-label <L> --group-b-issues <N|book:N[,...]>"
15756
15765
  };
15757
15766
  }
15758
15767
  return {
@@ -15922,7 +15931,7 @@ function renderHumanOwnerOptionLines(owner, locale2 = "en") {
15922
15931
  if (opt.aliases.length > 0) {
15923
15932
  const aliasHint = opt.aliases.join(", ");
15924
15933
  if (opt.form === "positional") {
15925
- spelling = opt.aliases.length > 0 ? opt.aliases.join("|") : spelling;
15934
+ spelling = opt.aliases.join("|");
15926
15935
  } else {
15927
15936
  spelling = `${spelling} (${aliasHint})`;
15928
15937
  }
@@ -16339,15 +16348,15 @@ var init_option_definitions = __esm({
16339
16348
  owner: "taishi",
16340
16349
  canonical: "--group-a-issues",
16341
16350
  aliases: [],
16342
- valueMetavar: "N[,N...]",
16351
+ valueMetavar: "N|book:N[,...]",
16343
16352
  required: false,
16344
16353
  repeatable: false,
16345
16354
  form: "option",
16346
16355
  modes: ["cohort"],
16347
16356
  requiredInModes: ["cohort"],
16348
16357
  description: {
16349
- en: "Cohort group A comma-separated positive issue numbers (required in cohort mode).",
16350
- zh: "cohort A \u7EC4\u9017\u53F7\u5206\u9694\u6B63\u6574\u6570 issue \u5217\u8868\uFF08cohort \u6A21\u5F0F\u5FC5\u586B\uFF09\u3002"
16358
+ en: "Cohort group A issues: bare N joins cwd book; book:N selects another book; escape a literal comma/backslash in a book key as \\, / \\\\ (required in cohort mode).",
16359
+ zh: "cohort A \u7EC4 issue\uFF1A\u88F8 N \u5F52\u5C5E cwd \u7C3F\uFF1Bbook:N \u663E\u5F0F\u8DE8\u7C3F\uFF1B\u7C3F\u952E\u4E2D\u7684\u9017\u53F7/\u53CD\u659C\u6760\u7528 \\, / \\\\ \u8F6C\u4E49\uFF08cohort \u6A21\u5F0F\u5FC5\u586B\uFF09\u3002"
16351
16360
  }
16352
16361
  },
16353
16362
  {
@@ -16371,15 +16380,15 @@ var init_option_definitions = __esm({
16371
16380
  owner: "taishi",
16372
16381
  canonical: "--group-b-issues",
16373
16382
  aliases: [],
16374
- valueMetavar: "N[,N...]",
16383
+ valueMetavar: "N|book:N[,...]",
16375
16384
  required: false,
16376
16385
  repeatable: false,
16377
16386
  form: "option",
16378
16387
  modes: ["cohort"],
16379
16388
  requiredInModes: ["cohort"],
16380
16389
  description: {
16381
- en: "Cohort group B comma-separated positive issue numbers (required in cohort mode).",
16382
- zh: "cohort B \u7EC4\u9017\u53F7\u5206\u9694\u6B63\u6574\u6570 issue \u5217\u8868\uFF08cohort \u6A21\u5F0F\u5FC5\u586B\uFF09\u3002"
16390
+ en: "Cohort group B issues: bare N joins cwd book; book:N selects another book; escape a literal comma/backslash in a book key as \\, / \\\\ (required in cohort mode).",
16391
+ zh: "cohort B \u7EC4 issue\uFF1A\u88F8 N \u5F52\u5C5E cwd \u7C3F\uFF1Bbook:N \u663E\u5F0F\u8DE8\u7C3F\uFF1B\u7C3F\u952E\u4E2D\u7684\u9017\u53F7/\u53CD\u659C\u6760\u7528 \\, / \\\\ \u8F6C\u4E49\uFF08cohort \u6A21\u5F0F\u5FC5\u586B\uFF09\u3002"
16383
16392
  }
16384
16393
  }
16385
16394
  ];
@@ -16474,7 +16483,7 @@ var init_option_definitions = __esm({
16474
16483
  usage: [
16475
16484
  "ak-role taishi [--ticket <N>]",
16476
16485
  "ak-role taishi [sweep] --attach <path>",
16477
- "ak-role taishi --cohort --group-a-label <L> --group-a-issues <N[,N...]> --group-b-label <L> --group-b-issues <N[,N...]>"
16486
+ "ak-role taishi --cohort --group-a-label <L> --group-a-issues <N|book:N[,...]> --group-b-label <L> --group-b-issues <N|book:N[,...]>"
16478
16487
  ],
16479
16488
  examples: [
16480
16489
  "ak-role taishi",
@@ -17906,16 +17915,75 @@ function parseTaishiTicketNumber(raw, flag = "--ticket") {
17906
17915
  }
17907
17916
  return value;
17908
17917
  }
17909
- function parseTaishiIssueNumberList(raw, flag) {
17918
+ function parseTaishiCohortIssueToken(raw, flag) {
17919
+ const trimmed = raw.trim();
17920
+ if (trimmed === "") {
17921
+ throw new CliUsageError(
17922
+ `${flag} requires a comma-separated list of N or book:N`
17923
+ );
17924
+ }
17925
+ const sep4 = trimmed.lastIndexOf(":");
17926
+ if (sep4 > 0) {
17927
+ const rhs = trimmed.slice(sep4 + 1);
17928
+ if (TAISHI_TICKET_NUMBER_PATTERN.test(rhs)) {
17929
+ const bookKey = trimmed.slice(0, sep4);
17930
+ if (bookKey.trim() === "") {
17931
+ throw new CliUsageError(
17932
+ `${flag} book:N requires a non-empty book key, got ${raw}`
17933
+ );
17934
+ }
17935
+ return {
17936
+ kind: "book-qualified",
17937
+ bookKey,
17938
+ issueNumber: parseTaishiTicketNumber(rhs, flag)
17939
+ };
17940
+ }
17941
+ }
17942
+ return {
17943
+ kind: "bare",
17944
+ issueNumber: parseTaishiTicketNumber(trimmed, flag)
17945
+ };
17946
+ }
17947
+ function splitTaishiCohortIssueListParts(raw) {
17948
+ const parts = [];
17949
+ let current = "";
17950
+ let escaped = false;
17951
+ for (const ch of raw) {
17952
+ if (escaped) {
17953
+ current += ch === "," || ch === "\\" ? ch : `\\${ch}`;
17954
+ escaped = false;
17955
+ continue;
17956
+ }
17957
+ if (ch === "\\") {
17958
+ escaped = true;
17959
+ continue;
17960
+ }
17961
+ if (ch === ",") {
17962
+ parts.push(current);
17963
+ current = "";
17964
+ continue;
17965
+ }
17966
+ current += ch;
17967
+ }
17968
+ parts.push(escaped ? `${current}\\` : current);
17969
+ return parts;
17970
+ }
17971
+ function parseTaishiCohortIssueTokenList(raw, flag) {
17910
17972
  const trimmed = raw.trim();
17911
17973
  if (trimmed === "") {
17912
- throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
17974
+ throw new CliUsageError(
17975
+ `${flag} requires a comma-separated list of N or book:N`
17976
+ );
17913
17977
  }
17914
- const parts = trimmed.split(",").map((part) => part.trim());
17978
+ const parts = splitTaishiCohortIssueListParts(trimmed).map(
17979
+ (part) => part.trim()
17980
+ );
17915
17981
  if (parts.some((part) => part === "")) {
17916
- throw new CliUsageError(`${flag} requires a comma-separated positive integer list`);
17982
+ throw new CliUsageError(
17983
+ `${flag} requires a comma-separated list of N or book:N`
17984
+ );
17917
17985
  }
17918
- return parts.map((part) => parseTaishiTicketNumber(part, flag));
17986
+ return parts.map((part) => parseTaishiCohortIssueToken(part, flag));
17919
17987
  }
17920
17988
  function requireOptionValue(flag, value, what) {
17921
17989
  if (value === void 0 || value.trim() === "") {
@@ -17974,7 +18042,7 @@ function parseTaishiArgv(args) {
17974
18042
  requireOptionValue(
17975
18043
  taken.def.canonical,
17976
18044
  taken.value,
17977
- "a comma-separated positive integer list"
18045
+ "a comma-separated list of N or book:N"
17978
18046
  )
17979
18047
  );
17980
18048
  continue;
@@ -18025,11 +18093,17 @@ function parseTaishiArgv(args) {
18025
18093
  groups: [
18026
18094
  {
18027
18095
  groupLabel: groupALabel,
18028
- issues: parseTaishiIssueNumberList(groupAIssuesRaw, "--group-a-issues")
18096
+ issues: parseTaishiCohortIssueTokenList(
18097
+ groupAIssuesRaw,
18098
+ "--group-a-issues"
18099
+ )
18029
18100
  },
18030
18101
  {
18031
18102
  groupLabel: groupBLabel,
18032
- issues: parseTaishiIssueNumberList(groupBIssuesRaw, "--group-b-issues")
18103
+ issues: parseTaishiCohortIssueTokenList(
18104
+ groupBIssuesRaw,
18105
+ "--group-b-issues"
18106
+ )
18033
18107
  }
18034
18108
  ]
18035
18109
  };
@@ -25457,6 +25531,40 @@ var init_reviewer_run = __esm({
25457
25531
  }
25458
25532
  });
25459
25533
 
25534
+ // src/taishi-book-key.ts
25535
+ import { statSync as statSync2 } from "node:fs";
25536
+ import { isAbsolute as isAbsolute5 } from "node:path";
25537
+ function resolveTaishiBookKey(projectRoot) {
25538
+ const identity = physicalPathIdentity(projectRoot);
25539
+ let stats;
25540
+ try {
25541
+ stats = statSync2(identity);
25542
+ } catch (error) {
25543
+ const code = errnoCode(error);
25544
+ if (code === "ENOENT" || code === "ENOTDIR") return `root:${identity}`;
25545
+ throw error;
25546
+ }
25547
+ if (!stats.isDirectory()) return `root:${identity}`;
25548
+ try {
25549
+ return resolveBookKeyFromGit(identity);
25550
+ } catch (error) {
25551
+ if (error instanceof ActivationGitRepositoryRequiredError && error.confirmedNonRepository) {
25552
+ return `root:${identity}`;
25553
+ }
25554
+ throw error;
25555
+ }
25556
+ }
25557
+ function isSyntheticTaishiBookKey(bookKey) {
25558
+ return bookKey.startsWith("root:") && isAbsolute5(bookKey.slice("root:".length));
25559
+ }
25560
+ var init_taishi_book_key = __esm({
25561
+ "src/taishi-book-key.ts"() {
25562
+ "use strict";
25563
+ init_activation_ledger_git();
25564
+ init_activation_ledger_topology();
25565
+ }
25566
+ });
25567
+
25460
25568
  // src/atomic-write.ts
25461
25569
  import { randomUUID as randomUUID3 } from "node:crypto";
25462
25570
  import { rename, rm, writeFile as writeFile13 } from "node:fs/promises";
@@ -25530,6 +25638,15 @@ function rowFromIssueMetricsPage(page) {
25530
25638
  lastActivityAt: page.lastActivityAt
25531
25639
  };
25532
25640
  }
25641
+ function normalizeTaishiLibraryIndexRow(row) {
25642
+ const rawBook = row.bookKey;
25643
+ if (typeof rawBook === "string" && rawBook !== "") {
25644
+ if (rawBook === row.bookKey) return row;
25645
+ return { ...row, bookKey: rawBook };
25646
+ }
25647
+ const projectRoot = physicalPathIdentity(row.projectRoot);
25648
+ return { ...row, projectRoot, bookKey: resolveTaishiBookKey(projectRoot) };
25649
+ }
25533
25650
  function sortRows(rows) {
25534
25651
  return [...rows].sort((a, b) => {
25535
25652
  const byBook = a.bookKey.localeCompare(b.bookKey);
@@ -25547,12 +25664,14 @@ function sortRows(rows) {
25547
25664
  function buildTaishiLibraryIndexPage(rows) {
25548
25665
  return {
25549
25666
  kind: "taishi-library-index",
25550
- rows: sortRows(rows)
25667
+ rows: sortRows(rows.map(normalizeTaishiLibraryIndexRow))
25551
25668
  };
25552
25669
  }
25553
- function findTaishiLibraryIndexRow(index, issueNumber) {
25670
+ function findTaishiLibraryIndexRow(index, issueNumber, bookKey) {
25554
25671
  if (index === void 0) return void 0;
25555
- return index.rows.find((row) => row.issueNumber === issueNumber);
25672
+ return index.rows.find(
25673
+ (row) => row.issueNumber === issueNumber && row.bookKey === bookKey
25674
+ );
25556
25675
  }
25557
25676
  function indexRowKey(row) {
25558
25677
  return `${row.bookKey}\0${row.projectRoot}`;
@@ -25564,6 +25683,7 @@ function upsertTaishiLibraryIndexRows(existing, upserts) {
25564
25683
  const byKey = /* @__PURE__ */ new Map();
25565
25684
  const keyByIssue = /* @__PURE__ */ new Map();
25566
25685
  const ingest = (row) => {
25686
+ row = normalizeTaishiLibraryIndexRow(row);
25567
25687
  const key = indexRowKey(row);
25568
25688
  if (row.issueNumber !== void 0) {
25569
25689
  const issueKey = issueBookKey(row.bookKey, row.issueNumber);
@@ -25602,7 +25722,14 @@ async function readTaishiLibraryIndexPage(ledgerHome) {
25602
25722
  }
25603
25723
  throw error;
25604
25724
  }
25605
- return JSON.parse(raw);
25725
+ const parsed = JSON.parse(raw);
25726
+ if (parsed === null || typeof parsed !== "object" || !Array.isArray(parsed.rows)) {
25727
+ const shape = parsed === null ? "null" : typeof parsed !== "object" ? typeof parsed : `object with non-array rows (${typeof parsed.rows})`;
25728
+ throw new Error(
25729
+ `taishi library-index at ${path} is malformed (${shape}; expected an index page with a rows array) \u2014 rejected at the read boundary`
25730
+ );
25731
+ }
25732
+ return buildTaishiLibraryIndexPage(parsed.rows);
25606
25733
  }
25607
25734
  async function writeTaishiLibraryIndexPage(ledgerHome, page) {
25608
25735
  const path = taishiLibraryIndexPath(ledgerHome);
@@ -25626,6 +25753,7 @@ var init_taishi_index = __esm({
25626
25753
  "use strict";
25627
25754
  init_atomic_write();
25628
25755
  init_activation_ledger_topology();
25756
+ init_taishi_book_key();
25629
25757
  LIBRARY_INDEX_LOCK_NAME = ".library-index.lock";
25630
25758
  LIBRARY_INDEX_LOCK_TIMEOUT_MS = 3e4;
25631
25759
  LIBRARY_INDEX_LOCK_RETRY_MS = 15;
@@ -25692,10 +25820,11 @@ async function aggregateGroup(index, input, ensureIssuePage) {
25692
25820
  let totalWallMs = 0;
25693
25821
  let hasReworkSample = false;
25694
25822
  const legWalls = [];
25695
- for (const issueNumber of input.issues) {
25696
- const row = findTaishiLibraryIndexRow(index, issueNumber);
25823
+ for (const ref of input.issues) {
25824
+ const { issueNumber, bookKey } = ref;
25825
+ const row = findTaishiLibraryIndexRow(index, issueNumber, bookKey);
25697
25826
  if (row === void 0) {
25698
- issueEntries.push({ issueNumber, status: "absent" });
25827
+ issueEntries.push({ issueNumber, status: "absent", bookKey });
25699
25828
  continue;
25700
25829
  }
25701
25830
  const page = await ensureIssuePage({
@@ -26270,7 +26399,12 @@ async function scanTaishiIssueRuns(input) {
26270
26399
  let bookNames;
26271
26400
  if (input.bookKey !== void 0 && input.bookKey.trim() !== "") {
26272
26401
  bookNames = [input.bookKey];
26273
- wholeBook = true;
26402
+ if (input.projectRoot !== void 0) {
26403
+ wholeBook = false;
26404
+ scopeRootIdentity = physicalPathIdentity(input.projectRoot);
26405
+ } else {
26406
+ wholeBook = true;
26407
+ }
26274
26408
  } else if (input.projectRoot !== void 0) {
26275
26409
  const resolved = tryResolveBookKeyFromProjectRoot(input.projectRoot);
26276
26410
  if (resolved !== void 0) {
@@ -27237,22 +27371,13 @@ function cachedPageMatchesRequestedScope(page, input) {
27237
27371
  }
27238
27372
  return page.issueNumber === requestedTicket;
27239
27373
  }
27240
- function tryResolveBookKey(projectRoot) {
27241
- try {
27242
- return resolveBookKeyFromGit(projectRoot);
27243
- } catch {
27244
- return void 0;
27245
- }
27246
- }
27247
27374
  function resolveIssueBookKey(input) {
27248
27375
  if (input.bookKey !== void 0 && input.bookKey.trim() !== "") {
27249
27376
  return input.bookKey;
27250
27377
  }
27251
- const fromGit = tryResolveBookKey(input.projectRoot);
27252
- if (fromGit !== void 0) return fromGit;
27253
- return `root:${physicalPathIdentity(input.projectRoot)}`;
27378
+ return resolveTaishiBookKey(input.projectRoot);
27254
27379
  }
27255
- async function readOrComputeTaishiIssuePage(input) {
27380
+ async function readOrComputeTaishiIssuePage(input, options) {
27256
27381
  const ledgerHome = resolveActivationLedgerHome();
27257
27382
  const projectRoot = physicalPathIdentity(input.projectRoot);
27258
27383
  const bookKey = resolveIssueBookKey(input);
@@ -27280,7 +27405,7 @@ async function readOrComputeTaishiIssuePage(input) {
27280
27405
  }
27281
27406
  }
27282
27407
  try {
27283
- return await runTaishiIssueMode(input);
27408
+ return await runTaishiIssueMode(input, void 0, options?.scanProjectRoot);
27284
27409
  } catch (error) {
27285
27410
  if (error instanceof TaishiIssueComputeError) throw error;
27286
27411
  throw new TaishiIssueComputeError({
@@ -27291,7 +27416,7 @@ async function readOrComputeTaishiIssuePage(input) {
27291
27416
  });
27292
27417
  }
27293
27418
  }
27294
- async function runTaishiIssueMode(input, precomputedScan) {
27419
+ async function runTaishiIssueMode(input, precomputedScan, scanProjectRoot) {
27295
27420
  assertTaishiChangedLinesInput(input.changedLines);
27296
27421
  const ledgerHome = resolveActivationLedgerHome();
27297
27422
  const projectRoot = input.projectRoot;
@@ -27299,6 +27424,7 @@ async function runTaishiIssueMode(input, precomputedScan) {
27299
27424
  const inputBookKey = "bookKey" in input && typeof input.bookKey === "string" && input.bookKey.trim() !== "" ? input.bookKey : void 0;
27300
27425
  const scan = precomputedScan ?? (inputBookKey !== void 0 ? await scanTaishiIssueRuns({
27301
27426
  bookKey: inputBookKey,
27427
+ ...scanProjectRoot === void 0 ? {} : { projectRoot: scanProjectRoot },
27302
27428
  ...ticketNumber === void 0 ? {} : { ticketNumber }
27303
27429
  }) : ticketNumber === void 0 ? await scanTaishiIssueRuns({ projectRoot }) : await scanTaishiIssueRuns({ projectRoot, ticketNumber }));
27304
27430
  const issueNumber = "issueNumber" in input ? input.issueNumber : void 0;
@@ -27386,13 +27512,14 @@ async function runTaishi(input) {
27386
27512
  if (input.mode === "cohort") {
27387
27513
  const ledgerHome = resolveActivationLedgerHome();
27388
27514
  return runTaishiCohortMode(ledgerHome, input, async ({ projectRoot, issueNumber, bookKey }) => {
27389
- const realBookKey = bookKey !== void 0 && !bookKey.startsWith("root:") ? bookKey : void 0;
27515
+ const realBookKey = bookKey !== void 0 && !isSyntheticTaishiBookKey(bookKey) ? bookKey : void 0;
27390
27516
  const ensured = await readOrComputeTaishiIssuePage({
27391
27517
  mode: "issue",
27392
27518
  projectRoot,
27393
27519
  issueNumber,
27520
+ ticketNumber: issueNumber,
27394
27521
  ...realBookKey === void 0 ? {} : { bookKey: realBookKey }
27395
- });
27522
+ }, { scanProjectRoot: projectRoot });
27396
27523
  return ensured.page;
27397
27524
  });
27398
27525
  }
@@ -27407,11 +27534,11 @@ var init_taishi_entry = __esm({
27407
27534
  "use strict";
27408
27535
  init_build();
27409
27536
  init_activation_ledger_topology();
27537
+ init_taishi_book_key();
27410
27538
  init_taishi_cohort();
27411
27539
  init_taishi_ledger();
27412
27540
  init_taishi_index();
27413
27541
  init_taishi_model_groups();
27414
- init_activation_ledger_git();
27415
27542
  init_taishi_page();
27416
27543
  TaishiIssueComputeError = class extends Error {
27417
27544
  code = "taishi-issue-compute-failed";
@@ -27456,7 +27583,7 @@ var init_taishi_entry = __esm({
27456
27583
 
27457
27584
  // src/public-cli/taishi-run.ts
27458
27585
  import { readFile as readFile16 } from "node:fs/promises";
27459
- import { isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
27586
+ import { isAbsolute as isAbsolute6, resolve as resolve7 } from "node:path";
27460
27587
  function resolveTaishiIssueBookKeyFromCwd(cwd = process.cwd()) {
27461
27588
  try {
27462
27589
  return resolveBookKeyFromGit(cwd);
@@ -27505,7 +27632,7 @@ async function buildTaishiSweepModeInputFromAttachmentPaths(attachmentPaths) {
27505
27632
  );
27506
27633
  }
27507
27634
  const sourcePath = attachmentPaths[0];
27508
- const absolute = isAbsolute5(sourcePath) ? sourcePath : resolve7(sourcePath);
27635
+ const absolute = isAbsolute6(sourcePath) ? sourcePath : resolve7(sourcePath);
27509
27636
  let bytes;
27510
27637
  try {
27511
27638
  bytes = await readFile16(absolute);
@@ -27547,9 +27674,26 @@ async function runPublicTaishi(argv, _env, io, parseTaishiArgv2) {
27547
27674
  return { exitCode: 0 };
27548
27675
  }
27549
27676
  if (parsed.query === "cohort") {
27677
+ let defaultBookKey;
27678
+ const resolveIssue = (token) => {
27679
+ if (token.kind === "book-qualified") {
27680
+ return { bookKey: token.bookKey, issueNumber: token.issueNumber };
27681
+ }
27682
+ defaultBookKey ??= resolveTaishiIssueBookKeyFromCwd();
27683
+ return { bookKey: defaultBookKey, issueNumber: token.issueNumber };
27684
+ };
27550
27685
  const result3 = await runTaishi({
27551
27686
  mode: "cohort",
27552
- groups: parsed.groups
27687
+ groups: [
27688
+ {
27689
+ groupLabel: parsed.groups[0].groupLabel,
27690
+ issues: parsed.groups[0].issues.map(resolveIssue)
27691
+ },
27692
+ {
27693
+ groupLabel: parsed.groups[1].groupLabel,
27694
+ issues: parsed.groups[1].issues.map(resolveIssue)
27695
+ }
27696
+ ]
27553
27697
  });
27554
27698
  io.stdout(`${JSON.stringify(result3, null, 2)}
27555
27699
  `);
@@ -27814,7 +27958,7 @@ function renderHelp() {
27814
27958
  const doc = helpDocument();
27815
27959
  const top = projectCommandHelp("top");
27816
27960
  const lines = [
27817
- `ak-role \u2014 ${top?.summary ?? "public role CLI"}`
27961
+ `ak-role \u2014 ${top.summary}`
27818
27962
  ];
27819
27963
  appendUsageAndExamples(lines, "top");
27820
27964
  lines.push("", PUBLIC_NAVIGATOR_HELP_NOTE);
@@ -27865,8 +28009,8 @@ function renderCommandHelp(command) {
27865
28009
  lines.push(`ak-role ${match.name}`);
27866
28010
  }
27867
28011
  appendUsageAndExamples(lines, command);
27868
- if (command in PUBLIC_ROLE_ARGV || command === "global") {
27869
- const owner = command === "global" ? "global" : command;
28012
+ if (command in PUBLIC_ROLE_ARGV) {
28013
+ const owner = command;
27870
28014
  lines.push("", "OPTIONS");
27871
28015
  lines.push(...renderHumanOwnerOptionLines(owner));
27872
28016
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2193",
3
+ "version": "0.1.2203",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -11,7 +11,7 @@ const GIT_DISCOVERY_ENV_KEYS = [
11
11
  ] as const;
12
12
 
13
13
  function envWithoutGitDiscovery(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
14
- const env: NodeJS.ProcessEnv = { ...base };
14
+ const env: NodeJS.ProcessEnv = { ...base, LC_ALL: "C" };
15
15
  for (const key of GIT_DISCOVERY_ENV_KEYS) {
16
16
  delete env[key];
17
17
  }
@@ -19,17 +19,42 @@ function envWithoutGitDiscovery(base: NodeJS.ProcessEnv = process.env): NodeJS.P
19
19
  }
20
20
 
21
21
  /**
22
- * Typed book-key discovery failure: a git child ran and reported non-repository status.
23
- * Original git cause (nonzero exit) is retained. Spawn/OS failures never become this type.
22
+ * #413 r2 U5 — single failure-classification owner. A git child nonzero exit is
23
+ * only a *confirmed* non-repository verdict when git's own diagnostic says so
24
+ * ("fatal: not a git repository …", forced to the C locale so the wording is
25
+ * deterministic). Every other diagnostic — dubious ownership exit 128,
26
+ * permissions, anything unknown — leaves the question open: git found or
27
+ * refuses to adjudicate a repository-shaped situation without certifying
28
+ * "non repository". Classification by diagnostic identity, not by exit code
29
+ * alone, because both faces share exit 128.
30
+ */
31
+ const CONFIRMED_NON_REPOSITORY_STDERR = /^fatal:\s*not a git repository/i;
32
+
33
+ export function isConfirmedNonRepositoryStderr(stderr: string): boolean {
34
+ return CONFIRMED_NON_REPOSITORY_STDERR.test(stderr);
35
+ }
36
+
37
+ /**
38
+ * Typed book-key discovery failure: a git child ran and exited nonzero.
39
+ * Original git cause (nonzero exit) is retained. Spawn/OS failures never become
40
+ * this type. `confirmedNonRepository` records whether git itself certified
41
+ * "no repository here" (true) or merely failed for an unadjudicated reason
42
+ * such as dubious ownership (false) — consumers may only synthesize fallback
43
+ * identities from the confirmed face.
24
44
  */
25
45
  export class ActivationGitRepositoryRequiredError extends Error {
26
46
  readonly code = "AK_ACTIVATION_GIT_REPOSITORY_REQUIRED" as const;
27
- constructor(detail: string, options?: { cause?: unknown }) {
47
+ readonly confirmedNonRepository: boolean;
48
+ constructor(
49
+ detail: string,
50
+ options?: { cause?: unknown; confirmedNonRepository?: boolean },
51
+ ) {
28
52
  super(
29
53
  `Workflow role activation requires a git repository cwd (git rev-parse --git-common-dir failed): ${detail || "unknown git error"}`,
30
54
  options?.cause === undefined ? undefined : { cause: options.cause },
31
55
  );
32
56
  this.name = "ActivationGitRepositoryRequiredError";
57
+ this.confirmedNonRepository = options?.confirmedNonRepository ?? false;
33
58
  }
34
59
  }
35
60
 
@@ -79,7 +104,10 @@ export function resolveBookKeyFromGit(cwd: string): string {
79
104
  : typeof err.message === "string"
80
105
  ? err.message
81
106
  : "";
82
- throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", { cause: error });
107
+ throw new ActivationGitRepositoryRequiredError(detail || "unknown git error", {
108
+ cause: error,
109
+ confirmedNonRepository: isConfirmedNonRepositoryStderr(detail),
110
+ });
83
111
  }
84
112
  if (commonDir.length === 0) {
85
113
  throw new Error("git rev-parse --git-common-dir returned an empty path");