@akagilnc/pi-workflow-roles 0.1.4782 → 0.1.4802

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 +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +104 -215
  4. package/dist/headless-host/production-host.js +149 -229
  5. package/dist/migrate-book-topology.js +1 -1
  6. package/dist/pi/role-turn-host.js +1 -1
  7. package/dist/public-cli/auto-resume.js +9 -15
  8. package/dist/public-cli/countersign-run.js +75 -87
  9. package/dist/public-cli/diarist-run.js +6 -26
  10. package/dist/public-cli/inspector-run.js +1 -1
  11. package/dist/public-cli/main.js +64 -177
  12. package/dist/public-cli/notary-run.js +0 -3
  13. package/dist/public-cli/option-definitions.js +1 -1
  14. package/dist/public-cli/post-admission.js +35 -65
  15. package/dist/public-cli/reviewer-run.js +1 -1
  16. package/dist/public-cli/run-lifecycle.js +14 -46
  17. package/dist/public-cli/seat-ticket-binding.js +11 -25
  18. package/dist/public-role-summons.js +2 -0
  19. package/package.json +1 -1
  20. package/souls/coder.md +1 -2
  21. package/src/headless-host/role-turn-host.ts +49 -12
  22. package/src/pi/role-turn-host.ts +1 -1
  23. package/src/public-cli/auto-resume.ts +19 -18
  24. package/src/public-cli/countersign-run.ts +76 -129
  25. package/src/public-cli/diarist-run.ts +6 -33
  26. package/src/public-cli/inspector-run.ts +1 -1
  27. package/src/public-cli/invocation.ts +5 -0
  28. package/src/public-cli/notary-run.ts +3 -7
  29. package/src/public-cli/option-definitions.ts +1 -1
  30. package/src/public-cli/post-admission.ts +37 -69
  31. package/src/public-cli/reviewer-run.ts +1 -1
  32. package/src/public-cli/run-lifecycle.ts +18 -66
  33. package/src/public-cli/seat-ticket-binding.ts +12 -28
  34. package/src/public-role-summons.ts +15 -6
  35. package/src/role-runtime.ts +9 -0
@@ -8,7 +8,7 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildAutoResumeContinuationPrompt, buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildAutoResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { bindAdmittedTicketNumber, buildInstructionTransportPrompt, freezeAttachmentsIntoRun, relocateAdmittedRunToTicket, } from "./invocation.js";
14
14
  import { readRecordedSubmissionRows } from "../submission-ledger.js";
@@ -655,12 +655,9 @@ export async function dispatchPostAdmissionTurn(input) {
655
655
  // Settlement already sealed accepted — a cleanup failure here must
656
656
  // not erase that fact or make the caller replay this court's
657
657
  // summons over already-delivered work (#840 已交劳动只整理终局不重做).
658
- // A later bare resume self-heals: buildRequestAfterLease finds the
659
- // open court already sealed and clears it then (documented
660
- // continue-under-failure contract, not a swallow — 失败诚实宪法 真因
661
- // 必须落痕) — durably, since every auto-resume attempt's io here is
662
- // dummyIo (#840 r9 判词 class 1).
663
- await recordBestEffortPostDispatchDiagnostic(admitted, env, `current-court cleanup failed after accepted settlement (best-effort continue, self-heals on next resume): ${describeErrorIdentity(error)}`, io);
658
+ // Preserve the cleanup failure as a post-dispatch diagnostic; the
659
+ // accepted settlement remains authoritative (#840 r9 判词 class 1).
660
+ await recordBestEffortPostDispatchDiagnostic(admitted, env, `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`, io);
664
661
  }
665
662
  }
666
663
  settledOutcome = {
@@ -809,8 +806,7 @@ export async function dispatchPostAdmissionTurn(input) {
809
806
  /**
810
807
  * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
811
808
  * seat-table model/engine/timeout axes, restored correlation, and either
812
- * - manual resume (no same-ticket summons): package envelope / optional caller
813
- * message, with engine-axis handbook via buildResumeContinuationPrompt, or
809
+ * - manual resume (no same-ticket summons): caller message bytes, or
814
810
  * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
815
811
  * attachment paths only — no「请重读」、no code-authored content substitute,
816
812
  * no engine handbook packaging (#750/#755/#879).
@@ -843,12 +839,7 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
843
839
  prompt = request.message;
844
840
  }
845
841
  else {
846
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
847
- prompt = buildResumeContinuationPrompt({
848
- packageRoot: env.packageRoot,
849
- ...pickEngineAxis(env),
850
- message: request.message,
851
- });
842
+ prompt = request.message;
852
843
  }
853
844
  }
854
845
  else if (summonsPrepared !== undefined) {
@@ -864,11 +855,7 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
864
855
  prompt = "";
865
856
  }
866
857
  else {
867
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
868
- prompt = buildResumeContinuationPrompt({
869
- packageRoot: env.packageRoot,
870
- ...pickEngineAxis(env),
871
- });
858
+ prompt = "";
872
859
  }
873
860
  return {
874
861
  packageRoot: env.packageRoot,
@@ -889,9 +876,10 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
889
876
  };
890
877
  }
891
878
  /**
892
- * Hold the writer lease through after-lease build, then hand off to dispatch.
893
- * Builder (or any throw before dispatch) must release here — dispatch's finally
894
- * only runs after this handoff (manual resume and station-child auto-resume).
879
+ * Build the turn request, then hand off to dispatch. When a writer lease is
880
+ * held, release it if build throws before handoff — dispatch's finally only
881
+ * runs after this handoff (manual resume and station-child auto-resume).
882
+ * #987: lease is optional; auto-resume no longer pre-acquires before host CLI.
895
883
  */
896
884
  async function dispatchAfterWriterLease(input) {
897
885
  let handedOffToDispatch = false;
@@ -901,7 +889,7 @@ async function dispatchAfterWriterLease(input) {
901
889
  return await input.dispatch(request);
902
890
  }
903
891
  finally {
904
- if (!handedOffToDispatch) {
892
+ if (!handedOffToDispatch && input.lease !== undefined) {
905
893
  await input.lease.release();
906
894
  }
907
895
  }
@@ -916,7 +904,7 @@ function isAlreadyFrozenSummonsAttachment(runDirectory, attachmentPath) {
916
904
  * Freeze same-ticket summons attachments into the retained run directory (#637).
917
905
  * No-op materials (no paths / instruction-only) skip the freeze.
918
906
  * Paths already under this run's attachments/ are the accepted freeze identity —
919
- * reuse them; do not re-freeze from external originals on bare resume.
907
+ * reuse them for the same internal re-summons flow.
920
908
  * Manual resume never calls this — old attachment semantics stay intact.
921
909
  */
922
910
  export async function prepareSummonsResumeMaterials(runDirectory, summons) {
@@ -937,23 +925,22 @@ export async function prepareSummonsResumeMaterials(runDirectory, summons) {
937
925
  return { instruction, instructionEmpty, attachments };
938
926
  }
939
927
  /**
940
- * Shared manual-resume orchestration for seats whose continuation is the
941
- * package resume envelope (#599 / #633): load once → structural rejection →
928
+ * Shared manual-resume orchestration for seats (#599 / #633):
929
+ * load once → structural rejection →
942
930
  * optional seat afterAdmittedLoad (method material / controlled failure) →
943
931
  * seat turn projection → station-child auto-resume or public manual resume.
944
932
  * Seat-owned loader
945
933
  * validation, turn builder, and adapters stay on the seat.
946
934
  *
947
- * Court open/recovery transaction (#637): under the existing writer lease,
948
- * read currentCourt, judge seal, clear (bound to the judged court id), freeze,
949
- * and record. No pre-lease clear or stale court-snapshot consumption.
935
+ * Court handling (#637): public manual resume reads only the open court's
936
+ * settlement identity; internal re-summons may freeze and record its materials.
950
937
  */
951
938
  export async function runPostAdmissionSeatResume(input) {
952
939
  let request = input.request;
953
940
  // Load once for runDirectory / structural rejection / afterAdmittedLoad.
954
941
  // Court identity for public manual resume is judged in the turn builder
955
942
  // (#987: no package writer-lease gate before host CLI resume). Station-child
956
- // auto-resume still acquires the shared lease in runWithAutoResumeLoop.
943
+ // auto-resume shares that rule via runWithAutoResumeLoop (no pre-acquire).
957
944
  let loaded;
958
945
  try {
959
946
  loaded = await input.load(request);
@@ -982,32 +969,17 @@ export async function runPostAdmissionSeatResume(input) {
982
969
  let preparedCleanup;
983
970
  const buildRequestAfterLease = async () => {
984
971
  let openCourtAttemptId;
985
- // Build uses the admitted for this resume (rehydrated when open court
986
- // materials ride). Settlement identity stays on the outer admitted.
987
- // Name kept for station-child callers that still build under lease.
988
- let admittedForBuild = loaded.admitted;
989
- // Bare resume: open-court pointer is the continue signal (not ledger seal).
972
+ // Settlement identity stays on the outer admitted.
973
+ const admittedForBuild = loaded.admitted;
974
+ // Bare resume keeps the open court's settlement identity, but public resume
975
+ // never re-delivers the prior summons or its attachments (#987).
990
976
  if (request.summons === undefined) {
991
977
  const openCourt = await readCurrentCourt(admittedForBuild.runDirectory);
992
978
  if (openCourt !== undefined) {
993
979
  openCourtAttemptId = openCourt.courtAttemptId;
994
- request = {
995
- runId: request.runId,
996
- ...(request.message === undefined
997
- ? {}
998
- : { message: request.message }),
999
- ...(openCourt.summons === undefined
1000
- ? {}
1001
- : { summons: openCourt.summons }),
1002
- };
1003
- if (openCourt.summons !== undefined) {
1004
- const reloaded = await input.load(request);
1005
- admittedForBuild = reloaded.admitted;
1006
- }
1007
980
  }
1008
981
  }
1009
- // Freeze external paths once; rewrite summons to the frozen identity so
1010
- // currentCourt + later bare resume reuse the accepted snapshot.
982
+ // Internal re-summons freezes external paths once and records that identity.
1011
983
  if (request.summons !== undefined) {
1012
984
  const prepared = await prepareSummonsResumeMaterials(admittedForBuild.runDirectory, request.summons);
1013
985
  if (prepared !== undefined &&
@@ -1045,10 +1017,10 @@ export async function runPostAdmissionSeatResume(input) {
1045
1017
  }
1046
1018
  return turnRequest;
1047
1019
  };
1048
- // Court recovery / open, then dispatch. Public manual resume does not take a
1049
- // package writer lease before the host CLI (#987 / ADR 0080 one-shot).
1050
- // Station-child same-ticket/same-parent resume is call-local auto-resume
1051
- // (#840 / #416) and still acquires the shared lease in its loop.
1020
+ // Court recovery / open, then dispatch. Public manual resume and station-child
1021
+ // auto-resume both pass through to the host CLI without a package writer-lease
1022
+ // pre-gate (#987 / ADR 0080). Station-child same-ticket/same-parent resume is
1023
+ // still call-local auto-resume (#840 / #416).
1052
1024
  // afterAdmittedPrepare runs inside this try so mint failure and cleanup share one finally.
1053
1025
  try {
1054
1026
  if (input.afterAdmittedPrepare !== undefined) {
@@ -1079,14 +1051,13 @@ export async function runPostAdmissionSeatResume(input) {
1079
1051
  // Same as public manual resume: prior-court sealed acceptance is not a
1080
1052
  // redispatch brake (#833). New-court station-child turns still auto-resume.
1081
1053
  dispatch: async (payload, lease, _isFirst, attemptIo) => dispatchAfterWriterLease({
1082
- lease,
1054
+ ...(lease === undefined ? {} : { lease }),
1083
1055
  build: async () => {
1084
1056
  // #840 r8 判词 class 2: this call-local retry must keep this
1085
1057
  // court's frozen summons / 交卷 body / attachments verbatim
1086
1058
  // (same object as firstTurn) and project only the minimal
1087
- // host-needed resume trigger — never the manual-resume engine
1088
- // handbook (buildResumeContinuationPrompt), which would replace
1089
- // a 审核循环 same-ticket continuation with a bare outsourcing
1059
+ // host-needed resume trigger — never engine handbook material,
1060
+ // which would replace a 审核循环 same-ticket continuation with a bare outsourcing
1090
1061
  // 「重新读」 envelope (#755 contract, resumeTurnRequestProjectionOptions
1091
1062
  // above). RESUME_TRANSPORT_ENVELOPE is the same package-owned,
1092
1063
  // non-semantic trigger that projection already uses for a
@@ -1110,7 +1081,7 @@ export async function runPostAdmissionSeatResume(input) {
1110
1081
  env,
1111
1082
  io: attemptIo,
1112
1083
  request: turnRequest,
1113
- lease,
1084
+ ...(lease === undefined ? {} : { lease }),
1114
1085
  adapters: stationAdapters,
1115
1086
  persistRunState: false,
1116
1087
  ...(input.effectiveEngine === undefined
@@ -1134,8 +1105,7 @@ export async function runPostAdmissionSeatResume(input) {
1134
1105
  });
1135
1106
  }
1136
1107
  catch (error) {
1137
- // Open-court rehydrate load under lease may still surface seat structural
1138
- // rejection (e.g. notary rejects caller message) — same exit face as pre-lease.
1108
+ // Seat preparation may still surface structural rejection.
1139
1109
  if (error instanceof CliUsageError) {
1140
1110
  presentStructuralRejection(error, input.io);
1141
1111
  return { exitCode: 2 };
@@ -1209,7 +1179,7 @@ export async function runPostAdmissionResumable(input) {
1209
1179
  },
1210
1180
  io: attemptIo,
1211
1181
  request,
1212
- lease,
1182
+ ...(lease === undefined ? {} : { lease }),
1213
1183
  adapters,
1214
1184
  persistRunState: false,
1215
1185
  // #600: every attempt (initial + auto-resume) writes seat engine when present.
@@ -1223,8 +1193,8 @@ export async function runPostAdmissionResumable(input) {
1223
1193
  * Manual resume: pass-through to the host CLI resume — no package writer-lease
1224
1194
  * pre-gate (#987), no sealed-accepted short-circuit (#833 / #416). Court open
1225
1195
  * (summons / message / open court) is built when using buildRequestAfterLease;
1226
- * sole-final stays per-attempt. Station-child auto-resume keeps shared lease
1227
- * acquire in runWithAutoResumeLoop + dispatchAfterWriterLease.
1196
+ * sole-final stays per-attempt. Station-child auto-resume shares the same
1197
+ * no-pre-gate rule via runWithAutoResumeLoop + dispatchAfterWriterLease.
1228
1198
  */
1229
1199
  export async function runPostAdmissionManualResume(input) {
1230
1200
  const { admitted, env, io, adapters, effectiveEngine, buildRequestAfterLease, } = input;
@@ -301,7 +301,7 @@ export async function runPublicReviewerResume(request, env, io) {
301
301
  ...(sandbox.executionCwd === undefined ? {} : { cwd: sandbox.executionCwd }),
302
302
  continuation: {
303
303
  kind: "resume",
304
- prompt: reviewerResumePrompt(activeEnv, effective.message),
304
+ prompt: effective.message ?? "",
305
305
  },
306
306
  });
307
307
  },
@@ -9,7 +9,7 @@ import { chmod, lstat, open, readdir, readFile, unlink, writeFile } from "node:f
9
9
  import { basename, isAbsolute, join } from "node:path";
10
10
  import { activationBookDirectory, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
11
  import { listBookRunDirectories } from "../role-run-placement.js";
12
- import { isSafePositiveTicketNumber, readBoardTicketNumber, } from "../run-ticket-number.js";
12
+ import { isSafePositiveTicketNumber } from "../run-ticket-number.js";
13
13
  import { CliUsageError } from "./cli-errors.js";
14
14
  import { readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
15
15
  export { clearTypedProviderHttpObservation, recordTypedProviderHttpStatus, readLatestTypedProviderHttpObservation, } from "../typed-provider-http.js";
@@ -31,44 +31,20 @@ export const AUTO_RESUME_LIMIT = 2;
31
31
  * Package-owned non-empty Chinese neutral resume transport (#959 / ADR 0073).
32
32
  * Used by:
33
33
  * - auto-resume (all seats via buildAutoResumeContinuationPrompt) — required so
34
- * hosts that reject empty stdin (codex) still receive a prompt;
35
- * - reviewer seat manual resume (reviewerResumePrompt) — same non-empty need on
36
- * that seat's own manual entry.
37
- * Generic bare `ak-role resume` stays empty-capable via selectResumeContinuationPrompt
38
- * / buildResumeContinuationPrompt — ADR 0080 keeps auto and generic-manual entries separate.
34
+ * hosts that reject empty stdin (codex) still receive a prompt.
35
+ * Public manual resume forwards only the caller's bytes (#987).
39
36
  */
40
37
  export const RESUME_TRANSPORT_ENVELOPE = "继续。";
41
- /**
42
- * Manual resume continuation selector (#471 / #600 / #736 / ADR 0080).
43
- * Message present → those bytes; absent → engine pointers only (may be empty).
44
- * Caller message (including blank) still wins verbatim when supplied.
45
- * Auto-resume must use buildAutoResumeContinuationPrompt — do not fold the
46
- * non-empty Chinese envelope into this shared manual selector (#959).
47
- */
48
- export function selectResumeContinuationPrompt(message, engineMaterial) {
49
- const lines = message !== undefined ? [message] : [];
50
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
51
- }
52
- /**
53
- * Manual resume continuation with engine material from the seat env (#600).
54
- * Bare manual resume stays empty-prompt-capable; auto-resume is a separate entry.
55
- */
56
- export function buildResumeContinuationPrompt(options) {
57
- return selectResumeContinuationPrompt(options.message, engineSessionMaterialFromOptions({
58
- packageRoot: options.packageRoot,
59
- ...pickEngineAxis(options),
60
- }));
61
- }
62
38
  /**
63
39
  * Auto-resume continuation only (#959 / ADR 0080).
64
40
  * Always non-empty: Chinese neutral envelope plus optional engine pointers.
65
41
  * Never call this from manual `ak-role resume`.
66
42
  */
67
43
  export function buildAutoResumeContinuationPrompt(options) {
68
- return selectResumeContinuationPrompt(RESUME_TRANSPORT_ENVELOPE, engineSessionMaterialFromOptions({
44
+ return appendEngineSessionMaterial([RESUME_TRANSPORT_ENVELOPE], engineSessionMaterialFromOptions({
69
45
  packageRoot: options.packageRoot,
70
46
  ...pickEngineAxis(options),
71
- }));
47
+ })).join("\n");
72
48
  }
73
49
  const RUN_STATE_FILE = "run-state.json";
74
50
  const WRITER_LOCK_FILE = "writer.lock";
@@ -398,8 +374,7 @@ export async function markRunTerminal(runDirectory) {
398
374
  if (current === undefined) {
399
375
  throw new Error("cannot mark terminal: run state missing");
400
376
  }
401
- // Preserve open currentCourt: terminal after a failed/incomplete court must still
402
- // let bare resume continue that court (#637).
377
+ // Preserve the open court's settlement identity after a failed/incomplete turn (#637).
403
378
  await writeRoleRunStateDisk(runDirectory, {
404
379
  runId: current.runId,
405
380
  role: current.role,
@@ -843,13 +818,14 @@ async function runHasFormedSessionPrincipal(runDirectory) {
843
818
  /**
844
819
  * Locate the latest retained run for one seat under a book (#637 / #747).
845
820
  * Same walk surface as findRunDirectoryById (listBookRunDirectories). Match by
846
- * parent run path (officer seats, #747) or by ticket number (countersign /
847
- * diarist principal). runId is UUIDv7 — lexicographic max is latest among runs
848
- * that formed a session principal. No parallel index.
821
+ * parent run path (officer / gate seats, #747 / #987). runId is UUIDv7 —
822
+ * lexicographic max is latest among runs that formed a session principal. No
823
+ * parallel index. Public ticket-number selection of a prior run was deleted
824
+ * (#987 Result 7); callers use explicit `ak-role resume <runId>`.
849
825
  * Only a truly missing book directory means no history; damage/permission errors propagate.
850
826
  */
851
827
  export async function findLatestRunIdForSeatTicket(input) {
852
- if (input.ticketNumber === undefined && input.parentRunPath === undefined) {
828
+ if (input.parentRunPath.trim() === "") {
853
829
  return undefined;
854
830
  }
855
831
  const ledgerHome = resolveActivationLedgerHome(input.home);
@@ -872,17 +848,9 @@ export async function findLatestRunIdForSeatTicket(input) {
872
848
  const runId = entry.slice(0, entry.length - suffix.length);
873
849
  if (runId.length === 0)
874
850
  continue;
875
- if (input.parentRunPath !== undefined) {
876
- const parentPath = await readRunParentPath(runDirectory);
877
- if (parentPath !== input.parentRunPath)
878
- continue;
879
- }
880
- else if (input.ticketNumber !== undefined) {
881
- // Same-ticket resume is board identity only — never migration-derived.
882
- const ticketNumber = await readBoardTicketNumber(runDirectory);
883
- if (ticketNumber !== input.ticketNumber)
884
- continue;
885
- }
851
+ const parentPath = await readRunParentPath(runDirectory);
852
+ if (parentPath !== input.parentRunPath)
853
+ continue;
886
854
  // Durable fact: never resume-select a provisional that never formed principal.
887
855
  if (!(await runHasFormedSessionPrincipal(runDirectory)))
888
856
  continue;
@@ -1,43 +1,29 @@
1
1
  /**
2
- * Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747 / #771).
2
+ * Shared same-parent resume seam for gate / officer seats (#635 / #637 / #747 / #987).
3
3
  *
4
- * ADR 0075(起居郎 LLM 自行认票): the diarist LLM recognizes the
5
- * court target; recognized ticket → provenance, truly unbound → no provenance,
6
- * or escalate when it cannot recognize one. Code does not re-judge the ticket
7
- * (锚定宪法; owner 2026-09-08: 代码不准做判断). Other seats reuse a typed
8
- * identity already handed over (起居郎 assertion / source-run / already-bound
9
- * resume) — they do not re-recognize from instruction. No CLI --ticket, no
10
- * attachment frontmatter. #747: officer same-parent resume also lives here.
11
- *
12
- * This module owns same-ticket / same-parent resume only.
4
+ * Lookup key is parent run path only (#747 / #987 Result 7). Public seats no
5
+ * longer select a prior run by ticket number — callers use explicit
6
+ * `ak-role resume <runId>`. Officer and gate countersign same-parent re-summons
7
+ * keep this seam. Lookup/resume failures propagate (失败诚实) — never wash into
8
+ * a fresh mint. Returns undefined when the caller declared an explicit fresh
9
+ * summons (`ak-role new`) or when no prior run exists; both mint new.
10
+ * freshSummons is required so no seat can drift back into its own skip branch.
13
11
  */
14
12
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
15
13
  import { findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
16
14
  /**
17
- * Sole same-seat → resume decision (#637 / #724 / #747).
18
- * Officer seats (notary/inspector/auditor) look up by parent run path; countersign
19
- * / diarist keep ticket-number principal. When found, runs resume with this
20
- * summons' materials. Lookup/resume failures propagate (失败诚实) — never wash
21
- * into a fresh mint. Returns undefined when the caller declared an explicit
22
- * fresh summons (`ak-role new`) or when no prior run exists; both mint new.
23
- * freshSummons is required so no seat can drift back into its own skip branch.
15
+ * Sole same-seat → resume decision by parent run path (#637 / #724 / #747 / #987).
24
16
  */
25
17
  export async function tryResumeSameTicketSeatRun(input) {
26
18
  if (input.freshSummons === true)
27
19
  return undefined;
28
- if (input.parentRunPath === undefined && input.ticketNumber === undefined) {
20
+ if (input.parentRunPath.trim() === "")
29
21
  return undefined;
30
- }
31
22
  const previousRunId = await findLatestRunIdForSeatTicket({
32
23
  home: input.home,
33
24
  bookKey: resolveBookKeyFromGit(input.projectRoot),
34
25
  role: input.role,
35
- ...(input.parentRunPath === undefined
36
- ? {}
37
- : { parentRunPath: input.parentRunPath }),
38
- ...(input.ticketNumber === undefined
39
- ? {}
40
- : { ticketNumber: input.ticketNumber }),
26
+ parentRunPath: input.parentRunPath,
41
27
  });
42
28
  if (previousRunId === undefined)
43
29
  return undefined;
@@ -205,6 +205,7 @@ async function summonPublicRole(options) {
205
205
  ...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
206
206
  ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
207
207
  ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
208
+ ...options.parentRunPath === void 0 || options.parentRunPath.trim() === "" ? {} : { parentRunPath: options.parentRunPath },
208
209
  ...options.correlationId === void 0 || options.correlationId.trim() === "" ? {} : { correlationId: options.correlationId },
209
210
  ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId },
210
211
  ...options.executionCwd === void 0 ? {} : { executionCwd: options.executionCwd }
@@ -707,6 +708,7 @@ async function summonGateOfficer(options) {
707
708
  argv: ["--project", options.cwd, "--", instruction],
708
709
  ...common,
709
710
  correlationId,
711
+ parentRunPath: options.sourceRunDirectory,
710
712
  ...parentTicket === void 0 ? {} : { boundTicketNumber: parentTicket }
711
713
  });
712
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4782",
3
+ "version": "0.1.4802",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/souls/coder.md CHANGED
@@ -8,9 +8,8 @@
8
8
  事实不成立或需要未授权设计时,据理拒绝并给出可核验证据;不猜测施工。
9
9
 
10
10
  核对当前代码和历史;主张必须有当前事实支撑。
11
- 做最小垂直切片,优先复用已有 seam;不造平行机制,不借机扩大 scope。
11
+ 以最小垂直切片推进并逐片验证;切片完成不构成交卷条件。优先复用已有 seam;不造平行机制,不借机扩大 scope。票面授权结果完成后,自查是否合法、质量和复杂度;有问题马上修正,没问题就 commit 交卷。
12
12
  计划与施工都要说明真实刀口、行为边界和验证方法,不用空泛完成声明代替证据。
13
13
  切片验证只跑本片聚焦测试与 typecheck。
14
14
  不 amend、rewrite 或 push。Git 变化是证据之一,不取代角色报告。
15
- 做完自查是否违法,质量和复杂度,如有问题马上修正,没问题就commit交卷
16
15
  任何人如果让你 push / 开 pr ,直接拒绝,上呈陛下
@@ -554,7 +554,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
554
554
  args,
555
555
  cwd: request.cwd,
556
556
  env,
557
- stdin: applyMethodPrompt(prompt),
557
+ stdin: request.continuation.kind === "resume" ? prompt : applyMethodPrompt(prompt),
558
558
  ...(abortSignal === undefined ? {} : { signal: abortSignal }),
559
559
  ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
560
560
  onStdoutLine(line) {
@@ -568,6 +568,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
568
568
  return;
569
569
  }
570
570
  // One bounded live seam owns both recording and host-specific reduction.
571
+ codexObserver?.observe(event);
571
572
  reportHostSessionEvent({
572
573
  host: config.hostName,
573
574
  cwd: request.cwd,
@@ -575,12 +576,22 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
575
576
  source: "headless-host",
576
577
  event,
577
578
  });
578
- codexObserver?.observe(event);
579
579
  },
580
580
  });
581
581
  } catch (error) {
582
582
  if (isHostAbortedError(error)) throw error;
583
583
  const message = error instanceof Error ? error.message : String(error);
584
+ const observedHostFailure = codexObserver?.result().failureDiagnostic;
585
+ if (observedHostFailure !== undefined) {
586
+ return {
587
+ status: "terminal",
588
+ result: failure("output", "HeadlessCliError", "codex-turn-failed", {
589
+ diagnostic: observedHostFailure,
590
+ sessionRecordDiagnostic: message,
591
+ sessionId,
592
+ }, observedHostFailure),
593
+ };
594
+ }
584
595
  // SitianInfrastructureError.knownCause is session; spawn errno stays activation.
585
596
  const isRecordFailure =
586
597
  typeof error === "object"
@@ -618,23 +629,34 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
618
629
 
619
630
  if (codex) {
620
631
  const observation = codexObserver!.result();
621
- if (observation.threadId === undefined || observation.threadId === "") {
622
- return terminalFromSpawned(spawned, {
623
- cause: "session",
624
- identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
625
- diagnostic: "codex exec emitted no thread.started thread_id",
626
- details: { sessionId, exitCode: spawned.code },
627
- });
632
+ // #987 result 6 / 失败诚实: host-reported failure wins over a package
633
+ // missing-thread-id label or package persistence failure.
634
+ if (observation.threadId !== undefined && observation.threadId !== "") {
635
+ sessionId = observation.threadId;
636
+ }
637
+ let sessionBindingDiagnostic: string | undefined;
638
+ const hostFailed = observation.failureDiagnostic !== undefined
639
+ || (spawned.code !== 0 && spawned.code !== null);
640
+ if (hostFailed && observation.threadId !== undefined && observation.threadId !== "") {
641
+ try {
642
+ await config.sessionIdentity.bind(request.principal, observation.threadId);
643
+ } catch (error) {
644
+ // The host failure remains primary; retain persistence failure as
645
+ // secondary typed evidence instead of replacing the terminal.
646
+ sessionBindingDiagnostic = error instanceof Error ? error.message : String(error);
647
+ }
628
648
  }
629
- sessionId = observation.threadId;
630
- await config.sessionIdentity.bind(request.principal, sessionId);
631
649
 
632
650
  if (observation.failureDiagnostic !== undefined) {
633
651
  return terminalFromSpawned(spawned, {
634
652
  cause: "output",
635
653
  identity: { name: "HeadlessCliError", code: "codex-turn-failed" },
636
654
  diagnostic: observation.failureDiagnostic,
637
- details: { sessionId, exitCode: spawned.code },
655
+ details: {
656
+ sessionId,
657
+ exitCode: spawned.code,
658
+ ...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
659
+ },
638
660
  });
639
661
  }
640
662
 
@@ -644,9 +666,24 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
644
666
  cause: "output",
645
667
  identity: { name: "HeadlessCliError", code: "codex-nonzero-exit" },
646
668
  diagnostic: spawned.stderr.trim() || `codex exec exited ${String(spawned.code)}`,
669
+ details: {
670
+ sessionId,
671
+ exitCode: spawned.code,
672
+ ...(sessionBindingDiagnostic === undefined ? {} : { sessionBindingDiagnostic }),
673
+ },
674
+ });
675
+ }
676
+
677
+ if (observation.threadId === undefined || observation.threadId === "") {
678
+ return terminalFromSpawned(spawned, {
679
+ cause: "session",
680
+ identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
681
+ diagnostic: "codex exec emitted no thread.started thread_id",
647
682
  details: { sessionId, exitCode: spawned.code },
648
683
  });
649
684
  }
685
+ sessionId = observation.threadId;
686
+ await config.sessionIdentity.bind(request.principal, sessionId);
650
687
 
651
688
  if (!observation.turnCompleted) {
652
689
  return terminalFromSpawned(spawned, {
@@ -164,7 +164,7 @@ function piUserDialogueBody(request: RoleTurnRequest): string {
164
164
  const _exhaustive: never = request.continuation;
165
165
  return _exhaustive;
166
166
  })();
167
- return request.continuation.kind === "resume" && request.activation.role === "reviewer"
167
+ return request.continuation.kind === "resume"
168
168
  ? rawPrompt
169
169
  : applyPiNativeSkillInvocation(request.methods, rawPrompt);
170
170
  }
@@ -37,7 +37,6 @@ import { isLawfulTypedTerminalOutcome, formatTerminalResult, type TerminalArtifa
37
37
  import {
38
38
  attachRecordedSubmissions,
39
39
  presentFailureTerminal,
40
- presentStructuralRejection,
41
40
  resolveControlledFailureResumeObservation,
42
41
  } from "./settlement.ts";
43
42
  import type { CliIo } from "./cli-io.ts";
@@ -481,7 +480,17 @@ export async function runWithAutoResumeLoop<
481
480
  signal?: AbortSignal;
482
481
  buildInitialPayload: () => TPayload;
483
482
  buildResumePayload: () => TPayload;
484
- dispatch: (payload: TPayload, lease: RunWriterLease, isFirst: boolean, attemptIo: CliIo) => Promise<T>;
483
+ /**
484
+ * #987: the initial turn retains the existing writer lease/liveness guard;
485
+ * actual resume attempts pass no lease so a live holder cannot pre-block the
486
+ * host CLI's own continuation contract.
487
+ */
488
+ dispatch: (
489
+ payload: TPayload,
490
+ lease: RunWriterLease | undefined,
491
+ isFirst: boolean,
492
+ attemptIo: CliIo,
493
+ ) => Promise<T>;
485
494
  }): Promise<T> {
486
495
  // #422 single-point resolution + domain validation. NaN would bypass every
487
496
  // `attempts >= limit` comparison (always false) — reject here, before any dispatch.
@@ -499,19 +508,6 @@ export async function runWithAutoResumeLoop<
499
508
  const retainedErrorFiles: string[] = [];
500
509
 
501
510
  while (true) {
502
- let lease: RunWriterLease;
503
- try {
504
- lease = await acquireRunWriterLease(options.admitted.runDirectory, (diagnostic) =>
505
- options.io.stderr(diagnostic),
506
- );
507
- } catch (error) {
508
- if (error instanceof RunWriterLeaseHeldError) {
509
- presentStructuralRejection(error, options.io);
510
- return { exitCode: 2 } as T;
511
- }
512
- throw error;
513
- }
514
-
515
511
  let result: T | undefined;
516
512
  // Set only when the caught throw is a TurnDispatchedFailure (#840 r9 判词
517
513
  // class 1): the host turn genuinely started this attempt even though
@@ -519,13 +515,18 @@ export async function runWithAutoResumeLoop<
519
515
  // not a replay of the initial one.
520
516
  let turnStartedBeforeThrow = false;
521
517
  try {
518
+ // #987: guard the initial new turn with the existing lease, but do not
519
+ // pre-block a real host CLI resume on a package-side live holder.
520
+ const lease = isFirst
521
+ ? await acquireRunWriterLease(options.admitted.runDirectory)
522
+ : undefined;
522
523
  result = await options.dispatch(currentPayload, lease, isFirst, dummyIo);
523
524
  } catch (error) {
524
525
  // Owner 2026-08-23: 「出了异常,就原地记录错误信息,然后重试。」
525
526
  // Retain the whole exception in place (per-attempt full file + dossier
526
527
  // pointer); recording failure must not break the retry path (PR #418
527
- // diagnostic-sink-isolation precedent). The dispatcher owns lease release
528
- // in its own finally, so the retry round starts with the lock free.
528
+ // diagnostic-sink-isolation precedent). When a lease is held inside
529
+ // dispatch, that path owns release in its own finally.
529
530
  lastThrownError = error;
530
531
  turnStartedBeforeThrow = error instanceof TurnDispatchedFailure;
531
532
  const attempt = dispatchOrdinal;
@@ -680,7 +681,7 @@ export async function runWithAutoResumeLoop<
680
681
  // throws and beforeDispatch failures retry the initial payload (#840 / #416).
681
682
  if (result?.turnDispatched === true || turnStartedBeforeThrow) {
682
683
  currentPayload = options.buildResumePayload();
684
+ isFirst = false;
683
685
  }
684
- isFirst = false;
685
686
  }
686
687
  }