@akagilnc/pi-workflow-roles 0.1.5365 → 0.1.5368

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.
@@ -9,7 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
10
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
11
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
- import { ensureRoleRunDirectory, ensureRoleRunPlacement, listBookRunDirectories, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
12
+ import { ensureRoleRunDirectory, ensureRoleRunPlacement, isUnboundRunDirectory, listBookRunDirectories, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
13
13
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
14
  import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
15
  import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
@@ -103,44 +103,53 @@ async function writeRoleInvocationLedger(source, role, effectiveModel) {
103
103
  * Host stays write-if-present (`string` only).
104
104
  */
105
105
  export async function recordEffectiveInvocationModel(runDirectory, model, engine, host, engineModel) {
106
- const ledgerPath = join(runDirectory, "invocation.json");
107
- const current = JSON.parse(await readFile(ledgerPath, "utf8"));
108
- const next = { ...current };
109
- if (model !== undefined) {
110
- next.provider = model.provider;
111
- next.model = model.model;
112
- if (model.thinking === undefined) {
113
- delete next.thinking;
106
+ await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => {
107
+ const next = { ...current };
108
+ if (model !== undefined) {
109
+ next.provider = model.provider;
110
+ next.model = model.model;
111
+ if (model.thinking === undefined) {
112
+ delete next.thinking;
113
+ }
114
+ else {
115
+ next.thinking = model.thinking;
116
+ }
114
117
  }
115
- else {
116
- next.thinking = model.thinking;
118
+ if (engine === null) {
119
+ delete next.engine;
117
120
  }
118
- }
119
- if (engine === null) {
120
- delete next.engine;
121
- }
122
- else if (engine !== undefined) {
123
- next.engine = engine;
124
- }
125
- if (engineModel === null) {
126
- delete next.engineModel;
127
- }
128
- else if (engineModel !== undefined) {
129
- next.engineModel = engineModel;
130
- }
131
- if (host !== undefined) {
132
- next.host = host;
133
- }
134
- await writeFile(ledgerPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
121
+ else if (engine !== undefined) {
122
+ next.engine = engine;
123
+ }
124
+ if (engineModel === null) {
125
+ delete next.engineModel;
126
+ }
127
+ else if (engineModel !== undefined) {
128
+ next.engineModel = engineModel;
129
+ }
130
+ if (host !== undefined) {
131
+ next.host = host;
132
+ }
133
+ return next;
134
+ });
135
+ }
136
+ /**
137
+ * Read one durable JSON object, let the caller keep its field rules, and write
138
+ * the page back. `undefined` means the caller decided not to change the page.
139
+ */
140
+ async function updateDurableJsonPage(path, update) {
141
+ const current = JSON.parse(await readFile(path, "utf8"));
142
+ const next = update(current);
143
+ if (next === undefined)
144
+ return;
145
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
135
146
  }
136
147
  /** Merge observed launch-time fields into the single existing invocation.json identity page. */
137
148
  async function mergeInvocationIdentityPage(runDirectory, fields) {
138
- const ledgerPath = join(runDirectory, "invocation.json");
139
- const current = JSON.parse(await readFile(ledgerPath, "utf8"));
140
- await writeFile(ledgerPath, `${JSON.stringify({
149
+ await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => ({
141
150
  ...current,
142
151
  ...fields,
143
- }, null, 2)}\n`, "utf8");
152
+ }));
144
153
  }
145
154
  /**
146
155
  * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
@@ -150,19 +159,19 @@ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath, audi
150
159
  if (sourceRunPath.trim() === "") {
151
160
  throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
152
161
  }
153
- const admittedPath = admitted.admittedRequestPath;
154
- const current = JSON.parse(await readFile(admittedPath, "utf8"));
155
- if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
156
- throw new Error(`persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`);
157
- }
158
- const sameSubject = auditorSubject === undefined || current.auditorSubject === auditorSubject;
159
- if (current.sourceRunPath === sourceRunPath && sameSubject)
160
- return;
161
- await writeFile(admittedPath, `${JSON.stringify({
162
- ...current,
163
- sourceRunPath,
164
- ...(auditorSubject === undefined ? {} : { auditorSubject }),
165
- }, null, 2)}\n`, "utf8");
162
+ await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
163
+ if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
164
+ throw new Error(`persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`);
165
+ }
166
+ const sameSubject = auditorSubject === undefined || current.auditorSubject === auditorSubject;
167
+ if (current.sourceRunPath === sourceRunPath && sameSubject)
168
+ return undefined;
169
+ return {
170
+ ...current,
171
+ sourceRunPath,
172
+ ...(auditorSubject === undefined ? {} : { auditorSubject }),
173
+ };
174
+ });
166
175
  }
167
176
  /**
168
177
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
@@ -177,17 +186,19 @@ export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
177
186
  }
178
187
  /** Persist each direct caller observed while a retained run is resumed. */
179
188
  export async function recordAdmittedCorrelation(admitted, correlationId) {
180
- const current = JSON.parse(await readFile(admitted.admittedRequestPath, "utf8"));
181
- const prior = [
182
- ...(Array.isArray(current.correlationIds)
183
- ? current.correlationIds.filter((value) => typeof value === "string" && value.trim() !== "")
184
- : []),
185
- ...(typeof current.correlationId === "string" && current.correlationId.trim() !== ""
186
- ? [current.correlationId]
187
- : []),
188
- ];
189
- const correlationIds = [...new Set([...prior, correlationId])];
190
- await writeFile(admitted.admittedRequestPath, `${JSON.stringify({ ...current, correlationId, correlationIds }, null, 2)}\n`, "utf8");
189
+ let correlationIds = [];
190
+ await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
191
+ const prior = [
192
+ ...(Array.isArray(current.correlationIds)
193
+ ? current.correlationIds.filter((value) => typeof value === "string" && value.trim() !== "")
194
+ : []),
195
+ ...(typeof current.correlationId === "string" && current.correlationId.trim() !== ""
196
+ ? [current.correlationId]
197
+ : []),
198
+ ];
199
+ correlationIds = [...new Set([...prior, correlationId])];
200
+ return { ...current, correlationId, correlationIds };
201
+ });
191
202
  await mergeInvocationIdentityPage(admitted.runDirectory, {
192
203
  correlationId,
193
204
  correlationIds,
@@ -198,7 +209,7 @@ export async function recordAdmittedCorrelation(admitted, correlationId) {
198
209
  }
199
210
  /** File a role run under its latest typed ticket identity. */
200
211
  export async function relocateAdmittedRunToTicket(admitted, authority, heldLease) {
201
- if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`))
212
+ if (admitted.ticketNumber === undefined || !isUnboundRunDirectory(admitted.runDirectory))
202
213
  return undefined;
203
214
  const oldRunDirectory = admitted.runDirectory;
204
215
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
@@ -264,12 +275,13 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
264
275
  }
265
276
  /** Persist the exact child identity; later parent binding never inspects siblings. */
266
277
  export async function recordChildDiaristRun(parent, childRunId) {
267
- const page = JSON.parse(await readFile(parent.admittedRequestPath, "utf8"));
268
- const existing = Array.isArray(page.childDiaristRunIds)
269
- ? page.childDiaristRunIds.filter((runId) => typeof runId === "string")
270
- : [];
271
- const childDiaristRunIds = existing.includes(childRunId) ? existing : [...existing, childRunId];
272
- await writeFile(parent.admittedRequestPath, `${JSON.stringify({ ...page, childDiaristRunIds }, null, 2)}\n`, "utf8");
278
+ await updateDurableJsonPage(parent.admittedRequestPath, (page) => {
279
+ const existing = Array.isArray(page.childDiaristRunIds)
280
+ ? page.childDiaristRunIds.filter((runId) => typeof runId === "string")
281
+ : [];
282
+ const childDiaristRunIds = existing.includes(childRunId) ? existing : [...existing, childRunId];
283
+ return { ...page, childDiaristRunIds };
284
+ });
273
285
  }
274
286
  /**
275
287
  * Bind ticket identity onto a run directory's durable pages when the admitted
@@ -279,10 +291,16 @@ export async function recordChildDiaristRun(parent, childRunId) {
279
291
  export async function bindTicketNumberOnRunDirectory(runDirectory, ticketNumber) {
280
292
  requireSafePositiveTicketNumber(ticketNumber, "bindTicketNumberOnRunDirectory");
281
293
  const admittedPath = join(runDirectory, "admitted-request.json");
282
- const admitted = JSON.parse(await readFile(admittedPath, "utf8"));
283
- if (admitted.ticketNumber === ticketNumber)
294
+ let unchanged = false;
295
+ await updateDurableJsonPage(admittedPath, (admitted) => {
296
+ if (admitted.ticketNumber === ticketNumber) {
297
+ unchanged = true;
298
+ return undefined;
299
+ }
300
+ return { ...admitted, ticketNumber };
301
+ });
302
+ if (unchanged)
284
303
  return;
285
- await writeFile(admittedPath, `${JSON.stringify({ ...admitted, ticketNumber }, null, 2)}\n`, "utf8");
286
304
  await mergeInvocationIdentityPage(runDirectory, { ticketNumber });
287
305
  }
288
306
  /** Add the identity returned by the production Pi launch seam to its existing ledger page. */
@@ -528,18 +546,31 @@ function requireOptionPath(flag, value) {
528
546
  * next option), and a leading `-` (read as the next Skill option). Not a
529
547
  * general free-text gate — only the projection admission seam.
530
548
  */
549
+ function skillArgTokenFault(value) {
550
+ if (value === undefined || value.trim() === "")
551
+ return "empty";
552
+ if (/\s/.test(value))
553
+ return "whitespace";
554
+ if (value.startsWith("-"))
555
+ return "optionLike";
556
+ return undefined;
557
+ }
531
558
  function requireSkillArgToken(value, messages) {
532
- if (value === undefined || value.trim() === "") {
533
- throw new CliUsageError(messages.empty);
534
- }
535
- if (/\s/.test(value)) {
536
- throw new CliUsageError(messages.whitespace);
537
- }
538
- if (value.startsWith("-")) {
539
- throw new CliUsageError(messages.optionLike);
540
- }
559
+ const fault = skillArgTokenFault(value);
560
+ if (fault !== undefined)
561
+ throw new CliUsageError(messages[fault]);
541
562
  return value;
542
563
  }
564
+ /**
565
+ * Durable reviewer base on resume. Same token rule as fresh `--base`,
566
+ * with the resume-owned missing/damaged split.
567
+ */
568
+ export function admittedReviewerBaseFault(value) {
569
+ const fault = skillArgTokenFault(value);
570
+ if (fault === undefined)
571
+ return undefined;
572
+ return fault === "empty" ? "missing" : "damaged";
573
+ }
543
574
  /**
544
575
  * Reviewer --base admission: nonempty single Skill-arg token (shared rule with
545
576
  * requireAuthorityRef). Multi-token / option-like values smuggle extra flags
@@ -789,9 +820,6 @@ export async function admitPublicRole(role, parsed, env, override) {
789
820
  });
790
821
  }
791
822
  case "worker-packet": {
792
- if (parsed.project !== undefined) {
793
- requireOptionPath("--project", parsed.project);
794
- }
795
823
  if (instruction.trim() === "") {
796
824
  throw new CliUsageError("fixer requires a nonblank repair instruction");
797
825
  }
@@ -844,19 +872,7 @@ export async function admitPublicRole(role, parsed, env, override) {
844
872
  });
845
873
  }
846
874
  case "collect-target": {
847
- if (parsed.project !== undefined) {
848
- requireOptionPath("--project", parsed.project);
849
- }
850
- let explicitPrNumber;
851
- if (parsed.prNumber !== undefined) {
852
- try {
853
- explicitPrNumber = parseCollectorPrNumber(parsed.prNumber);
854
- }
855
- catch (error) {
856
- const detail = error instanceof Error ? error.message : String(error);
857
- throw new CliUsageError(detail, { cause: error });
858
- }
859
- }
875
+ const explicitPrNumber = parsed.prNumber;
860
876
  const projectRoot = resolve(parsed.project ?? shared.cwd);
861
877
  let repository;
862
878
  if (parsed.repo !== undefined) {
@@ -907,15 +923,10 @@ export async function admitPublicRole(role, parsed, env, override) {
907
923
  return { ...admittedCollector, repository };
908
924
  }
909
925
  case "case-identity": {
910
- if (parsed.project !== undefined) {
911
- requireOptionPath("--project", parsed.project);
912
- }
913
- const issueNumber = parsed.issueNumber ?? Number.NaN;
914
- if (!Number.isInteger(issueNumber) ||
915
- issueNumber < 1 ||
916
- !DOCTOR_ISSUE_NUMBER_PATTERN.test(String(issueNumber))) {
917
- throw new CliUsageError(`doctor --issue must be a positive integer, got ${issueNumber}`);
926
+ if (parsed.issueNumber === undefined) {
927
+ throw new CliUsageError(`doctor --issue must be a positive integer, got ${Number.NaN}`);
918
928
  }
929
+ const issueNumber = parsed.issueNumber;
919
930
  let frozenAttachments = [];
920
931
  const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
921
932
  ...shared,
@@ -970,23 +981,22 @@ export async function admitPublicRole(role, parsed, env, override) {
970
981
  return { ...admittedDoctor, attachments: frozenAttachments };
971
982
  }
972
983
  case "source-locator": {
973
- if (parsed.project !== undefined) {
974
- requireOptionPath("--project", parsed.project);
975
- }
976
984
  const projectRoot = resolve(parsed.project ?? shared.cwd);
977
- let sourceRun;
978
- try {
979
- sourceRun = await resolveNotarySourceRunLocator({
980
- projectRoot,
981
- sourceRun: parsed.sourceRun ?? "",
982
- home: shared.home,
983
- });
984
- }
985
- catch (error) {
986
- if (error instanceof NotarySourceRunError) {
987
- throw new CliUsageError(error.message, { cause: error });
985
+ let sourceRun = override?.resolvedSourceRun;
986
+ if (sourceRun === undefined) {
987
+ try {
988
+ sourceRun = await resolveNotarySourceRunLocator({
989
+ projectRoot,
990
+ sourceRun: parsed.sourceRun ?? "",
991
+ home: shared.home,
992
+ });
993
+ }
994
+ catch (error) {
995
+ if (error instanceof NotarySourceRunError) {
996
+ throw new CliUsageError(error.message, { cause: error });
997
+ }
998
+ throw error;
988
999
  }
989
- throw error;
990
1000
  }
991
1001
  // Board ticket on the source run wins; otherwise the summons ticket. Code does not infer one.
992
1002
  const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
@@ -1007,9 +1017,6 @@ export async function admitPublicRole(role, parsed, env, override) {
1007
1017
  });
1008
1018
  }
1009
1019
  case "gleaner": {
1010
- if (parsed.project !== undefined) {
1011
- requireOptionPath("--project", parsed.project);
1012
- }
1013
1020
  const baseRevision = parsed.baseRevision ?? "";
1014
1021
  if (baseRevision.trim() === "") {
1015
1022
  throw new CliUsageError("--base requires a nonempty revision");
@@ -1024,16 +1031,19 @@ export async function admitPublicRole(role, parsed, env, override) {
1024
1031
  });
1025
1032
  }
1026
1033
  case "review-basis": {
1027
- const lens = requireReviewerLens(parsed.lens);
1028
- if (parsed.project !== undefined) {
1029
- requireOptionPath("--project", parsed.project);
1034
+ if (parsed.lens === undefined) {
1035
+ throw new CliUsageError("--lens requires completeness or correctness");
1036
+ }
1037
+ if (parsed.baseRevision === undefined) {
1038
+ throw new CliUsageError("--base requires a nonempty revision");
1030
1039
  }
1031
- const baseRevision = requireReviewerBaseRevision(parsed.baseRevision);
1032
1040
  const rawRefs = parsed.authorityRefs ?? [];
1033
1041
  if (rawRefs.length === 0) {
1034
1042
  throw new CliUsageError("reviewer requires --authority-ref <ref>");
1035
1043
  }
1036
- const authorityRefs = Object.freeze(rawRefs.map((ref) => requireAuthorityRef(ref)));
1044
+ const lens = parsed.lens;
1045
+ const baseRevision = parsed.baseRevision;
1046
+ const authorityRefs = Object.freeze([...rawRefs]);
1037
1047
  const admittedReviewer = await admitStandardMaterialInvocation("reviewer", {
1038
1048
  ...shared,
1039
1049
  instruction,
@@ -1048,9 +1058,6 @@ export async function admitPublicRole(role, parsed, env, override) {
1048
1058
  return { ...admittedReviewer, authorityRefs };
1049
1059
  }
1050
1060
  case "merge-envelope": {
1051
- if (parsed.project !== undefined) {
1052
- requireOptionPath("--project", parsed.project);
1053
- }
1054
1061
  if (instruction.trim() === "") {
1055
1062
  throw new CliUsageError("merger requires a nonblank task instruction");
1056
1063
  }
@@ -1106,9 +1113,6 @@ function admissionSubject(ticketNumber) {
1106
1113
  * Every public seat persists through this function. Seat-only fields are supplied by the caller.
1107
1114
  */
1108
1115
  async function placeRoleAdmission(options) {
1109
- if (options.project !== undefined) {
1110
- requireOptionPath("--project", options.project);
1111
- }
1112
1116
  const projectRoot = resolve(options.project ?? options.cwd);
1113
1117
  const runId = options.runId ?? (options.createRunId ?? uuidv7)();
1114
1118
  const ticketFields = ticketAdmissionFields(options.assertedTicketNumber);