@akagilnc/pi-workflow-roles 0.1.5797 → 0.1.5820

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 (80) hide show
  1. package/dist/acp-host/production-host.js +464 -355
  2. package/dist/acp-host/role-turn-host.js +484 -0
  3. package/dist/activation-ledger-session.js +105 -0
  4. package/dist/activation-ledger.js +8 -0
  5. package/dist/activation-trace.js +33 -0
  6. package/dist/audit-escalation.js +7 -0
  7. package/dist/audited-run-identity.js +21 -0
  8. package/dist/auditor-role.js +7 -0
  9. package/dist/book-topology-migration-placement.js +404 -0
  10. package/dist/book-topology-migration.js +125 -0
  11. package/dist/book-topology-migrators-t11.js +677 -0
  12. package/dist/book-topology-mixed-volume-migrators.js +322 -0
  13. package/dist/book-topology-partition-migrators.js +23 -0
  14. package/dist/book-topology-record-class-migrators.js +671 -0
  15. package/dist/book-topology-runs-migrator.js +272 -0
  16. package/dist/collector-role.js +109 -0
  17. package/dist/compliance-transport.js +5 -3
  18. package/dist/countersign-role.js +17 -0
  19. package/dist/diarist-role.js +8 -0
  20. package/dist/diarist.js +15 -0
  21. package/dist/doctor-role.js +80 -0
  22. package/dist/engine-detour-tool.js +51 -37
  23. package/dist/engine-detour.js +22 -11
  24. package/dist/factory-board.js +1478 -0
  25. package/dist/fixer-bash-seatbelt.js +13 -0
  26. package/dist/gatekeeper-role.js +1 -2
  27. package/dist/gh-api-runner.js +81 -0
  28. package/dist/gleaner-left-role.js +8 -0
  29. package/dist/headless-host/production-host.js +464 -355
  30. package/dist/headless-host/role-turn-host.js +691 -0
  31. package/dist/human-format.js +67 -0
  32. package/dist/inspector-role.js +12 -0
  33. package/dist/judge-recording-anti-forge.js +39 -0
  34. package/dist/judge-role.js +21 -3
  35. package/dist/merger-role.js +57 -0
  36. package/dist/migrate-book-topology.js +0 -2
  37. package/dist/navigator-role.js +6 -0
  38. package/dist/navigator-work-context.js +72 -0
  39. package/dist/notary-role.js +113 -0
  40. package/dist/oauth-keepalive.js +149 -0
  41. package/dist/package-contracts/terminating-infrastructure.js +0 -35
  42. package/dist/pi/adapter.js +272 -0
  43. package/dist/public-cli/cli.js +9 -3
  44. package/dist/public-cli/instruction-seat-run.js +163 -44
  45. package/dist/public-cli/main.js +241 -123
  46. package/dist/report-ticket-tool.js +24 -13
  47. package/dist/reviewer-role.js +33 -0
  48. package/dist/role-envelope.js +707 -0
  49. package/dist/role-runtime-dependencies.js +57 -0
  50. package/dist/role-runtime.js +1476 -0
  51. package/dist/secretariat-role.js +5 -0
  52. package/dist/stderr-jsonl.js +26 -0
  53. package/dist/submission-correctable-error.js +2 -6
  54. package/dist/submission-errors.js +0 -30
  55. package/dist/submission-gate.js +61 -54
  56. package/dist/submission-ledger.js +105 -98
  57. package/dist/ticket-snapshot.js +486 -0
  58. package/dist/ticket-trajectory.js +843 -0
  59. package/dist/worker-role.js +184 -0
  60. package/dist/worker-submission-gates.js +333 -0
  61. package/package.json +1 -1
  62. package/resources/engine-dispatch.md +6 -6
  63. package/resources/engines/agy.md +1 -1
  64. package/src/compliance-transport.ts +7 -3
  65. package/src/engine-detour-tool.ts +88 -57
  66. package/src/engine-detour.ts +31 -14
  67. package/src/gatekeeper-role.ts +1 -15
  68. package/src/host-contracts.ts +9 -12
  69. package/src/judge-role.ts +30 -7
  70. package/src/package-contracts/terminating-infrastructure.ts +8 -81
  71. package/src/pi/adapter.ts +0 -1
  72. package/src/public-cli/cli.ts +21 -14
  73. package/src/public-cli/instruction-seat-run.ts +227 -44
  74. package/src/report-ticket-tool.ts +27 -13
  75. package/src/role-envelope.ts +35 -35
  76. package/src/role-runtime.ts +63 -56
  77. package/src/submission-correctable-error.ts +1 -6
  78. package/src/submission-errors.ts +0 -34
  79. package/src/submission-gate.ts +71 -56
  80. package/src/submission-ledger.ts +109 -104
@@ -0,0 +1,272 @@
1
+ /**
2
+ * #865 T9: migrate retained runs into ticket-scoped or unbound placement.
3
+ * Inventory covers legacy flat `runs/`, existing `<ticket>/runs/`, and
4
+ * `unbound/runs/`. Attribution reuses the shared migrating-run helper;
5
+ * already-canonical ticket trees copy as-is. Unbound leaves that already
6
+ * hold a board typed ticketNumber place under that ticket (#863 stock).
7
+ */
8
+ import { cp, mkdir, rename, readdir, stat, writeFile } from "node:fs/promises";
9
+ import { basename, dirname, join, relative, sep } from "node:path";
10
+ import { destinationRunDirectory, isTicketNumberString, listBackupRunLeaves, listMigrationBookKeys, resolveMigratingRunTicket, } from "./book-topology-migration-placement.js";
11
+ import { reconcileMigrationPartition, } from "./book-topology-migration.js";
12
+ import { listBookRunDirectories } from "./role-run-placement.js";
13
+ import { rewriteRoleRunDurablePages, } from "./role-run-relocation.js";
14
+ import { renderCurrentSync } from "./run-dossier.js";
15
+ import { MIGRATION_TICKET_DERIVATION_PAGE, readMigrationBoardTicketNumber, } from "./run-ticket-number.js";
16
+ import { isEnoent } from "./unknown-value.js";
17
+ const RUNS_PARTITION = "runs";
18
+ const RUN_LEAF = /^([A-Za-z0-9][A-Za-z0-9._-]*)@([A-Za-z][A-Za-z0-9_-]*)$/;
19
+ function parseRunLeaf(name) {
20
+ const match = RUN_LEAF.exec(name);
21
+ if (match === null)
22
+ return undefined;
23
+ return { runId: match[1], role: match[2] };
24
+ }
25
+ async function pathExists(path) {
26
+ try {
27
+ await stat(path);
28
+ return true;
29
+ }
30
+ catch (error) {
31
+ if (isEnoent(error))
32
+ return false;
33
+ throw error;
34
+ }
35
+ }
36
+ async function writeDerivationPage(targetRunDirectory, derivation) {
37
+ const page = {
38
+ ticketNumber: derivation.ticketNumber,
39
+ derivation: "worktree-path-basename",
40
+ source: {
41
+ page: derivation.sourcePage,
42
+ field: "projectRoot",
43
+ path: derivation.projectRoot,
44
+ basename: basename(derivation.projectRoot),
45
+ },
46
+ };
47
+ await writeFile(join(targetRunDirectory, MIGRATION_TICKET_DERIVATION_PAGE), `${JSON.stringify(page, null, 2)}\n`, "utf8");
48
+ }
49
+ async function copyRunTree(source, target, isDirectory) {
50
+ await mkdir(dirname(target), { recursive: true });
51
+ if (isDirectory) {
52
+ await cp(source, target, { recursive: true, preserveTimestamps: true });
53
+ return;
54
+ }
55
+ await cp(source, target, { preserveTimestamps: true });
56
+ }
57
+ async function planBookMoves(backupBooksDirectory, booksDirectory, bookKey) {
58
+ const backupBook = join(backupBooksDirectory, bookKey);
59
+ const claimed = new Map();
60
+ const planned = [];
61
+ for (const leaf of await listBackupRunLeaves(backupBook)) {
62
+ const sourceIdentity = relative(backupBooksDirectory, leaf.sourcePath)
63
+ .split(sep)
64
+ .join("/");
65
+ const parsed = leaf.isDirectory ? parseRunLeaf(leaf.leafName) : undefined;
66
+ let targetPath;
67
+ let disposition;
68
+ let historicalRunDirectory;
69
+ let derivation;
70
+ if (leaf.layout === "issues") {
71
+ // issues/<ticket>/runs/<leaf> → <ticket>/runs/<leaf>; conflict refuses below.
72
+ const parts = leaf.relativePath.replaceAll("\\", "/").split("/");
73
+ const ticketStr = parts[1];
74
+ if (ticketStr !== undefined && isTicketNumberString(ticketStr)) {
75
+ targetPath = join(booksDirectory, bookKey, ticketStr, "runs", leaf.leafName);
76
+ disposition = "placed";
77
+ historicalRunDirectory = join(booksDirectory, bookKey, "issues", ticketStr, "runs", leaf.leafName);
78
+ }
79
+ else {
80
+ targetPath = join(booksDirectory, bookKey, "unbound", "runs", leaf.leafName);
81
+ disposition = "unbound";
82
+ historicalRunDirectory = join(booksDirectory, bookKey, leaf.relativePath);
83
+ }
84
+ derivation = undefined;
85
+ }
86
+ else if (leaf.layout === "unbound") {
87
+ // #863: board typed ticket on an unbound leaf → place under that ticket.
88
+ // No board ticket → stay unbound. Never invent from prose or derivation.
89
+ const boardTicket = leaf.isDirectory
90
+ ? await readMigrationBoardTicketNumber(leaf.sourcePath)
91
+ : undefined;
92
+ if (parsed !== undefined && boardTicket !== undefined) {
93
+ targetPath = destinationRunDirectory(booksDirectory, bookKey, boardTicket, parsed.runId, parsed.role);
94
+ disposition = "placed";
95
+ historicalRunDirectory = join(booksDirectory, bookKey, "unbound", "runs", leaf.leafName);
96
+ }
97
+ else {
98
+ targetPath = join(booksDirectory, bookKey, leaf.relativePath);
99
+ disposition = "unbound";
100
+ historicalRunDirectory = undefined;
101
+ }
102
+ derivation = undefined;
103
+ }
104
+ else if (leaf.layout !== "flat") {
105
+ // Already-canonical `<ticket>/runs/` — copy as-is.
106
+ targetPath = join(booksDirectory, bookKey, leaf.relativePath);
107
+ disposition = "placed";
108
+ historicalRunDirectory = undefined;
109
+ derivation = undefined;
110
+ }
111
+ else if (parsed === undefined) {
112
+ targetPath = join(booksDirectory, bookKey, "unbound", "runs", leaf.leafName);
113
+ disposition = "unbound";
114
+ historicalRunDirectory = join(booksDirectory, bookKey, "runs", leaf.leafName);
115
+ derivation = undefined;
116
+ }
117
+ else {
118
+ const ticket = await resolveMigratingRunTicket(leaf.sourcePath);
119
+ targetPath = destinationRunDirectory(booksDirectory, bookKey, ticket.ticketNumber, parsed.runId, parsed.role);
120
+ disposition = ticket.ticketNumber === undefined ? "unbound" : "placed";
121
+ historicalRunDirectory = join(booksDirectory, bookKey, "runs", leaf.leafName);
122
+ derivation =
123
+ ticket.derivation?.method === "project-root-basename"
124
+ && ticket.ticketNumber !== undefined
125
+ ? {
126
+ ticketNumber: ticket.ticketNumber,
127
+ projectRoot: ticket.derivation.source,
128
+ sourcePage: ticket.derivation.sourcePage,
129
+ }
130
+ : undefined;
131
+ }
132
+ const prior = claimed.get(targetPath);
133
+ if (prior !== undefined) {
134
+ throw new Error(`book topology migration refuses to overwrite ${targetPath} (already claimed by ${prior}) with ${leaf.sourcePath}`);
135
+ }
136
+ claimed.set(targetPath, leaf.sourcePath);
137
+ planned.push({
138
+ sourcePath: leaf.sourcePath,
139
+ sourceIdentity,
140
+ isDirectory: leaf.isDirectory,
141
+ disposition,
142
+ targetPath,
143
+ historicalRunDirectory,
144
+ derivation,
145
+ });
146
+ }
147
+ return planned;
148
+ }
149
+ export const bookTopologyRunsMigrator = {
150
+ partition: RUNS_PARTITION,
151
+ async migrate(context) {
152
+ const outcomes = [];
153
+ const { backupBooksDirectory, booksDirectory } = context;
154
+ for (const bookKey of await listMigrationBookKeys(backupBooksDirectory)) {
155
+ const planned = await planBookMoves(backupBooksDirectory, booksDirectory, bookKey);
156
+ const crossRunRewrites = planned
157
+ .filter((move) => move.isDirectory && move.historicalRunDirectory !== undefined)
158
+ .map((move) => ({
159
+ oldRunDirectory: move.historicalRunDirectory,
160
+ newRunDirectory: move.targetPath,
161
+ }));
162
+ for (const move of planned) {
163
+ if (await pathExists(move.targetPath)) {
164
+ throw new Error(`book topology migration refuses to overwrite ${move.targetPath} with ${move.sourcePath}`);
165
+ }
166
+ await copyRunTree(move.sourcePath, move.targetPath, move.isDirectory);
167
+ if (move.isDirectory) {
168
+ await rewriteRoleRunDurablePages({
169
+ pagesDirectory: move.targetPath,
170
+ oldRunDirectory: move.historicalRunDirectory ?? move.targetPath,
171
+ newRunDirectory: move.targetPath,
172
+ crossRunRewrites,
173
+ });
174
+ }
175
+ if (move.isDirectory && move.derivation !== undefined) {
176
+ await writeDerivationPage(move.targetPath, move.derivation);
177
+ }
178
+ outcomes.push({
179
+ disposition: move.disposition,
180
+ source: move.sourceIdentity,
181
+ });
182
+ }
183
+ }
184
+ return reconcileMigrationPartition(RUNS_PARTITION, "entries", outcomes);
185
+ },
186
+ };
187
+ /**
188
+ * Live in-place repair for already-migrated trees (#863 stock): plan the full
189
+ * board-bound unbound→ticket batch first, refuse any overwrite before mutation,
190
+ * rewrite durable pages (including batch `crossRunRewrites`) while sources still
191
+ * sit at unbound, then rename. Parse/write failures therefore leave sources in
192
+ * place so a retry can finish the same closure; rename-only retries still find
193
+ * remaining unbound sources. Leaves without a board ticket stay unbound.
194
+ */
195
+ export async function relocateBoardBoundUnboundRunsInBook(bookDirectory) {
196
+ const bookKey = basename(bookDirectory);
197
+ const booksDirectory = dirname(bookDirectory);
198
+ const unboundRuns = join(bookDirectory, "unbound", "runs");
199
+ let entries;
200
+ try {
201
+ entries = await readdir(unboundRuns, { withFileTypes: true });
202
+ }
203
+ catch (error) {
204
+ if (isEnoent(error))
205
+ return [];
206
+ throw error;
207
+ }
208
+ const planned = [];
209
+ for (const entry of entries) {
210
+ if (!entry.isDirectory())
211
+ continue;
212
+ const parsed = parseRunLeaf(entry.name);
213
+ if (parsed === undefined)
214
+ continue;
215
+ const sourcePath = join(unboundRuns, entry.name);
216
+ const boardTicket = await readMigrationBoardTicketNumber(sourcePath);
217
+ if (boardTicket === undefined)
218
+ continue;
219
+ const targetPath = destinationRunDirectory(booksDirectory, bookKey, boardTicket, parsed.runId, parsed.role);
220
+ planned.push({
221
+ sourcePath,
222
+ targetPath,
223
+ ticketNumber: boardTicket,
224
+ });
225
+ }
226
+ if (planned.length === 0)
227
+ return [];
228
+ for (const move of planned) {
229
+ if (await pathExists(move.targetPath)) {
230
+ throw new Error(`board-bound unbound relocate refuses to overwrite ${move.targetPath} with ${move.sourcePath}`);
231
+ }
232
+ }
233
+ const crossRunRewrites = planned.map((move) => ({
234
+ oldRunDirectory: move.sourcePath,
235
+ newRunDirectory: move.targetPath,
236
+ }));
237
+ const relocatedBySource = new Map(planned.map((move) => [move.sourcePath, move]));
238
+ // Rewrite before any rename so durable-page failures remain retryable from
239
+ // the same unbound sources (failure-honesty: no relocatedCount=0 whitewash).
240
+ for (const runDir of await listBookRunDirectories(bookDirectory)) {
241
+ const move = relocatedBySource.get(runDir);
242
+ await rewriteRoleRunDurablePages({
243
+ pagesDirectory: runDir,
244
+ oldRunDirectory: move?.sourcePath ?? runDir,
245
+ newRunDirectory: move?.targetPath ?? runDir,
246
+ crossRunRewrites,
247
+ });
248
+ }
249
+ for (const move of planned) {
250
+ await mkdir(dirname(move.targetPath), { recursive: true });
251
+ await rename(move.sourcePath, move.targetPath);
252
+ // Same live relocate seam: derived host.original follows the new run directory.
253
+ renderCurrentSync(move.targetPath);
254
+ }
255
+ return planned.map((move) => ({
256
+ from: move.sourcePath,
257
+ to: move.targetPath,
258
+ ticketNumber: move.ticketNumber,
259
+ }));
260
+ }
261
+ /**
262
+ * Walk every book under `booksDirectory` and relocate board-bound unbound
263
+ * runs in place without the whole-books topology migration's in-flight gate.
264
+ */
265
+ export async function relocateBoardBoundUnboundRunsInBooks(booksDirectory) {
266
+ const relocated = [];
267
+ for (const bookKey of await listMigrationBookKeys(booksDirectory)) {
268
+ const batch = await relocateBoardBoundUnboundRunsInBook(join(booksDirectory, bookKey));
269
+ relocated.push(...batch);
270
+ }
271
+ return relocated;
272
+ }
@@ -0,0 +1,109 @@
1
+ import { registerFiledSubmissionTool } from "./filed-submission.js";
2
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.js";
3
+ import { parseCollectorPrNumber, parseCollectorRepository, } from "./collector-config.js";
4
+ import { COLLECTOR_OUTPUT_TOOL, } from "./package-contracts/collector-output.js";
5
+ export { COLLECTOR_OUTPUT_TOOL };
6
+ export const COLLECTOR_REQUIRED_TOOLS = [COLLECTOR_OUTPUT_TOOL];
7
+ /** Host tools that grant construction seat privileges — blocked for 收证席 (ADR 0064). */
8
+ export const COLLECTOR_CONSTRUCTION_TOOLS = Object.freeze(["write", "edit"]);
9
+ /** Envelope-owned transport flags (ADR 0018 / #676 E). */
10
+ export const COLLECTOR_TRANSPORT_FLAGS = Object.freeze([
11
+ Object.freeze({
12
+ name: "ak-collector-repo",
13
+ definition: Object.freeze({
14
+ description: "GitHub owner/repo target for Collector (github.com only; conservative ASCII grammar).",
15
+ type: "string",
16
+ }),
17
+ }),
18
+ Object.freeze({
19
+ name: "ak-collector-pr",
20
+ definition: Object.freeze({
21
+ description: "Optional positive safe-integer pull request number for Collector. Omit when the role will locate the PR via host CLI.",
22
+ type: "string",
23
+ }),
24
+ }),
25
+ Object.freeze({
26
+ name: "ak-collector-request-manifest",
27
+ definition: Object.freeze({
28
+ description: "Caller path to a request manifest JSON file (passed as-is; package does not read or validate).",
29
+ type: "string",
30
+ }),
31
+ }),
32
+ ]);
33
+ function buildMethodContext(activation) {
34
+ const lines = [
35
+ "<collector_method>",
36
+ `host: github.com`,
37
+ `repository: ${activation.repository.canonical}`,
38
+ `prNumber: ${activation.prNumber === undefined ? "未绑定" : String(activation.prNumber)}`,
39
+ ...(activation.requestManifestPath === undefined
40
+ ? []
41
+ : [`requestManifestPath: ${activation.requestManifestPath}`]),
42
+ "</collector_method>",
43
+ ];
44
+ return lines.join("\n");
45
+ }
46
+ /**
47
+ * Shared envelope installs the submission tool and material callback after lifecycle gates.
48
+ * Role module does not self-hook events, setActiveTools, or mode/fork checks (ADR 0018 / #676 E).
49
+ */
50
+ export function createCollectorRoleRuntime(pi, dependencies) {
51
+ let toolsRegistered = false;
52
+ return {
53
+ async activate(_ctx) {
54
+ const soul = (await dependencies.loadSoul()).trim();
55
+ if (soul.length === 0)
56
+ throw new Error("Collector soul is empty");
57
+ const repoFlag = pi.getFlag("ak-collector-repo");
58
+ const prFlag = pi.getFlag("ak-collector-pr");
59
+ const requestManifestFlag = pi.getFlag("ak-collector-request-manifest");
60
+ if (typeof repoFlag !== "string" || repoFlag.trim().length === 0) {
61
+ throw new Error("Collector requires --ak-collector-repo");
62
+ }
63
+ const repository = parseCollectorRepository(repoFlag);
64
+ let prNumber;
65
+ if (typeof prFlag === "string" && prFlag.trim().length > 0) {
66
+ prNumber = parseCollectorPrNumber(prFlag);
67
+ }
68
+ else if (typeof prFlag === "number") {
69
+ prNumber = parseCollectorPrNumber(prFlag);
70
+ }
71
+ // #1165: opaque caller path — keep spaces/"" when the flag was provided.
72
+ const requestManifestPath = typeof requestManifestFlag === "string" ? requestManifestFlag : undefined;
73
+ return {
74
+ soul,
75
+ repository,
76
+ prNumber,
77
+ ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
78
+ };
79
+ },
80
+ assembleMaterials(activation, baseSystemPrompt) {
81
+ return [
82
+ baseSystemPrompt,
83
+ "",
84
+ "<collector_soul>",
85
+ activation.soul,
86
+ "</collector_soul>",
87
+ "",
88
+ buildMethodContext(activation),
89
+ ].join("\n");
90
+ },
91
+ onToolCall(_activation, event) {
92
+ if (COLLECTOR_CONSTRUCTION_TOOLS.includes(event.toolName)) {
93
+ return {
94
+ block: true,
95
+ reason: `通进司为收证席,禁用施工工具 ${event.toolName}`,
96
+ };
97
+ }
98
+ return undefined;
99
+ },
100
+ registerBusinessTools(getActivation) {
101
+ if (toolsRegistered)
102
+ return;
103
+ toolsRegistered = true;
104
+ registerFiledSubmissionTool(pi, roleSubmissionDeclaration("collector"), {
105
+ readyError: () => (getActivation() === undefined ? "通进司未激活" : undefined),
106
+ });
107
+ },
108
+ };
109
+ }
@@ -53,9 +53,11 @@ async function projectAuditorTerminal(summoned) {
53
53
  const outcome = summoned.terminal?.roleOutcome;
54
54
  if (outcome === void 0) {
55
55
  const detail = summoned.stderr?.trim();
56
- throw new Error(
57
- detail && detail.length > 0 ? `Auditor public summon produced no terminal (exit ${summoned.exitCode}: ${detail})` : `Auditor public summon produced no terminal (exit ${summoned.exitCode})`
58
- );
56
+ return {
57
+ status: "transport_failure",
58
+ diagnostic: detail && detail.length > 0 ? `Auditor public summon produced no terminal (exit ${summoned.exitCode}: ${detail})` : `Auditor public summon produced no terminal (exit ${summoned.exitCode})`,
59
+ ...summoned.terminal === void 0 ? {} : { terminal: summoned.terminal }
60
+ };
59
61
  }
60
62
  const usage = await usageFromSummonedSession(summoned);
61
63
  if (outcome.kind === "no_receipt") {
@@ -0,0 +1,17 @@
1
+ import { courtReviewSubmissionSchema } from "./review-submission.js";
2
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.js";
3
+ import { COUNTERSIGN_OUTPUT_TOOL_NAME, validateRecordedCountersignOutput, } from "./countersign-contracts.js";
4
+ export { COUNTERSIGN_OUTPUT_TOOL_NAME } from "./countersign-contracts.js";
5
+ // #836 r16 class 1: fix/note/decisionGate are LLM/human-read narrative content —
6
+ // no code branches on their length or nested presence. `status` alone is the
7
+ // machine discriminator the queue reads.
8
+ // #1055: the three-state field is an enum+required host generation constraint.
9
+ // #1195: clauses is generation-required on this seat; package execute still does
10
+ // not reject a missing table (ADR 0055).
11
+ /** 给事中票庭审读五问的交卷形状(ADR 0074);形状指引,非 schema 闸。 */
12
+ export const countersignVerdictSchema = courtReviewSubmissionSchema;
13
+ /**
14
+ * 决定工具规格。生命周期装配(注册、activate、prompt 注入、singleton 检查、
15
+ * terminate)归注册信封 owner——src/role-runtime.ts(ADR 0018 / #572 R2 判词)。
16
+ */
17
+ export const COUNTERSIGN_TOOL_SPEC = roleSubmissionDeclaration("countersign");
@@ -0,0 +1,8 @@
1
+ import { DIARIST_OUTPUT_TOOL_NAME, diaristOutputSchema, validateRecordedDiaristOutput, } from "./diarist-contracts.js";
2
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.js";
3
+ export { DIARIST_OUTPUT_TOOL_NAME, diaristOutputSchema, } from "./diarist-contracts.js";
4
+ export { validateRecordedDiaristOutput };
5
+ /**
6
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
7
+ */
8
+ export const DIARIST_TOOL_SPEC = roleSubmissionDeclaration("diarist");
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 起居郎 volume commit — ADR 0075 `diarist-is-role` / #901 / #1090.
3
+ * LLM submits bounds; mechanical layer appends a projection commit from
4
+ * session volumes (pure append, no cross-submit dedupe). No human view.
5
+ * No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
6
+ */
7
+ import { reprojectTicketProvenanceBatch } from "./ticket-provenance.js";
8
+ /**
9
+ * Append per-ticket diary projections from LLM-submitted bounds (#1090 / #1107).
10
+ * Shape projection of sessions is the caller's job (accept hook);
11
+ * this seam only copies source facts into each ticket's volume.
12
+ */
13
+ export async function commitDiaristProjection(input) {
14
+ await reprojectTicketProvenanceBatch(input);
15
+ }
@@ -0,0 +1,80 @@
1
+ import { registerFiledSubmissionTool } from "./filed-submission.js";
2
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.js";
3
+ import { ComplianceResponseRetentionError } from "./compliance-transport.js";
4
+ import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.js";
5
+ import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, validateDoctorOutput } from "./doctor-contracts.js";
6
+ import { sitianReport } from "./sitian-facade.js";
7
+ import { errorText } from "./unknown-value.js";
8
+ export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
9
+ export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/<n>/runs directory", type: "string" } };
10
+ function appendCandidate(ctx, data) {
11
+ try {
12
+ sitianReport({
13
+ level: "event",
14
+ kind: "candidate",
15
+ cwd: ctx.cwd,
16
+ sessionParent: ctx.sessionManager.getSessionFile(),
17
+ payload: data,
18
+ source: "doctor-role",
19
+ });
20
+ }
21
+ catch (error) {
22
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${errorText(error)}`, { cause: error });
23
+ }
24
+ const append = ctx.sessionManager.appendCustomEntry;
25
+ if (typeof append === "function") {
26
+ try {
27
+ append.call(ctx.sessionManager, DOCTOR_CANDIDATE_ENTRY_TYPE, data);
28
+ }
29
+ catch (error) {
30
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${errorText(error)}`, { cause: error });
31
+ }
32
+ }
33
+ }
34
+ export function createDoctorRoleRuntime(pi, dependencies, host) {
35
+ let activation;
36
+ let registered = false;
37
+ pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
38
+ return { async activate() {
39
+ const path = pi.getFlag(DOCTOR_CASE_FLAG.name);
40
+ if (typeof path !== "string" || !path.trim())
41
+ throw new Error("Doctor requires --ak-doctor-case");
42
+ const soul = (await dependencies.loadSoul()).trim();
43
+ if (!soul)
44
+ throw new Error("Doctor soul is empty");
45
+ const patient = await dependencies.loadCase(path);
46
+ activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
47
+ if (!registered) {
48
+ registered = true;
49
+ pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id, params) { if (!activation)
50
+ throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text", text: JSON.stringify(details) }], details }; } });
51
+ registerFiledSubmissionTool(pi, roleSubmissionDeclaration("doctor"), {
52
+ readyError: () => (activation === undefined ? "太医署未激活" : undefined),
53
+ beforeAccept: async ({ toolCallId, parameters, ctx }) => {
54
+ if (activation === undefined)
55
+ throw new Error("太医署未激活");
56
+ const active = activation;
57
+ const testimony = validateDoctorOutput(parameters, active.patient, active.store);
58
+ try {
59
+ appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity });
60
+ }
61
+ catch (error) {
62
+ host.failInfrastructure(error, ctx, toolCallId);
63
+ }
64
+ return testimony;
65
+ },
66
+ });
67
+ pi.on("before_agent_start", (event) => { if (!activation)
68
+ throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; });
69
+ }
70
+ const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME];
71
+ const names = pi.getAllTools().map((tool) => tool.name);
72
+ for (const name of required)
73
+ if (names.filter((item) => item === name).length !== 1)
74
+ throw new Error(`Doctor required tool collision or missing: ${name}`);
75
+ pi.setActiveTools(required);
76
+ const active = pi.getActiveTools?.() ?? required;
77
+ if (active.length !== 2 || !required.every((name) => active.includes(name)))
78
+ throw new Error("Doctor active tool narrowing failed");
79
+ } };
80
+ }
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import {
3
3
  ENGINE_DETOUR_TOOL_NAME,
4
- engineDetourFailureDiagnostic,
4
+ engineDetourFailureSeatText,
5
5
  isEngineDetourFailure,
6
6
  resolveEngineModel,
7
7
  resolveEngineName,
@@ -12,6 +12,7 @@ import {
12
12
  reportEngineDetourCall
13
13
  } from "./engine-detour-usage.js";
14
14
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
15
+ import { serializeThrownValue } from "./serialize-thrown-value.js";
15
16
  const engineDetourArgsSchema = Type.Object(
16
17
  {
17
18
  argv: Type.Array(Type.String({ minLength: 1 }), {
@@ -31,6 +32,27 @@ function isCallerCancellation(error, signal) {
31
32
  function asError(error, fallback) {
32
33
  return error instanceof Error ? error : new Error(String(error).trim() || fallback);
33
34
  }
35
+ function engineFailureResult(input) {
36
+ const details = {
37
+ tool: ENGINE_DETOUR_TOOL_NAME,
38
+ ...input.code === void 0 ? {} : { code: input.code },
39
+ ...input.stdout === void 0 ? {} : { stdout: input.stdout },
40
+ ...input.stderr === void 0 ? {} : { stderr: input.stderr },
41
+ ...input.errorCode === void 0 ? {} : { errorCode: input.errorCode },
42
+ ...input.ledgerError === void 0 ? {} : { ledgerError: input.ledgerError }
43
+ };
44
+ let text = input.text;
45
+ if (input.ledgerError !== void 0 && input.ledgerError.length > 0) {
46
+ if (text.length > 0 && !text.endsWith("\n")) text += "\n";
47
+ text += input.ledgerError;
48
+ }
49
+ return {
50
+ content: [{ type: "text", text }],
51
+ details,
52
+ // Identified detour failure is a tool error, not a seat-run death (#1213).
53
+ isError: true
54
+ };
55
+ }
34
56
  function createEngineDetourToolDefinition(input) {
35
57
  const engineName = input.engineName;
36
58
  const engineModel = input.engineModel;
@@ -44,13 +66,6 @@ function createEngineDetourToolDefinition(input) {
44
66
  async execute(toolCallId, params, signal, _onUpdate, ctx) {
45
67
  const args = params;
46
68
  const argv = Array.isArray(args.argv) ? args.argv : [];
47
- if (argv.length === 0 || argv.some((part) => typeof part !== "string" || part.length === 0)) {
48
- input.fail(
49
- new Error("\u52B3\u52A1\u5F15\u64CE argv \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32\u6570\u7EC4"),
50
- toolCallId,
51
- ctx
52
- );
53
- }
54
69
  const sessionParent = ctx.sessionManager?.getSessionFile?.();
55
70
  const startedAt = Date.now();
56
71
  const runDirectory = typeof ctx.runDirectory === "string" && ctx.runDirectory.length > 0 ? ctx.runDirectory : void 0;
@@ -71,25 +86,21 @@ function createEngineDetourToolDefinition(input) {
71
86
  ...observed2.stdoutByteLength === void 0 ? {} : { stdoutByteLength: observed2.stdoutByteLength }
72
87
  });
73
88
  };
74
- const failAfterLedger = (engineCause, observed2, aggregateMessage) => {
89
+ const recordCallOrLedgerError = (observed2) => {
75
90
  try {
76
91
  recordCall(observed2);
92
+ return void 0;
77
93
  } catch (recordError) {
78
- input.fail(
79
- new AggregateError(
80
- [
81
- engineCause,
82
- asError(recordError, "engine detour usage ledger write failed")
83
- ],
84
- aggregateMessage,
85
- { cause: engineCause }
86
- ),
87
- toolCallId,
88
- ctx
89
- );
94
+ return serializeThrownValue(recordError);
90
95
  }
91
- input.fail(engineCause, toolCallId, ctx);
92
96
  };
97
+ if (argv.length === 0 || argv.some((part) => typeof part !== "string" || part.length === 0)) {
98
+ const ledgerError = recordCallOrLedgerError({});
99
+ return engineFailureResult({
100
+ text: "\u52B3\u52A1\u5F15\u64CE argv \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32\u6570\u7EC4",
101
+ ...ledgerError === void 0 ? {} : { ledgerError }
102
+ });
103
+ }
93
104
  let result;
94
105
  try {
95
106
  result = await runEngineDetourOnce({
@@ -99,20 +110,26 @@ function createEngineDetourToolDefinition(input) {
99
110
  });
100
111
  } catch (error) {
101
112
  if (isCallerCancellation(error, signal)) throw error;
102
- return failAfterLedger(
103
- asError(error, "\u52B3\u52A1\u5F15\u64CE spawn \u5931\u8D25"),
104
- {},
105
- "engine detour spawn and usage ledger both failed"
106
- );
113
+ const engineError = asError(error, "\u52B3\u52A1\u5F15\u64CE spawn \u5931\u8D25");
114
+ const ledgerError = recordCallOrLedgerError({});
115
+ const errno = typeof engineError.code === "string" ? engineError.code : void 0;
116
+ return engineFailureResult({
117
+ text: engineError.message,
118
+ ...errno === void 0 ? {} : { errorCode: errno },
119
+ ...ledgerError === void 0 ? {} : { ledgerError }
120
+ });
107
121
  }
108
122
  const stdoutByteLength = engineDetourStdoutByteLength(result.stdout);
109
123
  const observed = { code: result.code, stdoutByteLength };
110
124
  if (isEngineDetourFailure(result)) {
111
- return failAfterLedger(
112
- new Error(engineDetourFailureDiagnostic(result)),
113
- observed,
114
- "engine detour child-close and usage ledger both failed"
115
- );
125
+ const ledgerError = recordCallOrLedgerError(observed);
126
+ return engineFailureResult({
127
+ text: engineDetourFailureSeatText(result),
128
+ code: result.code,
129
+ stdout: result.stdout,
130
+ stderr: result.stderr,
131
+ ...ledgerError === void 0 ? {} : { ledgerError }
132
+ });
116
133
  }
117
134
  recordCall(observed);
118
135
  return {
@@ -126,7 +143,7 @@ function createEngineDetourToolDefinition(input) {
126
143
  }
127
144
  };
128
145
  }
129
- function registerEngineDetourTool(roleHost, hostActions) {
146
+ function registerEngineDetourTool(roleHost) {
130
147
  const getFlag = (name) => roleHost.getFlag(name);
131
148
  const engineName = resolveEngineName(getFlag);
132
149
  if (engineName === void 0) {
@@ -135,10 +152,7 @@ function registerEngineDetourTool(roleHost, hostActions) {
135
152
  const engineModel = resolveEngineModel(getFlag);
136
153
  const definition = createEngineDetourToolDefinition({
137
154
  engineName,
138
- ...engineModel === void 0 ? {} : { engineModel },
139
- fail(error, toolCallId, ctx) {
140
- hostActions.failInfrastructure(error, ctx, toolCallId);
141
- }
155
+ ...engineModel === void 0 ? {} : { engineModel }
142
156
  });
143
157
  roleHost.registerTool(definition);
144
158
  return true;