@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3572

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 (35) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +755 -60
  4. package/dist/diarist-contracts.js +72 -0
  5. package/dist/package-contracts/terminating-tools.js +20 -2
  6. package/dist/packaged-role-registry.js +20 -0
  7. package/dist/public-cli/main.js +533 -634
  8. package/dist/public-cli/registry.js +4 -1
  9. package/extensions/role-runtime.ts +1 -0
  10. package/package.json +1 -1
  11. package/resources/diarist-collect.md +6 -10
  12. package/souls/diarist.md +9 -0
  13. package/src/acp-host/production-host.ts +1 -0
  14. package/src/acp-host/role-envelope.ts +2 -1
  15. package/src/diarist-contracts.ts +88 -0
  16. package/src/diarist-role.ts +60 -0
  17. package/src/diarist.ts +253 -179
  18. package/src/host-contracts.ts +6 -1
  19. package/src/package-contracts/terminating-tools.ts +22 -2
  20. package/src/packaged-role-registry.ts +20 -0
  21. package/src/pi/role-turn-host.ts +8 -0
  22. package/src/public-cli/cli.ts +29 -0
  23. package/src/public-cli/countersign-run.ts +9 -129
  24. package/src/public-cli/diarist-run.ts +312 -0
  25. package/src/public-cli/invocation.ts +23 -2
  26. package/src/public-cli/option-definitions.ts +18 -0
  27. package/src/public-cli/post-admission.ts +2 -2
  28. package/src/public-cli/registry.ts +3 -0
  29. package/src/public-cli/run-lifecycle.ts +31 -3
  30. package/src/public-cli/settlement.ts +50 -2
  31. package/src/public-cli/terminal.ts +2 -1
  32. package/src/role-runtime.ts +117 -6
  33. package/src/ticket-provenance-contracts.ts +1 -0
  34. package/src/ticket-provenance.ts +0 -23
  35. package/src/diarist-llm-collector.ts +0 -324
@@ -146,6 +146,14 @@ function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
146
146
  return ["--ak-role", "gatekeeper"];
147
147
  case "navigator":
148
148
  return ["--ak-role", "navigator"];
149
+ case "diarist":
150
+ return [
151
+ "--ak-role",
152
+ "diarist",
153
+ ...(activation.sourcesPath === undefined
154
+ ? []
155
+ : ["--ak-diarist-sources", activation.sourcesPath]),
156
+ ];
149
157
  default: {
150
158
  const _exhaustive: never = activation;
151
159
  return _exhaustive;
@@ -51,6 +51,7 @@ import {
51
51
  parseCoderArgv,
52
52
  parseCollectorArgv,
53
53
  parseCountersignArgv,
54
+ parseDiaristArgv,
54
55
  parseGleanerLeftArgv,
55
56
  parseDoctorArgv,
56
57
  parseFixerArgv,
@@ -78,6 +79,7 @@ import { runPublicCoder, runPublicCoderResume } from "./coder-run.ts";
78
79
  import { runPublicInstructionSeat, runPublicInstructionSeatResume } from "./instruction-seat-run.ts";
79
80
  import { runPublicCollector, runPublicCollectorResume } from "./collector-run.ts";
80
81
  import { runPublicCountersign, runPublicCountersignResume } from "./countersign-run.ts";
82
+ import { runPublicDiarist, runPublicDiaristResume } from "./diarist-run.ts";
81
83
  import { runPublicGleanerLeft, runPublicGleanerLeftResume } from "./gleaner-left-run.ts";
82
84
  import { runPublicDoctor, runPublicDoctorResume } from "./doctor-run.ts";
83
85
  import { runPublicFixer, runPublicFixerResume } from "./fixer-run.ts";
@@ -126,6 +128,7 @@ const RESUME_SEAT_DISPATCH: Record<
126
128
  inspector: { seat: "inspector", run: runPublicInspectorResume },
127
129
  gatekeeper: { seat: "gatekeeper", run: runPublicInstructionSeatResume },
128
130
  navigator: { seat: "navigator", run: runPublicInstructionSeatResume },
131
+ diarist: { seat: "diarist", run: runPublicDiaristResume },
129
132
  };
130
133
  import {
131
134
  INTERNAL_ROLE_ENTRYPOINT_RELATIVE,
@@ -168,6 +171,7 @@ export const PUBLIC_ROLE_ARGV = {
168
171
  reviewer: { parse: parseReviewerArgv, options: optionsForOwner("reviewer") },
169
172
  gatekeeper: { parse: parseGatekeeperArgv, options: optionsForOwner("gatekeeper") },
170
173
  navigator: { parse: parseNavigatorArgv, options: optionsForOwner("navigator") },
174
+ diarist: { parse: parseDiaristArgv, options: optionsForOwner("diarist") },
171
175
  /** Deterministic analysis seat (#336) — argv parse only; no LLM admission. */
172
176
  analyst: { parse: parseAnalystArgv, options: optionsForOwner("analyst") },
173
177
  } as const;
@@ -1311,6 +1315,31 @@ export async function runAkRole(
1311
1315
  };
1312
1316
  }
1313
1317
 
1318
+ // Diarist public run path (#708): 起居郎 is summoned like any other seat.
1319
+ if (parsed.command === "diarist") {
1320
+ const agentDir = resolveAgentDir(env, home);
1321
+ const cwd = env.cwd ?? process.cwd();
1322
+ const config = await loadAndValidateConfig(home, env.packageRoot);
1323
+ const credentials =
1324
+ env.credentials ?? (await loadCredentialProviders(agentDir));
1325
+ const seat = resolveEffectiveSeat(
1326
+ config,
1327
+ "diarist",
1328
+ credentials,
1329
+ invocationFromParsed(parsed),
1330
+ );
1331
+ const result = await runPublicDiarist(
1332
+ parsed.args,
1333
+ createRoleEnvironment(env, { role: "diarist", home, agentDir, cwd, credentials, seat, config }),
1334
+ io,
1335
+ PUBLIC_ROLE_ARGV.diarist.parse,
1336
+ );
1337
+ return {
1338
+ exitCode: result.exitCode,
1339
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal }),
1340
+ };
1341
+ }
1342
+
1314
1343
  // Coder public run path with package-owned TDD method (#109).
1315
1344
  if (parsed.command === "coder") {
1316
1345
  const agentDir = resolveAgentDir(env, home);
@@ -1,20 +1,11 @@
1
1
  /**
2
- * Public Countersign Role run: admit ticket materials → diarist pipeline step →
3
- * shared post-admission coordinator → settle Terminal result
4
- * (#572 / ADR 0074 / ADR 0075). #599: manual resume continues the exact session.
5
- * Diarist is a prior station on the court pipeline, not a countersign call.
6
- * Unbound admission resolves ticket via shared seat LLM bind (#635) before the diary station.
2
+ * Public Countersign Role run: admit ticket materials → shared post-admission
3
+ * coordinator → settle Terminal result (#572 / ADR 0074). #599: manual resume
4
+ * continues the exact session. Unbound admission resolves its ticket via the
5
+ * shared seat LLM bind (#635). Who runs 起居郎, and when, is the caller's
6
+ * composition (ADR 0010 / ADR 0075 `no-call-rule`) — not this seat's business.
7
7
  */
8
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
9
- import {
10
- createDiaristIssueFaceFetcher,
11
- DiaristIssueSourceError,
12
- resolveDiaristGithubOrigin,
13
- runDiarist,
14
- type DiaristIssueFace,
15
- type DiaristRunResult,
16
- } from "../diarist.ts";
17
- import { appendIssueSourceFailureDiagnostic } from "../ticket-provenance.ts";
18
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
19
10
  import { CliUsageError } from "./cli-errors.ts";
20
11
  import {
@@ -29,7 +20,6 @@ import {
29
20
  ticketNumberFromProbe,
30
21
  tryResumeSameTicketSeatRun,
31
22
  } from "./seat-ticket-binding.ts";
32
- import { tryHomeFromAkRolesPath } from "../activation-ledger-topology.ts";
33
23
  import {
34
24
  prepareSummonsResumeMaterials,
35
25
  runPostAdmissionOneShot,
@@ -194,7 +184,6 @@ export async function runPublicCountersign(
194
184
  turnRequest,
195
185
  buildCountersignTurnRequest(admitted, turnProjection),
196
186
  );
197
- await runCountersignDiaristStation(admitted, env);
198
187
  },
199
188
  }),
200
189
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
@@ -222,10 +211,9 @@ function countersignAdapters(options?: {
222
211
 
223
212
  /**
224
213
  * Resume a previously admitted Countersign run (#599 / DK-3 / #637).
225
- * Restores role/ticket/session identity. Every court re-entry runs the diarist
226
- * station first (ADR 0075 refresh-every-court). Same-ticket summons deliver this
227
- * turn's instruction + frozen attachments on the resume prompt; manual resume
228
- * keeps package-envelope / caller-message semantics and birth attachments.
214
+ * Restores role/ticket/session identity. Same-ticket summons deliver this turn's
215
+ * instruction + frozen attachments on the resume prompt; manual resume keeps
216
+ * package-envelope / caller-message semantics and birth attachments.
229
217
  */
230
218
  export async function runPublicCountersignResume(
231
219
  request: PublicResumeRequest,
@@ -261,115 +249,7 @@ export async function runPublicCountersignResume(
261
249
  ),
262
250
  );
263
251
  },
264
- adapters: countersignAdapters({
265
- beforeDispatch: async (admitted) => {
266
- await runCountersignDiaristStation(admitted, env);
267
- },
268
- }),
252
+ adapters: countersignAdapters(),
269
253
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
270
254
  });
271
255
  }
272
-
273
- /**
274
- * Court-pipeline prior station: refresh ticket-provenance before countersign turn.
275
- * Caller-invisible — collector failures append durable volume diagnostics and the
276
- * station continues; issue-source / ADR source-read / watermark honesty failures
277
- * leave typed durable diagnostics and propagate (失败诚实).
278
- * Missing ticketNumber (true-unbound after pre-court resolution) skips the station
279
- * — no diary is minted for a true-unbound run.
280
- *
281
- * Issue body/comments come from the shared GitHub seam only. Attachments stay
282
- * attachments — never merged and mislabeled as issue-body-comment.
283
- * Bound ticket never silently degrades to “no issue face”.
284
- */
285
- export async function runCountersignDiaristStation(
286
- admitted: AdmittedCountersignInvocation,
287
- env: Pick<CountersignRunEnv, "cwd" | "packageRoot">,
288
- ): Promise<DiaristRunResult | undefined> {
289
- if (admitted.ticketNumber === undefined) return undefined;
290
-
291
- const issueFace = await loadBoundIssueFace(admitted);
292
- const home = tryHomeFromAkRolesPath(admitted.runDirectory);
293
-
294
- const result = await runDiarist({
295
- ticketNumber: admitted.ticketNumber,
296
- cwd: admitted.projectRoot,
297
- ...(home === undefined ? {} : { home }),
298
- issueFace,
299
- sessionCwds: [admitted.projectRoot, env.cwd],
300
- ...(env.packageRoot === undefined ? {} : { packageRoot: env.packageRoot }),
301
- });
302
- return result;
303
- }
304
-
305
- /**
306
- * Acquire issue face for a bound ticket. Failures are typed + durable on the
307
- * ticket-provenance volume, then propagated — never washed into empty face.
308
- */
309
- function persistIssueSourceFailure(
310
- admitted: AdmittedCountersignInvocation,
311
- ticketNumber: number,
312
- error: DiaristIssueSourceError,
313
- ): never {
314
- const home = tryHomeFromAkRolesPath(admitted.runDirectory);
315
- appendIssueSourceFailureDiagnostic({
316
- ticketNumber,
317
- cwd: admitted.projectRoot,
318
- ...(home === undefined ? {} : { home }),
319
- cause: error.message,
320
- reason: error.reason,
321
- });
322
- throw error;
323
- }
324
-
325
- async function loadBoundIssueFace(
326
- admitted: AdmittedCountersignInvocation,
327
- ): Promise<DiaristIssueFace> {
328
- const ticketNumber = admitted.ticketNumber;
329
- if (ticketNumber === undefined) {
330
- throw new Error("loadBoundIssueFace requires a bound ticketNumber");
331
- }
332
-
333
- const origin = resolveDiaristGithubOrigin(admitted.projectRoot);
334
- if (origin === undefined) {
335
- persistIssueSourceFailure(
336
- admitted,
337
- ticketNumber,
338
- new DiaristIssueSourceError(
339
- "origin-unresolved",
340
- `bound ticket #${ticketNumber} issue face requires a resolvable github.com origin remote`,
341
- ),
342
- );
343
- }
344
-
345
- const fetcher = createDiaristIssueFaceFetcher();
346
- let face: DiaristIssueFace | undefined;
347
- try {
348
- face = await fetcher({
349
- owner: origin.owner,
350
- repo: origin.repo,
351
- ticketNumber,
352
- });
353
- } catch (error) {
354
- const typed =
355
- error instanceof DiaristIssueSourceError
356
- ? error
357
- : new DiaristIssueSourceError(
358
- "issue-unavailable",
359
- `issue face fetch failed for ${origin.owner}/${origin.repo}#${ticketNumber}`,
360
- { cause: error },
361
- );
362
- persistIssueSourceFailure(admitted, ticketNumber, typed);
363
- }
364
- if (face === undefined) {
365
- persistIssueSourceFailure(
366
- admitted,
367
- ticketNumber,
368
- new DiaristIssueSourceError(
369
- "issue-unavailable",
370
- `issue face unavailable for ${origin.owner}/${origin.repo}#${ticketNumber}`,
371
- ),
372
- );
373
- }
374
- return face;
375
- }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Public Diarist (起居郎) Role run — #708 / ADR 0075 `diarist-is-role`.
3
+ * Same admit → post-admission → settle shape as the other instruction seats.
4
+ * Semantic collection is the role's own turn; this seat only freezes the
5
+ * mechanical source catalog the turn selects from (`diarist-collector-is-own-turn`).
6
+ * Who calls it and in what order is the caller's business (ADR 0010 `no-call-rule`).
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { writeFile } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+
12
+ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
13
+ import {
14
+ loadDiaristIssueFace,
15
+ prepareDiaristSourceCatalog,
16
+ serializeDiaristSourceCatalog,
17
+ } from "../diarist.ts";
18
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
19
+ import { CliUsageError } from "./cli-errors.ts";
20
+ import {
21
+ admitDiaristInvocation,
22
+ buildInstructionTransportPrompt,
23
+ type AdmittedDiaristInvocation,
24
+ type ParseDiaristArgvResult,
25
+ } from "./invocation.ts";
26
+ import {
27
+ applyInstructionTicketProbe,
28
+ probeInstructionTicket,
29
+ ticketNumberFromProbe,
30
+ tryResumeSameTicketSeatRun,
31
+ } from "./seat-ticket-binding.ts";
32
+ import {
33
+ prepareSummonsResumeMaterials,
34
+ runPostAdmissionOneShot,
35
+ runPostAdmissionSeatResume,
36
+ resumeTurnRequestProjectionOptions,
37
+ type PostAdmissionAdapters,
38
+ type PostAdmissionEnv,
39
+ } from "./post-admission.ts";
40
+ import {
41
+ loadResumableDiaristRun,
42
+ markRunAdmitted,
43
+ readRoleRunIdentity,
44
+ type PublicResumeRequest,
45
+ type SameTicketSummonsMaterials,
46
+ } from "./run-lifecycle.ts";
47
+ import {
48
+ presentStructuralRejection,
49
+ trySettleDiaristTerminalResult,
50
+ } from "./settlement.ts";
51
+ import type { CliIo } from "./cli-io.ts";
52
+ import type { TerminalResult } from "./terminal.ts";
53
+ import {
54
+ projectRoleTurnRequest,
55
+ type RoleTurnRequestProjectionOptions,
56
+ } from "./turn-request.ts";
57
+
58
+ export type DiaristRunEnv = PostAdmissionEnv & {
59
+ principalAuthority: DurablePrincipalAuthority;
60
+ createRunId?: () => string;
61
+ };
62
+
63
+ /** Frozen catalog filename inside the run dossier (durable material, not argv). */
64
+ const DIARIST_SOURCE_CATALOG_FILE = "diarist-sources.json" as const;
65
+
66
+ /** Neutral path identifier only (ADR 0073) — catalog bytes stay on disk. */
67
+ function withDiaristCatalogPath(
68
+ prompt: string,
69
+ sourcesPath: string | undefined,
70
+ ): string {
71
+ if (sourcesPath === undefined || sourcesPath.trim() === "") return prompt;
72
+ return `${prompt}\n\n已冻结来源文件(路径):\n- ${sourcesPath}`;
73
+ }
74
+
75
+ /** Project admitted invocation onto the host-neutral turn request. */
76
+ export function buildDiaristTurnRequest(
77
+ admitted: AdmittedDiaristInvocation,
78
+ options: RoleTurnRequestProjectionOptions,
79
+ sourcesPath?: string,
80
+ ): RoleTurnRequest {
81
+ const continuation =
82
+ sourcesPath === undefined
83
+ ? options.continuation
84
+ : {
85
+ ...options.continuation,
86
+ prompt: withDiaristCatalogPath(options.continuation.prompt, sourcesPath),
87
+ };
88
+ return projectRoleTurnRequest(
89
+ admitted,
90
+ {
91
+ activation: {
92
+ role: "diarist" as const,
93
+ ...(sourcesPath === undefined ? {} : { sourcesPath }),
94
+ },
95
+ },
96
+ { ...options, continuation },
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Mechanical source enumeration for a bound summons: establish the per-ticket
102
+ * volume, freeze this turn's candidate catalog into the run dossier.
103
+ * A true-unbound summons has no ticket, so no diary is minted — undefined.
104
+ *
105
+ * A run that has not settled yet (crash-resume, open-court continuation) keeps
106
+ * the catalog its candidateIndexes were minted against — re-enumerating there
107
+ * would rebind the role's indexes to bytes it never saw. Only a settled run
108
+ * re-enumerates on the next same-ticket summons (ADR 0075 增量).
109
+ */
110
+ async function freezeDiaristSourceCatalog(
111
+ admitted: AdmittedDiaristInvocation,
112
+ env: Pick<DiaristRunEnv, "cwd" | "home">,
113
+ ): Promise<string | undefined> {
114
+ if (admitted.ticketNumber === undefined) return undefined;
115
+ const path = join(admitted.runDirectory, DIARIST_SOURCE_CATALOG_FILE);
116
+ if (existsSync(path)) {
117
+ const identity = await readRoleRunIdentity(admitted.runDirectory);
118
+ if (identity !== undefined && identity.state !== "terminal") return path;
119
+ }
120
+ const issueFace = await loadDiaristIssueFace({
121
+ ticketNumber: admitted.ticketNumber,
122
+ projectRoot: admitted.projectRoot,
123
+ home: env.home,
124
+ });
125
+ const catalog = await prepareDiaristSourceCatalog({
126
+ ticketNumber: admitted.ticketNumber,
127
+ cwd: admitted.projectRoot,
128
+ home: env.home,
129
+ issueFace,
130
+ sessionCwds: [admitted.projectRoot, env.cwd],
131
+ });
132
+ await writeFile(path, serializeDiaristSourceCatalog(catalog), "utf8");
133
+ return path;
134
+ }
135
+
136
+ function diaristAdapters(options?: {
137
+ beforeDispatch?: (
138
+ admitted: AdmittedDiaristInvocation,
139
+ ) => void | Promise<void>;
140
+ }): PostAdmissionAdapters<AdmittedDiaristInvocation> {
141
+ return {
142
+ trySettle: (admitted, authority, scope) =>
143
+ trySettleDiaristTerminalResult(admitted, authority, scope),
144
+ shouldPresentSettled: () => true,
145
+ ...(options?.beforeDispatch === undefined
146
+ ? {}
147
+ : { beforeDispatch: options.beforeDispatch }),
148
+ };
149
+ }
150
+
151
+ export async function runPublicDiarist(
152
+ argv: readonly string[],
153
+ env: DiaristRunEnv,
154
+ io: CliIo,
155
+ parseDiaristArgv: (args: readonly string[]) => ParseDiaristArgvResult,
156
+ ): Promise<{
157
+ exitCode: number;
158
+ admitted?: AdmittedDiaristInvocation;
159
+ terminal?: TerminalResult;
160
+ }> {
161
+ let parsed: ParseDiaristArgvResult;
162
+ try {
163
+ parsed = parseDiaristArgv(argv);
164
+ } catch (error) {
165
+ if (error instanceof CliUsageError) {
166
+ presentStructuralRejection(error, io);
167
+ return { exitCode: 2 };
168
+ }
169
+ throw error;
170
+ }
171
+
172
+ // #637: same ticket → resume this seat's prior run with this summons' materials.
173
+ // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
174
+ // controlled failure (bare pre-admit throw skips terminal settlement).
175
+ const projectRoot = parsed.project ?? env.cwd;
176
+ const ticketProbe = await probeInstructionTicket(
177
+ parsed.instruction,
178
+ projectRoot,
179
+ env,
180
+ );
181
+ const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
182
+ if (probedTicketNumber !== undefined) {
183
+ const summons: SameTicketSummonsMaterials = {
184
+ instruction: parsed.instruction,
185
+ instructionEmpty: parsed.instruction.trim() === "",
186
+ attachmentPaths: parsed.attachmentPaths,
187
+ };
188
+ const resumed = await tryResumeSameTicketSeatRun({
189
+ home: env.home,
190
+ projectRoot,
191
+ role: "diarist",
192
+ ticketNumber: probedTicketNumber,
193
+ freshSummons: env.freshSummons,
194
+ summons,
195
+ resume: (runId, materials) =>
196
+ runPublicDiaristResume(
197
+ { runId, ...(materials === undefined ? {} : { summons: materials }) },
198
+ env,
199
+ io,
200
+ ),
201
+ });
202
+ if (resumed !== undefined) return resumed;
203
+ }
204
+
205
+ let admitted: AdmittedDiaristInvocation;
206
+ try {
207
+ admitted = await admitDiaristInvocation({
208
+ home: env.home,
209
+ principalAuthority: env.principalAuthority,
210
+ cwd: env.cwd,
211
+ instruction: parsed.instruction,
212
+ attachmentPaths: parsed.attachmentPaths,
213
+ ...(parsed.project === undefined ? {} : { project: parsed.project }),
214
+ ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
215
+ ...(env.model === undefined ? {} : { model: env.model }),
216
+ ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
217
+ });
218
+ } catch (error) {
219
+ if (error instanceof CliUsageError) {
220
+ presentStructuralRejection(error, io);
221
+ return { exitCode: 2 };
222
+ }
223
+ throw error;
224
+ }
225
+
226
+ await markRunAdmitted(admitted, env.principalAuthority);
227
+
228
+ const turnProjection: RoleTurnRequestProjectionOptions = {
229
+ packageRoot: env.packageRoot,
230
+ home: env.home,
231
+ agentDir: env.agentDir,
232
+ ...(env.model === undefined ? {} : { model: env.model }),
233
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
234
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
235
+ ...(env.correlationId === undefined || env.correlationId.trim() === ""
236
+ ? {}
237
+ : { correlationId: env.correlationId }),
238
+ continuation: {
239
+ kind: "initial",
240
+ prompt: buildInstructionTransportPrompt(
241
+ admitted,
242
+ engineSessionMaterialFromOptions({
243
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
244
+ packageRoot: env.packageRoot,
245
+ }),
246
+ ),
247
+ },
248
+ };
249
+ // Mutable shell: ticket bind + catalog freeze re-project activation before executeTurn.
250
+ const turnRequest = buildDiaristTurnRequest(admitted, turnProjection);
251
+
252
+ return await runPostAdmissionOneShot({
253
+ admitted,
254
+ env,
255
+ io,
256
+ request: turnRequest,
257
+ adapters: diaristAdapters({
258
+ beforeDispatch: async (admittedSeat) => {
259
+ await applyInstructionTicketProbe(admittedSeat, ticketProbe);
260
+ const sourcesPath = await freezeDiaristSourceCatalog(admittedSeat, env);
261
+ Object.assign(
262
+ turnRequest,
263
+ buildDiaristTurnRequest(admittedSeat, turnProjection, sourcesPath),
264
+ );
265
+ },
266
+ }),
267
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Resume a previously admitted Diarist run (#708 / ADR 0079 同票传召 = resume).
273
+ * A re-entry into a settled run re-enumerates fresh sources; the offered
274
+ * watermark keeps the pass incremental so already-seen blocks are not
275
+ * re-offered. An unfinished run keeps its frozen catalog (see freeze above).
276
+ */
277
+ export async function runPublicDiaristResume(
278
+ request: PublicResumeRequest,
279
+ env: DiaristRunEnv,
280
+ io: CliIo,
281
+ ): Promise<{
282
+ exitCode: number;
283
+ admitted?: AdmittedDiaristInvocation;
284
+ terminal?: TerminalResult;
285
+ }> {
286
+ return await runPostAdmissionSeatResume({
287
+ request,
288
+ env,
289
+ io,
290
+ load: (effective) =>
291
+ loadResumableDiaristRun(env.home, effective.runId, env.principalAuthority),
292
+ buildTurnRequest: async (admitted, effective) => {
293
+ const summonsPrepared = await prepareSummonsResumeMaterials(
294
+ admitted.runDirectory,
295
+ effective.summons,
296
+ );
297
+ const sourcesPath = await freezeDiaristSourceCatalog(admitted, env);
298
+ return buildDiaristTurnRequest(
299
+ admitted,
300
+ resumeTurnRequestProjectionOptions(
301
+ admitted,
302
+ effective,
303
+ env,
304
+ summonsPrepared,
305
+ ),
306
+ sourcesPath,
307
+ );
308
+ },
309
+ adapters: diaristAdapters(),
310
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
311
+ });
312
+ }
@@ -151,6 +151,10 @@ export type AdmittedNavigatorInvocation = AdmittedRoleInvocationBase & {
151
151
  readonly role: "navigator";
152
152
  };
153
153
 
154
+ export type AdmittedDiaristInvocation = AdmittedRoleInvocationBase & {
155
+ readonly role: "diarist";
156
+ };
157
+
154
158
  export type CoderPhase = "plan" | "apply";
155
159
 
156
160
  export type AdmittedCoderInvocation = AdmittedRoleInvocationBase & {
@@ -234,6 +238,7 @@ export type AdmittedRoleInvocation =
234
238
  | AdmittedInspectorInvocation
235
239
  | AdmittedGatekeeperInvocation
236
240
  | AdmittedNavigatorInvocation
241
+ | AdmittedDiaristInvocation
237
242
  | AdmittedCoderInvocation
238
243
  | AdmittedFixerInvocation
239
244
  | AdmittedCollectorInvocation
@@ -586,6 +591,7 @@ export type ParseCountersignArgvResult = ParseInstructionArgvResult;
586
591
  export type ParseInspectorArgvResult = ParseInstructionArgvResult;
587
592
  export type ParseGatekeeperArgvResult = ParseInstructionArgvResult;
588
593
  export type ParseNavigatorArgvResult = ParseInstructionArgvResult;
594
+ export type ParseDiaristArgvResult = ParseInstructionArgvResult;
589
595
 
590
596
  /** Positive ticket number for analyst query-scope face (and shared integer parse). */
591
597
  export function parsePositiveTicketNumber(
@@ -606,7 +612,7 @@ export function parsePositiveTicketNumber(
606
612
  /** 共享解析体:同形 owner 的 argv → instruction/attachments/project。 */
607
613
  function parseInstructionArgv(
608
614
  args: readonly string[],
609
- owner: "judge" | "countersign" | "inspector" | "gatekeeper" | "navigator",
615
+ owner: "judge" | "countersign" | "inspector" | "gatekeeper" | "navigator" | "diarist",
610
616
  ): ParseInstructionArgvResult {
611
617
  const attachmentPaths: string[] = [];
612
618
  let project: string | undefined;
@@ -846,6 +852,10 @@ export function parseNavigatorArgv(args: readonly string[]): ParseNavigatorArgvR
846
852
  return parseInstructionArgv(args, "navigator");
847
853
  }
848
854
 
855
+ export function parseDiaristArgv(args: readonly string[]): ParseDiaristArgvResult {
856
+ return parseInstructionArgv(args, "diarist");
857
+ }
858
+
849
859
  /**
850
860
  * Parse Coder-specific argv after the `coder` token.
851
861
  * Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.coder (#342).
@@ -1045,6 +1055,7 @@ export type AdmitInspectorInvocationOptions = AdmitJudgeInvocationOptions & {
1045
1055
 
1046
1056
  export type AdmitGatekeeperInvocationOptions = AdmitInspectorInvocationOptions;
1047
1057
  export type AdmitNavigatorInvocationOptions = AdmitInspectorInvocationOptions;
1058
+ export type AdmitDiaristInvocationOptions = AdmitInspectorInvocationOptions;
1048
1059
 
1049
1060
  /**
1050
1061
  * Shared instruction-seat admission for Judge and Inspector: project check,
@@ -1053,7 +1064,7 @@ export type AdmitNavigatorInvocationOptions = AdmitInspectorInvocationOptions;
1053
1064
  * Ticket binding is post-admission via shared seat LLM path (#635).
1054
1065
  */
1055
1066
  async function admitStandardMaterialInvocation<
1056
- R extends "judge" | "inspector" | "gatekeeper" | "navigator",
1067
+ R extends "judge" | "inspector" | "gatekeeper" | "navigator" | "diarist",
1057
1068
  >(
1058
1069
  role: R,
1059
1070
  options: AdmitJudgeInvocationOptions & { correlationId?: string },
@@ -1173,6 +1184,16 @@ export async function admitNavigatorInvocation(
1173
1184
  return admitStandardMaterialInvocation("navigator", options);
1174
1185
  }
1175
1186
 
1187
+ /**
1188
+ * Admit a direct Diarist (起居郎) run (#708): freeze attachments, persist the
1189
+ * request, reserve session placement — same instruction-seat face.
1190
+ */
1191
+ export async function admitDiaristInvocation(
1192
+ options: AdmitDiaristInvocationOptions,
1193
+ ): Promise<AdmittedDiaristInvocation> {
1194
+ return admitStandardMaterialInvocation("diarist", options);
1195
+ }
1196
+
1176
1197
  /** Shared prompt transport for instruction-seat roles (judge/countersign/inspector). */
1177
1198
  export function buildInstructionTransportPrompt(
1178
1199
  admitted: { instruction: string; instructionEmpty: boolean; attachments: readonly { frozenPath: string }[] },
@@ -34,6 +34,7 @@ export type OptionOwner =
34
34
  | "inspector"
35
35
  | "gatekeeper"
36
36
  | "navigator"
37
+ | "diarist"
37
38
  | "analyst";
38
39
 
39
40
  /**
@@ -427,6 +428,11 @@ const COUNTERSIGN_OPTIONS = [
427
428
  bindOwner("countersign", SHARED_ATTACH_SEMANTICS),
428
429
  ] as const satisfies readonly PublicOptionDefinition[];
429
430
 
431
+ const DIARIST_OPTIONS = [
432
+ bindOwner("diarist", SHARED_PROJECT_SEMANTICS),
433
+ bindOwner("diarist", SHARED_ATTACH_SEMANTICS),
434
+ ] as const satisfies readonly PublicOptionDefinition[];
435
+
430
436
  const CODER_OPTIONS = [
431
437
  {
432
438
  id: "phase",
@@ -784,6 +790,7 @@ export const PUBLIC_OPTION_TABLE = {
784
790
  inspector: INSPECTOR_OPTIONS,
785
791
  gatekeeper: GATEKEEPER_OPTIONS,
786
792
  navigator: NAVIGATOR_OPTIONS,
793
+ diarist: DIARIST_OPTIONS,
787
794
  analyst: ANALYST_OPTIONS,
788
795
  } as const satisfies Record<OptionOwner, readonly PublicOptionDefinition[]>;
789
796
 
@@ -804,6 +811,7 @@ export const PUBLIC_ROLE_OPTION_OWNERS = [
804
811
  "inspector",
805
812
  "gatekeeper",
806
813
  "navigator",
814
+ "diarist",
807
815
  "analyst",
808
816
  ] as const satisfies readonly PublicRoleOptionOwner[];
809
817
 
@@ -1201,6 +1209,16 @@ const ROLE_COMMAND_HELP = {
1201
1209
  'ak-role navigator "刚完成 coder apply 收敛,下一步?"',
1202
1210
  ],
1203
1211
  },
1212
+ diarist: {
1213
+ command: "diarist",
1214
+ summary:
1215
+ "Direct Diarist (起居郎) pass: gather and organize this case's decision basis into its 起居录.",
1216
+ usage: ["ak-role diarist [options] [instruction]"],
1217
+ examples: [
1218
+ 'ak-role diarist "整理 #708 的本案依据。"',
1219
+ 'ak-role diarist --attach ./design.md "补录本轮设计修订。"',
1220
+ ],
1221
+ },
1204
1222
  notary: {
1205
1223
  command: "notary",
1206
1224
  summary: "Direct Notary document check (quote fidelity + ticket alignment); zero prompt/attachment.",