@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621
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/acp-host/production-host.js +20239 -10556
- package/dist/analyst-gate-cycles-read.js +387 -0
- package/dist/archivist-record-entry.js +22 -1
- package/dist/audit-escalation.js +6 -0
- package/dist/auditor-soul.js +74 -0
- package/dist/collector-config.js +86 -0
- package/dist/collector-evidence.js +316 -0
- package/dist/collector-github.js +527 -0
- package/dist/collector-ledger.js +853 -0
- package/dist/collector-tool-schemas.js +33 -0
- package/dist/compliance-transport.js +104 -52
- package/dist/diarist-mechanical.js +411 -0
- package/dist/diarist-ticket-resolution.js +177 -0
- package/dist/diarist.js +311 -0
- package/dist/doctor-auditor.js +25 -0
- package/dist/doctor-contracts.js +2 -0
- package/dist/doctor-evidence.js +142 -0
- package/dist/gatekeeper-role.js +104 -81
- package/dist/host-transition-prior-native.js +78 -0
- package/dist/institutional-resolution.js +1 -95
- package/dist/judge-auditor.js +26 -0
- package/dist/ledger-session-read.js +227 -0
- package/dist/merger-git-state.js +78 -0
- package/dist/navigator-attendance.js +15 -7
- package/dist/navigator-public-session.js +170 -0
- package/dist/navigator-session-contracts.js +36 -40
- package/dist/notary-source-run.js +122 -0
- package/dist/package-contracts/auditor-output.js +66 -0
- package/dist/package-contracts/evidence-child-output.js +34 -0
- package/dist/package-contracts/judge-output.js +2 -0
- package/dist/package-contracts/terminating-tools.js +25 -2
- package/dist/package-resources/method-skill.js +254 -0
- package/dist/packaged-role-registry.js +36 -0
- package/dist/pi/durable-principal.js +61 -0
- package/dist/pi/in-process-session.js +25 -6
- package/dist/pi/known-failure.js +52 -0
- package/dist/pi/role-turn-host.js +430 -0
- package/dist/public-cli/auto-resume.js +414 -0
- package/dist/public-cli/cli-errors.js +8 -0
- package/dist/public-cli/cli-io.js +1 -0
- package/dist/public-cli/command-renderer.js +5 -0
- package/dist/public-cli/doctor-run.js +84 -0
- package/dist/public-cli/inspector-run.js +136 -0
- package/dist/public-cli/instruction-seat-run.js +256 -0
- package/dist/public-cli/invocation.js +2225 -0
- package/dist/public-cli/judge-run.js +118 -0
- package/dist/public-cli/load-production-acp-host.js +40 -0
- package/dist/public-cli/main.js +1140 -652
- package/dist/public-cli/notary-run.js +161 -0
- package/dist/public-cli/option-definitions.js +1192 -0
- package/dist/public-cli/post-admission.js +667 -0
- package/dist/public-cli/public-run-credentials.js +49 -0
- package/dist/public-cli/registry.js +4 -1
- package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
- package/dist/public-cli/run-lifecycle.js +1344 -0
- package/dist/public-cli/seat-ticket-binding.js +113 -0
- package/dist/public-cli/settlement.js +3441 -0
- package/dist/public-cli/terminal.js +135 -0
- package/dist/public-cli/turn-request.js +25 -0
- package/dist/public-role-summons.js +301 -0
- package/dist/receipt-delivery-policy.js +10 -0
- package/dist/reviewer-child-executor.js +80 -8
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/run-terminal-artifacts.js +195 -0
- package/dist/run-ticket-number.js +40 -0
- package/dist/session-assistant-usage.js +107 -0
- package/dist/shape-unreadable-failure.js +34 -0
- package/dist/submission-errors.js +3 -0
- package/dist/submission-ledger.js +388 -0
- package/dist/ticket-provenance-contracts.js +115 -0
- package/dist/ticket-provenance.js +340 -0
- package/extensions/role-runtime.ts +7 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -1
- package/souls/doctor-auditor.md +1 -0
- package/souls/evidence-child.md +1 -0
- package/src/acp-host/production-host.ts +3 -0
- package/src/acp-host/role-envelope.ts +2 -2
- package/src/acp-host/role-turn-host.ts +9 -1
- package/src/analyst-gate-cycles-read.ts +37 -2
- package/src/archivist-record-entry.ts +45 -1
- package/src/audit-escalation.ts +17 -0
- package/src/auditor-role.ts +22 -0
- package/src/auditor-soul.ts +42 -0
- package/src/compliance-transport.ts +177 -66
- package/src/doctor-auditor.ts +10 -14
- package/src/doctor-contracts.ts +1 -0
- package/src/doctor-role.ts +2 -2
- package/src/evidence-child-role.ts +22 -0
- package/src/gatekeeper-pass-envelope.ts +109 -0
- package/src/gatekeeper-role.ts +145 -104
- package/src/host-contracts.ts +9 -1
- package/src/institutional-resolution.ts +8 -163
- package/src/judge-auditor.ts +10 -15
- package/src/judge-role.ts +8 -0
- package/src/navigator-attendance.ts +25 -9
- package/src/navigator-public-session.ts +217 -0
- package/src/navigator-session-contracts.ts +61 -54
- package/src/notary-source-run.ts +3 -1
- package/src/package-contracts/auditor-output.ts +82 -0
- package/src/package-contracts/evidence-child-output.ts +51 -0
- package/src/package-contracts/judge-output.ts +1 -0
- package/src/package-contracts/reviewer-output.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +31 -1
- package/src/packaged-role-registry.ts +43 -0
- package/src/pi/adapter.ts +2 -1
- package/src/pi/in-process-session.ts +38 -12
- package/src/pi/role-turn-host.ts +34 -0
- package/src/public-cli/cli.ts +14 -6
- package/src/public-cli/instruction-seat-run.ts +289 -50
- package/src/public-cli/invocation.ts +66 -25
- package/src/public-cli/option-definitions.ts +61 -0
- package/src/public-cli/post-admission.ts +7 -1
- package/src/public-cli/registry.ts +4 -1
- package/src/public-cli/run-lifecycle.ts +22 -3
- package/src/public-cli/settlement.ts +120 -5
- package/src/public-cli/terminal.ts +2 -0
- package/src/public-role-summons.ts +431 -0
- package/src/receipt-delivery-policy.ts +10 -0
- package/src/reviewer-agent.ts +1 -1
- package/src/reviewer-child-executor.ts +114 -12
- package/src/reviewer-execution-ledger.ts +3 -2
- package/src/role-runtime.ts +158 -17
- package/src/session-assistant-usage.ts +119 -0
- package/src/session-opening-materials.ts +1 -1
- package/src/shape-unreadable-failure.ts +49 -0
- package/src/submission-errors.ts +3 -0
- package/dist/evidence-child-executor.js +0 -829
- package/src/evidence-child-executor.ts +0 -1140
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 起居郎 pre-court ticket resolution (#582 / diarist-resolves-ticket-llm-layer).
|
|
3
|
+
* Unbound countersign: LLM typed assertion over accepted instruction, then two
|
|
4
|
+
* mechanical checks on N (complete decimal number in instruction; live GitHub issue).
|
|
5
|
+
* Verification failure is controlled failure — never washed to true-unbound.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { createGhApiRunner, projectGhIssueBody, } from "./collector-github.js";
|
|
11
|
+
import { ENGINE_DETOUR_STAGED_PROMPT_TOKEN, runEngineDetourOnce, } from "./engine-detour.js";
|
|
12
|
+
export const DIARIST_RESOLVE_TICKET_METHOD_RELATIVE = "resources/diarist-resolve-ticket.md";
|
|
13
|
+
export class DiaristTicketResolutionError extends Error {
|
|
14
|
+
code = "diarist-ticket-resolution";
|
|
15
|
+
reason;
|
|
16
|
+
constructor(reason, message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "DiaristTicketResolutionError";
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function resolveDiaristResolveTicketMethodPath(packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
|
|
23
|
+
return join(packageRoot, DIARIST_RESOLVE_TICKET_METHOD_RELATIVE);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decision-key exact identity: complete decimal number N appears in instruction.
|
|
27
|
+
* A longer number's digit substring (e.g. 82 inside #582) is not N.
|
|
28
|
+
*/
|
|
29
|
+
export function instructionContainsTicketNumber(instruction, ticketNumber) {
|
|
30
|
+
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1)
|
|
31
|
+
return false;
|
|
32
|
+
// Complete number token: not preceded or followed by another digit.
|
|
33
|
+
return new RegExp(`(?<!\\d)${String(ticketNumber)}(?!\\d)`).test(instruction);
|
|
34
|
+
}
|
|
35
|
+
/** Sole stdout shapes: {assertion:"ticket",ticketNumber:N} | {assertion:"true-unbound"}. */
|
|
36
|
+
export function parseDiaristTicketResolverStdout(stdout) {
|
|
37
|
+
const trimmed = stdout.trim();
|
|
38
|
+
if (trimmed.length === 0) {
|
|
39
|
+
throw new DiaristTicketResolutionError("empty-stdout", "diarist ticket resolver stdout is empty");
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(trimmed);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new DiaristTicketResolutionError("unparseable-json", "diarist ticket resolver stdout is not JSON", { cause: error });
|
|
47
|
+
}
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
49
|
+
throw new DiaristTicketResolutionError("not-object", "diarist ticket resolver stdout must be one JSON object");
|
|
50
|
+
}
|
|
51
|
+
const record = parsed;
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(record, "assertion")) {
|
|
53
|
+
throw new DiaristTicketResolutionError("assertion-missing", "diarist ticket resolver stdout missing assertion");
|
|
54
|
+
}
|
|
55
|
+
if (record.assertion === "true-unbound")
|
|
56
|
+
return { kind: "true-unbound" };
|
|
57
|
+
const n = record.ticketNumber;
|
|
58
|
+
if (record.assertion === "ticket" &&
|
|
59
|
+
typeof n === "number" &&
|
|
60
|
+
Number.isSafeInteger(n) &&
|
|
61
|
+
n >= 1) {
|
|
62
|
+
return { kind: "ticket", ticketNumber: n };
|
|
63
|
+
}
|
|
64
|
+
throw new DiaristTicketResolutionError("assertion-uninterpretable", "diarist ticket resolver assertion is not ticket|true-unbound with safe ticketNumber");
|
|
65
|
+
}
|
|
66
|
+
const HERMES_RESOLVER_TOOLSET = "context_engine";
|
|
67
|
+
/** Hermes resolver: method bytes + instruction only; empty toolset; staged prompt. */
|
|
68
|
+
export function createHermesDiaristTicketResolver(options = {}) {
|
|
69
|
+
const methodPath = resolveDiaristResolveTicketMethodPath(options.packageRoot);
|
|
70
|
+
if (!existsSync(methodPath)) {
|
|
71
|
+
throw new Error(`diarist resolve-ticket method material missing (${methodPath})`);
|
|
72
|
+
}
|
|
73
|
+
const method = readFileSync(methodPath, "utf8");
|
|
74
|
+
if (method.trim().length === 0) {
|
|
75
|
+
throw new Error(`diarist resolve-ticket method material is empty (${methodPath})`);
|
|
76
|
+
}
|
|
77
|
+
const executable = options.executable ?? "hermes";
|
|
78
|
+
return async (input) => {
|
|
79
|
+
const argv = [
|
|
80
|
+
executable,
|
|
81
|
+
"chat",
|
|
82
|
+
"--query-file",
|
|
83
|
+
ENGINE_DETOUR_STAGED_PROMPT_TOKEN,
|
|
84
|
+
"-Q",
|
|
85
|
+
"--no-restore-cwd",
|
|
86
|
+
"--ignore-rules",
|
|
87
|
+
"-t",
|
|
88
|
+
HERMES_RESOLVER_TOOLSET,
|
|
89
|
+
];
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = await runEngineDetourOnce({
|
|
93
|
+
argv,
|
|
94
|
+
stagedPrompt: JSON.stringify({ method, instruction: input.instruction }),
|
|
95
|
+
cwd: options.cwd ?? process.cwd(),
|
|
96
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
97
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new DiaristTicketResolutionError("engine-failed", "diarist ticket resolver engine failed to launch", { cause: error });
|
|
102
|
+
}
|
|
103
|
+
if (result.code !== 0) {
|
|
104
|
+
throw new DiaristTicketResolutionError("engine-failed", `diarist ticket resolver engine exited ${result.code}: ${result.stderr.slice(0, 500)}`);
|
|
105
|
+
}
|
|
106
|
+
return parseDiaristTicketResolverStdout(result.stdout);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Live ticket check over shared gh issue-body projection. */
|
|
110
|
+
export function createGhTicketExistenceChecker(options) {
|
|
111
|
+
const runner = options?.runner ?? createGhApiRunner();
|
|
112
|
+
return async (input) => {
|
|
113
|
+
const projected = await projectGhIssueBody(runner, {
|
|
114
|
+
owner: input.owner,
|
|
115
|
+
repo: input.repo,
|
|
116
|
+
ticketNumber: input.ticketNumber,
|
|
117
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
118
|
+
});
|
|
119
|
+
if (projected.status === "available")
|
|
120
|
+
return true;
|
|
121
|
+
if (projected.status === "unavailable"
|
|
122
|
+
&& projected.reason === "http-non-2xx"
|
|
123
|
+
&& projected.httpStatus === 404) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (projected.status === "unavailable" && projected.reason === "pull-request") {
|
|
127
|
+
throw new DiaristTicketResolutionError("ticket-is-pull-request", `ticket #${input.ticketNumber} resolves to a pull request, not an issue`, { cause: projected });
|
|
128
|
+
}
|
|
129
|
+
if (projected.status === "unavailable") {
|
|
130
|
+
throw new DiaristTicketResolutionError("ticket-unavailable", `ticket verification unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
131
|
+
}
|
|
132
|
+
throw new DiaristTicketResolutionError("ticket-invalid", `ticket verification returned an invalid issue payload for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Mechanical verify after LLM assertion. true-unbound passes; ticket needs both checks. */
|
|
136
|
+
export async function verifyDiaristTicketAssertion(input) {
|
|
137
|
+
if (input.assertion.kind === "true-unbound")
|
|
138
|
+
return input.assertion;
|
|
139
|
+
const n = input.assertion.ticketNumber;
|
|
140
|
+
if (!instructionContainsTicketNumber(input.instruction, n)) {
|
|
141
|
+
throw new DiaristTicketResolutionError("number-not-in-instruction", `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`);
|
|
142
|
+
}
|
|
143
|
+
if (input.origin === undefined) {
|
|
144
|
+
throw new DiaristTicketResolutionError("origin-unresolved", `diarist ticket assertion #${n} requires a resolvable github.com origin remote`);
|
|
145
|
+
}
|
|
146
|
+
let exists;
|
|
147
|
+
try {
|
|
148
|
+
exists = await input.checkExistence({
|
|
149
|
+
owner: input.origin.owner,
|
|
150
|
+
repo: input.origin.repo,
|
|
151
|
+
ticketNumber: n,
|
|
152
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof DiaristTicketResolutionError)
|
|
157
|
+
throw error;
|
|
158
|
+
throw new DiaristTicketResolutionError("ticket-unavailable", `diarist ticket assertion #${n} live verification failed`, { cause: error });
|
|
159
|
+
}
|
|
160
|
+
if (!exists) {
|
|
161
|
+
throw new DiaristTicketResolutionError("ticket-missing", `diarist ticket assertion #${n} does not exist as a live issue on ${input.origin.owner}/${input.origin.repo}`);
|
|
162
|
+
}
|
|
163
|
+
return input.assertion;
|
|
164
|
+
}
|
|
165
|
+
export async function resolveDiaristTicketFromInstruction(input) {
|
|
166
|
+
const assertion = await input.resolver({
|
|
167
|
+
instruction: input.instruction,
|
|
168
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
169
|
+
});
|
|
170
|
+
return verifyDiaristTicketAssertion({
|
|
171
|
+
assertion,
|
|
172
|
+
instruction: input.instruction,
|
|
173
|
+
origin: input.origin,
|
|
174
|
+
checkExistence: input.checkExistence,
|
|
175
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
176
|
+
});
|
|
177
|
+
}
|
package/dist/diarist.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 起居郎 mechanical halves — ADR 0075 `diarist-is-role` / `diarist-collector-is-own-turn`.
|
|
3
|
+
* Semantic collection is the diarist role's own LLM turn; this module keeps the
|
|
4
|
+
* mechanical safeguard band only: source enumeration into a frozen catalog, and
|
|
5
|
+
* the verbatim reverse-verify → idempotent sitian append → watermark commit.
|
|
6
|
+
* No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { extractReferencedAdrPaths } from "./adr-path-refs.js";
|
|
10
|
+
import { createGhApiRunner, createGhCollectorGitHubTransport, projectGhIssueBody, } from "./collector-github.js";
|
|
11
|
+
import { blockToLlmEntry, buildDiaristAnchors, mechanicalSafeguardPipeline, readAdrDecisionKeyBlocks, readCcSessionBlocks, readIssueFaceBlocks, } from "./diarist-mechanical.js";
|
|
12
|
+
import { parseGitHubOriginRemote } from "./reviewer-pinned-git.js";
|
|
13
|
+
import { appendIssueSourceFailureDiagnostic, appendQuoteVerifyFailureDiagnostic, appendTicketProvenanceEntry, ensureTicketProvenanceVolume, readOfferedIdentities, readTicketProvenance, recordOfferedIdentities, resolveTicketProvenanceVolume, ticketProvenanceEntryIdentity, writeTicketProvenanceHumanView, } from "./ticket-provenance.js";
|
|
14
|
+
import { readSitianRecords } from "./sitian-facade.js";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
/** Bound-ticket issue source failure — not a soft degrade to empty face. */
|
|
17
|
+
export class DiaristIssueSourceError extends Error {
|
|
18
|
+
code = "diarist-issue-source";
|
|
19
|
+
reason;
|
|
20
|
+
constructor(reason, message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.name = "DiaristIssueSourceError";
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Production issue-face capability over shared gh execution seams.
|
|
28
|
+
* Body fetch/parse is sole-owned by projectGhIssueBody; this maps hard disposition
|
|
29
|
+
* (typed DiaristIssueSourceError) instead of Reviewer Spec soft-undefined.
|
|
30
|
+
* Comment list failures after body success keep true cause under comments-failed.
|
|
31
|
+
*/
|
|
32
|
+
export function createDiaristIssueFaceFetcher(options) {
|
|
33
|
+
const runner = options?.runner ?? createGhApiRunner();
|
|
34
|
+
const transport = options?.transport ?? createGhCollectorGitHubTransport();
|
|
35
|
+
return async (input) => {
|
|
36
|
+
const projected = await projectGhIssueBody(runner, input);
|
|
37
|
+
let body;
|
|
38
|
+
if (projected.status === "available") {
|
|
39
|
+
body = projected.body;
|
|
40
|
+
}
|
|
41
|
+
else if (projected.status === "unavailable") {
|
|
42
|
+
if (projected.reason === "pull-request") {
|
|
43
|
+
throw new DiaristIssueSourceError("issue-is-pull-request", `ticket #${input.ticketNumber} resolves to a pull request, not an issue face`, projected.cause === undefined ? undefined : { cause: projected.cause });
|
|
44
|
+
}
|
|
45
|
+
throw new DiaristIssueSourceError("issue-unavailable", `issue face unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`, projected.cause === undefined ? undefined : { cause: projected.cause });
|
|
46
|
+
}
|
|
47
|
+
else if (projected.reason === "not-json") {
|
|
48
|
+
throw new DiaristIssueSourceError("issue-not-json", `issue face payload is not JSON for ${input.owner}/${input.repo}#${input.ticketNumber}`, projected.cause === undefined ? undefined : { cause: projected.cause });
|
|
49
|
+
}
|
|
50
|
+
else if (projected.reason === "not-object") {
|
|
51
|
+
throw new DiaristIssueSourceError("issue-not-object", `issue face payload must be a JSON object for ${input.owner}/${input.repo}#${input.ticketNumber}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new DiaristIssueSourceError("issue-body-invalid", `issue face body must be string or null for ${input.owner}/${input.repo}#${input.ticketNumber}`);
|
|
55
|
+
}
|
|
56
|
+
let listed;
|
|
57
|
+
try {
|
|
58
|
+
listed = await transport.listIssueComments({
|
|
59
|
+
owner: input.owner,
|
|
60
|
+
repo: input.repo,
|
|
61
|
+
prNumber: input.ticketNumber,
|
|
62
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new DiaristIssueSourceError("comments-failed", `issue comments fetch failed for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: error });
|
|
67
|
+
}
|
|
68
|
+
const bodyUrl = `https://github.com/${input.owner}/${input.repo}/issues/${input.ticketNumber}`;
|
|
69
|
+
return {
|
|
70
|
+
body,
|
|
71
|
+
bodyUrl,
|
|
72
|
+
comments: listed.items.map((c) => ({
|
|
73
|
+
id: c.id,
|
|
74
|
+
body: c.body,
|
|
75
|
+
createdAt: c.createdAt,
|
|
76
|
+
htmlUrl: c.htmlUrl,
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** github.com owner/repo from project origin remote; undefined when absent/non-github. */
|
|
82
|
+
export function resolveDiaristGithubOrigin(projectRoot) {
|
|
83
|
+
let remoteUrl;
|
|
84
|
+
try {
|
|
85
|
+
remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
86
|
+
cwd: projectRoot,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
}).trim();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (remoteUrl.length === 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
return parseGitHubOriginRemote(remoteUrl);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Acquire the issue face for a bound ticket. Failures are typed and durable on
|
|
100
|
+
* the ticket-provenance volume, then propagated — never washed into empty face.
|
|
101
|
+
*/
|
|
102
|
+
export async function loadDiaristIssueFace(input) {
|
|
103
|
+
const persistAndThrow = (error) => {
|
|
104
|
+
appendIssueSourceFailureDiagnostic({
|
|
105
|
+
ticketNumber: input.ticketNumber,
|
|
106
|
+
cwd: input.projectRoot,
|
|
107
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
108
|
+
cause: error.message,
|
|
109
|
+
reason: error.reason,
|
|
110
|
+
});
|
|
111
|
+
return error;
|
|
112
|
+
};
|
|
113
|
+
const origin = resolveDiaristGithubOrigin(input.projectRoot);
|
|
114
|
+
if (origin === undefined) {
|
|
115
|
+
throw persistAndThrow(new DiaristIssueSourceError("origin-unresolved", `bound ticket #${input.ticketNumber} issue face requires a resolvable github.com origin remote`));
|
|
116
|
+
}
|
|
117
|
+
const fetcher = input.fetcher ?? createDiaristIssueFaceFetcher();
|
|
118
|
+
try {
|
|
119
|
+
return await fetcher({
|
|
120
|
+
owner: origin.owner,
|
|
121
|
+
repo: origin.repo,
|
|
122
|
+
ticketNumber: input.ticketNumber,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw persistAndThrow(error instanceof DiaristIssueSourceError
|
|
127
|
+
? error
|
|
128
|
+
: new DiaristIssueSourceError("issue-unavailable", `issue face fetch failed for ${origin.owner}/${origin.repo}#${input.ticketNumber}`, { cause: error }));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Identities already processed for this ticket:
|
|
133
|
+
* - volume record identities (selected / verify-fail residue)
|
|
134
|
+
* - offered watermark (blocks shown to the diarist, selected or not)
|
|
135
|
+
*/
|
|
136
|
+
async function loadSeenEntryIdentities(ticketNumber, cwd, home) {
|
|
137
|
+
const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
138
|
+
const { records } = await readSitianRecords(recordFile);
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const record of records) {
|
|
141
|
+
if (typeof record.identity === "string" && record.identity.length > 0) {
|
|
142
|
+
seen.add(record.identity);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const identity of readOfferedIdentities(ticketNumber, cwd, home)) {
|
|
146
|
+
seen.add(identity);
|
|
147
|
+
}
|
|
148
|
+
return seen;
|
|
149
|
+
}
|
|
150
|
+
function blockEntryIdentity(ticketNumber, block) {
|
|
151
|
+
return ticketProvenanceEntryIdentity({
|
|
152
|
+
ticketNumber,
|
|
153
|
+
sourceKind: block.sourceKind,
|
|
154
|
+
sourceRef: block.sourceRef,
|
|
155
|
+
transcript: block.transcript,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Single typed candidate stream: cc sessions + GitHub issue face/comments/decree + referenced ADRs.
|
|
160
|
+
* Mechanical layer does not prose-filter for relevance (锚定宪法).
|
|
161
|
+
* Attachments are never merged in as fake issue-body-comment.
|
|
162
|
+
*/
|
|
163
|
+
function loadSourceBlocks(input) {
|
|
164
|
+
const cwds = input.sessionCwds ?? [input.cwd];
|
|
165
|
+
const blocks = [...readCcSessionBlocks({ cwds })];
|
|
166
|
+
if (input.issueFace !== undefined) {
|
|
167
|
+
blocks.push(...readIssueFaceBlocks({ face: input.issueFace }));
|
|
168
|
+
const faceText = [
|
|
169
|
+
input.issueFace.body,
|
|
170
|
+
...input.issueFace.comments.map((c) => c.body),
|
|
171
|
+
].join("\n");
|
|
172
|
+
const adrPaths = extractReferencedAdrPaths(faceText);
|
|
173
|
+
if (adrPaths.length > 0) {
|
|
174
|
+
blocks.push(...readAdrDecisionKeyBlocks({
|
|
175
|
+
cwd: input.cwd,
|
|
176
|
+
adrPaths,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return blocks;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Mechanical half A — source enumeration into a frozen catalog.
|
|
184
|
+
* Establishes the per-ticket volume + human view for every bound run (ADR 0075
|
|
185
|
+
* `ticket-provenance-file` 每票一份起居录), then offers only blocks whose entry
|
|
186
|
+
* identity is not already on the volume or the offered watermark (增量幂等).
|
|
187
|
+
*/
|
|
188
|
+
export async function prepareDiaristSourceCatalog(input) {
|
|
189
|
+
ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
|
|
190
|
+
const volume = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
|
|
191
|
+
writeTicketProvenanceHumanView({
|
|
192
|
+
ticketNumber: input.ticketNumber,
|
|
193
|
+
cwd: input.cwd,
|
|
194
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
195
|
+
entries: volume.entries,
|
|
196
|
+
});
|
|
197
|
+
const rawBlocks = loadSourceBlocks(input);
|
|
198
|
+
// Safeguard only (notify filter + dedupe) — never prose-based exclusion.
|
|
199
|
+
const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
|
|
200
|
+
const seen = await loadSeenEntryIdentities(input.ticketNumber, input.cwd, input.home);
|
|
201
|
+
const fresh = safeguarded.filter((block) => !seen.has(blockEntryIdentity(input.ticketNumber, block)));
|
|
202
|
+
return {
|
|
203
|
+
ticketNumber: input.ticketNumber,
|
|
204
|
+
cwd: input.cwd,
|
|
205
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
206
|
+
candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export function serializeDiaristSourceCatalog(catalog) {
|
|
210
|
+
return JSON.stringify(catalog);
|
|
211
|
+
}
|
|
212
|
+
/** Read a frozen catalog written by the seat. Unreadable/malformed fails loudly. */
|
|
213
|
+
export function loadDiaristSourceCatalog(path) {
|
|
214
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
215
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
216
|
+
throw new Error(`diarist source catalog is not an object (${path})`);
|
|
217
|
+
}
|
|
218
|
+
const record = parsed;
|
|
219
|
+
if (typeof record.ticketNumber !== "number" || typeof record.cwd !== "string") {
|
|
220
|
+
throw new Error(`diarist source catalog is missing ticket coordinates (${path})`);
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(record.candidates)) {
|
|
223
|
+
throw new Error(`diarist source catalog is missing candidates (${path})`);
|
|
224
|
+
}
|
|
225
|
+
return parsed;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Mechanical half B — commit the diarist turn's selections.
|
|
229
|
+
* Verbatim reverse-verify → idempotent sitian append → watermark → human view.
|
|
230
|
+
* Verify failure records a single typed diagnostic and drops that selection; it
|
|
231
|
+
* never bounces the receipt (第 0 条) and never enters the volume as an entry.
|
|
232
|
+
*/
|
|
233
|
+
export async function commitDiaristSelections(input) {
|
|
234
|
+
const { ticketNumber, cwd } = input.catalog;
|
|
235
|
+
const homeOpt = input.catalog.home === undefined ? {} : { home: input.catalog.home };
|
|
236
|
+
const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.catalog.home);
|
|
237
|
+
const anchors = buildDiaristAnchors({ ticketNumber });
|
|
238
|
+
// Entry-count baseline: sitian entry identity already makes a repeat of an
|
|
239
|
+
// already-recorded block a no-op append, so the honest `appended` is what the
|
|
240
|
+
// volume gained — not how many rows survived verify.
|
|
241
|
+
const before = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
|
|
242
|
+
let acceptedRows = 0;
|
|
243
|
+
let rejectedQuotes = 0;
|
|
244
|
+
let unknownCandidate = false;
|
|
245
|
+
for (const selection of input.selections) {
|
|
246
|
+
const block = input.catalog.candidates[selection.candidateIndex];
|
|
247
|
+
if (block === undefined) {
|
|
248
|
+
unknownCandidate = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const projected = blockToLlmEntry(block, {
|
|
252
|
+
anchors,
|
|
253
|
+
quotes: selection.quotes,
|
|
254
|
+
...(selection.note === undefined ? {} : { note: selection.note }),
|
|
255
|
+
});
|
|
256
|
+
if (!projected.ok) {
|
|
257
|
+
rejectedQuotes += 1;
|
|
258
|
+
// Single diagnostic expression — never a disguised diary entry.
|
|
259
|
+
appendQuoteVerifyFailureDiagnostic({
|
|
260
|
+
ticketNumber,
|
|
261
|
+
cwd,
|
|
262
|
+
...homeOpt,
|
|
263
|
+
cause: projected.cause,
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
appendTicketProvenanceEntry({
|
|
268
|
+
ticketNumber,
|
|
269
|
+
cwd,
|
|
270
|
+
...homeOpt,
|
|
271
|
+
entry: projected.entry,
|
|
272
|
+
source: "diarist",
|
|
273
|
+
});
|
|
274
|
+
acceptedRows += 1;
|
|
275
|
+
}
|
|
276
|
+
// Watermark advances only after the durable volume writes above, so a crash
|
|
277
|
+
// mid-commit retries the batch on the next summons. Entry identity and
|
|
278
|
+
// quote-verify diagnostic identity are stable — retry duplicates neither.
|
|
279
|
+
const identities = input.catalog.candidates.map((block) => blockEntryIdentity(ticketNumber, block));
|
|
280
|
+
const alreadyWatermarked = readOfferedIdentities(ticketNumber, cwd, input.catalog.home);
|
|
281
|
+
const watermarked = identities.filter((identity) => !alreadyWatermarked.has(identity)).length;
|
|
282
|
+
recordOfferedIdentities({ ticketNumber, cwd, ...homeOpt, identities });
|
|
283
|
+
// Refresh the human view from the full volume (includes prior summons).
|
|
284
|
+
const volume = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
|
|
285
|
+
const humanViewFile = writeTicketProvenanceHumanView({
|
|
286
|
+
ticketNumber,
|
|
287
|
+
cwd,
|
|
288
|
+
...homeOpt,
|
|
289
|
+
entries: volume.entries,
|
|
290
|
+
});
|
|
291
|
+
return {
|
|
292
|
+
ticketNumber,
|
|
293
|
+
offered: input.catalog.candidates.length,
|
|
294
|
+
appended: volume.entries.length - before.entries.length,
|
|
295
|
+
rejectedQuotes,
|
|
296
|
+
watermarked,
|
|
297
|
+
volumeRecordFile: volumePaths.recordFile,
|
|
298
|
+
humanViewFile,
|
|
299
|
+
collectorStatus: input.catalog.candidates.length === 0
|
|
300
|
+
? "skipped-no-fresh"
|
|
301
|
+
: acceptedRows > 0
|
|
302
|
+
? "ok"
|
|
303
|
+
: input.selections.length === 0
|
|
304
|
+
? "empty-selection"
|
|
305
|
+
: "nothing-appended",
|
|
306
|
+
failureCauses: [
|
|
307
|
+
...(unknownCandidate ? ["unknown-candidate"] : []),
|
|
308
|
+
...(rejectedQuotes > 0 ? ["quote-verify-rejected"] : []),
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { auditorRunDirectory } from "./auditor-dossier-tool.js";
|
|
2
|
+
import { runComplianceAudit, } from "./compliance-transport.js";
|
|
3
|
+
import { readDoctorAuditSubjects, requireAuditMaterials, resolveAuditDossier, } from "./dossier-resolution.js";
|
|
4
|
+
export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
|
|
5
|
+
/**
|
|
6
|
+
* Doctor compliance via public auditor activation (#675 / ADR 0062 / owner r11).
|
|
7
|
+
* 审刑院 is an independent role; subject=doctor selects souls/doctor-auditor.md.
|
|
8
|
+
*/
|
|
9
|
+
export function createPiDoctorAuditor() {
|
|
10
|
+
return async (options) => {
|
|
11
|
+
const dossier = resolveAuditDossier();
|
|
12
|
+
requireAuditMaterials(dossier);
|
|
13
|
+
const subjects = readDoctorAuditSubjects(options.context);
|
|
14
|
+
requireAuditMaterials(subjects);
|
|
15
|
+
return runComplianceAudit({
|
|
16
|
+
subject: "doctor",
|
|
17
|
+
context: options.context,
|
|
18
|
+
...(auditorRunDirectory(options.context) === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { runDirectory: auditorRunDirectory(options.context) }),
|
|
21
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
22
|
+
...(options.summonAuditor === undefined ? {} : { summonAuditor: options.summonAuditor }),
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/doctor-contracts.js
CHANGED
|
@@ -6,6 +6,7 @@ const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
|
|
|
6
6
|
const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
|
|
7
7
|
const DOCTOR_ACCEPTED_TEXT = "\u592A\u533B\u7F72\u56DE\u6267\u5DF2\u63A5\u53D7";
|
|
8
8
|
const DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT = "\u592A\u533B\u7F72\u56DE\u6267\u5DF2\u63A5\u53D7\uFF1B\u5BA1\u8BA1\u65E0\u56DE\u6267";
|
|
9
|
+
const DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT = "\u592A\u533B\u7F72\u56DE\u6267\u5DF2\u63A5\u53D7\uFF1B\u5BA1\u8BA1\u5F62\u72B6\u4E0D\u53EF\u8BFB";
|
|
9
10
|
const DOCTOR_OUTPUT_TOOL_DESCRIPTION = "\u63D0\u4EA4\u552F\u4E00\u7EC8\u5C40\u5355\u6848\u8BC1\u8BCD\uFF1Bcompleted \u5141\u8BB8\u7A7A findings\uFF1Bruntime \u8865\u8BB0\u6D3E\u751F\u6210\u672C\u5165\u56DE\u6267\u3002";
|
|
10
11
|
const DOCTOR_TARGET_KINDS = ["law", "gate", "template", "station", "seat"];
|
|
11
12
|
const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
|
|
@@ -184,6 +185,7 @@ function validateDoctorOutput(value, patient, store) {
|
|
|
184
185
|
}
|
|
185
186
|
export {
|
|
186
187
|
DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT,
|
|
188
|
+
DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT,
|
|
187
189
|
DOCTOR_ACCEPTED_TEXT,
|
|
188
190
|
DOCTOR_EVIDENCE_TOOL_NAME,
|
|
189
191
|
DOCTOR_OUTPUT_TOOL_DESCRIPTION,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { sha256Hex } from "./sha256.js";
|
|
4
|
+
import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, validateAcceptedDetails } from "./package-contracts/terminating-tools.js";
|
|
5
|
+
function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
6
|
+
async function discoverCaseFiles(root) { const found = []; async function walk(dir, depth) { for (const item of await readdir(dir, { withFileTypes: true })) {
|
|
7
|
+
const path = resolve(dir, item.name);
|
|
8
|
+
if (item.isDirectory()) {
|
|
9
|
+
await walk(path, depth + 1);
|
|
10
|
+
}
|
|
11
|
+
else if (item.isFile() && (item.name.endsWith(".jsonl") || (item.name === "stderr.log" && depth === 1)))
|
|
12
|
+
found.push(path);
|
|
13
|
+
} } await walk(root, 0); return found.sort(); }
|
|
14
|
+
function sourceList(count, sources) { return { count, sources: [...new Set(sources)].sort() }; }
|
|
15
|
+
function accumulate(metric, value, source) { metric.count += value; if (value)
|
|
16
|
+
metric.sources.push(source); }
|
|
17
|
+
function timestamp(row) { return typeof row.timestamp === "string" && Number.isFinite(Date.parse(row.timestamp)) ? row.timestamp : undefined; }
|
|
18
|
+
function isMissingPathError(error) { return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"); }
|
|
19
|
+
async function stableRunsIdentity(root) { let cursor = root; while (true) {
|
|
20
|
+
try {
|
|
21
|
+
const git = await stat(resolve(cursor, ".git"));
|
|
22
|
+
if (git.isDirectory() || git.isFile())
|
|
23
|
+
return relative(cursor, root).split(sep).join("/");
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!isMissingPathError(error))
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const parent = dirname(cursor);
|
|
30
|
+
if (parent === cursor)
|
|
31
|
+
return root;
|
|
32
|
+
cursor = parent;
|
|
33
|
+
} }
|
|
34
|
+
function deriveSession(content, id) {
|
|
35
|
+
const rows = [];
|
|
36
|
+
const degradationReasons = [];
|
|
37
|
+
for (const line of content.split("\n"))
|
|
38
|
+
if (line.trim()) {
|
|
39
|
+
try {
|
|
40
|
+
const row = JSON.parse(line);
|
|
41
|
+
if (!record(row)) {
|
|
42
|
+
degradationReasons.push(`non-object session row in ${id}`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
rows.push(row);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof SyntaxError) {
|
|
49
|
+
degradationReasons.push(`malformed JSON tail in ${id}: ${error.message}`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const started = rows.find((row) => row.type === "session");
|
|
56
|
+
const startedAt = started && timestamp(started);
|
|
57
|
+
if (!startedAt)
|
|
58
|
+
degradationReasons.push(`Pi session header is missing: ${id}`);
|
|
59
|
+
let accepted, observedCommit, turns = 0, calls = 0, tokens = 0;
|
|
60
|
+
const statuses = [], commits = [];
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
const message = record(row.message) ? row.message : undefined;
|
|
63
|
+
if (message?.role === "assistant") {
|
|
64
|
+
for (const part of Array.isArray(message.content) ? message.content : [])
|
|
65
|
+
if (record(part) && part.type === "toolCall")
|
|
66
|
+
calls++;
|
|
67
|
+
if (typeof message.responseId === "string") {
|
|
68
|
+
turns++;
|
|
69
|
+
const usage = record(message.usage) ? message.usage : undefined;
|
|
70
|
+
if (usage && typeof usage.output === "number")
|
|
71
|
+
tokens += usage.output;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (message?.role === "toolResult" && message.isError !== true && typeof message.toolName === "string" && isTerminatingToolName(message.toolName) && record(message.details)) {
|
|
75
|
+
let details;
|
|
76
|
+
try {
|
|
77
|
+
details = validateAcceptedDetails(message.toolName, message.details);
|
|
78
|
+
}
|
|
79
|
+
catch (error) { // Contract: README.md#Doctor — non-accepted terminating receipts are expected-negative evidence and are skipped; all other validation failures propagate with their cause.
|
|
80
|
+
if (error instanceof AcceptedDetailsContractError)
|
|
81
|
+
continue;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
accepted = row;
|
|
85
|
+
const facts = acceptedFacts(message.toolName, details);
|
|
86
|
+
if (facts.commit && facts.commit !== observedCommit) {
|
|
87
|
+
commits.push({ source: id, commit: facts.commit });
|
|
88
|
+
observedCommit = facts.commit;
|
|
89
|
+
}
|
|
90
|
+
statuses.length = 0;
|
|
91
|
+
if (facts.status !== undefined) {
|
|
92
|
+
statuses.push({ source: id, status: facts.status });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
statuses.push({ source: id, status: "terminating receipt has no receipt-level status" });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const acceptedAt = accepted && timestamp(accepted);
|
|
100
|
+
const final = acceptedAt ? accepted : rows.at(-1);
|
|
101
|
+
const endedAt = final && timestamp(final);
|
|
102
|
+
const wall = startedAt && endedAt ? Date.parse(endedAt) - Date.parse(startedAt) : undefined;
|
|
103
|
+
if (wall !== undefined && wall < 0)
|
|
104
|
+
degradationReasons.push(`non-monotonic session timestamps in ${id}`);
|
|
105
|
+
const degradationReason = degradationReasons.length ? degradationReasons.join("; ") : undefined;
|
|
106
|
+
const complete = !!acceptedAt && !degradationReason && wall !== undefined && wall >= 0;
|
|
107
|
+
const session = complete
|
|
108
|
+
? { source: id, startedAt: startedAt, endedAt: endedAt, wallMilliseconds: wall, completion: "accepted" }
|
|
109
|
+
: { source: id, ...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}), ...(wall !== undefined && wall >= 0 ? { wallMilliseconds: wall } : {}), completion: "incomplete", ...(degradationReason ? { degradationReason } : {}) };
|
|
110
|
+
return { session, turns, calls, tokens, statuses, commits };
|
|
111
|
+
}
|
|
112
|
+
/** Read Pi's retained session directory as the sole raw material for one case. */
|
|
113
|
+
export async function loadDoctorCase(runsPath) {
|
|
114
|
+
const root = await realpath(runsPath);
|
|
115
|
+
const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/);
|
|
116
|
+
if (!match)
|
|
117
|
+
throw new Error("Doctor case must be an .ak-roles/books/<book>/issues/<n>/runs directory");
|
|
118
|
+
const evidence = [], sessions = [], statuses = [], commits = [];
|
|
119
|
+
const turns = { count: 0, sources: [] }, calls = { count: 0, sources: [] }, tokens = { count: 0, sources: [] };
|
|
120
|
+
for (const path of await discoverCaseFiles(root)) {
|
|
121
|
+
const id = relative(root, path).split(sep).join("/");
|
|
122
|
+
const bytes = await readFile(path);
|
|
123
|
+
const content = bytes.toString("utf8");
|
|
124
|
+
const kind = id.endsWith(".jsonl") ? "session" : "stderr";
|
|
125
|
+
evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content });
|
|
126
|
+
if (kind === "stderr")
|
|
127
|
+
continue;
|
|
128
|
+
const result = deriveSession(content, id);
|
|
129
|
+
sessions.push(result.session);
|
|
130
|
+
statuses.push(...result.statuses);
|
|
131
|
+
commits.push(...result.commits);
|
|
132
|
+
accumulate(turns, result.turns, id);
|
|
133
|
+
accumulate(calls, result.calls, id);
|
|
134
|
+
accumulate(tokens, result.tokens, id);
|
|
135
|
+
}
|
|
136
|
+
const runDirs = (await readdir(root, { withFileTypes: true })).filter((item) => item.isDirectory()).map((item) => item.name).sort();
|
|
137
|
+
const legs = evidence.filter((entry) => entry.kind === "session").map((entry) => entry.id);
|
|
138
|
+
const retryDirs = runDirs.filter((name) => /(?:^|[-_])retry(?:[-_]|$)/i.test(name));
|
|
139
|
+
const rawBytes = evidence.filter((entry) => entry.kind === "session").reduce((sum, entry) => sum + entry.byteLength, 0);
|
|
140
|
+
const cost = { invocations: sourceList(runDirs.length, runDirs), legs: sourceList(legs.length, legs), modelApiTurns: sourceList(turns.count, turns.sources), outputTokens: sourceList(tokens.count, tokens.sources), toolCalls: sourceList(calls.count, calls.sources), retries: { ...sourceList(retryDirs.length, retryDirs), evidence: "literal run-dir naming" }, statuses, commits, sessions, outputBytes: { ...sourceList(rawBytes, legs), payload: "raw JSONL bytes", providerWireBytes: "unavailable" } };
|
|
141
|
+
return { version: 1, identity: { issueNumber: Number(match[1]), runsPath: await stableRunsIdentity(root) }, evidence, cost };
|
|
142
|
+
}
|