@akagilnc/pi-workflow-roles 0.1.1883 → 0.1.1918
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.
- package/dist/evidence-child-executor.js +153 -16
- package/dist/navigator-attendance.js +64 -29
- package/dist/public-cli/main.js +786 -373
- package/dist/reviewer-child-executor.js +1 -1
- package/dist/typed-provider-http.js +60 -0
- package/dist/upstream-error-testimony.js +19 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/evidence-child-executor.ts +177 -24
- package/src/factory-board.ts +60 -3
- package/src/navigator-attendance.ts +86 -33
- package/src/public-cli/cli.ts +96 -39
- package/src/public-cli/coder-run.ts +36 -11
- package/src/public-cli/collector-run.ts +8 -3
- package/src/public-cli/doctor-run.ts +1 -0
- package/src/public-cli/explicit-internal.ts +61 -19
- package/src/public-cli/fixer-run.ts +36 -11
- package/src/public-cli/invocation.ts +137 -101
- package/src/public-cli/judge-run.ts +39 -11
- package/src/public-cli/merger-run.ts +36 -11
- package/src/public-cli/reviewer-run.ts +35 -11
- package/src/public-cli/run-lifecycle.ts +89 -76
- package/src/public-cli/settlement.ts +256 -66
- package/src/reviewer-child-executor.ts +1 -1
- package/src/role-runtime.ts +18 -8
- package/src/ticket-frontmatter.ts +94 -0
- package/src/ticket-trajectory.ts +145 -5
- package/src/typed-provider-http.ts +99 -0
- package/src/upstream-error-testimony.ts +45 -0
package/src/ticket-trajectory.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./package-contracts/terminating-tools.ts";
|
|
39
39
|
import { PACKAGED_ROLE_REGISTRY } from "./packaged-role-registry.ts";
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
/** Declared refresh bound for the same viewing surface (seconds). */
|
|
42
43
|
export const DEFAULT_REFRESH_BOUNDARY_SECONDS = 30;
|
|
43
44
|
|
|
@@ -328,6 +329,8 @@ type InvocationInfo = {
|
|
|
328
329
|
model?: string;
|
|
329
330
|
provider?: string;
|
|
330
331
|
thinking?: string;
|
|
332
|
+
ticketNumber?: number;
|
|
333
|
+
correlationId?: string;
|
|
331
334
|
};
|
|
332
335
|
|
|
333
336
|
async function readInvocation(runDir: string): Promise<InvocationInfo | undefined> {
|
|
@@ -348,6 +351,16 @@ async function readInvocation(runDir: string): Promise<InvocationInfo | undefine
|
|
|
348
351
|
info.model = rawModel;
|
|
349
352
|
}
|
|
350
353
|
}
|
|
354
|
+
if (
|
|
355
|
+
typeof parsed.ticketNumber === "number" &&
|
|
356
|
+
Number.isInteger(parsed.ticketNumber) &&
|
|
357
|
+
parsed.ticketNumber >= 1
|
|
358
|
+
) {
|
|
359
|
+
info.ticketNumber = parsed.ticketNumber;
|
|
360
|
+
}
|
|
361
|
+
if (typeof parsed.correlationId === "string" && parsed.correlationId.trim() !== "") {
|
|
362
|
+
info.correlationId = parsed.correlationId;
|
|
363
|
+
}
|
|
351
364
|
return info;
|
|
352
365
|
} catch (error) {
|
|
353
366
|
// Only genuine absence activates the invocation fallback. Malformed JSON and
|
|
@@ -408,9 +421,8 @@ async function maxMtimeMs(paths: readonly string[]): Promise<number> {
|
|
|
408
421
|
return max;
|
|
409
422
|
}
|
|
410
423
|
|
|
411
|
-
async function
|
|
412
|
-
const
|
|
413
|
-
const ledgerCoord = ["issues", String(issueNumber), "runs", runId].join("/");
|
|
424
|
+
async function parseRunDirectory(runDir: string, ledgerCoord: string): Promise<ParsedRun> {
|
|
425
|
+
const runId = basename(runDir);
|
|
414
426
|
const evidenceTarget = await realpathOrLexicalIfMissing(runDir);
|
|
415
427
|
const evidenceHref = pathToFileURL(evidenceTarget).href;
|
|
416
428
|
const sessionDir = join(runDir, "session");
|
|
@@ -496,6 +508,12 @@ async function parseRun(ledgerDir: string, issueNumber: number, runId: string):
|
|
|
496
508
|
};
|
|
497
509
|
}
|
|
498
510
|
|
|
511
|
+
async function parseRun(ledgerDir: string, issueNumber: number, runId: string): Promise<ParsedRun> {
|
|
512
|
+
const runDir = join(ledgerDir, "issues", String(issueNumber), "runs", runId);
|
|
513
|
+
const ledgerCoord = ["issues", String(issueNumber), "runs", runId].join("/");
|
|
514
|
+
return parseRunDirectory(runDir, ledgerCoord);
|
|
515
|
+
}
|
|
516
|
+
|
|
499
517
|
async function listRunIds(ledgerDir: string, issueNumber: number): Promise<string[]> {
|
|
500
518
|
const runsDir = join(ledgerDir, "issues", String(issueNumber), "runs");
|
|
501
519
|
try {
|
|
@@ -686,20 +704,142 @@ ${stationBlocks || "<p data-empty=\"true\">no runs</p>"}
|
|
|
686
704
|
/**
|
|
687
705
|
* Load one ticket's runs via the S1 tracer path (read-only ledger scan).
|
|
688
706
|
* Factory board reuses this — no parallel receipt parser.
|
|
707
|
+
*
|
|
708
|
+
* Flat-run attribution (#176 option A): read typed `ticketNumber` from each
|
|
709
|
+
* `runs/<run>@<role>/invocation.json`. Legacy `issues/<N>/runs` remains a
|
|
710
|
+
* read-only compatibility entrance. Unbound flat runs never join a ticket.
|
|
711
|
+
*/
|
|
712
|
+
|
|
713
|
+
/** One flat run's typed binding projection from invocation.json. */
|
|
714
|
+
export type FlatRunTicketBinding = {
|
|
715
|
+
readonly runDir: string;
|
|
716
|
+
readonly runFolder: string;
|
|
717
|
+
readonly ticketNumber?: number;
|
|
718
|
+
readonly role?: string;
|
|
719
|
+
readonly correlationId?: string;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Book-level flat-run index built once per lane/render and reused per ticket.
|
|
724
|
+
* Pure derived view of flat runs' invocation.json files — not a parallel ledger.
|
|
725
|
+
*/
|
|
726
|
+
export type TicketTrajectoryBookIndex = {
|
|
727
|
+
/** ticket number → flat runs bound via invocation.json ticketNumber. */
|
|
728
|
+
readonly runsByTicket: ReadonlyMap<number, readonly FlatRunTicketBinding[]>;
|
|
729
|
+
/** Flat runs with no typed ticketNumber (unknown/unbound seam). */
|
|
730
|
+
readonly unboundRuns: readonly FlatRunTicketBinding[];
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
async function listFlatRunDirectories(ledgerDir: string): Promise<string[]> {
|
|
734
|
+
const runsDir = join(resolve(ledgerDir), "runs");
|
|
735
|
+
try {
|
|
736
|
+
const entries = await readdir(runsDir, { withFileTypes: true });
|
|
737
|
+
return entries
|
|
738
|
+
.filter((entry) => entry.isDirectory())
|
|
739
|
+
.map((entry) => join(runsDir, entry.name))
|
|
740
|
+
.sort();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (isMissingPathError(error)) return [];
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export async function buildTicketTrajectoryBookIndex(
|
|
748
|
+
ledgerDir: string,
|
|
749
|
+
): Promise<TicketTrajectoryBookIndex> {
|
|
750
|
+
const root = resolve(ledgerDir);
|
|
751
|
+
const runsByTicket = new Map<number, FlatRunTicketBinding[]>();
|
|
752
|
+
const unboundRuns: FlatRunTicketBinding[] = [];
|
|
753
|
+
for (const runDir of await listFlatRunDirectories(root)) {
|
|
754
|
+
const invocation = await readInvocation(runDir);
|
|
755
|
+
const binding: FlatRunTicketBinding = {
|
|
756
|
+
runDir,
|
|
757
|
+
runFolder: basename(runDir),
|
|
758
|
+
...(invocation?.ticketNumber !== undefined
|
|
759
|
+
? { ticketNumber: invocation.ticketNumber }
|
|
760
|
+
: {}),
|
|
761
|
+
...(invocation?.role !== undefined ? { role: invocation.role } : {}),
|
|
762
|
+
...(invocation?.correlationId !== undefined
|
|
763
|
+
? { correlationId: invocation.correlationId }
|
|
764
|
+
: {}),
|
|
765
|
+
};
|
|
766
|
+
if (binding.ticketNumber !== undefined) {
|
|
767
|
+
const list = runsByTicket.get(binding.ticketNumber) ?? [];
|
|
768
|
+
list.push(binding);
|
|
769
|
+
runsByTicket.set(binding.ticketNumber, list);
|
|
770
|
+
} else {
|
|
771
|
+
unboundRuns.push(binding);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return { runsByTicket, unboundRuns };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export type UnboundTrajectoryRun = {
|
|
778
|
+
readonly run: TicketTrajectoryRun;
|
|
779
|
+
readonly role: string;
|
|
780
|
+
readonly observedAt: string;
|
|
781
|
+
readonly correlationId?: string;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Load unbound flat runs for the board unknown seam.
|
|
786
|
+
* Never joins any ticket. Built from the book index (once).
|
|
689
787
|
*/
|
|
788
|
+
export async function loadUnboundTrajectoryRuns(
|
|
789
|
+
ledgerDir: string,
|
|
790
|
+
bookIndex?: TicketTrajectoryBookIndex,
|
|
791
|
+
): Promise<readonly UnboundTrajectoryRun[]> {
|
|
792
|
+
const root = resolve(ledgerDir);
|
|
793
|
+
const index = bookIndex ?? (await buildTicketTrajectoryBookIndex(root));
|
|
794
|
+
const out: UnboundTrajectoryRun[] = [];
|
|
795
|
+
for (const binding of index.unboundRuns) {
|
|
796
|
+
const ledgerCoord = ["runs", binding.runFolder].join("/");
|
|
797
|
+
const run = await parseRunDirectory(binding.runDir, ledgerCoord);
|
|
798
|
+
out.push({
|
|
799
|
+
run,
|
|
800
|
+
role: binding.role ?? run.station,
|
|
801
|
+
observedAt: run.startedAt ?? run.lastActivityAt ?? "",
|
|
802
|
+
...(binding.correlationId === undefined
|
|
803
|
+
? {}
|
|
804
|
+
: { correlationId: binding.correlationId }),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
|
|
690
810
|
export async function loadTicketTrajectoryRuns(
|
|
691
811
|
ledgerDir: string,
|
|
692
812
|
issueNumber: number,
|
|
813
|
+
bookIndex?: TicketTrajectoryBookIndex,
|
|
693
814
|
): Promise<TicketTrajectoryRun[]> {
|
|
694
815
|
if (!Number.isInteger(issueNumber) || issueNumber < 1) {
|
|
695
816
|
throw new Error("issueNumber must be a positive integer");
|
|
696
817
|
}
|
|
697
818
|
const root = resolve(ledgerDir);
|
|
698
|
-
const
|
|
819
|
+
const index = bookIndex ?? (await buildTicketTrajectoryBookIndex(root));
|
|
820
|
+
|
|
699
821
|
const runs: ParsedRun[] = [];
|
|
822
|
+
const seenRunDirs = new Set<string>();
|
|
823
|
+
|
|
824
|
+
// Legacy hand-dispatch path: issues/<N>/runs (read-only compatibility).
|
|
825
|
+
const runIds = await listRunIds(root, issueNumber);
|
|
700
826
|
for (const runId of runIds) {
|
|
701
|
-
|
|
827
|
+
const parsed = await parseRun(root, issueNumber, runId);
|
|
828
|
+
const legacyDir = join(root, "issues", String(issueNumber), "runs", runId);
|
|
829
|
+
seenRunDirs.add(resolve(legacyDir));
|
|
830
|
+
runs.push(parsed);
|
|
702
831
|
}
|
|
832
|
+
|
|
833
|
+
// Flat runs bound by typed invocation.json ticketNumber.
|
|
834
|
+
const flatBindings = index.runsByTicket.get(issueNumber) ?? [];
|
|
835
|
+
for (const binding of flatBindings) {
|
|
836
|
+
const resolvedRunDir = resolve(binding.runDir);
|
|
837
|
+
if (seenRunDirs.has(resolvedRunDir)) continue;
|
|
838
|
+
seenRunDirs.add(resolvedRunDir);
|
|
839
|
+
const ledgerCoord = ["runs", binding.runFolder].join("/");
|
|
840
|
+
runs.push(await parseRunDirectory(binding.runDir, ledgerCoord));
|
|
841
|
+
}
|
|
842
|
+
|
|
703
843
|
return runs;
|
|
704
844
|
}
|
|
705
845
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared durable typed-provider HTTP observation owner.
|
|
3
|
+
* Published with both public-cli and navigator-attendance entries.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const TYPED_HTTP_FILE = "typed-provider-http.json";
|
|
9
|
+
|
|
10
|
+
export type TypedProviderHttpObservation = {
|
|
11
|
+
readonly httpStatus: number;
|
|
12
|
+
readonly provider: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function typedProviderHttpPath(runDirectory: string): string {
|
|
16
|
+
return join(runDirectory, TYPED_HTTP_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clear any prior attempt's typed provider HTTP observation.
|
|
21
|
+
* Each initial/resume dispatch must start without inherited 429 evidence so
|
|
22
|
+
* only the current attempt can qualify v1 resume.
|
|
23
|
+
*/
|
|
24
|
+
export async function clearTypedProviderHttpObservation(
|
|
25
|
+
runDirectory: string,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
await unlink(typedProviderHttpPath(runDirectory));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (
|
|
31
|
+
error instanceof Error &&
|
|
32
|
+
"code" in error &&
|
|
33
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Record a typed provider HTTP status observation for the admitted run.
|
|
43
|
+
* Only non-success HTTP statuses are persisted (authorized error evidence).
|
|
44
|
+
* A 2xx response clears any prior observation so success cannot leave stale
|
|
45
|
+
* error evidence. Never inspects diagnostic prose.
|
|
46
|
+
*/
|
|
47
|
+
export async function recordTypedProviderHttpStatus(
|
|
48
|
+
runDirectory: string,
|
|
49
|
+
observation: { readonly httpStatus: number; readonly provider: string },
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
if (observation.httpStatus >= 200 && observation.httpStatus < 300) {
|
|
52
|
+
await clearTypedProviderHttpObservation(runDirectory);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const body: TypedProviderHttpObservation = {
|
|
56
|
+
httpStatus: observation.httpStatus,
|
|
57
|
+
provider: observation.provider,
|
|
58
|
+
};
|
|
59
|
+
await writeFile(
|
|
60
|
+
typedProviderHttpPath(runDirectory),
|
|
61
|
+
`${JSON.stringify(body)}\n`,
|
|
62
|
+
"utf8",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read the latest durable typed provider HTTP observation, if any.
|
|
68
|
+
* Only ENOENT means absence (undefined). Read/parse/shape failures preserve the
|
|
69
|
+
* real cause for the existing controlled-failure chain — never laundered as absence.
|
|
70
|
+
*/
|
|
71
|
+
export async function readLatestTypedProviderHttpObservation(
|
|
72
|
+
runDirectory: string,
|
|
73
|
+
): Promise<TypedProviderHttpObservation | undefined> {
|
|
74
|
+
let text: string;
|
|
75
|
+
try {
|
|
76
|
+
text = await readFile(typedProviderHttpPath(runDirectory), "utf8");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (
|
|
79
|
+
error instanceof Error &&
|
|
80
|
+
"code" in error &&
|
|
81
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
82
|
+
) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const raw: unknown = JSON.parse(text);
|
|
88
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
89
|
+
throw new Error("typed provider HTTP observation must be a JSON object");
|
|
90
|
+
}
|
|
91
|
+
const record = raw as Record<string, unknown>;
|
|
92
|
+
if (typeof record.httpStatus !== "number") {
|
|
93
|
+
throw new Error("typed provider HTTP observation missing numeric httpStatus");
|
|
94
|
+
}
|
|
95
|
+
if (typeof record.provider !== "string" || record.provider.trim() === "") {
|
|
96
|
+
throw new Error("typed provider HTTP observation missing non-empty provider");
|
|
97
|
+
}
|
|
98
|
+
return { httpStatus: record.httpStatus, provider: record.provider };
|
|
99
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single authority for upstream-error testimony (#307).
|
|
3
|
+
* Non-2xx HTTP or non-empty SDK diagnostics = upstream testimony.
|
|
4
|
+
* body/code/errno project only from a confirmed-remote node.
|
|
5
|
+
* Seams keep their own shape/cause-chain reading and wrap via these primitives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** True when value is a numeric HTTP status outside 2xx. */
|
|
9
|
+
export function isNonSuccessHttpStatus(status: unknown): status is number {
|
|
10
|
+
return typeof status === "number" && (status < 200 || status >= 300);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Upstream testimony is a directly observed HTTP response or SDK-structured
|
|
15
|
+
* remote error. Pi stopReason, selected provider/model names, silent stream
|
|
16
|
+
* death, errorMessage prose (e.g. "500: …"), and locally synthesized errors
|
|
17
|
+
* are not testimony.
|
|
18
|
+
*/
|
|
19
|
+
export function hasUpstreamErrorTestimony(input: {
|
|
20
|
+
readonly httpStatus?: number;
|
|
21
|
+
readonly diagnostics?: unknown;
|
|
22
|
+
}): boolean {
|
|
23
|
+
if (isNonSuccessHttpStatus(input.httpStatus)) return true;
|
|
24
|
+
return Array.isArray(input.diagnostics) && input.diagnostics.length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Project body/code/errno only when the caller already confirmed remote testimony
|
|
29
|
+
* on the same node. Plain local Error.code is never provider testimony.
|
|
30
|
+
*/
|
|
31
|
+
export function projectConfirmedRemotePayload(input: {
|
|
32
|
+
readonly body?: unknown;
|
|
33
|
+
readonly code?: unknown;
|
|
34
|
+
readonly errno?: unknown;
|
|
35
|
+
}): {
|
|
36
|
+
readonly body?: unknown;
|
|
37
|
+
readonly code?: unknown;
|
|
38
|
+
readonly errno?: unknown;
|
|
39
|
+
} {
|
|
40
|
+
return {
|
|
41
|
+
...(input.body === undefined ? {} : { body: input.body }),
|
|
42
|
+
...(input.code === undefined ? {} : { code: input.code }),
|
|
43
|
+
...(input.errno === undefined ? {} : { errno: input.errno }),
|
|
44
|
+
};
|
|
45
|
+
}
|