@akagilnc/pi-workflow-roles 0.1.1893 → 0.1.1941
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 +12 -6
- package/dist/package-contracts/reviewer-output.js +16 -0
- package/dist/public-cli/main.js +447 -173
- package/dist/reviewer-construction.js +46 -6
- package/dist/reviewer-dispatch.js +64 -1
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/reviewer-pinned-git.js +33 -0
- package/package.json +1 -1
- package/src/evidence-child-executor.ts +13 -6
- package/src/factory-board.ts +60 -3
- package/src/package-contracts/reviewer-output.ts +19 -0
- package/src/public-cli/cli.ts +96 -39
- package/src/public-cli/coder-run.ts +11 -4
- package/src/public-cli/collector-run.ts +7 -3
- package/src/public-cli/fixer-run.ts +11 -4
- package/src/public-cli/invocation.ts +183 -102
- package/src/public-cli/judge-run.ts +11 -4
- package/src/public-cli/merger-run.ts +11 -4
- package/src/public-cli/reviewer-run.ts +23 -4
- package/src/public-cli/run-lifecycle.ts +97 -1
- package/src/public-cli/settlement.ts +8 -0
- package/src/reviewer-construction.ts +64 -7
- package/src/reviewer-dispatch.ts +81 -2
- package/src/reviewer-execution-ledger.ts +2 -1
- package/src/reviewer-pinned-git.ts +43 -0
- package/src/reviewer-role.ts +143 -132
- package/src/reviewer-settlement.ts +4 -0
- package/src/role-runtime.ts +186 -3
- package/src/ticket-frontmatter.ts +94 -0
- package/src/ticket-trajectory.ts +145 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed ticket face contract (#176 option A).
|
|
3
|
+
*
|
|
4
|
+
* Machine field lives in YAML frontmatter only:
|
|
5
|
+
* ---
|
|
6
|
+
* ticketNumber: <positive integer>
|
|
7
|
+
* ---
|
|
8
|
+
*
|
|
9
|
+
* Admission reads frozen attachment bytes for this field alone.
|
|
10
|
+
* No filename, path, or free-text inference. Absent / invalid / conflicting
|
|
11
|
+
* values are unbound (undefined) — never a reject gate.
|
|
12
|
+
*
|
|
13
|
+
* Buffer bodies use strict UTF-8 (decode failure → unbound). Opening and
|
|
14
|
+
* closing fences must each be an exclusive line of exactly `---`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { exactUtf8 } from "./exact-utf8.ts";
|
|
18
|
+
|
|
19
|
+
/** Positive integer GitHub/issue identity carried on a typed ticket face. */
|
|
20
|
+
export type TicketIdentity = number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse one attachment body for the typed frontmatter ticketNumber field.
|
|
24
|
+
* Returns undefined when the contract is absent or not well-formed.
|
|
25
|
+
*/
|
|
26
|
+
export function parseTicketNumberFrontmatter(
|
|
27
|
+
bytes: Buffer | string,
|
|
28
|
+
): TicketIdentity | undefined {
|
|
29
|
+
let text: string;
|
|
30
|
+
if (typeof bytes === "string") {
|
|
31
|
+
text = bytes;
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
text = exactUtf8(bytes, "ticket face");
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Opening fence must be an exclusive first line of exactly `---`.
|
|
41
|
+
let cursor: number;
|
|
42
|
+
if (text.startsWith("---\n")) {
|
|
43
|
+
cursor = 4;
|
|
44
|
+
} else if (text.startsWith("---\r\n")) {
|
|
45
|
+
cursor = 5;
|
|
46
|
+
} else {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Closing fence must likewise be an exclusive line of exactly `---`.
|
|
51
|
+
const fmLines: string[] = [];
|
|
52
|
+
let closed = false;
|
|
53
|
+
while (cursor < text.length) {
|
|
54
|
+
const nl = text.indexOf("\n", cursor);
|
|
55
|
+
const lineEnd = nl === -1 ? text.length : nl;
|
|
56
|
+
let line = text.slice(cursor, lineEnd);
|
|
57
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
58
|
+
if (line === "---") {
|
|
59
|
+
closed = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
fmLines.push(line);
|
|
63
|
+
if (nl === -1) break;
|
|
64
|
+
cursor = nl + 1;
|
|
65
|
+
}
|
|
66
|
+
if (!closed) return undefined;
|
|
67
|
+
|
|
68
|
+
let found: number | undefined;
|
|
69
|
+
for (const line of fmLines) {
|
|
70
|
+
const match = /^[ \t]*ticketNumber[ \t]*:[ \t]*(\d+)[ \t]*$/.exec(line);
|
|
71
|
+
if (match === null) continue;
|
|
72
|
+
const value = Number(match[1]);
|
|
73
|
+
if (!Number.isInteger(value) || value < 1) return undefined;
|
|
74
|
+
if (found !== undefined && found !== value) return undefined;
|
|
75
|
+
found = value;
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve at most one ticketNumber across frozen attachment bodies.
|
|
82
|
+
* Zero matches → unbound. Multiple distinct values → unbound (do not guess).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveTicketNumberFromAttachmentBodies(
|
|
85
|
+
bodies: readonly (Buffer | string)[],
|
|
86
|
+
): TicketIdentity | undefined {
|
|
87
|
+
const found = new Set<number>();
|
|
88
|
+
for (const body of bodies) {
|
|
89
|
+
const value = parseTicketNumberFrontmatter(body);
|
|
90
|
+
if (value !== undefined) found.add(value);
|
|
91
|
+
}
|
|
92
|
+
if (found.size !== 1) return undefined;
|
|
93
|
+
return found.values().next().value;
|
|
94
|
+
}
|
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
|
|