@akagilnc/pi-workflow-roles 0.1.2068 → 0.1.2071

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.2071",
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) {
@@ -73,21 +73,16 @@ import {
73
73
  type PackagedMethodSkillProvenance,
74
74
  } from "../package-resources/method-skill.ts";
75
75
  import {
76
- bindCurrentDurableTerminalToMarker,
77
76
  classifyPackagedRoleTerminalResult,
77
+ findLatestDurablePackagedRoleTerminal,
78
78
  isAcceptedPackagedRoleTerminalResult,
79
79
  isReceiptSettlementBindingClear,
80
- markerMatchesExpectedIdentity,
81
- type ExpectedInvocationIdentity,
80
+ NAVIGATOR_INVOCATION_ENTRY,
81
+ parseInvocationMarkerIdentity,
82
82
  type InvocationMarkerIdentity,
83
83
  } from "../navigator-invocation-identity.ts";
84
84
  import type { NavigatorPhase } from "../navigator-attendance.ts";
85
85
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, parseNoReceiptLifecycleFacts, type NoReceiptLifecycleFacts } from "../receipt-delivery-policy.ts";
86
- import { packagedRoleMetadata } from "../packaged-role-registry.ts";
87
- import {
88
- workSubjectKeyFromProjectRoot,
89
- workSubjectKeysEqual,
90
- } from "../work-subject-identity.ts";
91
86
  import {
92
87
  ensureRunArtifactsDir,
93
88
  type AdmittedCoderInvocation,
@@ -493,12 +488,6 @@ type SessionEntry = {
493
488
  parentSession?: string;
494
489
  };
495
490
 
496
- /** Optional independent identity from admitted/shared lifecycle (not attendance self-fields). */
497
- export type NavigatorAttendanceIdentity = {
498
- readonly phase?: NavigatorPhase;
499
- readonly subjectKey?: string;
500
- };
501
-
502
491
  function isMissingPathError(error: unknown): boolean {
503
492
  return (
504
493
  error instanceof Error &&
@@ -2001,91 +1990,20 @@ function navigatorPhaseValue(value: unknown): NavigatorPhase {
2001
1990
  return null;
2002
1991
  }
2003
1992
 
2004
- function attendanceIdentityFromAdmitted(
2005
- admitted: AdmittedRoleInvocation,
2006
- ): NavigatorAttendanceIdentity {
2007
- // Public CLI sessions live under the machine ledger; Navigator derives subject from
2008
- // the project/cwd work identity, not the per-run session directory spelling.
2009
- const subjectKey = workSubjectKeyFromProjectRoot(admitted.projectRoot);
2010
- if (admitted.role === "coder" || admitted.role === "fixer") {
2011
- return { phase: admitted.phase, subjectKey };
2012
- }
2013
- return { phase: null, subjectKey };
2014
- }
2015
-
2016
1993
  /**
2017
- * Independent expected role/phase/subject for marker correlation.
2018
- * Role comes from the durable packaged terminal tool; phase/subject from admitted
2019
- * lifecycle, Developer session cwd, or registry — never attendance self-fields.
2020
- */
2021
- function independentExpectedIdentity(
2022
- entries: readonly SessionEntry[],
2023
- terminalRole: string,
2024
- supplied?: NavigatorAttendanceIdentity,
2025
- ): ExpectedInvocationIdentity {
2026
- let subjectKey: string | undefined;
2027
- for (const entry of entries) {
2028
- if (entry?.type !== "session") continue;
2029
- if (typeof entry.cwd === "string" && entry.cwd.trim() !== "") {
2030
- subjectKey = workSubjectKeyFromProjectRoot(entry.cwd);
2031
- }
2032
- break;
2033
- }
2034
- if (typeof supplied?.subjectKey === "string") {
2035
- subjectKey = supplied.subjectKey;
2036
- }
2037
-
2038
- let phase: NavigatorPhase | undefined;
2039
- let allowedPhases: readonly NavigatorPhase[] | undefined;
2040
- // null is a real phase fact (Judge/Reviewer/…); only omit when not independently known.
2041
- if (supplied !== undefined && Object.hasOwn(supplied, "phase")) {
2042
- phase = supplied.phase ?? null;
2043
- } else {
2044
- const meta = packagedRoleMetadata(terminalRole);
2045
- if (meta !== undefined) {
2046
- if (meta.phases.length === 1) {
2047
- phase = meta.phases[0] as NavigatorPhase;
2048
- } else {
2049
- allowedPhases = meta.phases as readonly NavigatorPhase[];
2050
- }
2051
- }
2052
- }
2053
-
2054
- return {
2055
- role: terminalRole,
2056
- ...(phase !== undefined ? { phase } : {}),
2057
- ...(allowedPhases !== undefined ? { allowedPhases } : {}),
2058
- ...(subjectKey !== undefined ? { subjectKey } : {}),
2059
- };
2060
- }
2061
-
2062
- /**
2063
- * Attendance must match the bound marker identity and current durable terminal role.
2064
- * Self-shape of attendance fields is not correlation. Marker already matched the
2065
- * independent expected identity before this check runs.
1994
+ * Minimal attendance provenance against the bound marker (ADR 0043).
1995
+ * Keep only invocationId + post-terminal ordering. Runtime-self-produced
1996
+ * role/phase/subject/version are not re-reconciled here (ADR 0042).
2066
1997
  */
2067
1998
  function navigatorAttendanceCorrelatedWithBoundMarker(
2068
1999
  details: Record<string, unknown>,
2069
2000
  attendanceIndex: number,
2070
- terminal: { index: number; role: string },
2001
+ terminalIndex: number,
2071
2002
  marker: InvocationMarkerIdentity,
2072
2003
  ): boolean {
2073
- if (attendanceIndex <= terminal.index) return false;
2074
- if (details.version !== 1) return false;
2075
-
2076
- // Role comes from the packaged terminal tool — compare, do not self-validate.
2077
- if (details.role !== terminal.role) return false;
2078
- // Marker role must already equal terminal role (checked by caller); attendance follows marker.
2079
- if (details.role !== marker.role) return false;
2080
-
2004
+ if (attendanceIndex <= terminalIndex) return false;
2081
2005
  // Exact current invocation token is the bound marker principal.
2082
2006
  if (details.invocationId !== marker.invocationId) return false;
2083
-
2084
- // Phase and subject ride the same marker identity truth table.
2085
- if (details.phase !== marker.phase) return false;
2086
- if (typeof details.subjectKey !== "string") return false;
2087
- if (!workSubjectKeysEqual(details.subjectKey, marker.subjectKey)) return false;
2088
-
2089
2007
  return true;
2090
2008
  }
2091
2009
 
@@ -2157,45 +2075,37 @@ function parseNavigatorAttendanceDetails(
2157
2075
 
2158
2076
  export function extractNavigatorFact(
2159
2077
  entries: readonly SessionEntry[],
2160
- identity?: NavigatorAttendanceIdentity,
2161
2078
  ): TerminalNavigatorFact {
2162
2079
  // Affirmative attendance only. Missing / uncorrelated / unparseable is never no-advice.
2163
- // One truth table: durable classifier + singleton marker binding + marker identity match.
2164
- const binding = bindCurrentDurableTerminalToMarker(entries);
2165
- if (binding.kind === "absent") {
2080
+ // Minimal provenance: latest durable terminal + nearest preceding marker +
2081
+ // invocationId + post-terminal order. Marker↔terminal cardinality belongs to
2082
+ // receipt settlement (isReceiptSettlementBindingClear), not attendance extraction.
2083
+ const terminal = findLatestDurablePackagedRoleTerminal(entries);
2084
+ if (terminal === undefined) {
2166
2085
  return {
2167
2086
  disposition: "unavailable",
2168
2087
  source: "unknown",
2169
2088
  reason: "Navigator attendance has no durable packaged role terminal",
2170
2089
  };
2171
2090
  }
2172
- if (binding.kind === "ambiguous") {
2173
- return {
2174
- disposition: "unavailable",
2175
- source: "unknown",
2176
- reason: "Navigator attendance is ambiguous across multiple durable role terminals",
2177
- };
2178
- }
2179
- if (binding.kind === "unbound") {
2180
- return {
2181
- disposition: "unavailable",
2182
- source: "unknown",
2183
- reason: "Navigator attendance is uncorrelated with session invocation facts",
2184
- };
2185
- }
2186
2091
 
2187
- const { terminal, marker } = binding;
2188
- // Marker role must match the durable terminal tool's role.
2189
- if (marker.role !== terminal.role) {
2092
+ let markerIndex = -1;
2093
+ for (let i = terminal.index - 1; i >= 0; i -= 1) {
2094
+ const entry = entries[i];
2095
+ if (entry?.type === "custom" && entry.customType === NAVIGATOR_INVOCATION_ENTRY) {
2096
+ markerIndex = i;
2097
+ break;
2098
+ }
2099
+ }
2100
+ if (markerIndex < 0) {
2190
2101
  return {
2191
2102
  disposition: "unavailable",
2192
2103
  source: "unknown",
2193
2104
  reason: "Navigator attendance is uncorrelated with session invocation facts",
2194
2105
  };
2195
2106
  }
2196
- // Marker phase/subject (and role) must match independently admitted expected identity.
2197
- const expected = independentExpectedIdentity(entries, terminal.role, identity);
2198
- if (!markerMatchesExpectedIdentity(marker, expected)) {
2107
+ const marker = parseInvocationMarkerIdentity(entries[markerIndex]?.data);
2108
+ if (marker === undefined) {
2199
2109
  return {
2200
2110
  disposition: "unavailable",
2201
2111
  source: "unknown",
@@ -2218,7 +2128,7 @@ export function extractNavigatorFact(
2218
2128
  !navigatorAttendanceCorrelatedWithBoundMarker(
2219
2129
  details,
2220
2130
  i,
2221
- { index: terminal.index, role: terminal.role },
2131
+ terminal.index,
2222
2132
  marker,
2223
2133
  )
2224
2134
  ) {
@@ -2249,7 +2159,7 @@ async function extractNavigatorFactFromAdmittedSession(
2249
2159
  ): Promise<TerminalNavigatorFact> {
2250
2160
  try {
2251
2161
  const entries = await readBoundSessionEntries(admitted.sessionFile);
2252
- return extractNavigatorFact(entries, attendanceIdentityFromAdmitted(admitted));
2162
+ return extractNavigatorFact(entries);
2253
2163
  } catch (error) {
2254
2164
  if (isMissingPathError(error)) {
2255
2165
  return {
@@ -2483,10 +2393,7 @@ async function settleLawfulJudgeTerminalResult(
2483
2393
  if (roleOutcome === undefined) {
2484
2394
  return undefined;
2485
2395
  }
2486
- const navigator = extractNavigatorFact(
2487
- entries,
2488
- attendanceIdentityFromAdmitted(admitted),
2489
- );
2396
+ const navigator = extractNavigatorFact(entries);
2490
2397
  // Lawful outcome exists — artifact publication keeps original errno/name.
2491
2398
  const artifacts = await publishJudgeArtifacts(
2492
2399
  admitted,
@@ -2539,10 +2446,7 @@ async function settleLawfulCoderTerminalResult(
2539
2446
  if (entries === undefined) return undefined;
2540
2447
  const extracted = extractCoderRoleOutcome(entries);
2541
2448
  if (extracted === undefined) return undefined;
2542
- const navigator = extractNavigatorFact(
2543
- entries,
2544
- attendanceIdentityFromAdmitted(admitted),
2545
- );
2449
+ const navigator = extractNavigatorFact(entries);
2546
2450
  const artifacts = await publishCoderArtifacts(
2547
2451
  admitted,
2548
2452
  extracted.outcome,
@@ -2745,10 +2649,7 @@ async function settleLawfulFixerTerminalResult(
2745
2649
  if (entries === undefined) return undefined;
2746
2650
  const extracted = extractFixerRoleOutcome(entries);
2747
2651
  if (extracted === undefined) return undefined;
2748
- const navigator = extractNavigatorFact(
2749
- entries,
2750
- attendanceIdentityFromAdmitted(admitted),
2751
- );
2652
+ const navigator = extractNavigatorFact(entries);
2752
2653
  const methodInvocations = extractFixerMethodInvocations(entries, {
2753
2654
  allowedLocations: [
2754
2655
  options.methodSkillPath,
@@ -2915,10 +2816,7 @@ async function settleLawfulCollectorTerminalResult(
2915
2816
  return undefined;
2916
2817
  }
2917
2818
  assertCollectorReceiptMatchesAdmitted(extracted.receipt, admitted);
2918
- const navigator = extractNavigatorFact(
2919
- entries,
2920
- attendanceIdentityFromAdmitted(admitted),
2921
- );
2819
+ const navigator = extractNavigatorFact(entries);
2922
2820
  const artifacts = await publishCollectorArtifacts(
2923
2821
  admitted,
2924
2822
  extracted.outcome,
@@ -3095,10 +2993,7 @@ async function settleLawfulDoctorTerminalResult(
3095
2993
  throw error;
3096
2994
  }
3097
2995
  }
3098
- const navigator = extractNavigatorFact(
3099
- entries,
3100
- attendanceIdentityFromAdmitted(admitted),
3101
- );
2996
+ const navigator = extractNavigatorFact(entries);
3102
2997
  const artifacts = await publishDoctorArtifacts(
3103
2998
  admitted,
3104
2999
  extracted.outcome,
@@ -3353,10 +3248,7 @@ async function settleLawfulReviewerTerminalResult(
3353
3248
  if (entries === undefined) return undefined;
3354
3249
  const extracted = extractReviewerRoleOutcome(entries);
3355
3250
  if (extracted === undefined) return undefined;
3356
- const navigator = extractNavigatorFact(
3357
- entries,
3358
- attendanceIdentityFromAdmitted(admitted),
3359
- );
3251
+ const navigator = extractNavigatorFact(entries);
3360
3252
  const methodInvocations = extractReviewerMethodInvocations(entries, {
3361
3253
  allowedLocations: [
3362
3254
  options.methodSkillPath,
@@ -3623,10 +3515,7 @@ async function settleLawfulMergerTerminalResult(
3623
3515
  });
3624
3516
  // Every invocation must expand the merge-only method before conflict work.
3625
3517
  if (methodInvocations.length === 0) return undefined;
3626
- const navigator = extractNavigatorFact(
3627
- entries,
3628
- attendanceIdentityFromAdmitted(admitted),
3629
- );
3518
+ const navigator = extractNavigatorFact(entries);
3630
3519
  const artifacts = await publishMergerArtifacts(
3631
3520
  admitted,
3632
3521
  extracted.outcome,