@akagilnc/pi-workflow-roles 0.1.5614 → 0.1.5650

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/acp-host/production-host.js +960 -465
  2. package/dist/activation-ledger-topology.js +12 -0
  3. package/dist/external-host-turn-loop.js +281 -0
  4. package/dist/headless-host/production-host.js +900 -406
  5. package/dist/migrate-book-topology.js +8 -5
  6. package/dist/pi/role-turn-host.js +33 -3
  7. package/dist/public-cli/instruction-seat-run.js +177 -46
  8. package/dist/public-cli/invocation.js +5 -12
  9. package/dist/public-cli/main.js +541 -165
  10. package/dist/public-cli/post-admission.js +171 -18
  11. package/dist/public-cli/settlement.js +20 -10
  12. package/dist/report-ticket-tool.js +136 -0
  13. package/dist/role-run-placement.js +1 -2
  14. package/dist/role-run-relocation.js +24 -4
  15. package/package.json +1 -1
  16. package/resources/engine-dispatch.md +6 -3
  17. package/resources/engines/claude-code.md +4 -5
  18. package/resources/missing-ticket-reask.md +1 -0
  19. package/src/acp-host/role-turn-host.ts +41 -31
  20. package/src/activation-ledger-topology.ts +16 -0
  21. package/src/external-host-turn-loop.ts +103 -12
  22. package/src/headless-host/role-turn-host.ts +41 -25
  23. package/src/pi/role-turn-host.ts +37 -3
  24. package/src/public-cli/instruction-seat-run.ts +248 -50
  25. package/src/public-cli/invocation.ts +15 -27
  26. package/src/public-cli/post-admission.ts +250 -33
  27. package/src/public-cli/run-lifecycle.ts +5 -5
  28. package/src/public-cli/settlement.ts +38 -11
  29. package/src/public-cli/terminal.ts +6 -0
  30. package/src/report-ticket-tool.ts +176 -0
  31. package/src/role-envelope.ts +28 -3
  32. package/src/role-run-placement.ts +0 -2
  33. package/src/role-run-relocation.ts +45 -4
  34. package/src/role-runtime.ts +6 -0
  35. package/src/worker-submission-gates.ts +26 -3
@@ -28,6 +28,17 @@ function tryHomeFromAkRolesPath(path) {
28
28
  }
29
29
  return path.slice(0, match.index);
30
30
  }
31
+ function tryBookKeyFromAkRolesPath(path) {
32
+ const home = tryHomeFromAkRolesPath(path);
33
+ if (home === void 0 || home.length === 0) return void 0;
34
+ const booksRoot = join(resolveActivationLedgerHome(home), "books");
35
+ const rel = relative(booksRoot, resolve(path));
36
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
37
+ return void 0;
38
+ }
39
+ const bookKey = rel.split(sep)[0];
40
+ return bookKey === void 0 || bookKey === "" ? void 0 : bookKey;
41
+ }
31
42
  function homeFromRunDirectory(runDirectory) {
32
43
  const home = tryHomeFromAkRolesPath(runDirectory);
33
44
  if (home === void 0 || home.length === 0) {
@@ -258,5 +269,6 @@ export {
258
269
  physicallyContainedIn,
259
270
  resolveActivationLedgerHome,
260
271
  resolveActivationLedgerHomeForPath,
272
+ tryBookKeyFromAkRolesPath,
261
273
  tryHomeFromAkRolesPath
262
274
  };
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Middle external-host role-turn loop (#820 / ADR 0082).
3
+ * One copy: serial, abort merge/race, closeRound re-ask rounds,
4
+ * host-aborted, round-limit. Last hop = ExternalHostTurnDriver (four verbs).
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { basename } from "node:path";
8
+ import { tryBookKeyFromAkRolesPath } from "./activation-ledger-topology.js";
9
+ import { projectThrownFailureLeaf, retainPackageFault } from "./public-cli/settlement.js";
10
+ import { describeErrorIdentity, findRunDirectoryById } from "./public-cli/run-lifecycle.js";
11
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.js";
12
+ import { parseRunLeaf } from "./role-run-placement.js";
13
+ import { projectTurnRequestLiveRunDirectory } from "./role-run-relocation.js";
14
+ import { reportRunRecord } from "./sitian-facade.js";
15
+ import { isOneShotWorkerReminderCode } from "./submission-errors.js";
16
+ /**
17
+ * #1171: mid-turn report-ticket may have moved the leg. Prefer the recorded
18
+ * path when it still exists; otherwise re-locate by run id inside the book.
19
+ * Undefined means vanished and unlocatable — callers must not revive the dead leaf.
20
+ */
21
+ export async function resolveLiveRunDirectoryPath(recordedPath, home) {
22
+ if (existsSync(recordedPath))
23
+ return recordedPath;
24
+ const parsed = parseRunLeaf(basename(recordedPath));
25
+ const bookKey = tryBookKeyFromAkRolesPath(recordedPath);
26
+ if (parsed === undefined || bookKey === undefined)
27
+ return undefined;
28
+ return await findRunDirectoryById(home, parsed.runId, bookKey, parsed.role);
29
+ }
30
+ /**
31
+ * Prefer the request path when it still exists; otherwise re-locate by run id
32
+ * (resolveLiveRunDirectoryPath). Mutates the live request so later turn-record /
33
+ * exit-copy / fault notes stay on the new leaf.
34
+ */
35
+ export async function syncTurnRequestLivePlacement(request) {
36
+ const live = await resolveLiveRunDirectoryPath(request.runDirectory, request.home);
37
+ if (live === undefined) {
38
+ const parsed = parseRunLeaf(basename(request.runDirectory));
39
+ throw new Error(`admitted run ${parsed?.runId ?? "(unparseable)"} missing after host turn; not found under ${request.home}`);
40
+ }
41
+ if (live !== request.runDirectory) {
42
+ projectTurnRequestLiveRunDirectory(request, live);
43
+ }
44
+ return live;
45
+ }
46
+ /**
47
+ * Soft live-path sync for cleanup / dossier work: lookup failure stays beside the
48
+ * caller and never revives a vanished leaf. Same authority as syncTurnRequestLivePlacement.
49
+ */
50
+ export async function trySyncTurnRequestLivePlacement(request) {
51
+ try {
52
+ return { runDirectory: await syncTurnRequestLivePlacement(request) };
53
+ }
54
+ catch (resolveError) {
55
+ return { resolveError };
56
+ }
57
+ }
58
+ /**
59
+ * Retain a package fault on the live leaf when locatable. Lookup failure must not
60
+ * replace the original fault or block later cleanup — reuse retainPackageFault when
61
+ * the leaf exists; otherwise present both causes on stderr (Pi transport path shape).
62
+ */
63
+ export async function retainPackageFaultBesideLivePlacement(request, input) {
64
+ const live = await trySyncTurnRequestLivePlacement(request);
65
+ if ("resolveError" in live) {
66
+ const resolveDiagnostic = `live run path resolve failed beside fault: ${describeErrorIdentity(live.resolveError)}`;
67
+ try {
68
+ process.stderr.write(`${input.diagnostic}\n`);
69
+ process.stderr.write(`${resolveDiagnostic}\n`);
70
+ }
71
+ catch {
72
+ // Best-effort presentation beside an already-chosen terminal.
73
+ }
74
+ return;
75
+ }
76
+ await retainPackageFault({
77
+ runDirectory: live.runDirectory,
78
+ diagnostic: input.diagnostic,
79
+ ...(Object.hasOwn(input, "error") ? { error: input.error } : {}),
80
+ });
81
+ }
82
+ /**
83
+ * What one host start was given: one `turn-delivery` row of history.jsonl per start. History is
84
+ * the volume whose write failure does not stop a run (#833), so a row that cannot be
85
+ * written is noted and the host still starts.
86
+ */
87
+ export async function recordTurnDelivery(runDirectory, delivered, source) {
88
+ try {
89
+ reportRunRecord(runDirectory, "turn-delivery", {
90
+ systemPrompt: delivered.systemPrompt,
91
+ ...(delivered.outputSchema === undefined ? {} : { outputSchema: delivered.outputSchema }),
92
+ }, source);
93
+ }
94
+ catch (error) {
95
+ await retainPackageFault({
96
+ runDirectory,
97
+ diagnostic: `turn delivery record failed beside host start: ${describeErrorIdentity(error)}`,
98
+ error,
99
+ });
100
+ }
101
+ }
102
+ export function mergeRoleTurnAbortSignals(prepared, request) {
103
+ if (request === undefined)
104
+ return prepared;
105
+ if (prepared === undefined)
106
+ return request;
107
+ return AbortSignal.any([prepared, request]);
108
+ }
109
+ export function isHostAbortedError(error) {
110
+ return typeof error === "object" && error !== null && error.code === "host-aborted";
111
+ }
112
+ export function hostAbortedError(message = "host aborted") {
113
+ return Object.assign(new Error(message), { code: "host-aborted" });
114
+ }
115
+ /** Race host work against envelope/parent abort (#593). */
116
+ export function raceAgainstHostAbort(work, abortSignal, message = "host aborted") {
117
+ if (abortSignal?.aborted)
118
+ return Promise.reject(hostAbortedError(message));
119
+ if (abortSignal === undefined)
120
+ return work;
121
+ return new Promise((resolve, reject) => {
122
+ let settled = false;
123
+ const onAbort = () => {
124
+ if (settled)
125
+ return;
126
+ settled = true;
127
+ work.catch(() => { });
128
+ reject(hostAbortedError(message));
129
+ };
130
+ abortSignal.addEventListener("abort", onAbort, { once: true });
131
+ work.then((value) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ abortSignal.removeEventListener("abort", onAbort);
136
+ resolve(value);
137
+ }, (error) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ abortSignal.removeEventListener("abort", onAbort);
142
+ reject(error);
143
+ });
144
+ });
145
+ }
146
+ export function externalHostFailure(cause, name, code, details, diagnostic) {
147
+ return asFailure({
148
+ cause,
149
+ identity: { name, code },
150
+ ...(diagnostic === undefined ? {} : { diagnostic }),
151
+ ...(details === undefined ? {} : { details }),
152
+ });
153
+ }
154
+ function asFailure(knownFailure, stderr = "") {
155
+ return { code: null, stderr, timedOut: false, knownFailure };
156
+ }
157
+ async function settleHostAborted(prepared, sessionId) {
158
+ const closure = await prepared.closeRound();
159
+ if ("failure" in closure)
160
+ return asFailure(closure.failure);
161
+ return asFailure({
162
+ cause: "session",
163
+ identity: { name: "HostAborted", code: "host-aborted" },
164
+ ...(sessionId === undefined ? {} : { details: { sessionId } }),
165
+ });
166
+ }
167
+ /** Shared external-host retry/resume loop. Caller owns session open + teardown. */
168
+ export async function driveExternalRoleTurnRounds(prepared, request, driver) {
169
+ let prompt = prepared.prompt;
170
+ const abortSignal = mergeRoleTurnAbortSignals(prepared.abortSignal, request.signal);
171
+ let result = { code: 0, stderr: "", timedOut: false };
172
+ // #1132: the first round is the initial delivery. Each later counted re-ask
173
+ // spends the configured ceiling. The one commit reminder and the one prefix
174
+ // reminder are not counted re-asks (ADR 0066/0070).
175
+ const countedReaskLimit = deliveryLimitFromConfig(request.deliveryRequestLimit);
176
+ const exemptReminders = new Set();
177
+ let countedReasks = 0;
178
+ for (let attempt = 0;; attempt += 1) {
179
+ if (abortSignal?.aborted)
180
+ return settleHostAborted(prepared, driver.currentSessionId());
181
+ let round;
182
+ try {
183
+ round = await driver.runRound({ prompt, abortSignal, attempt });
184
+ }
185
+ catch (error) {
186
+ if (isHostAbortedError(error))
187
+ return settleHostAborted(prepared, driver.currentSessionId());
188
+ throw error;
189
+ }
190
+ if (round.status === "terminal")
191
+ return round.result;
192
+ // Only this round's exit and stderr belong to this round's terminal.
193
+ // ACP has no child exit; its successful protocol completion carries code 0.
194
+ result = {
195
+ code: round.code !== undefined ? round.code : 0,
196
+ stderr: round.stderr ?? "",
197
+ timedOut: round.timedOut === true,
198
+ ...(round.signal === undefined ? {} : { signal: round.signal }),
199
+ };
200
+ let closure;
201
+ try {
202
+ closure = await prepared.closeRound();
203
+ }
204
+ catch (error) {
205
+ await retainPackageFaultBesideLivePlacement(request, {
206
+ diagnostic: `round closure failed beside host terminal: ${describeErrorIdentity(error)}`,
207
+ error,
208
+ });
209
+ return result.code === 0 && !result.timedOut && result.signal === undefined
210
+ ? { ...result, knownFailure: projectThrownFailureLeaf(error) }
211
+ : result;
212
+ }
213
+ if (closure.accepted) {
214
+ try {
215
+ await driver.afterAccepted?.();
216
+ }
217
+ catch (error) {
218
+ await retainPackageFaultBesideLivePlacement(request, {
219
+ diagnostic: `post-acceptance close failed beside host terminal: ${describeErrorIdentity(error)}`,
220
+ error,
221
+ });
222
+ }
223
+ return result;
224
+ }
225
+ if ("failure" in closure) {
226
+ await retainPackageFaultBesideLivePlacement(request, {
227
+ diagnostic: `round closure failed beside host terminal: ${JSON.stringify(closure.failure)}`,
228
+ });
229
+ // This is an explicit envelope failure (e.g. required audit/ledger failure),
230
+ // not an inferred host cause. Preserve the process facts alongside it.
231
+ return result.code === 0 && !result.timedOut && result.signal === undefined
232
+ ? { ...result, knownFailure: closure.failure }
233
+ : result;
234
+ }
235
+ if (result.code !== 0 || result.signal !== undefined || result.timedOut)
236
+ return result;
237
+ const reminderCode = closure.retry.code;
238
+ const exempt = isOneShotWorkerReminderCode(reminderCode) && !exemptReminders.has(reminderCode);
239
+ if (exempt)
240
+ exemptReminders.add(reminderCode);
241
+ else {
242
+ countedReasks += 1;
243
+ if (countedReasks > countedReaskLimit) {
244
+ await retainPackageFaultBesideLivePlacement(request, {
245
+ diagnostic: `${driver.roundLimitName}: round-retry-limit`,
246
+ });
247
+ return result;
248
+ }
249
+ }
250
+ prompt = closure.retry.message;
251
+ driver.afterRetry?.();
252
+ }
253
+ }
254
+ /** Required envelope shutdown fails a clean host result; an existing host
255
+ * failure stays primary. Ordinary cleanup is handled at its owning seam. */
256
+ export async function disposeExternalRoleTurn(prepared, request, outcome) {
257
+ try {
258
+ await prepared.dispose?.();
259
+ }
260
+ catch (error) {
261
+ await retainPackageFaultBesideLivePlacement(request, {
262
+ diagnostic: `required envelope shutdown failed beside host terminal: ${describeErrorIdentity(error)}`,
263
+ error,
264
+ });
265
+ if (outcome.code === 0 && !outcome.timedOut && outcome.signal === undefined
266
+ && outcome.knownFailure === undefined) {
267
+ return { ...outcome, knownFailure: projectThrownFailureLeaf(error) };
268
+ }
269
+ }
270
+ return outcome;
271
+ }
272
+ export function createSerializedRoleTurnHost(execute) {
273
+ let serial = Promise.resolve();
274
+ return {
275
+ executeTurn(request) {
276
+ const execution = serial.then(() => execute(request));
277
+ serial = execution.then(() => undefined, () => undefined);
278
+ return execution;
279
+ },
280
+ };
281
+ }