@akagilnc/pi-workflow-roles 0.1.4321 → 0.1.4387

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 (71) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +5 -1
  3. package/dist/acp-host/production-host.js +1308 -587
  4. package/dist/doctor-contracts.js +2 -2
  5. package/dist/engine-detour-tool.js +2 -8
  6. package/dist/gatekeeper-role.js +4 -19
  7. package/dist/headless-host/production-host.js +2613 -1892
  8. package/dist/merger-contracts.js +2 -2
  9. package/dist/migrate-book-topology.js +34 -9
  10. package/dist/notary-contracts.js +2 -2
  11. package/dist/package-contracts/auditor-output.js +2 -2
  12. package/dist/package-contracts/fixer-output.js +2 -2
  13. package/dist/package-contracts/gatekeeper-output.js +2 -2
  14. package/dist/package-contracts/navigator-output.js +2 -2
  15. package/dist/package-contracts/terminating-infrastructure.js +7 -2
  16. package/dist/package-contracts/terminating-tools.js +6 -0
  17. package/dist/packaged-role-registry.js +20 -1
  18. package/dist/public-cli/case-dossier-delivery.js +45 -28
  19. package/dist/public-cli/countersign-run.js +439 -0
  20. package/dist/public-cli/invocation.js +10 -0
  21. package/dist/public-cli/main.js +388 -101
  22. package/dist/public-cli/option-definitions.js +14 -0
  23. package/dist/public-cli/post-admission.js +44 -49
  24. package/dist/public-cli/run-lifecycle.js +13 -1
  25. package/dist/public-cli/settlement.js +14 -53
  26. package/dist/public-role-summons.js +14 -0
  27. package/dist/secretariat-contracts.js +8 -0
  28. package/dist/sitian-appender.js +21 -4
  29. package/extensions/role-runtime.ts +1 -0
  30. package/package.json +1 -1
  31. package/scripts/build-package.mjs +1 -0
  32. package/souls/secretariat.md +26 -0
  33. package/souls/ticket-law.md +9 -0
  34. package/src/collector-tool-schemas.ts +2 -2
  35. package/src/countersign-role.ts +2 -2
  36. package/src/diarist-role.ts +2 -2
  37. package/src/doctor-contracts.ts +2 -2
  38. package/src/engine-detour-tool.ts +3 -9
  39. package/src/gatekeeper-pass-envelope.ts +35 -2
  40. package/src/gatekeeper-role.ts +8 -31
  41. package/src/gleaner-left-role.ts +2 -2
  42. package/src/host-contracts.ts +2 -1
  43. package/src/inspector-role.ts +2 -2
  44. package/src/judge-role.ts +2 -2
  45. package/src/merger-contracts.ts +2 -2
  46. package/src/notary-contracts.ts +2 -2
  47. package/src/package-contracts/auditor-output.ts +2 -2
  48. package/src/package-contracts/fixer-output.ts +2 -2
  49. package/src/package-contracts/gatekeeper-output.ts +2 -2
  50. package/src/package-contracts/navigator-output.ts +2 -2
  51. package/src/package-contracts/terminating-infrastructure.ts +13 -3
  52. package/src/package-contracts/terminating-tools.ts +9 -1
  53. package/src/packaged-role-registry.ts +20 -1
  54. package/src/public-cli/case-dossier-delivery.ts +48 -29
  55. package/src/public-cli/cli.ts +10 -0
  56. package/src/public-cli/countersign-run.ts +29 -5
  57. package/src/public-cli/invocation.ts +28 -2
  58. package/src/public-cli/option-definitions.ts +17 -0
  59. package/src/public-cli/post-admission.ts +45 -58
  60. package/src/public-cli/run-lifecycle.ts +29 -2
  61. package/src/public-cli/secretariat-run.ts +266 -0
  62. package/src/public-cli/settlement.ts +31 -55
  63. package/src/public-cli/terminal.ts +2 -1
  64. package/src/public-role-summons.ts +23 -1
  65. package/src/reviewer-role.ts +2 -2
  66. package/src/role-runtime-dependencies.ts +1 -0
  67. package/src/role-runtime.ts +203 -18
  68. package/src/secretariat-contracts.ts +27 -0
  69. package/src/secretariat-role.ts +210 -0
  70. package/src/sitian-appender.ts +36 -3
  71. package/src/worker-role.ts +2 -2
@@ -355,6 +355,10 @@ const DIARIST_OPTIONS = [
355
355
  bindOwner("diarist", SHARED_PROJECT_SEMANTICS),
356
356
  bindOwner("diarist", SHARED_ATTACH_SEMANTICS),
357
357
  ];
358
+ const SECRETARIAT_OPTIONS = [
359
+ bindOwner("secretariat", SHARED_PROJECT_SEMANTICS),
360
+ bindOwner("secretariat", SHARED_ATTACH_SEMANTICS),
361
+ ];
358
362
  const CODER_OPTIONS = [
359
363
  {
360
364
  id: "phase",
@@ -721,6 +725,7 @@ export const PUBLIC_OPTION_TABLE = {
721
725
  navigator: NAVIGATOR_OPTIONS,
722
726
  auditor: AUDITOR_OPTIONS,
723
727
  diarist: DIARIST_OPTIONS,
728
+ secretariat: SECRETARIAT_OPTIONS,
724
729
  analyst: ANALYST_OPTIONS,
725
730
  };
726
731
  /** Role/deterministic owners that appear on PUBLIC_ROLE_ARGV. */
@@ -740,6 +745,7 @@ export const PUBLIC_ROLE_OPTION_OWNERS = [
740
745
  "navigator",
741
746
  "auditor",
742
747
  "diarist",
748
+ "secretariat",
743
749
  "analyst",
744
750
  ];
745
751
  export function optionsForOwner(owner) {
@@ -1051,6 +1057,14 @@ const ROLE_COMMAND_HELP = {
1051
1057
  'ak-role diarist --attach ./design.md "补录本轮设计修订。"',
1052
1058
  ],
1053
1059
  },
1060
+ secretariat: {
1061
+ command: "secretariat",
1062
+ summary: "Secretariat (中书省): rewrite ticket per 票面法 and drive countersign to converged or escalate.",
1063
+ usage: ["ak-role secretariat [options] [instruction]"],
1064
+ examples: [
1065
+ 'ak-role secretariat "整理 #924 票面并送庭。"',
1066
+ ],
1067
+ },
1054
1068
  notary: {
1055
1069
  command: "notary",
1056
1070
  summary: "Direct Notary document check (quote fidelity + ticket alignment); zero prompt/attachment.",
@@ -10,12 +10,13 @@ import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
  import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
- import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
13
+ import { bindAdmittedTicketNumber, buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
+ import { readRecordedSubmissionRows } from "../submission-ledger.js";
14
15
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
16
  import { pickEngineAxis } from "../package-resources/engine-material.js";
16
17
  import { resolveHostAwareSessionAvailability } from "../session-identity.js";
17
18
  import { isOfficerReviewSeat } from "../host-contracts.js";
18
- import { deliverCaseDossierAsAttachment, projectCaseDossierPointerSection, } from "./case-dossier-delivery.js";
19
+ import { deliverCaseDossierAsAttachment } from "./case-dossier-delivery.js";
19
20
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
20
21
  function describeCaughtError(error) {
21
22
  if (error instanceof Error) {
@@ -24,13 +25,6 @@ function describeCaughtError(error) {
24
25
  }
25
26
  return { message: String(error) };
26
27
  }
27
- /** Append one system section to a continuation prompt, keeping its kind. */
28
- function appendContinuationSection(continuation, section) {
29
- const prompt = `${continuation.prompt}\n\n${section}`;
30
- return continuation.kind === "initial"
31
- ? { kind: "initial", prompt }
32
- : { kind: "resume", prompt };
33
- }
34
28
  /**
35
29
  * Nested gate summons (station child) on an officer seat: dialogue content is
36
30
  * peer words only (#879). ADR 0081 case dossier still hangs via the existing
@@ -262,11 +256,32 @@ export async function dispatchPostAdmissionTurn(input) {
262
256
  */
263
257
  let afterDispatchApplied = false;
264
258
  const finishAfterTurn = async (result) => {
265
- if (adapters.afterDispatch === undefined || afterDispatchApplied)
259
+ if (afterDispatchApplied)
266
260
  return result;
267
261
  afterDispatchApplied = true;
268
262
  try {
269
- await adapters.afterDispatch(admitted, lease);
263
+ // #858: an unbound seat may assert its ticket on the existing receipt.
264
+ // Read the original accepted payload; do not rewrite it, infer from prose,
265
+ // or reject missing/malformed declarations. An existing binding wins.
266
+ if (admitted.ticketNumber === undefined) {
267
+ const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, { home: homeFromRunDirectory(admitted.runDirectory), sessionParent: join(admitted.runDirectory, "session", "session.jsonl") });
268
+ const asserted = rows
269
+ .filter((row) => row.kind === "accepted" && row.role === admitted.role)
270
+ .map((row) => row.accepted)
271
+ .find((payload) => {
272
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
273
+ return false;
274
+ const value = payload.ticketNumber;
275
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
276
+ });
277
+ const ticketNumber = asserted === undefined
278
+ ? undefined
279
+ : asserted.ticketNumber;
280
+ if (ticketNumber !== undefined)
281
+ await bindAdmittedTicketNumber(admitted, ticketNumber);
282
+ }
283
+ if (adapters.afterDispatch !== undefined)
284
+ await adapters.afterDispatch(admitted, lease);
270
285
  return result;
271
286
  }
272
287
  catch (error) {
@@ -362,10 +377,11 @@ export async function dispatchPostAdmissionTurn(input) {
362
377
  }
363
378
  // Turn request is assembled after beforeDispatch so this turn sees whatever it
364
379
  // settled — the seat's ticket bind re-projection and any court diarist station
365
- // writes (#742). Case dossier delivery (ADR 0081 / #709) rides here once for
366
- // every public entry. #879: station-child officer dialogue keeps peer body
367
- // intact — 起居录 hangs via existing attachments freeze (not prompt wrap,
368
- // not RoleTurnRequest.materials). Other entries keep the neutral prompt section.
380
+ // writes (#742). Case dossier delivery (ADR 0081 / #709 / #858) rides here once
381
+ // for every public entry on the existing attachments → readingMaterial face
382
+ // (station-child and ordinary share one seam). Dialogue continuation stays
383
+ // caller/peer opaque — never splice system path sections into user dialogue.
384
+ // No package-resume parallel face or typed resume identity.
369
385
  let turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
370
386
  if (env.stationChild !== undefined) {
371
387
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
@@ -378,32 +394,16 @@ export async function dispatchPostAdmissionTurn(input) {
378
394
  if (typeof liveHost === "string" && liveHost.trim() !== "") {
379
395
  turnRequest = { ...turnRequest, host: liveHost.trim() };
380
396
  }
381
- if (isStationChildOfficerDialogue(admitted.role, env)) {
382
- // 0081 non-body face: freeze pointer section under run/attachments/.
383
- // Peer dialogue continuation.prompt stays parent payload only — the seat
384
- // consumes the freeze via loadCaseDossierReadingMaterial → existing
385
- // agent-start readingMaterial / systemPrompt.materials fold (not prompt splice,
386
- // not RoleTurnRequest.materials).
387
- await deliverCaseDossierAsAttachment({
388
- ticketNumber: admitted.ticketNumber,
389
- projectRoot: admitted.projectRoot,
390
- home: env.home,
391
- runDirectory: admitted.runDirectory,
392
- });
393
- }
394
- else {
395
- const dossierSection = await projectCaseDossierPointerSection({
396
- ticketNumber: admitted.ticketNumber,
397
- projectRoot: admitted.projectRoot,
398
- home: env.home,
399
- });
400
- if (dossierSection !== undefined) {
401
- turnRequest = {
402
- ...turnRequest,
403
- continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
404
- };
405
- }
406
- }
397
+ // 0081 non-dialogue face: freeze pointer section under run/attachments/.
398
+ // Seat consumes via loadCaseDossierReadingMaterial → existing agent-start
399
+ // readingMaterial / systemPrompt.materials fold. Caller instruction, empty
400
+ // request, and resume --message stay verbatim.
401
+ await deliverCaseDossierAsAttachment({
402
+ ticketNumber: admitted.ticketNumber,
403
+ projectRoot: admitted.projectRoot,
404
+ home: env.home,
405
+ runDirectory: admitted.runDirectory,
406
+ });
407
407
  // Authoritative host write happens here, at the real dispatch boundary —
408
408
  // immediately before the turn actually starts, after every retryable
409
409
  // pre-turn step above has succeeded on this attempt (#840 r9 判词 class 2).
@@ -784,9 +784,9 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
784
784
  ...(env.model === undefined ? {} : { model: env.model }),
785
785
  ...pickEngineAxis(env),
786
786
  ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
787
- ...(admitted.correlationId === undefined && env.correlationId === undefined
787
+ ...(env.correlationId === undefined && admitted.correlationId === undefined
788
788
  ? {}
789
- : { correlationId: admitted.correlationId ?? env.correlationId }),
789
+ : { correlationId: env.correlationId ?? admitted.correlationId }),
790
790
  continuation: {
791
791
  kind: "resume",
792
792
  prompt,
@@ -999,12 +999,7 @@ export async function runPostAdmissionSeatResume(input) {
999
999
  dispatch: async (turnRequest) => {
1000
1000
  const result = await dispatchPostAdmissionTurn({
1001
1001
  admitted: loaded.admitted,
1002
- env: {
1003
- ...input.env,
1004
- ...(loaded.admitted.correlationId === undefined
1005
- ? {}
1006
- : { correlationId: loaded.admitted.correlationId }),
1007
- },
1002
+ env: input.env,
1008
1003
  io: attemptIo,
1009
1004
  request: turnRequest,
1010
1005
  lease,
@@ -169,7 +169,8 @@ async function readRoleRunStateDisk(runDirectory) {
169
169
  record.role !== "gatekeeper" &&
170
170
  record.role !== "navigator" &&
171
171
  record.role !== "auditor" &&
172
- record.role !== "diarist") {
172
+ record.role !== "diarist" &&
173
+ record.role !== "secretariat") {
173
174
  return undefined;
174
175
  }
175
176
  if (record.state !== "admitted" &&
@@ -1411,6 +1412,17 @@ export async function loadResumableDiaristRun(home, runId, authority) {
1411
1412
  };
1412
1413
  return seatLoadedResult(loaded, admitted);
1413
1414
  }
1415
+ export async function loadResumableSecretariatRun(home, runId, authority) {
1416
+ const loaded = await loadResumableRunRecord(home, runId, authority);
1417
+ if (loaded.run.role !== "secretariat") {
1418
+ throw new CliUsageError(`role run ${runId} belongs to ${loaded.run.role}, not secretariat`);
1419
+ }
1420
+ const admitted = {
1421
+ role: "secretariat",
1422
+ ...resumedBaseAdmitted(loaded),
1423
+ };
1424
+ return seatLoadedResult(loaded, admitted);
1425
+ }
1414
1426
  /**
1415
1427
  * Load a resumable Merger run for resume. Derived envelope + internal input path
1416
1428
  * are restored from the admitted request (#114).
@@ -33,6 +33,7 @@ import { NOTARY_OUTPUT_TOOL_NAME, } from "../notary-contracts.js";
33
33
  import { COUNTERSIGN_OUTPUT_TOOL_NAME, } from "../countersign-contracts.js";
34
34
  import { GLEANER_LEFT_OUTPUT_TOOL_NAME, } from "../gleaner-left-contracts.js";
35
35
  import { DIARIST_OUTPUT_TOOL_NAME, } from "../diarist-contracts.js";
36
+ import { SECRETARIAT_OUTPUT_TOOL_NAME, } from "../secretariat-contracts.js";
36
37
  import { INSPECTOR_OUTPUT_TOOL_NAME, } from "../inspector-contracts.js";
37
38
  import { GATEKEEPER_OUTPUT_TOOL_NAME, } from "../package-contracts/gatekeeper-output.js";
38
39
  import { NAVIGATOR_OUTPUT_TOOL_NAME, } from "../package-contracts/navigator-output.js";
@@ -672,44 +673,7 @@ async function loadBoundAuditorVolumes(sessionFile) {
672
673
  const parentId = parentEntries.find((entry) => entry.type === "session")?.id;
673
674
  if (parentId === undefined)
674
675
  return undefined;
675
- const isResumeEnvelopeBytes = (value) => {
676
- if (typeof value !== "string")
677
- return false;
678
- if (value.length === 0)
679
- return true;
680
- const nl = value.indexOf("\n");
681
- const firstLine = nl === -1 ? value : value.slice(0, nl);
682
- const body = firstLine === "" && nl !== -1 ? value.slice(nl + 1) : value;
683
- return body.startsWith("本次配置的劳务引擎及其手册:") || body.startsWith("- engine:");
684
- };
685
- const isResumeEnvelope = (msg) => {
686
- if (!isRecord(msg) || msg.role !== "user")
687
- return false;
688
- const text = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : undefined;
689
- if (isResumeEnvelopeBytes(text))
690
- return true;
691
- const content = msg.content;
692
- if (Array.isArray(content)) {
693
- return content.some((p) => isRecord(p) && (isResumeEnvelopeBytes(p.text) || isResumeEnvelopeBytes(p.content)));
694
- }
695
- return false;
696
- };
697
- let latestParentUserIndex = -1;
698
- for (let i = parentEntries.length - 1; i >= 0; i -= 1) {
699
- const entry = parentEntries[i];
700
- if (entry?.type !== "message" || entry.message?.role !== "user")
701
- continue;
702
- if (isResumeEnvelope(entry.message))
703
- continue;
704
- latestParentUserIndex = i;
705
- break;
706
- }
707
676
  const childDirectories = [join(dirname(sessionFile), "auditor-roles")];
708
- // Auto-resume seam (owner A): stale check must ignore resume envelope and
709
- // prioritize retention. Previous `attemptEntryIndex < latest` discarded the
710
- // first attempt's child after resume advanced latest, losing retentionFailure
711
- // when retry had no compliance entry. Fix: ignore envelope for staleness and
712
- // prefer any valid compliance failure before falling back to primary.
713
677
  const valid = [];
714
678
  let sawAnyDirectory = false;
715
679
  for (const childDirectory of childDirectories) {
@@ -759,9 +723,6 @@ async function loadBoundAuditorVolumes(sessionFile) {
759
723
  const attemptEntryId = typeof bindingParent?.attemptEntryId === "string"
760
724
  ? bindingParent.attemptEntryId
761
725
  : undefined;
762
- const attemptEntryIndex = attemptEntryId === undefined
763
- ? -1
764
- : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
765
726
  const boundSessionFile = typeof bindingParent?.sessionFile === "string"
766
727
  ? bindingParent.sessionFile
767
728
  : typeof header.parentSession === "string"
@@ -769,10 +730,8 @@ async function loadBoundAuditorVolumes(sessionFile) {
769
730
  : undefined;
770
731
  if (boundSessionFile !== sessionFile)
771
732
  continue;
772
- if (bindingParent !== undefined &&
773
- (bindingParent.sessionId !== parentId || attemptEntryIndex < latestParentUserIndex)) {
733
+ if (bindingParent !== undefined && bindingParent.sessionId !== parentId)
774
734
  continue;
775
- }
776
735
  if (bindingParent === undefined && header.parentSession !== sessionFile)
777
736
  continue;
778
737
  valid.push({
@@ -781,8 +740,8 @@ async function loadBoundAuditorVolumes(sessionFile) {
781
740
  sessionFile,
782
741
  ...(attemptEntryId === undefined ? {} : { attemptEntryId }),
783
742
  });
784
- // Keep every qualifying interval in the current parent-user range.
785
- // A single first-match break drops later same-user summons failures (#636).
743
+ // Keep every interval bound to this parent. Auditor payload is relayed as
744
+ // recorded; code does not expire it from later user-message shape (#858).
786
745
  }
787
746
  }
788
747
  }
@@ -843,14 +802,6 @@ function providerStopFallbackFromAuditorVolumes(volumes) {
843
802
  }
844
803
  return undefined;
845
804
  }
846
- /** Recover a provider stop from the auditor child bound to the current parent attempt. */
847
- export async function readBoundAuditorKnownFailure(sessionFile) {
848
- const volumes = await loadBoundAuditorVolumes(sessionFile);
849
- if (volumes === undefined)
850
- return undefined;
851
- return complianceFailureFromAuditorVolumes(volumes)
852
- ?? providerStopFallbackFromAuditorVolumes(volumes);
853
- }
854
805
  /** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
855
806
  async function readBoundAuditorComplianceFailure(sessionFile) {
856
807
  const volumes = await loadBoundAuditorVolumes(sessionFile);
@@ -2361,6 +2312,16 @@ async function settleLawfulDiaristTerminalResult(admitted, authority, scope) {
2361
2312
  export async function trySettleDiaristTerminalResult(admitted, authority, scope) {
2362
2313
  return settleLawfulDiaristTerminalResult(admitted, authority, scope);
2363
2314
  }
2315
+ async function settleLawfulSecretariatTerminalResult(admitted, authority, scope) {
2316
+ return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2317
+ role: "secretariat",
2318
+ toolName: SECRETARIAT_OUTPUT_TOOL_NAME,
2319
+ }, scope);
2320
+ }
2321
+ /** Try to settle a lawful Secretariat Terminal; undefined only for genuine absence. */
2322
+ export async function trySettleSecretariatTerminalResult(admitted, authority, scope) {
2323
+ return settleLawfulSecretariatTerminalResult(admitted, authority, scope);
2324
+ }
2364
2325
  async function settleLawfulInspectorTerminalResult(admitted, authority, scope) {
2365
2326
  return settleLawfulSeatAcceptedTerminalResult(admitted, authority, {
2366
2327
  role: "inspector",
@@ -169,11 +169,15 @@ async function summonPublicRole(options) {
169
169
  env: {
170
170
  ...built.env,
171
171
  stationChild: true,
172
+ // Forward composition-root adapters so nested court stations (e.g.
173
+ // countersign → diarist) select the same faux/production table (#924).
174
+ ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
172
175
  ...config.autoResumeLimit === void 0 ? {} : { autoResumeLimit: config.autoResumeLimit },
173
176
  ...options.signal === void 0 ? {} : { signal: options.signal },
174
177
  ...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
175
178
  ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
176
179
  ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
180
+ ...options.correlationId === void 0 || options.correlationId.trim() === "" ? {} : { correlationId: options.correlationId },
177
181
  ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
178
182
  }
179
183
  };
@@ -276,6 +280,16 @@ async function summonPublicRole(options) {
276
280
  result = stepped.ok;
277
281
  break;
278
282
  }
283
+ case "countersign": {
284
+ const [{ runPublicCountersign }, { parseCountersignArgv }] = await Promise.all([
285
+ import("./public-cli/countersign-run.js"),
286
+ import("./public-cli/invocation.js")
287
+ ]);
288
+ const stepped = await runPrepared(parseCountersignArgv, (env, once) => runPublicCountersign(options.argv, env, io, once));
289
+ if ("fail" in stepped) return stepped.fail;
290
+ result = stepped.ok;
291
+ break;
292
+ }
279
293
  }
280
294
  const stderr = captured?.stderrText();
281
295
  const runDirectory = result.admitted?.runDirectory;
@@ -0,0 +1,8 @@
1
+ const SECRETARIAT_OUTPUT_TOOL_NAME = "ak_secretariat_output";
2
+ const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME = "ak_secretariat_summon_countersign";
3
+ const SECRETARIAT_ACCEPTED_TEXT = "\u4E2D\u4E66\u7701\u56DE\u6267\u5DF2\u63A5\u53D7";
4
+ export {
5
+ SECRETARIAT_ACCEPTED_TEXT,
6
+ SECRETARIAT_OUTPUT_TOOL_NAME,
7
+ SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME
8
+ };
@@ -152,25 +152,41 @@ function resolveSitianVolumeCategory(kind) {
152
152
  }
153
153
  return kind;
154
154
  }
155
+ const SITIAN_RECORDS_LEAF = "records.jsonl";
156
+ function ticketProvenanceUnderBookPaths(ledgerHome, bookKey, ticketId) {
157
+ const sessionDir = join(activationBookDirectory(ledgerHome, bookKey), ticketId);
158
+ return { sessionDir, recordFile: join(sessionDir, SITIAN_RECORDS_LEAF) };
159
+ }
155
160
  function resolveSitianRecordPathInLedger(input, ledgerHome) {
156
161
  const category = resolveSitianVolumeCategory(input.kind);
157
162
  const ticketNumber = category === "ticket-provenance" ? typeof input.subject === "string" && /^[1-9][0-9]*$/.test(input.subject) ? input.subject : typeof input.subject === "object" && typeof input.subject.ticketNumber === "number" && Number.isSafeInteger(input.subject.ticketNumber) && input.subject.ticketNumber > 0 ? String(input.subject.ticketNumber) : void 0 : void 0;
158
163
  let sessionDir;
164
+ let recordFile;
159
165
  if (ticketNumber !== void 0) {
160
- const bookDir = activationBookDirectory(
166
+ const paths = ticketProvenanceUnderBookPaths(
161
167
  ledgerHome,
162
- resolveBookKeyFromGit(input.cwd ?? process.cwd())
168
+ resolveBookKeyFromGit(input.cwd ?? process.cwd()),
169
+ ticketNumber
163
170
  );
164
- sessionDir = join(bookDir, ticketNumber);
171
+ sessionDir = paths.sessionDir;
172
+ recordFile = paths.recordFile;
165
173
  } else {
166
174
  if (input.sessionParent === void 0 || input.sessionParent.length === 0 || !physicallyContainedIn(ledgerHome, input.sessionParent)) {
167
175
  throw new Error("Sitian record ownership requires a parent session inside the ledger home");
168
176
  }
169
177
  sessionDir = join(dirname(input.sessionParent), category);
178
+ recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
170
179
  }
171
- const recordFile = join(sessionDir, "records.jsonl");
172
180
  return { sessionDir, recordFile, ledgerHome };
173
181
  }
182
+ function projectTicketRecordsPathShape() {
183
+ const { recordFile } = ticketProvenanceUnderBookPaths(
184
+ join("~", ".ak-roles"),
185
+ "<\u7C3F>",
186
+ "<\u7968\u53F7>"
187
+ );
188
+ return recordFile.replace(/\\/g, "/");
189
+ }
174
190
  function resolveSitianRecordPath(input) {
175
191
  const ledgerHome = input.home !== void 0 && input.home.length > 0 ? resolveActivationLedgerHome(input.home) : resolveActivationLedgerHomeForPath(input.sessionParent);
176
192
  return resolveSitianRecordPathInLedger(input, ledgerHome);
@@ -210,6 +226,7 @@ function appendSitianRecord(input) {
210
226
  export {
211
227
  S4_SUBMISSION_LEDGER_KINDS,
212
228
  appendSitianRecord,
229
+ projectTicketRecordsPathShape,
213
230
  resolveSitianRecordPath,
214
231
  resolveSitianRecordPathInLedger,
215
232
  resolveSitianVolumeCategory
@@ -130,6 +130,7 @@ export default function roleRuntime(pi: ExtensionAPI): void {
130
130
  loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
131
131
  loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
132
132
  loadDiaristSoul: () => loadMainRoleSessionMaterials("diarist"),
133
+ loadSecretariatSoul: () => loadMainRoleSessionMaterials("secretariat"),
133
134
  loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
134
135
  loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
135
136
  loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4321",
3
+ "version": "0.1.4387",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -33,6 +33,7 @@ const entries = [
33
33
  "canonical-json",
34
34
  "compliance-transport",
35
35
  "countersign-contracts",
36
+ "secretariat-contracts",
36
37
  "doctor-contracts",
37
38
  "engine-detour",
38
39
  "engine-detour-tool",
@@ -0,0 +1,26 @@
1
+ 中书省 Soul
2
+ 你是中书省,出令省。调用者给你票,你交出可送庭的票面:送庭,封驳则改,改则再送,直到署或上呈。
3
+ 立法与材料
4
+
5
+ * 第一法源:全局宪法、本仓宪法、本票起居录。
6
+ * 第二法源:适用 ADR,按票面触及的席位、接缝、术语自行查全,草稿引没引都算,逐条列决定;CONTEXT.md。
7
+ * 审查对象:派单文,票面、父票与相关票。
8
+ * 输出参考:《票面法》《质量法》、给事中与符宝郎 Soul。票面最终要过他们的审。
9
+ * 第二法源合法性:第二法源是否合法,违背第一法源,又没有陛下原话的;直接封驳,拿不准的,上呈,不准署
10
+
11
+ 合法性检查
12
+
13
+ * 审查派单文、票面、父票与相关票。逐项对第一、第二法源:不得违法源,不得拿局部原话替整段意思,不得以第二法源反压第一法源。违法则封驳;拿不准且关系裁量的,上呈。
14
+
15
+ 审票
16
+
17
+ * 该有的都有,不该有的都没有。法源要求的决定、边界、行为、验收均须落到票面,或明确由谁承接;无依据的设计、推演、夹带、越权内容不得写进票面。每一句只能是原话、可见行为、可跑验收或法源路径。
18
+ * 票面写下来的事,必须就是陛下要办的那件事。原话读整段,结合起居录、上下文与相关票判断真实意图;不得断章取义,也不得因“有一句话能对应”就视为正确。逐项检查目标、偏差、结果、边界与验收是否写对、有没有写偏、漏掉或扩大。能改则改票重送,不能自行裁定则上呈。
19
+
20
+ 每次重交整张票重审,不沿用上庭结论。
21
+ 判词
22
+ 事实与处方分开对宪:裁决落法度与事实;施工设计与实现细节过早堆上票面即为失职。
23
+
24
+ * 署(converged)——自检无误,可以送下一个衙门;
25
+ * 封驳(continue)——存在必须退回重议的问题;写明缺什么、属哪一步;
26
+ * 上呈(escalate)——无法正常署/封驳时。上呈求助
@@ -0,0 +1,9 @@
1
+ # 票面法
2
+
3
+ 票面面向执行,只交代:当前应然、当前偏差与证据、获授权的结果、覆盖各项主张的可观察验收;标题与次序不作契约。
4
+
5
+ 历史只在证明现行法、冲突或根因不可缺时保留,不写考古追责。
6
+
7
+ 重大决定须追至本票起居录或现行法源;不得把票面、prompt、驳回语或实现现状升格为授权。相关 ADR 写明路径。
8
+
9
+ 事实与处方分开:票面钉行为结果,不提前钉未裁定的实现。类级问题按性质圈界,位置与数量只作证据,不作白名单。
@@ -1,6 +1,6 @@
1
1
  import { Type, type Static } from "typebox";
2
2
  import { openToolObject } from "./open-tool-schema.ts";
3
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
3
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
4
4
 
5
5
  // #836 r16 class 3: execute() reads no params for this tool
6
6
  // (src/collector-role.ts:431-459) — a closed root only rejects the role for
@@ -124,7 +124,7 @@ export const collectorOutputBaseSchema = openToolObject(
124
124
  );
125
125
 
126
126
  /** Runtime owns the observed evidence; the model submits findings and signals sole-final submission. */
127
- export const collectorOutputArgsSchema = withInfrastructureFailureDeclaration(
127
+ export const collectorOutputArgsSchema = withTerminatingOutputDeclarations(
128
128
  collectorOutputBaseSchema,
129
129
  );
130
130
 
@@ -1,7 +1,7 @@
1
1
  import type { Static } from "typebox";
2
2
  import { Type } from "typebox";
3
3
 
4
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
4
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
5
5
  import {
6
6
  COUNTERSIGN_OUTPUT_TOOL_NAME,
7
7
  validateRecordedCountersignOutput,
@@ -22,7 +22,7 @@ export type { CountersignVerdict };
22
22
  // classifies the recorded value and reasks the countersign itself when it
23
23
  // isn't one of the three states — code, not the transport, does that work.
24
24
  /** 给事中票庭审读五问的交卷形状(ADR 0074);形状指引,非 schema 闸。 */
25
- export const countersignVerdictSchema = withInfrastructureFailureDeclaration(
25
+ export const countersignVerdictSchema = withTerminatingOutputDeclarations(
26
26
  Type.Object(
27
27
  {
28
28
  countersignStatus: Type.Unknown({ description: "converged | continue | escalate。非三态时请重读后重交,勿改标。" }),
@@ -2,7 +2,7 @@ import type { Static } from "typebox";
2
2
  import { Type } from "typebox";
3
3
 
4
4
  import { openToolObject } from "./open-tool-schema.ts";
5
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
5
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
6
6
  import {
7
7
  DIARIST_OUTPUT_TOOL_NAME,
8
8
  validateRecordedDiaristOutput,
@@ -20,7 +20,7 @@ export { validateRecordedDiaristOutput };
20
20
  * 起居郎交卷形状;形状指引,非 schema 闸。
21
21
  * #901:交边界(sessions)+ 可选坏行补写(amendments);正文由机械投影。
22
22
  */
23
- export const diaristOutputSchema = withInfrastructureFailureDeclaration(
23
+ export const diaristOutputSchema = withTerminatingOutputDeclarations(
24
24
  openToolObject(
25
25
  Type.Object({
26
26
  status: Type.Unknown({
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { canonicalJson } from "./canonical-json.ts";
3
3
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
4
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
4
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
5
5
 
6
6
  export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
7
7
  export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
@@ -87,7 +87,7 @@ const doctorSubmissionVariants = Type.Union([
87
87
  missingEvidence: Type.Array(Type.Object({ need: nonblank, targetKeys: evidenceIds }, { additionalProperties: true }), { description: "如实证词所需而尚缺的证据" }),
88
88
  }, { additionalProperties: false, description: "证据不足以支撑如实案证词" }),
89
89
  ]);
90
- export const doctorSubmissionSchema = withInfrastructureFailureDeclaration(
90
+ export const doctorSubmissionSchema = withTerminatingOutputDeclarations(
91
91
  openToolObjectFromUnion(doctorSubmissionVariants),
92
92
  );
93
93
  // #836 r16 class 3: action tool — code reads evidenceId to look up the Map entry
@@ -5,7 +5,6 @@
5
5
  * Engine process failures stop through the host infrastructure-failure seam.
6
6
  * Caller AbortSignal cancellation propagates unchanged.
7
7
  */
8
- import { basename } from "node:path";
9
8
  import { Type, type Static } from "typebox";
10
9
  import type { HostContext, HostToolDefinition, HostToolResult, RoleHost } from "./host-contracts.ts";
11
10
 
@@ -21,14 +20,9 @@ import {
21
20
  engineDetourStdoutByteLength,
22
21
  reportEngineDetourCall,
23
22
  } from "./engine-detour-usage.ts";
23
+ import { runIdFromRunDirectory } from "./run-terminal-artifacts.ts";
24
+
24
25
 
25
- /** runDirectory leaf is `<runId>@<role>`; subject is optional. */
26
- function basenameRunId(runDirectory: string): string | undefined {
27
- const leaf = basename(runDirectory);
28
- const at = leaf.indexOf("@");
29
- if (at <= 0) return undefined;
30
- return leaf.slice(0, at);
31
- }
32
26
 
33
27
  // #836 r16 class 3: argv required/minItems/element-minLength stay — execute()
34
28
  // must obtain the first item as the executable and spawn it (below; #82-98).
@@ -131,7 +125,7 @@ export function createEngineDetourToolDefinition(input: {
131
125
  const runDirectory = typeof ctx.runDirectory === "string" && ctx.runDirectory.length > 0
132
126
  ? ctx.runDirectory
133
127
  : undefined;
134
- const runId = runDirectory === undefined ? undefined : basenameRunId(runDirectory);
128
+ const runId = runDirectory === undefined ? undefined : runIdFromRunDirectory(runDirectory);
135
129
  // Public-invocation scope + selected host from Host envelope only — never
136
130
  // courtAttemptId, never sidecar file, never pre-spawn invocation.json I/O.
137
131
  const invocationScopeId =