@akagilnc/pi-workflow-roles 0.1.2068 → 0.1.2076

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.
@@ -90,13 +90,6 @@ function navigatorUnavailableError(source, error, cause = source) {
90
90
  const message = error instanceof Error ? error.message : String(error);
91
91
  return error instanceof NavigatorUnavailableError ? error : new NavigatorUnavailableError(source, message, cause, error);
92
92
  }
93
- function navigatorAdviceConsistentWithSettlement(next, settlement) {
94
- if (settlement.kind !== "accepted") return true;
95
- if (settlement.status === "unfinished" && next.role === "judge") return false;
96
- if (settlement.role === "fixer" && settlement.phase === "apply" && settlement.status === "completed" && next.role === "fixer" && next.phase === "apply") return false;
97
- if (next.role !== "merger") return true;
98
- return settlement.role === "judge" && settlement.status === "converged";
99
- }
100
93
  const prepareSchema = Type.Object({}, { additionalProperties: true });
101
94
  const ROUTE_ENTRY = "ak-navigator-route";
102
95
  const CONTEXT_ENTRY = "ak-navigator-context";
@@ -269,17 +262,29 @@ function selectNavigatorCandidate(candidates, settlement) {
269
262
  if (settlement.kind !== "accepted") return void 0;
270
263
  const usable = candidates.filter((candidate) => candidate.next !== void 0);
271
264
  if (usable.length === 0) return void 0;
272
- const matched = usable.filter(
265
+ const rolePhaseMatched = usable.filter(
273
266
  (candidate) => candidate.matches !== void 0 && candidate.matches.role === settlement.role && candidate.matches.phase === settlement.phase
274
267
  );
275
- if (matched.length > 0) {
268
+ if (rolePhaseMatched.length > 0) {
276
269
  if (settlement.status !== void 0) {
277
- const statusSpecific = matched.find((candidate) => candidate.matches?.statuses?.includes(settlement.status) === true);
278
- if (statusSpecific !== void 0) return statusSpecific;
270
+ const statusSpecific = rolePhaseMatched.find(
271
+ (candidate) => candidate.matches?.statuses?.includes(settlement.status) === true
272
+ );
273
+ if (statusSpecific !== void 0) {
274
+ return { candidate: statusSpecific, matchedToSettlement: true };
275
+ }
279
276
  }
280
- return matched.find((candidate) => candidate.matches?.statuses === void 0);
277
+ const rolePhaseGeneric = rolePhaseMatched.find(
278
+ (candidate) => candidate.matches?.statuses === void 0
279
+ );
280
+ if (rolePhaseGeneric !== void 0) {
281
+ return { candidate: rolePhaseGeneric, matchedToSettlement: true };
282
+ }
283
+ return void 0;
281
284
  }
282
- return usable.find((candidate) => candidate.matches === void 0);
285
+ const unbound = usable.find((candidate) => candidate.matches === void 0);
286
+ if (unbound === void 0) return void 0;
287
+ return { candidate: unbound, matchedToSettlement: false };
283
288
  }
284
289
  function formatNavigatorReport(report) {
285
290
  const playbookFailure = report.routePlaybookReadFailure === void 0 ? [] : [`\u8DEF\u4E66\u8BFB\u53D6\u5931\u8D25\uFF1A${oneLine(report.routePlaybookReadFailure)}`];
@@ -375,6 +380,7 @@ function createNavigatorAttendance(options) {
375
380
  const prepare = async () => {
376
381
  const boundSettlement = prepareBoundSettlement;
377
382
  prepareBoundSettlement = void 0;
383
+ preparationNoReceipt = false;
378
384
  const invocationId = invocationPrincipal;
379
385
  activeInvocationId = invocationId;
380
386
  if (contextError !== void 0) throw navigatorUnavailableError("context", contextError);
@@ -704,27 +710,25 @@ ${helpContext}
704
710
  let prepared = await preparation;
705
711
  session?.appendEntry(SETTLEMENT_ENTRY, { invocationId, subjectKey, role: settlement.role, phase: settlement.phase, kind: settlement.kind, ...settlement.status === void 0 ? {} : { status: settlement.status } });
706
712
  let selected = selectNavigatorCandidate(prepared, settlement);
707
- if (selected?.next !== void 0 && !navigatorAdviceConsistentWithSettlement(selected.next, settlement)) {
713
+ if (selected?.candidate.next !== void 0 && !selected.matchedToSettlement) {
708
714
  prepareBoundSettlement = settlement;
709
715
  prepared = await prepare();
710
716
  selected = selectNavigatorCandidate(prepared, settlement);
711
- if (selected?.next !== void 0 && !navigatorAdviceConsistentWithSettlement(selected.next, settlement)) {
712
- throw new Error("Navigator advice contradicts the accepted settlement");
713
- }
714
717
  }
715
- if (selected?.next === void 0 && preparationNoReceipt) {
718
+ const selectedCandidate = selected?.candidate;
719
+ if (selectedCandidate?.next === void 0 && preparationNoReceipt) {
716
720
  report = { disposition: "no-advice" };
717
- } else if (selected?.next === void 0) {
721
+ } else if (selectedCandidate?.next === void 0) {
718
722
  throw new Error("Navigator prepared no machine-usable next direction");
719
723
  } else {
720
- const selectedRoute = selected.route;
724
+ const selectedRoute = selectedCandidate.route;
721
725
  const routeChanged = selectedRoute !== void 0 && !routeEqual(previousRoute, selectedRoute);
722
- const command = renderPublicAkRoleCommand(selected.next);
726
+ const command = renderPublicAkRoleCommand(selectedCandidate.next);
723
727
  report = {
724
728
  disposition: "recommendation",
725
729
  ...routeChanged ? { route: selectedRoute } : {},
726
- next: selected.next,
727
- ...selected.reason === void 0 ? {} : { reason: oneLine(selected.reason) },
730
+ next: selectedCandidate.next,
731
+ ...selectedCandidate.reason === void 0 ? {} : { reason: oneLine(selectedCandidate.reason) },
728
732
  ...command === void 0 ? {} : { command }
729
733
  };
730
734
  if (selectedRoute !== void 0) {
@@ -1056,7 +1060,6 @@ export {
1056
1060
  createNavigatorPrepareTool,
1057
1061
  decorateSettlementWithNavigation,
1058
1062
  formatNavigatorReport,
1059
- navigatorAdviceConsistentWithSettlement,
1060
1063
  navigatorModelSettingPath,
1061
1064
  navigatorProviderFailure,
1062
1065
  navigatorProviderFailureFromError,
@@ -14295,9 +14295,6 @@ var init_merger_contracts = __esm({
14295
14295
  });
14296
14296
 
14297
14297
  // src/packaged-role-registry.ts
14298
- function packagedRoleMetadata(role) {
14299
- return PACKAGED_ROLE_REGISTRY.find((entry) => entry.role === role);
14300
- }
14301
14298
  var PACKAGED_ROLE_REGISTRY;
14302
14299
  var init_packaged_role_registry = __esm({
14303
14300
  "src/packaged-role-registry.ts"() {
@@ -14812,9 +14809,6 @@ function physicalPathIdentity(path) {
14812
14809
  }
14813
14810
  }
14814
14811
  }
14815
- function physicallyContainedIn(root, candidate) {
14816
- return pathContainedIn(physicalPathIdentity(root), physicalPathIdentity(candidate));
14817
- }
14818
14812
  function errnoCode(error) {
14819
14813
  return error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
14820
14814
  }
@@ -19428,48 +19422,6 @@ var init_engine_detour = __esm({
19428
19422
  });
19429
19423
 
19430
19424
  // src/work-subject-identity.ts
19431
- import { resolve as resolve6 } from "node:path";
19432
- function issueRoot(value) {
19433
- const normalized = value.replaceAll("\\", "/");
19434
- const marker = ".ak/work/issues/";
19435
- const index = normalized.indexOf(marker);
19436
- if (index < 0) return void 0;
19437
- const issue = normalized.slice(index + marker.length).split("/")[0]?.split("#")[0];
19438
- return issue === void 0 || issue === "" ? void 0 : normalized.slice(0, index + marker.length) + issue;
19439
- }
19440
- function workIdentityFromCwd(cwd) {
19441
- const resolvedCwd = resolve6(cwd, ".");
19442
- const cwdIssue = issueRoot(resolvedCwd);
19443
- if (cwdIssue !== void 0) return cwdIssue;
19444
- if (resolvedCwd.includes("/.ak/work/")) return resolvedCwd;
19445
- return void 0;
19446
- }
19447
- function isMachineLedgerSessionPath(sessionPath) {
19448
- return physicallyContainedIn(resolveActivationLedgerHome(), sessionPath);
19449
- }
19450
- function subjectPath(sessionDir, cwd = process.cwd()) {
19451
- if (sessionDir === "") {
19452
- return workIdentityFromCwd(cwd) ?? resolve6(cwd, ".ak/work");
19453
- }
19454
- const resolvedSession = resolve6(cwd, sessionDir || ".ak/work");
19455
- if (isMachineLedgerSessionPath(resolvedSession)) {
19456
- return workIdentityFromCwd(cwd) ?? resolve6(cwd, ".ak/work");
19457
- }
19458
- const issue = issueRoot(resolvedSession);
19459
- if (issue !== void 0) return issue;
19460
- const runsMarker = "/runs/";
19461
- const runsIndex = resolvedSession.indexOf(runsMarker);
19462
- if (runsIndex >= 0) {
19463
- return resolvedSession.slice(0, runsIndex);
19464
- }
19465
- return resolvedSession;
19466
- }
19467
- function workSubjectKeyFromProjectRoot(projectRoot) {
19468
- return subjectPath("", projectRoot);
19469
- }
19470
- function workSubjectKeysEqual(left, right) {
19471
- return physicalPathIdentity(left) === physicalPathIdentity(right);
19472
- }
19473
19425
  var init_work_subject_identity = __esm({
19474
19426
  "src/work-subject-identity.ts"() {
19475
19427
  "use strict";
@@ -19510,18 +19462,6 @@ function parseInvocationMarkerIdentity(data) {
19510
19462
  subjectKey: record4.subjectKey
19511
19463
  };
19512
19464
  }
19513
- function markerMatchesExpectedIdentity(marker, expected) {
19514
- if (marker.role !== expected.role) return false;
19515
- if (expected.phase !== void 0) {
19516
- if (marker.phase !== expected.phase) return false;
19517
- } else if (expected.allowedPhases !== void 0) {
19518
- if (!expected.allowedPhases.includes(marker.phase)) return false;
19519
- }
19520
- if (expected.subjectKey !== void 0) {
19521
- if (!workSubjectKeysEqual(marker.subjectKey, expected.subjectKey)) return false;
19522
- }
19523
- return true;
19524
- }
19525
19465
  function classifyPackagedRoleTerminalResult(message) {
19526
19466
  if (typeof message.toolName !== "string") return { kind: "nonterminal" };
19527
19467
  if (!PACKAGED_ROLE_OUTPUT_TOOLS.has(message.toolName)) return { kind: "nonterminal" };
@@ -21011,55 +20951,9 @@ function navigatorPhaseValue(value) {
21011
20951
  if (value === "plan" || value === "apply") return value;
21012
20952
  return null;
21013
20953
  }
21014
- function attendanceIdentityFromAdmitted(admitted) {
21015
- const subjectKey = workSubjectKeyFromProjectRoot(admitted.projectRoot);
21016
- if (admitted.role === "coder" || admitted.role === "fixer") {
21017
- return { phase: admitted.phase, subjectKey };
21018
- }
21019
- return { phase: null, subjectKey };
21020
- }
21021
- function independentExpectedIdentity(entries, terminalRole, supplied) {
21022
- let subjectKey;
21023
- for (const entry of entries) {
21024
- if (entry?.type !== "session") continue;
21025
- if (typeof entry.cwd === "string" && entry.cwd.trim() !== "") {
21026
- subjectKey = workSubjectKeyFromProjectRoot(entry.cwd);
21027
- }
21028
- break;
21029
- }
21030
- if (typeof supplied?.subjectKey === "string") {
21031
- subjectKey = supplied.subjectKey;
21032
- }
21033
- let phase;
21034
- let allowedPhases;
21035
- if (supplied !== void 0 && Object.hasOwn(supplied, "phase")) {
21036
- phase = supplied.phase ?? null;
21037
- } else {
21038
- const meta = packagedRoleMetadata(terminalRole);
21039
- if (meta !== void 0) {
21040
- if (meta.phases.length === 1) {
21041
- phase = meta.phases[0];
21042
- } else {
21043
- allowedPhases = meta.phases;
21044
- }
21045
- }
21046
- }
21047
- return {
21048
- role: terminalRole,
21049
- ...phase !== void 0 ? { phase } : {},
21050
- ...allowedPhases !== void 0 ? { allowedPhases } : {},
21051
- ...subjectKey !== void 0 ? { subjectKey } : {}
21052
- };
21053
- }
21054
- function navigatorAttendanceCorrelatedWithBoundMarker(details, attendanceIndex, terminal, marker) {
21055
- if (attendanceIndex <= terminal.index) return false;
21056
- if (details.version !== 1) return false;
21057
- if (details.role !== terminal.role) return false;
21058
- if (details.role !== marker.role) return false;
20954
+ function navigatorAttendanceCorrelatedWithBoundMarker(details, attendanceIndex, terminalIndex, marker) {
20955
+ if (attendanceIndex <= terminalIndex) return false;
21059
20956
  if (details.invocationId !== marker.invocationId) return false;
21060
- if (details.phase !== marker.phase) return false;
21061
- if (typeof details.subjectKey !== "string") return false;
21062
- if (!workSubjectKeysEqual(details.subjectKey, marker.subjectKey)) return false;
21063
20957
  return true;
21064
20958
  }
21065
20959
  function parseNavigatorAttendanceDetails(details) {
@@ -21110,39 +21004,32 @@ function parseNavigatorAttendanceDetails(details) {
21110
21004
  reason: "Navigator attendance disposition is unparseable"
21111
21005
  };
21112
21006
  }
21113
- function extractNavigatorFact(entries, identity) {
21114
- const binding = bindCurrentDurableTerminalToMarker(entries);
21115
- if (binding.kind === "absent") {
21007
+ function extractNavigatorFact(entries) {
21008
+ const terminal = findLatestDurablePackagedRoleTerminal(entries);
21009
+ if (terminal === void 0) {
21116
21010
  return {
21117
21011
  disposition: "unavailable",
21118
21012
  source: "unknown",
21119
21013
  reason: "Navigator attendance has no durable packaged role terminal"
21120
21014
  };
21121
21015
  }
21122
- if (binding.kind === "ambiguous") {
21123
- return {
21124
- disposition: "unavailable",
21125
- source: "unknown",
21126
- reason: "Navigator attendance is ambiguous across multiple durable role terminals"
21127
- };
21128
- }
21129
- if (binding.kind === "unbound") {
21130
- return {
21131
- disposition: "unavailable",
21132
- source: "unknown",
21133
- reason: "Navigator attendance is uncorrelated with session invocation facts"
21134
- };
21016
+ let markerIndex = -1;
21017
+ for (let i = terminal.index - 1; i >= 0; i -= 1) {
21018
+ const entry = entries[i];
21019
+ if (entry?.type === "custom" && entry.customType === NAVIGATOR_INVOCATION_ENTRY) {
21020
+ markerIndex = i;
21021
+ break;
21022
+ }
21135
21023
  }
21136
- const { terminal, marker } = binding;
21137
- if (marker.role !== terminal.role) {
21024
+ if (markerIndex < 0) {
21138
21025
  return {
21139
21026
  disposition: "unavailable",
21140
21027
  source: "unknown",
21141
21028
  reason: "Navigator attendance is uncorrelated with session invocation facts"
21142
21029
  };
21143
21030
  }
21144
- const expected = independentExpectedIdentity(entries, terminal.role, identity);
21145
- if (!markerMatchesExpectedIdentity(marker, expected)) {
21031
+ const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
21032
+ if (marker === void 0) {
21146
21033
  return {
21147
21034
  disposition: "unavailable",
21148
21035
  source: "unknown",
@@ -21163,7 +21050,7 @@ function extractNavigatorFact(entries, identity) {
21163
21050
  if (!navigatorAttendanceCorrelatedWithBoundMarker(
21164
21051
  details,
21165
21052
  i,
21166
- { index: terminal.index, role: terminal.role },
21053
+ terminal.index,
21167
21054
  marker
21168
21055
  )) {
21169
21056
  return {
@@ -21184,7 +21071,7 @@ function extractNavigatorFact(entries, identity) {
21184
21071
  async function extractNavigatorFactFromAdmittedSession(admitted) {
21185
21072
  try {
21186
21073
  const entries = await readBoundSessionEntries(admitted.sessionFile);
21187
- return extractNavigatorFact(entries, attendanceIdentityFromAdmitted(admitted));
21074
+ return extractNavigatorFact(entries);
21188
21075
  } catch (error) {
21189
21076
  if (isMissingPathError2(error)) {
21190
21077
  return {
@@ -21347,10 +21234,7 @@ async function settleLawfulJudgeTerminalResult(admitted) {
21347
21234
  if (roleOutcome === void 0) {
21348
21235
  return void 0;
21349
21236
  }
21350
- const navigator = extractNavigatorFact(
21351
- entries,
21352
- attendanceIdentityFromAdmitted(admitted)
21353
- );
21237
+ const navigator = extractNavigatorFact(entries);
21354
21238
  const artifacts = await publishJudgeArtifacts(
21355
21239
  admitted,
21356
21240
  roleOutcome,
@@ -21371,10 +21255,7 @@ async function settleLawfulCoderTerminalResult(admitted, options = {}) {
21371
21255
  if (entries === void 0) return void 0;
21372
21256
  const extracted = extractCoderRoleOutcome(entries);
21373
21257
  if (extracted === void 0) return void 0;
21374
- const navigator = extractNavigatorFact(
21375
- entries,
21376
- attendanceIdentityFromAdmitted(admitted)
21377
- );
21258
+ const navigator = extractNavigatorFact(entries);
21378
21259
  const artifacts = await publishCoderArtifacts(
21379
21260
  admitted,
21380
21261
  extracted.outcome,
@@ -21506,10 +21387,7 @@ async function settleLawfulFixerTerminalResult(admitted, options) {
21506
21387
  if (entries === void 0) return void 0;
21507
21388
  const extracted = extractFixerRoleOutcome(entries);
21508
21389
  if (extracted === void 0) return void 0;
21509
- const navigator = extractNavigatorFact(
21510
- entries,
21511
- attendanceIdentityFromAdmitted(admitted)
21512
- );
21390
+ const navigator = extractNavigatorFact(entries);
21513
21391
  const methodInvocations = extractFixerMethodInvocations(entries, {
21514
21392
  allowedLocations: [
21515
21393
  options.methodSkillPath,
@@ -21635,10 +21513,7 @@ async function settleLawfulCollectorTerminalResult(admitted) {
21635
21513
  return void 0;
21636
21514
  }
21637
21515
  assertCollectorReceiptMatchesAdmitted(extracted.receipt, admitted);
21638
- const navigator = extractNavigatorFact(
21639
- entries,
21640
- attendanceIdentityFromAdmitted(admitted)
21641
- );
21516
+ const navigator = extractNavigatorFact(entries);
21642
21517
  const artifacts = await publishCollectorArtifacts(
21643
21518
  admitted,
21644
21519
  extracted.outcome,
@@ -21762,10 +21637,7 @@ async function settleLawfulDoctorTerminalResult(admitted) {
21762
21637
  throw error;
21763
21638
  }
21764
21639
  }
21765
- const navigator = extractNavigatorFact(
21766
- entries,
21767
- attendanceIdentityFromAdmitted(admitted)
21768
- );
21640
+ const navigator = extractNavigatorFact(entries);
21769
21641
  const artifacts = await publishDoctorArtifacts(
21770
21642
  admitted,
21771
21643
  extracted.outcome,
@@ -21926,10 +21798,7 @@ async function settleLawfulReviewerTerminalResult(admitted, options) {
21926
21798
  if (entries === void 0) return void 0;
21927
21799
  const extracted = extractReviewerRoleOutcome(entries);
21928
21800
  if (extracted === void 0) return void 0;
21929
- const navigator = extractNavigatorFact(
21930
- entries,
21931
- attendanceIdentityFromAdmitted(admitted)
21932
- );
21801
+ const navigator = extractNavigatorFact(entries);
21933
21802
  const methodInvocations = extractReviewerMethodInvocations(entries, {
21934
21803
  allowedLocations: [
21935
21804
  options.methodSkillPath,
@@ -22109,10 +21978,7 @@ async function settleLawfulMergerTerminalResult(admitted, options) {
22109
21978
  ]
22110
21979
  });
22111
21980
  if (methodInvocations.length === 0) return void 0;
22112
- const navigator = extractNavigatorFact(
22113
- entries,
22114
- attendanceIdentityFromAdmitted(admitted)
22115
- );
21981
+ const navigator = extractNavigatorFact(entries);
22116
21982
  const artifacts = await publishMergerArtifacts(
22117
21983
  admitted,
22118
21984
  extracted.outcome,
@@ -22439,8 +22305,6 @@ var init_settlement = __esm({
22439
22305
  init_method_skill();
22440
22306
  init_navigator_invocation_identity();
22441
22307
  init_receipt_delivery_policy();
22442
- init_packaged_role_registry();
22443
- init_work_subject_identity();
22444
22308
  init_invocation();
22445
22309
  init_terminal();
22446
22310
  CONCISE_DIAGNOSTIC_MAX_CHARS = 480;
@@ -24040,7 +23904,7 @@ var init_judge_run = __esm({
24040
23904
 
24041
23905
  // src/public-cli/merger-run.ts
24042
23906
  import { mkdir as mkdir4, writeFile as writeFile11 } from "node:fs/promises";
24043
- import { join as join17, resolve as resolve7 } from "node:path";
23907
+ import { join as join17, resolve as resolve6 } from "node:path";
24044
23908
  function buildMergerActivationExtraArgs(admitted, options) {
24045
23909
  const prompt = buildMergerTransportPrompt(admitted);
24046
23910
  const skillPath = resolvePackagedMethodSkillPath(
@@ -24265,7 +24129,7 @@ async function loadMergerMethodMaterial(packageRoot2) {
24265
24129
  );
24266
24130
  }
24267
24131
  async function admitMergerShellForActivationFailure(options) {
24268
- const projectRoot = resolve7(options.project ?? options.cwd);
24132
+ const projectRoot = resolve6(options.project ?? options.cwd);
24269
24133
  const runId = (options.createRunId ?? uuidv7)();
24270
24134
  const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } = roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "merger", home: options.home });
24271
24135
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
@@ -24920,8 +24784,8 @@ var init_atomic_write = __esm({
24920
24784
  import { open as open3, readFile as readFile10, unlink as unlink4 } from "node:fs/promises";
24921
24785
  import { dirname as dirname8, join as join20 } from "node:path";
24922
24786
  function sleep(ms) {
24923
- return new Promise((resolve9) => {
24924
- setTimeout(resolve9, ms);
24787
+ return new Promise((resolve8) => {
24788
+ setTimeout(resolve8, ms);
24925
24789
  });
24926
24790
  }
24927
24791
  async function withTaishiLibraryIndexLock(ledgerHome, fn) {
@@ -26808,7 +26672,7 @@ var init_taishi_entry = __esm({
26808
26672
 
26809
26673
  // src/public-cli/taishi-run.ts
26810
26674
  import { readFile as readFile15 } from "node:fs/promises";
26811
- import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
26675
+ import { isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
26812
26676
  async function buildTaishiIssueModeInputFromPublicArgv(parsed, ledgerHome) {
26813
26677
  const ticket = parsed.ticket;
26814
26678
  const directRoot = parsed.projectRoot;
@@ -26854,7 +26718,7 @@ async function buildTaishiSweepModeInputFromAttachmentPaths(attachmentPaths) {
26854
26718
  );
26855
26719
  }
26856
26720
  const sourcePath = attachmentPaths[0];
26857
- const absolute = isAbsolute5(sourcePath) ? sourcePath : resolve8(sourcePath);
26721
+ const absolute = isAbsolute5(sourcePath) ? sourcePath : resolve7(sourcePath);
26858
26722
  let bytes;
26859
26723
  try {
26860
26724
  bytes = await readFile15(absolute);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2068",
3
+ "version": "0.1.2076",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -197,33 +197,6 @@ export type NavigatorContextProjection = {
197
197
  liveRoleHelp: Array<{ role: NavigatorTargetRole; help: string }>;
198
198
  };
199
199
 
200
- /**
201
- * Shared terminal-internal consistency for Navigator advice vs the just-accepted
202
- * settlement (family #224/#226/#227). One table for all seats — not per-role guards.
203
- *
204
- * Merger closes delivery. It is only consistent immediately after judge converged.
205
- * Fresh accepted work from any other seat must not skip to merger.
206
- *
207
- * Unfinished is an open handoff (ADR 0050): recommending judge would send half-done
208
- * work to audit. Machine criterion anchors only status=unfinished — refused and
209
- * partially_completed remain settled terminals whose judge path stays lawful.
210
- * Positive continuation is free-form via rebind + status-matched candidates
211
- * (ADR 0010/0061); this only suppresses self-contradictory typed emission.
212
- */
213
- export function navigatorAdviceConsistentWithSettlement(
214
- next: NavigatorRouteTarget,
215
- settlement: NavigatorSettlement,
216
- ): boolean {
217
- if (settlement.kind !== "accepted") return true;
218
- // #227: open unfinished handoff must not be typed as next=judge.
219
- if (settlement.status === "unfinished" && next.role === "judge") return false;
220
- // #265: completed Fixer apply must not be sent back to repeat the same work.
221
- if (settlement.role === "fixer" && settlement.phase === "apply" && settlement.status === "completed"
222
- && next.role === "fixer" && next.phase === "apply") return false;
223
- if (next.role !== "merger") return true;
224
- return settlement.role === "judge" && settlement.status === "converged";
225
- }
226
-
227
200
  // Provider admission is ADR 0060 object root only. Nested advisory shape
228
201
  // (candidates/next/route/matches/reason/command) is never a gate — every object
229
202
  // root reaches the unique execute/normalize path exactly once.
@@ -478,25 +451,52 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
478
451
  });
479
452
  }
480
453
 
481
- export function selectNavigatorCandidate(candidates: readonly NavigatorCandidate[], settlement: NavigatorSettlement): NavigatorCandidate | undefined {
454
+ /**
455
+ * Candidate pick plus whether matches keyed it to this settlement.
456
+ * Single owner for role/phase/status match truth (selection ranking + stale-context rebind).
457
+ * Structural only — never inspects next.role for routing legality.
458
+ */
459
+ export type NavigatorCandidateSelection = {
460
+ readonly candidate: NavigatorCandidate;
461
+ /** True when matches keyed this candidate to the settlement (no stale-context rebind). */
462
+ readonly matchedToSettlement: boolean;
463
+ };
464
+
465
+ export function selectNavigatorCandidate(
466
+ candidates: readonly NavigatorCandidate[],
467
+ settlement: NavigatorSettlement,
468
+ ): NavigatorCandidateSelection | undefined {
482
469
  if (settlement.kind !== "accepted") return undefined;
483
470
  const usable = candidates.filter((candidate) => candidate.next !== undefined);
484
471
  if (usable.length === 0) return undefined;
485
- const matched = usable.filter((candidate) =>
472
+ const rolePhaseMatched = usable.filter((candidate) =>
486
473
  candidate.matches !== undefined
487
474
  && candidate.matches.role === settlement.role
488
475
  && candidate.matches.phase === settlement.phase,
489
476
  );
490
477
  // Status-specific candidates outrank role/phase generics regardless of declaration order.
491
- if (matched.length > 0) {
478
+ if (rolePhaseMatched.length > 0) {
492
479
  if (settlement.status !== undefined) {
493
- const statusSpecific = matched.find((candidate) => candidate.matches?.statuses?.includes(settlement.status!) === true);
494
- if (statusSpecific !== undefined) return statusSpecific;
480
+ const statusSpecific = rolePhaseMatched.find(
481
+ (candidate) => candidate.matches?.statuses?.includes(settlement.status!) === true,
482
+ );
483
+ if (statusSpecific !== undefined) {
484
+ return { candidate: statusSpecific, matchedToSettlement: true };
485
+ }
486
+ }
487
+ const rolePhaseGeneric = rolePhaseMatched.find(
488
+ (candidate) => candidate.matches?.statuses === undefined,
489
+ );
490
+ if (rolePhaseGeneric !== undefined) {
491
+ return { candidate: rolePhaseGeneric, matchedToSettlement: true };
495
492
  }
496
- return matched.find((candidate) => candidate.matches?.statuses === undefined);
493
+ return undefined;
497
494
  }
498
495
  // v1 direction-only / broken matches: absent match metadata must not drop a usable next.
499
- return usable.find((candidate) => candidate.matches === undefined);
496
+ // Not settlement-keyed caller may run one stale-context rebind.
497
+ const unbound = usable.find((candidate) => candidate.matches === undefined);
498
+ if (unbound === undefined) return undefined;
499
+ return { candidate: unbound, matchedToSettlement: false };
500
500
  }
501
501
 
502
502
  export function formatNavigatorReport(report: NavigatorReport): string {
@@ -631,6 +631,9 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
631
631
  // pi.appendEntry at lifecycle start — not optional sessionManager probing.
632
632
  const boundSettlement = prepareBoundSettlement;
633
633
  prepareBoundSettlement = undefined;
634
+ // Each prepare owns the no-receipt flag; a later settlement-bound rebind must
635
+ // not inherit a speculative no-receipt outcome.
636
+ preparationNoReceipt = false;
634
637
  const invocationId = invocationPrincipal;
635
638
  activeInvocationId = invocationId;
636
639
  if (contextError !== undefined) throw navigatorUnavailableError("context", contextError);
@@ -958,40 +961,34 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
958
961
  let prepared = await preparation;
959
962
  session?.appendEntry(SETTLEMENT_ENTRY, { invocationId, subjectKey, role: settlement.role, phase: settlement.phase, kind: settlement.kind, ...(settlement.status === undefined ? {} : { status: settlement.status }) });
960
963
  let selected = selectNavigatorCandidate(prepared, settlement);
961
- // Family #224/#226/#227 shared seam: speculative prepare runs before the
962
- // current terminal exists and may treat prior history as decisive. When
963
- // selected next contradicts this accepted settlement, discard it and
964
- // re-prepare once bound to the settlement (single shared mechanism).
965
- if (
966
- selected?.next !== undefined
967
- && !navigatorAdviceConsistentWithSettlement(selected.next, settlement)
968
- ) {
964
+ // Speculative prepare runs before this terminal exists and may treat prior
965
+ // history as decisive. When selection provenance says matches did not key
966
+ // this candidate to the settlement, rebind once with currentSettlement.
967
+ // Stale-context repair only reachable without any next.role legality
968
+ // table. After selection (speculative or rebound), advice is passed
969
+ // through as-is (ADR 0010 / ADR 0061: caller may ignore).
970
+ if (selected?.candidate.next !== undefined && !selected.matchedToSettlement) {
969
971
  prepareBoundSettlement = settlement;
970
972
  prepared = await prepare();
971
973
  selected = selectNavigatorCandidate(prepared, settlement);
972
- if (
973
- selected?.next !== undefined
974
- && !navigatorAdviceConsistentWithSettlement(selected.next, settlement)
975
- ) {
976
- throw new Error("Navigator advice contradicts the accepted settlement");
977
- }
978
974
  }
979
975
  // Budget exhaustion is affirmative typed no-advice; malformed submitted
980
976
  // advice remains the existing unavailable path.
981
- if (selected?.next === undefined && preparationNoReceipt) {
977
+ const selectedCandidate = selected?.candidate;
978
+ if (selectedCandidate?.next === undefined && preparationNoReceipt) {
982
979
  report = { disposition: "no-advice" };
983
- } else if (selected?.next === undefined) {
980
+ } else if (selectedCandidate?.next === undefined) {
984
981
  throw new Error("Navigator prepared no machine-usable next direction");
985
982
  } else {
986
- const selectedRoute = selected.route;
983
+ const selectedRoute = selectedCandidate.route;
987
984
  const routeChanged = selectedRoute !== undefined && !routeEqual(previousRoute, selectedRoute);
988
985
  // Single owner: public registry renderer (ADR 0052). Model command prose is never authority.
989
- const command = renderPublicAkRoleCommand(selected.next);
986
+ const command = renderPublicAkRoleCommand(selectedCandidate.next);
990
987
  report = {
991
988
  disposition: "recommendation",
992
989
  ...(routeChanged ? { route: selectedRoute } : {}),
993
- next: selected.next,
994
- ...(selected.reason === undefined ? {} : { reason: oneLine(selected.reason) }),
990
+ next: selectedCandidate.next,
991
+ ...(selectedCandidate.reason === undefined ? {} : { reason: oneLine(selectedCandidate.reason) }),
995
992
  ...(command === undefined ? {} : { command }),
996
993
  };
997
994
  if (selectedRoute !== undefined) {