@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193

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 (108) hide show
  1. package/dist/acp-host/production-host.js +2368 -1155
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-auditor.js +1 -1
  7. package/dist/doctor-evidence.js +3 -2
  8. package/dist/dossier-resolution.js +21 -8
  9. package/dist/gatekeeper-role.js +34 -27
  10. package/dist/headless-host/description.js +7 -8
  11. package/dist/headless-host/production-host.js +2375 -1149
  12. package/dist/host-contracts.js +16 -0
  13. package/dist/inspector-contracts.js +8 -0
  14. package/dist/migrate-book-topology.js +278917 -0
  15. package/dist/navigator-public-session.js +22 -25
  16. package/dist/notary-source-run.js +7 -4
  17. package/dist/packaged-role-registry.js +1 -1
  18. package/dist/pi/durable-principal.js +9 -6
  19. package/dist/pi/role-turn-host.js +43 -13
  20. package/dist/public-cli/auto-resume.js +2 -5
  21. package/dist/public-cli/case-dossier-delivery.js +60 -1
  22. package/dist/public-cli/diarist-run.js +27 -25
  23. package/dist/public-cli/inspector-run.js +17 -3
  24. package/dist/public-cli/instruction-seat-run.js +17 -17
  25. package/dist/public-cli/invocation.js +127 -63
  26. package/dist/public-cli/main.js +1685 -696
  27. package/dist/public-cli/notary-run.js +7 -4
  28. package/dist/public-cli/option-definitions.js +2 -2
  29. package/dist/public-cli/post-admission.js +113 -71
  30. package/dist/public-cli/run-lifecycle.js +209 -58
  31. package/dist/public-cli/seat-ticket-binding.js +3 -6
  32. package/dist/public-cli/settlement.js +29 -10
  33. package/dist/public-role-summons.js +8 -0
  34. package/dist/role-run-placement.js +84 -0
  35. package/dist/role-run-relocation.js +358 -0
  36. package/dist/run-terminal-artifacts.js +2 -1
  37. package/dist/run-ticket-number.js +51 -32
  38. package/dist/sitian-appender.js +11 -26
  39. package/dist/submission-ledger.js +106 -25
  40. package/dist/ticket-provenance.js +3 -1
  41. package/dist/user-dialogue-stdin.js +33 -0
  42. package/extensions/role-runtime.ts +1 -0
  43. package/package.json +3 -2
  44. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  45. package/scripts/build-package.mjs +34 -2
  46. package/souls/fixer.md +3 -2
  47. package/src/analyst-ledger.ts +14 -15
  48. package/src/archivist-record-entry.ts +285 -38
  49. package/src/archivist-record-topology.ts +43 -18
  50. package/src/auditor-dossier-tool.ts +2 -1
  51. package/src/book-topology-migration-placement.ts +493 -0
  52. package/src/book-topology-migration.ts +220 -0
  53. package/src/book-topology-migrators-t11.ts +856 -0
  54. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  55. package/src/book-topology-partition-migrators.ts +25 -0
  56. package/src/book-topology-record-class-migrators.ts +779 -0
  57. package/src/book-topology-runs-migrator.ts +244 -0
  58. package/src/diarist-contracts.ts +116 -0
  59. package/src/diarist-role.ts +6 -0
  60. package/src/diarist.ts +2 -0
  61. package/src/doctor-auditor.ts +1 -1
  62. package/src/doctor-evidence.ts +4 -1
  63. package/src/doctor-role.ts +1 -1
  64. package/src/dossier-resolution.ts +26 -8
  65. package/src/external-host-turn-loop.ts +9 -0
  66. package/src/gatekeeper-pass-envelope.ts +6 -0
  67. package/src/gatekeeper-role.ts +47 -28
  68. package/src/headless-host/description.ts +8 -11
  69. package/src/headless-host/role-turn-host.ts +20 -5
  70. package/src/host-contracts.ts +22 -3
  71. package/src/inspector-contracts.ts +7 -0
  72. package/src/judge-role.ts +4 -0
  73. package/src/navigator-public-session.ts +33 -30
  74. package/src/navigator-work-context.ts +3 -4
  75. package/src/notary-role.ts +1 -0
  76. package/src/notary-source-run.ts +9 -4
  77. package/src/packaged-role-registry.ts +1 -1
  78. package/src/pi/adapter.ts +19 -4
  79. package/src/pi/durable-principal.ts +9 -13
  80. package/src/pi/role-turn-host.ts +48 -14
  81. package/src/public-cli/auto-resume.ts +2 -6
  82. package/src/public-cli/case-dossier-delivery.ts +86 -1
  83. package/src/public-cli/countersign-run.ts +182 -32
  84. package/src/public-cli/diarist-run.ts +34 -26
  85. package/src/public-cli/inspector-run.ts +26 -2
  86. package/src/public-cli/instruction-seat-run.ts +24 -20
  87. package/src/public-cli/invocation.ts +194 -58
  88. package/src/public-cli/notary-run.ts +13 -4
  89. package/src/public-cli/option-definitions.ts +2 -2
  90. package/src/public-cli/post-admission.ts +147 -78
  91. package/src/public-cli/run-lifecycle.ts +258 -65
  92. package/src/public-cli/seat-ticket-binding.ts +3 -6
  93. package/src/public-cli/settlement.ts +38 -10
  94. package/src/public-role-summons.ts +31 -7
  95. package/src/role-envelope.ts +9 -30
  96. package/src/role-run-placement.ts +128 -0
  97. package/src/role-run-relocation.ts +491 -0
  98. package/src/role-runtime-dependencies.ts +1 -0
  99. package/src/role-runtime.ts +106 -24
  100. package/src/run-terminal-artifacts.ts +3 -1
  101. package/src/run-ticket-number.ts +80 -20
  102. package/src/sitian-appender.ts +25 -28
  103. package/src/submission-ledger.ts +148 -30
  104. package/src/ticket-provenance.ts +4 -1
  105. package/src/ticket-trajectory.ts +22 -35
  106. package/src/user-dialogue-stdin.ts +37 -0
  107. package/src/worker-role.ts +8 -4
  108. package/src/worker-submission-gates.ts +6 -6
@@ -1,7 +1,10 @@
1
+ import { join } from "node:path";
1
2
  import { resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
3
+ import { courtAttemptIdFromHostContext, runDirectoryFromHostContext, } from "./host-contracts.js";
2
4
  import { isAuditEscalationProjection } from "./audit-escalation.js";
3
5
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
4
6
  import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from "./sitian-facade.js";
7
+ import { findRunDirectoryById } from "./public-cli/run-lifecycle.js";
5
8
  import { isCorrectableExecuteError } from "./submission-correctable-error.js";
6
9
  import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
7
10
  /**
@@ -10,8 +13,8 @@ import { failOnInfrastructureFailureDeclaration } from "./package-contracts/term
10
13
  * otherwise session header id. Never a shared "unbound" bucket.
11
14
  */
12
15
  function runIdentity(context) {
13
- const directory = process.env.AK_ROLE_RUN_DIR;
14
- if (typeof directory === "string" && directory.length > 0) {
16
+ const directory = runDirectoryFromHostContext(context);
17
+ if (directory !== undefined) {
15
18
  const fromDir = runIdFromRunDirectory(directory);
16
19
  if (fromDir !== undefined)
17
20
  return fromDir;
@@ -27,26 +30,45 @@ function runIdentity(context) {
27
30
  */
28
31
  export const COURT_ATTEMPT_ENV = "AK_ROLE_COURT_ATTEMPT";
29
32
  function attemptIdentity(context, runId) {
30
- const courtAttempt = process.env[COURT_ATTEMPT_ENV];
31
- if (typeof courtAttempt === "string" && courtAttempt.length > 0)
33
+ const courtAttempt = courtAttemptIdFromHostContext(context);
34
+ if (courtAttempt !== undefined)
32
35
  return courtAttempt;
33
36
  return context.sessionManager.getHeader?.()?.id ?? context.sessionManager.getLeafId?.() ?? `${runId}:initial`;
34
37
  }
35
- function submissionRecordFile(cwd, runId, home) {
36
- const ledgerHome = resolveActivationLedgerHome(home);
38
+ /**
39
+ * Resolve the submission-ledger record file for a run.
40
+ * Unknown run (no sessionParent and no discoverable directory) → undefined so
41
+ * read APIs return their empty set at the read boundary. Write paths always
42
+ * supply sessionParent and keep the ownership gate.
43
+ */
44
+ async function submissionRecordFile(cwd, runId, scope) {
45
+ const ledgerHome = resolveActivationLedgerHome(scope.home);
46
+ let sessionParent = scope.sessionParent;
47
+ if (sessionParent === undefined) {
48
+ const discoveredRun = await findRunDirectoryById(scope.home, runId);
49
+ if (discoveredRun === undefined)
50
+ return undefined;
51
+ sessionParent = join(discoveredRun, "session", "session.jsonl");
52
+ }
37
53
  return resolveSitianRecordPathInLedger({
38
54
  level: "event",
39
55
  kind: "candidate",
40
56
  subject: { runId },
41
57
  cwd,
58
+ sessionParent,
42
59
  }, ledgerHome).recordFile;
43
60
  }
44
- async function readOwnedSubmissionRecords(cwd, runId, home) {
45
- const file = submissionRecordFile(cwd, runId, home);
61
+ async function readOwnedSubmissionRecords(cwd, runId, scope = {}) {
62
+ const file = await submissionRecordFile(cwd, runId, scope);
63
+ if (file === undefined) {
64
+ return { file: undefined, owned: [] };
65
+ }
46
66
  const { records } = await readSitianRecords(file);
47
67
  return {
48
68
  file,
49
- owned: records.filter((record) => typeof record.subject === "object" && record.subject !== null && record.subject.runId === runId),
69
+ owned: records.filter((record) => typeof record.subject === "object"
70
+ && record.subject !== null
71
+ && record.subject.runId === runId),
50
72
  };
51
73
  }
52
74
  function resolveReadScope(homeOrScope) {
@@ -85,8 +107,8 @@ function recordAttemptId(record) {
85
107
  * outranking the current attempt's own real host-turn failure signal
86
108
  * (#637 original intent, restored narrowly).
87
109
  */
88
- export async function hasFreshAttemptSubmission(cwd, runId, attemptId, home) {
89
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, home);
110
+ export async function hasFreshAttemptSubmission(cwd, runId, attemptId, homeOrScope) {
111
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, resolveReadScope(homeOrScope));
90
112
  return owned.some((record) => {
91
113
  if (recordAttemptId(record) !== attemptId)
92
114
  return false;
@@ -151,10 +173,8 @@ function rowFromPayload(kind, payload, accepted, roleFallback) {
151
173
  * recorded attemptId + toolCallId so distinct court attempts stay distinct (#881).
152
174
  * `accepted` is the original payload; never rebuilt from a status/facts envelope.
153
175
  */
154
- export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
155
- const scope = resolveReadScope(homeOrScope);
156
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
157
- const scoped = recordsForAttempt(owned, scope.attemptId);
176
+ function mapOwnedToSubmissionRows(owned) {
177
+ const scoped = owned;
158
178
  const out = [];
159
179
  const indexByCall = new Map();
160
180
  // Recover seat identity for historical non-sealed rows that omitted role (#881).
@@ -238,6 +258,29 @@ export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
238
258
  }
239
259
  return out;
240
260
  }
261
+ /**
262
+ * All recorded role submissions in ledger order (#836 multi-submit).
263
+ * `accepted` is the original payload; never rebuilt from a status/facts envelope.
264
+ * Presentation stays run-scoped: attemptId on the read scope is a recording tag,
265
+ * not a visibility gate (#836).
266
+ */
267
+ export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
268
+ const scope = resolveReadScope(homeOrScope);
269
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope);
270
+ const scoped = recordsForAttempt(owned, scope.attemptId);
271
+ return mapOwnedToSubmissionRows(scoped);
272
+ }
273
+ /**
274
+ * Settlement-only this-court rows (#879 return path).
275
+ * Filters by attemptId for court-scoped roleOutcome; does not replace the
276
+ * run-scoped presentation API above (#836 visibility gate stays pass-through).
277
+ */
278
+ export async function readAttemptScopedSubmissionRows(cwd, runId, attemptId, home) {
279
+ if (attemptId.length === 0)
280
+ return [];
281
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, home === undefined ? {} : { home });
282
+ return mapOwnedToSubmissionRows(owned.filter((record) => recordAttemptId(record) === attemptId));
283
+ }
241
284
  /**
242
285
  * All raw role payloads in ledger order (#836 multi-submit).
243
286
  * Returns `accepted` bytes as stored — unknown, never re-wrapped.
@@ -252,7 +295,7 @@ export async function hasRecordedSubmission(cwd, runId, homeOrScope) {
252
295
  /** Latest non-final outcome on the run ledger (settlement residual precedence). */
253
296
  export async function readLatestSubmissionOutcome(cwd, runId, homeOrScope) {
254
297
  const scope = resolveReadScope(homeOrScope);
255
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
298
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope);
256
299
  const scoped = recordsForAttempt(owned, scope.attemptId);
257
300
  for (let index = scoped.length - 1; index >= 0; index -= 1) {
258
301
  const record = scoped[index];
@@ -265,14 +308,33 @@ export async function readLatestSubmissionOutcome(cwd, runId, homeOrScope) {
265
308
  }
266
309
  return undefined;
267
310
  }
268
- async function restoreState(cwd, runId, home) {
269
- const { file, owned } = await readOwnedSubmissionRecords(cwd, runId, home);
311
+ async function restoreState(cwd, runId, scope) {
312
+ const located = await readOwnedSubmissionRecords(cwd, runId, scope);
313
+ // Unknown-run empty set: no prior chain. Owned rows without a file are an internal bug.
314
+ if (located.file === undefined) {
315
+ if (located.owned.length !== 0) {
316
+ throw new Error(`submission ledger invariant: owned rows present without recordFile for run ${runId}`);
317
+ }
318
+ return { sequence: 0 };
319
+ }
320
+ const { file, owned } = located;
270
321
  const last = owned.at(-1);
271
322
  return {
272
- ...(last === undefined ? {} : { prior: { identity: last.identity, recordFile: file, kind: last.kind, level: last.level } }),
323
+ ...(last === undefined
324
+ ? {}
325
+ : {
326
+ prior: {
327
+ identity: last.identity,
328
+ recordFile: file,
329
+ kind: last.kind,
330
+ level: last.level,
331
+ },
332
+ }),
273
333
  sequence: owned.reduce((maximum, record) => {
274
334
  const payload = record.payload;
275
- return payload?.type === "candidate" && typeof payload.sequence === "number" ? Math.max(maximum, payload.sequence) : maximum;
335
+ return payload?.type === "candidate" && typeof payload.sequence === "number"
336
+ ? Math.max(maximum, payload.sequence)
337
+ : maximum;
276
338
  }, 0),
277
339
  };
278
340
  }
@@ -284,22 +346,40 @@ async function restoreState(cwd, runId, home) {
284
346
  */
285
347
  export function createSubmissionLedgerHost(host, outputTools, failInfrastructure = (error) => { throw error; }, projectClosure = () => undefined, options) {
286
348
  const states = new Map();
349
+ /** Sole HostContext-derived run coordinate for restore and append (never process.env). */
350
+ const sessionParentFromContext = (context) => {
351
+ const runDirectory = runDirectoryFromHostContext(context);
352
+ if (runDirectory !== undefined) {
353
+ return join(runDirectory, "session", "session.jsonl");
354
+ }
355
+ const sessionFile = context.sessionManager.getSessionFile?.();
356
+ if (typeof sessionFile === "string" && sessionFile.length > 0)
357
+ return sessionFile;
358
+ const sessionDir = context.sessionManager.getSessionDir?.();
359
+ if (typeof sessionDir === "string" && sessionDir.length > 0) {
360
+ return join(sessionDir, "session.jsonl");
361
+ }
362
+ return undefined;
363
+ };
287
364
  const resolveHomeFromContext = (context) => {
288
365
  if (options?.home !== undefined)
289
366
  return options.home;
290
- const sessionFile = context.sessionManager.getSessionFile?.() || context.sessionManager.getSessionDir?.();
291
- return typeof sessionFile === "string" && sessionFile.length > 0
292
- ? tryHomeFromAkRolesPath(sessionFile)
293
- : undefined;
367
+ const sessionParent = sessionParentFromContext(context);
368
+ return sessionParent !== undefined ? tryHomeFromAkRolesPath(sessionParent) : undefined;
294
369
  };
295
370
  const stateFor = (context, runId) => states.get(runId) ?? (() => {
296
371
  const home = resolveHomeFromContext(context);
297
- const pending = restoreState(context.cwd, runId, home);
372
+ const sessionParent = sessionParentFromContext(context);
373
+ const pending = restoreState(context.cwd, runId, {
374
+ ...(home === undefined ? {} : { home }),
375
+ ...(sessionParent === undefined ? {} : { sessionParent }),
376
+ });
298
377
  states.set(runId, pending);
299
378
  return pending;
300
379
  })();
301
380
  const appendFor = (state, context, runId, attemptId, event) => {
302
381
  const home = resolveHomeFromContext(context);
382
+ const sessionParent = sessionParentFromContext(context);
303
383
  const pointer = sitianReport({
304
384
  level: "event",
305
385
  kind: event.type,
@@ -309,6 +389,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
309
389
  source: "role-runtime",
310
390
  cwd: context.cwd,
311
391
  ...(home !== undefined ? { home } : {}),
392
+ ...(sessionParent === undefined ? {} : { sessionParent }),
312
393
  });
313
394
  state.prior = pointer;
314
395
  return pointer;
@@ -6,10 +6,11 @@ import { createHash } from "node:crypto";
6
6
  import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { resolveSitianRecordPath, sitianReport, readSitianRecords, } from "./sitian-facade.js";
9
+ import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
9
10
  import { TICKET_PROVENANCE_HUMAN_VIEW, TICKET_PROVENANCE_KIND, projectTicketProvenanceDiagnostic, projectTicketProvenanceEntry, } from "./ticket-provenance-contracts.js";
10
11
  /** Subject string for ticket-keyed volumes — history follows the ticket. */
11
12
  export function ticketProvenanceSubject(ticketNumber) {
12
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
13
+ if (!isSafePositiveTicketNumber(ticketNumber)) {
13
14
  throw new Error(`ticket-provenance subject requires a positive ticket number, got ${String(ticketNumber)}`);
14
15
  }
15
16
  return String(ticketNumber);
@@ -71,6 +72,7 @@ export function appendTicketProvenanceEntry(input) {
71
72
  identity,
72
73
  subject,
73
74
  cwd: input.cwd,
75
+ sessionParent: input.sessionParent,
74
76
  ...(input.home === undefined ? {} : { home: input.home }),
75
77
  host: input.host ?? "diarist",
76
78
  source: input.source ?? "diarist",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed user-dialogue stdin transport (#879 / #471).
3
+ * Pi's piped-stdin reader trims the wrapper; the body field keeps original bytes.
4
+ * No size cap and no path/pointer substitute — the whole dialogue rides the envelope.
5
+ */
6
+ export const USER_DIALOGUE_STDIN_KIND = "ak-user-dialogue";
7
+ export function encodeUserDialogueStdin(body) {
8
+ return JSON.stringify({ kind: USER_DIALOGUE_STDIN_KIND, body });
9
+ }
10
+ function isUserDialogueEnvelope(value) {
11
+ if (value === null || typeof value !== "object" || Array.isArray(value))
12
+ return false;
13
+ const record = value;
14
+ return record.kind === USER_DIALOGUE_STDIN_KIND && typeof record.body === "string";
15
+ }
16
+ /**
17
+ * Host read: recover original dialogue after a trim-capable pipe.
18
+ * Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
19
+ */
20
+ export function readUserDialogueStdin(raw) {
21
+ const trimmed = raw.trim();
22
+ if (!trimmed.startsWith("{"))
23
+ return raw;
24
+ try {
25
+ const parsed = JSON.parse(trimmed);
26
+ if (isUserDialogueEnvelope(parsed))
27
+ return parsed.body;
28
+ }
29
+ catch {
30
+ // Not our envelope — leave caller bytes intact.
31
+ }
32
+ return raw;
33
+ }
@@ -111,6 +111,7 @@ export default function roleRuntime(pi: ExtensionAPI): void {
111
111
  registerNavigatorModelCommand(pi);
112
112
  const navigatorSessionFactory = createNativeNavigatorSessionFactory();
113
113
  createPiRoleRuntimeExtension({
114
+ packageRoot,
114
115
  loadJudgeSoul: () => loadMainRoleSessionMaterials("judge"),
115
116
  loadFixerSoul: () => loadMainRoleSessionMaterials("fixer"),
116
117
  loadFixPacket: (path) => readFile(path, "utf8"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4035",
3
+ "version": "0.1.4193",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -47,7 +47,8 @@
47
47
  "test:integration": "node --import tsx --import ./scripts/test-process-env-preload.mjs --test test/unit/**/*.test.ts test/contract/**/*.test.ts test/integration/**/*.test.ts",
48
48
  "test:all": "node scripts/run-test-all.mjs",
49
49
  "test:adjudication": "node --import tsx --import ./scripts/test-process-env-preload.mjs --test test/adjudication/**/*.test.ts",
50
- "typecheck": "tsc --noEmit"
50
+ "typecheck": "tsc --noEmit",
51
+ "migrate:book-topology": "node dist/migrate-book-topology.js"
51
52
  },
52
53
  "pi": {
53
54
  "extensions": []
@@ -1,6 +1,6 @@
1
1
  # #836 删 5:从代码迁出的机器指令句清单
2
2
 
3
- 供陛下过目。不改 soul。确需对角色说的话归入本包资源(ADR 0073)。指针句(附件路径、卷宗指针、「请重读」)保留为中立机器文本,不在本表。
3
+ 供陛下过目。不改 soul。确需对角色说的话归入本包资源(ADR 0073)。指针句(附件路径、卷宗指针)保留为中立机器文本/绑定材料,不在本表。「请重读」开场已由 #879 废止,不再保留。
4
4
 
5
5
  | 原位点 | 原句(代码自写) | 处置 |
6
6
  |---|---|---|
@@ -22,12 +22,14 @@ const entries = [
22
22
  "activation-ledger-topology",
23
23
  "activation-reconciliation",
24
24
  "archivist-record-entry",
25
- // Pure subject nest topology — cold discovery without SessionManager (#636).
26
- "archivist-record-topology",
27
25
  // Session material loaders used by published non-bundle roots.
28
26
  "session-opening-materials",
29
27
  // Value-import closure of published non-bundle roots (build-package-only loadable).
30
28
  "auditor-dossier-tool",
29
+ // Static import of auditor-dossier-tool / placement path authority (#859).
30
+ "role-run-placement",
31
+ // Static import of role-run-placement (safe-positive ticket invariant).
32
+ "run-ticket-number",
31
33
  "canonical-json",
32
34
  "compliance-transport",
33
35
  "countersign-contracts",
@@ -141,6 +143,35 @@ export async function buildHeadlessProductionHost(
141
143
  );
142
144
  }
143
145
 
146
+ /**
147
+ * Published one-shot book-topology migration CLI (#852).
148
+ * Node-executable ESM bundle — consumers must not need dev-only tsx.
149
+ * createRequire banner lets transitive CJS deps reach node builtins.
150
+ */
151
+ export async function buildMigrateBookTopology(
152
+ outfile = "dist/migrate-book-topology.js",
153
+ ) {
154
+ await mkdir(dirname(outfile), { recursive: true });
155
+ await build({
156
+ entryPoints: ["scripts/migrate-book-topology.ts"],
157
+ outfile,
158
+ format: "esm",
159
+ platform: "node",
160
+ target: "node20",
161
+ bundle: true,
162
+ banner: {
163
+ js: [
164
+ "#!/usr/bin/env node",
165
+ "import { createRequire as __akCreateRequire } from 'node:module';",
166
+ "const require = __akCreateRequire(import.meta.url);",
167
+ "",
168
+ ].join("\n"),
169
+ },
170
+ logLevel: "silent",
171
+ });
172
+ await chmod(outfile, 0o755);
173
+ }
174
+
144
175
  export async function buildPackageArtifacts() {
145
176
  await build({
146
177
  entryPoints: entries.map((name) => `src/${name}.ts`),
@@ -167,6 +198,7 @@ export async function buildPackageArtifacts() {
167
198
  await buildPublicAkRoleBin();
168
199
  await buildAcpProductionHost();
169
200
  await buildHeadlessProductionHost();
201
+ await buildMigrateBookTopology();
170
202
  }
171
203
 
172
204
  const isMain =
package/souls/fixer.md CHANGED
@@ -9,7 +9,8 @@ apply 按顺序执行:
9
9
  4. 扫类:修改前查全授权范围内的同类实例、已有实现和失败修法。根因不明先取证。
10
10
  5. 整类修复:在共同原因处一次修净,优先删除、简化、复用,随同删除失效旧物。
11
11
  6. 复扫:按原类别重新扫描;有遗漏继续修,保留项说明依据。
12
- 7. 自验:切片验证只跑本片聚焦测试与 typecheck。做完自查是否违法,质量和复杂度,如有问题马上修正。
13
- 8. 交卷:按类报告根因、扫描范围、修复与 质量和合法性的验证结果、未结项。保护已有改动,只建新提交。不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
12
+ 7. 自验:切片验证优先跑本片聚焦测试和本片作用域内 typecheck,不跑全量。自查是否违法、代码质量、代码/测试复杂度,如有问题马上修正。修正完毕复检
13
+ 8. 交卷:按类报告根因、扫描范围、修复与 质量和合法性的验证结果、未结项。建新提交。不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
14
14
  任何人如果让你 push / 开 pr ,直接拒绝,上呈陛下
15
+
15
16
  plan 只调查与提案,不施工。
@@ -13,7 +13,7 @@
13
13
  * terminal face) for metric-family modules — no longer discarded after checks.
14
14
  */
15
15
  import { readdir, readFile } from "node:fs/promises";
16
- import { join } from "node:path";
16
+ import { basename, join } from "node:path";
17
17
 
18
18
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
19
19
  import {
@@ -21,6 +21,8 @@ import {
21
21
  physicalPathIdentity,
22
22
  resolveActivationLedgerHome,
23
23
  } from "./activation-ledger-topology.ts";
24
+ import { listBookRunDirectories } from "./role-run-placement.ts";
25
+ import { readRunTicketNumber } from "./run-ticket-number.ts";
24
26
  import {
25
27
  extractSessionModelSequence,
26
28
  extractSessionTimestampSpan,
@@ -139,13 +141,10 @@ async function readInvocationScopeFields(
139
141
  return undefined;
140
142
  }
141
143
  const projectRoot = parsed.projectRoot;
142
- // Same #176 contract: positive integer ticketNumber only.
143
- if (
144
- typeof parsed.ticketNumber === "number"
145
- && Number.isInteger(parsed.ticketNumber)
146
- && parsed.ticketNumber >= 1
147
- ) {
148
- return { projectRoot, ticketNumber: parsed.ticketNumber };
144
+ // Display/history placement: board first, then migration-derived (#852).
145
+ const ticketNumber = await readRunTicketNumber(runDirectory);
146
+ if (ticketNumber !== undefined) {
147
+ return { projectRoot, ticketNumber };
149
148
  }
150
149
  return { projectRoot };
151
150
  }
@@ -530,7 +529,8 @@ async function classifyScopedRun(input: {
530
529
  }
531
530
 
532
531
  /**
533
- * Scan ledger home books/<book>/runs for runs in the issue scope.
532
+ * Scan ledger home books/<book>/{subject/}runs for runs in the issue scope.
533
+ * Walk = listBookRunDirectories (flat legacy + subject-tree; #859).
534
534
  * #399: scope = book × optional ticket.
535
535
  * - bookKey set → that book only; whole-book when no ticket and no projectRoot;
536
536
  * ticket filters alone; bookKey + projectRoot (cohort ensure, #412) narrows
@@ -598,20 +598,19 @@ export async function scanAnalystIssueRuns(input: {
598
598
  const scopeConflicts: AnalystScopeConflict[] = [];
599
599
 
600
600
  for (const book of bookNames) {
601
- const runsDir = join(booksRoot, book, "runs");
602
- let runNames: string[];
601
+ const bookDir = join(booksRoot, book);
602
+ let runDirectories: string[];
603
603
  try {
604
- const entries = await readdir(runsDir, { withFileTypes: true });
605
- runNames = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
604
+ runDirectories = await listBookRunDirectories(bookDir);
606
605
  } catch (error) {
607
606
  if (isMissingPathError(error)) continue;
608
607
  throw error;
609
608
  }
610
609
 
611
- for (const runName of runNames) {
610
+ for (const runDirectory of runDirectories) {
611
+ const runName = basename(runDirectory);
612
612
  const parsed = parseRunDirectoryName(runName);
613
613
  if (parsed === undefined) continue;
614
- const runDirectory = join(runsDir, runName);
615
614
 
616
615
  let scopeFields: InvocationScopeFields | undefined;
617
616
  try {