@akagilnc/pi-workflow-roles 0.1.3648 → 0.1.3652

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 (66) hide show
  1. package/dist/acp-host/production-host.js +2919 -1750
  2. package/dist/activation-ledger-session.js +105 -0
  3. package/dist/activation-ledger.js +139 -0
  4. package/dist/activation-trace.js +33 -0
  5. package/dist/agent-start-materials.js +11 -0
  6. package/dist/auditor-role.js +12 -0
  7. package/dist/canonical-skill-binding.js +64 -0
  8. package/dist/collector-identity.js +151 -0
  9. package/dist/collector-receipt.js +95 -0
  10. package/dist/collector-role.js +441 -0
  11. package/dist/countersign-role.js +28 -0
  12. package/dist/diarist-role.js +29 -0
  13. package/dist/doctor-role.js +74 -0
  14. package/dist/fixer-bash-seatbelt.js +13 -0
  15. package/dist/gatekeeper-pass-envelope.js +72 -0
  16. package/dist/gleaner-left-role.js +26 -0
  17. package/dist/inspector-role.js +29 -0
  18. package/dist/judge-role.js +106 -0
  19. package/dist/merger-role.js +66 -0
  20. package/dist/navigator-role.js +12 -0
  21. package/dist/notary-role.js +137 -0
  22. package/dist/oauth-keepalive.js +158 -0
  23. package/dist/package-contracts/terminating-tools.js +2 -16
  24. package/dist/packaged-role-registry.js +1 -18
  25. package/dist/pi/adapter.js +211 -0
  26. package/dist/pi/role-turn-host.js +0 -2
  27. package/dist/public-cli/instruction-seat-run.js +2 -6
  28. package/dist/public-cli/invocation.js +0 -7
  29. package/dist/public-cli/main.js +94 -191
  30. package/dist/public-cli/option-definitions.js +0 -14
  31. package/dist/public-cli/registry.js +1 -2
  32. package/dist/public-cli/run-lifecycle.js +1 -3
  33. package/dist/public-cli/settlement.js +0 -10
  34. package/dist/public-role-summons.js +0 -14
  35. package/dist/reviewer-child-executor.js +163 -70
  36. package/dist/reviewer-construction.js +2 -2
  37. package/dist/reviewer-role.js +159 -0
  38. package/dist/reviewer-settlement.js +66 -0
  39. package/dist/role-runtime.js +1394 -0
  40. package/dist/stderr-jsonl.js +26 -0
  41. package/dist/tool-execution-observation.js +141 -0
  42. package/dist/worker-role.js +343 -0
  43. package/dist/worker-submission-gates.js +305 -0
  44. package/extensions/role-runtime.ts +1 -2
  45. package/package.json +1 -1
  46. package/src/acp-host/production-host.ts +0 -1
  47. package/src/host-contracts.ts +0 -2
  48. package/src/package-contracts/terminating-tools.ts +1 -19
  49. package/src/packaged-role-registry.ts +1 -19
  50. package/src/pi/role-turn-host.ts +0 -2
  51. package/src/public-cli/cli.ts +1 -5
  52. package/src/public-cli/instruction-seat-run.ts +5 -14
  53. package/src/public-cli/invocation.ts +3 -21
  54. package/src/public-cli/option-definitions.ts +0 -16
  55. package/src/public-cli/registry.ts +1 -2
  56. package/src/public-cli/run-lifecycle.ts +0 -6
  57. package/src/public-cli/settlement.ts +0 -32
  58. package/src/public-cli/terminal.ts +0 -1
  59. package/src/public-role-summons.ts +0 -15
  60. package/src/reviewer-child-executor.ts +199 -98
  61. package/src/reviewer-construction.ts +3 -3
  62. package/src/role-runtime.ts +1 -33
  63. package/dist/package-contracts/evidence-child-output.js +0 -31
  64. package/souls/evidence-child.md +0 -1
  65. package/src/evidence-child-role.ts +0 -22
  66. package/src/package-contracts/evidence-child-output.ts +0 -46
@@ -0,0 +1,105 @@
1
+ import { lstatSync, realpathSync, statSync, writeFileSync, } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { errnoCode, errorText, } from "./activation-ledger-topology.js";
4
+ /**
5
+ * Typed missing durable session principal. Callers discriminate with instanceof/code;
6
+ * never by parsing message prose. Original filesystem causes are retained.
7
+ */
8
+ export class ActivationSessionFileMissingError extends Error {
9
+ code = "AK_ACTIVATION_SESSION_FILE_MISSING";
10
+ path;
11
+ constructor(path, options) {
12
+ super(`Workflow role activation durable session file does not exist: ${path}`, options?.cause === undefined ? undefined : { cause: options.cause });
13
+ this.name = "ActivationSessionFileMissingError";
14
+ this.path = path;
15
+ }
16
+ }
17
+ function materializeDeferredSessionFile(sessionManager, resolvedFile) {
18
+ const header = sessionManager.getHeader?.();
19
+ if (header === null || header === undefined || header.type !== "session") {
20
+ throw new ActivationSessionFileMissingError(resolvedFile);
21
+ }
22
+ // Activation does not own record placement (ADR 0065 / #221). Parent directory is the
23
+ // SessionManager principal's own dir — write the header in place; do not pull ledgerHome
24
+ // containment or ensureRealDirectoryTree back onto this admission path.
25
+ try {
26
+ writeFileSync(resolvedFile, `${JSON.stringify(header)}\n`, { flag: "wx" });
27
+ }
28
+ catch (error) {
29
+ if (errnoCode(error) !== "EEXIST") {
30
+ throw new Error(`Workflow role activation failed to materialize durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
31
+ }
32
+ // Lost a create race — validate the winner below.
33
+ }
34
+ // Rebind full SessionManager so subsequent Pi appends use O_APPEND (flushed=true)
35
+ // instead of exclusive wx create against the file we just wrote.
36
+ if (typeof sessionManager.setSessionFile === "function") {
37
+ sessionManager.setSessionFile(resolvedFile);
38
+ }
39
+ }
40
+ /**
41
+ * Admit a durable Pi session file principal (ADR 0048/0065).
42
+ * Requires an existing regular file at admission (or a live SessionManager header that
43
+ * can be materialized onto the same path). Rejects relative paths, directories,
44
+ * non-files, and nonexistent paths that cannot be materialized. Original filesystem
45
+ * causes are retained.
46
+ *
47
+ * Record-placement (session must live under the ledger book) is NOT enforced here —
48
+ * that check lives on the archivist record entry (createRecordSession). Activation only
49
+ * binds the durable principal identity for the role run.
50
+ *
51
+ * Upstream Pi defers exclusive create until the first assistant message. When the path
52
+ * is the live SessionManager principal and only the header is in memory, admission
53
+ * materializes that header onto the same path before the fact is written so the role
54
+ * fact never points at a session that may be created later.
55
+ */
56
+ export function durableSessionPointer(sessionManager) {
57
+ const file = sessionManager.getSessionFile?.();
58
+ if (typeof file !== "string" || file.length === 0) {
59
+ throw new Error("Workflow role activation requires a durable Pi session file principal (getSessionFile); directory-only or --no-session invocations are rejected");
60
+ }
61
+ if (!isAbsolute(file)) {
62
+ throw new Error(`Workflow role activation requires an absolute durable session file path; got relative path: ${file}`);
63
+ }
64
+ const resolvedFile = resolve(file);
65
+ try {
66
+ lstatSync(resolvedFile);
67
+ }
68
+ catch (error) {
69
+ if (errnoCode(error) !== "ENOENT") {
70
+ throw new Error(`Workflow role activation failed to stat durable session file (${resolvedFile}): ${errorText(error)}`, { cause: error });
71
+ }
72
+ try {
73
+ materializeDeferredSessionFile(sessionManager, resolvedFile);
74
+ }
75
+ catch (materializeError) {
76
+ // Missing principal: rethrow typed missing-file identity with the original ENOENT cause.
77
+ // Other materialize failures keep their own typed/native identity.
78
+ if (materializeError instanceof ActivationSessionFileMissingError) {
79
+ throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
80
+ }
81
+ throw materializeError;
82
+ }
83
+ }
84
+ let realFile;
85
+ try {
86
+ realFile = realpathSync(resolvedFile);
87
+ }
88
+ catch (error) {
89
+ throw new ActivationSessionFileMissingError(resolvedFile, { cause: error });
90
+ }
91
+ let info;
92
+ try {
93
+ info = statSync(realFile);
94
+ }
95
+ catch (error) {
96
+ throw new Error(`Workflow role activation failed to stat durable session file (${realFile}): ${errorText(error)}`, { cause: error });
97
+ }
98
+ if (info.isDirectory()) {
99
+ throw new Error(`Workflow role activation durable session principal must be a file, not a directory: ${realFile}`);
100
+ }
101
+ if (!info.isFile()) {
102
+ throw new Error(`Workflow role activation durable session principal is not a regular file: ${realFile}`);
103
+ }
104
+ return { kind: "session-file", path: realFile };
105
+ }
@@ -0,0 +1,139 @@
1
+ import { constants, closeSync, fstatSync, openSync, writeSync, } from "node:fs";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { ActivationLedgerError, activationWaitingLedgerPath, assertLedgerFileInsideHome, ensureRealDirectoryTree, errorText, } from "./activation-ledger-topology.js";
4
+ /** Private open flags: O_NOFOLLOW closes the lstat→open TOCTOU window. */
5
+ const ACTIVATION_LEDGER_APPEND_OPEN_FLAGS = constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY | constants.O_NOFOLLOW;
6
+ export { ActivationGitRepositoryRequiredError, resolveBookKeyFromGit, } from "./activation-ledger-git.js";
7
+ export { ActivationSessionFileMissingError, durableSessionPointer, } from "./activation-ledger-session.js";
8
+ export { ActivationLedgerError, activationBookDirectory, activationWaitingLedgerPath, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
9
+ export const ACCEPTED_ACTIVATION_EVENT = "accepted-activation";
10
+ /**
11
+ * Package-owned index-only top-level keys for accepted-activation facts (ADR 0049).
12
+ * Sole machine key contract: projection and tests consume this descriptor — not prose markers.
13
+ */
14
+ export const ACCEPTED_ACTIVATION_FACT_KEYS = Object.freeze([
15
+ "event",
16
+ "role",
17
+ "observedAt",
18
+ "bookKey",
19
+ "session",
20
+ "correlation",
21
+ ]);
22
+ const _acceptedActivationFactKeysMatch = true;
23
+ void _acceptedActivationFactKeysMatch;
24
+ /**
25
+ * Host correlation channel (not a CLI flag): a non-blank AK_CORRELATION_ID carries
26
+ * the caller id verbatim; missing/blank/whitespace-only yields the typed absent identity.
27
+ */
28
+ export function correlationIdentityFromEnv(env = process.env) {
29
+ const raw = env.AK_CORRELATION_ID;
30
+ if (typeof raw === "string" && raw.trim().length > 0) {
31
+ return { kind: "caller", id: raw };
32
+ }
33
+ return { kind: "absent" };
34
+ }
35
+ /**
36
+ * Descriptor-driven top-level pick: only ACCEPTED_ACTIVATION_FACT_KEYS leave this boundary.
37
+ * Nested session/correlation are rebuilt closed before the pick (ADR 0049 zero-content by construction).
38
+ */
39
+ function projectAcceptedActivationFact(input) {
40
+ const closed = {
41
+ event: ACCEPTED_ACTIVATION_EVENT,
42
+ role: input.role,
43
+ observedAt: input.observedAt,
44
+ bookKey: input.bookKey,
45
+ session: { kind: "session-file", path: input.session.path },
46
+ correlation: input.correlation.kind === "caller"
47
+ ? { kind: "caller", id: input.correlation.id }
48
+ : { kind: "absent" },
49
+ };
50
+ // Descriptor is the sole top-level emission contract (typed pick; no content keys).
51
+ return Object.fromEntries(ACCEPTED_ACTIVATION_FACT_KEYS.map((key) => [key, closed[key]]));
52
+ }
53
+ /** Construct the closed fact from trusted typed inputs only (descriptor projection — no content keys). */
54
+ export function buildAcceptedActivationFact(input) {
55
+ return projectAcceptedActivationFact(input);
56
+ }
57
+ /** Serialize only the closed index fields (descriptor projection — no content keys). */
58
+ export function serializeAcceptedActivationFact(fact) {
59
+ return `${JSON.stringify(projectAcceptedActivationFact(fact))}\n`;
60
+ }
61
+ /**
62
+ * Bare O_APPEND write of one complete line under an absolute ledger home.
63
+ * Private helper: production-bound to writeSync; owns open/write/close honesty only.
64
+ * Close failure cannot mask open/write; simultaneous close is nested evidence.
65
+ */
66
+ function appendActivationLedgerLine(ledgerPath, line, options) {
67
+ if (!isAbsolute(options.ledgerHome)) {
68
+ throw new ActivationLedgerError(`activation ledger home must be absolute: ${options.ledgerHome}`);
69
+ }
70
+ const resolvedLedger = resolve(ledgerPath);
71
+ const resolvedHome = resolve(options.ledgerHome);
72
+ const parent = dirname(resolvedLedger);
73
+ ensureRealDirectoryTree(resolvedHome, parent);
74
+ assertLedgerFileInsideHome(resolvedLedger, resolvedHome);
75
+ // Fail-closed guard (same shape as PR #418's publication seam): on platforms
76
+ // without O_NOFOLLOW (e.g. Windows, nodejs/node#41590) the JS bitwise-or in
77
+ // ACTIVATION_LEDGER_APPEND_OPEN_FLAGS would silently drop the flag and the
78
+ // lstat→open TOCTOU anti-symlink protection would vanish while the comment
79
+ // above still claims it. Refuse loudly via the existing typed ledger failure
80
+ // channel instead of appending unprotected; never silently degrade.
81
+ if (typeof constants.O_NOFOLLOW !== "number") {
82
+ throw new ActivationLedgerError("activation ledger append requires O_NOFOLLOW open-flag support (anti-symlink TOCTOU protection must not be silently dropped); refusing to append");
83
+ }
84
+ const bytes = Buffer.isBuffer(line) ? line : Buffer.from(line);
85
+ let ledgerFd;
86
+ let primaryFailure;
87
+ try {
88
+ try {
89
+ ledgerFd = openSync(resolvedLedger, ACTIVATION_LEDGER_APPEND_OPEN_FLAGS, 0o644);
90
+ }
91
+ catch (error) {
92
+ // Native ELOOP/filesystem cause retained; no errno dispatch.
93
+ throw new ActivationLedgerError(`activation ledger failed to open ledger file (${resolvedLedger}): ${errorText(error)}`, { cause: error });
94
+ }
95
+ let opened;
96
+ try {
97
+ opened = fstatSync(ledgerFd);
98
+ }
99
+ catch (error) {
100
+ throw new ActivationLedgerError(`activation ledger failed to fstat ledger file (${resolvedLedger}): ${errorText(error)}`, { cause: error });
101
+ }
102
+ if (!opened.isFile()) {
103
+ throw new ActivationLedgerError(`activation ledger is not a regular file: ${resolvedLedger}`);
104
+ }
105
+ const written = writeSync(ledgerFd, bytes, 0, bytes.length, null);
106
+ if (written !== bytes.length) {
107
+ throw new ActivationLedgerError(`activation ledger short write: wrote ${written} of ${bytes.length} bytes to ${resolvedLedger}`);
108
+ }
109
+ }
110
+ catch (error) {
111
+ primaryFailure = error;
112
+ }
113
+ if (ledgerFd !== undefined) {
114
+ try {
115
+ closeSync(ledgerFd);
116
+ }
117
+ catch (closeFailure) {
118
+ if (primaryFailure !== undefined) {
119
+ throw new AggregateError([primaryFailure, closeFailure], "activation ledger operation and close failed", { cause: primaryFailure });
120
+ }
121
+ throw closeFailure;
122
+ }
123
+ }
124
+ if (primaryFailure !== undefined)
125
+ throw primaryFailure;
126
+ }
127
+ /**
128
+ * Append one complete JSONL record with one O_APPEND write of the full record.
129
+ * Shared-ledger contract: concurrent successful append-only producers cannot
130
+ * overwrite one another. A short write is an honest infrastructure failure
131
+ * (ADR 0049) — no non-append rollback/truncate. Close failure cannot mask the
132
+ * primary write cause; simultaneous close evidence is retained.
133
+ */
134
+ export function appendAcceptedActivationFact(ledgerPath, fact, options) {
135
+ appendActivationLedgerLine(ledgerPath, Buffer.from(serializeAcceptedActivationFact(fact), "utf8"), { ledgerHome: options.ledgerHome });
136
+ }
137
+ export function appendAcceptedActivationToBook(options) {
138
+ appendAcceptedActivationFact(activationWaitingLedgerPath(options.ledgerHome, options.fact.bookKey), options.fact, { ledgerHome: options.ledgerHome });
139
+ }
@@ -0,0 +1,33 @@
1
+ import { Type } from "typebox";
2
+ const causeSchema = Type.Object({
3
+ identity: Type.String({ minLength: 1 }),
4
+ name: Type.String({ minLength: 1 }),
5
+ message: Type.String(),
6
+ evidenceId: Type.Optional(Type.String({ minLength: 1 })),
7
+ }, { additionalProperties: false });
8
+ export const activationTraceRecordSchema = Type.Object({
9
+ role: Type.String({ minLength: 1 }),
10
+ stageId: Type.String({ pattern: "^[a-z][a-z0-9-]*$" }),
11
+ status: Type.Literal("failed"),
12
+ timestamp: Type.String({ format: "date-time" }),
13
+ cause: causeSchema,
14
+ }, { additionalProperties: false });
15
+ let activationCauseEvidence = 0;
16
+ const retainedActivationCauses = new Map();
17
+ export function namedActivationCause(error) {
18
+ const evidenceId = `activation-cause-${++activationCauseEvidence}`;
19
+ retainedActivationCauses.set(evidenceId, error);
20
+ if (error instanceof Error) {
21
+ const code = error.code;
22
+ const name = error.name || "Error";
23
+ return { identity: typeof code === "string" && code.length > 0 ? code : name, name, message: error.message, evidenceId };
24
+ }
25
+ let message;
26
+ try {
27
+ message = typeof error === "string" ? error : JSON.stringify(error) ?? String(error);
28
+ return { identity: "UnknownThrownCause", name: "UnknownThrownCause", message, evidenceId };
29
+ }
30
+ catch {
31
+ return { identity: "UnknownThrownCause", name: "UnknownThrownCause", message: String(error), evidenceId };
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Sole production fold of typed agent-start materials into provider-visible
3
+ * system-prompt bytes. Pi adapter and Grok ACP adapter both call this at the
4
+ * send boundary; structured materials are the authority, this string is the
5
+ * provider wire form.
6
+ */
7
+ export function renderAgentStartMaterials(body, materials) {
8
+ if (materials.length === 0)
9
+ return body;
10
+ return [body, ...materials.map((material) => JSON.stringify(material))].join("\n\n");
11
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public Auditor (审刑院) role — filed-officer envelope (#675).
3
+ */
4
+ import { AUDITOR_ACCEPTED_TEXT, AUDITOR_OUTPUT_TOOL_NAME, auditorOutputSchema, } from "./package-contracts/auditor-output.js";
5
+ export { AUDITOR_ACCEPTED_TEXT, AUDITOR_OUTPUT_TOOL_NAME };
6
+ export const AUDITOR_TOOL_SPEC = {
7
+ name: AUDITOR_OUTPUT_TOOL_NAME,
8
+ label: "审刑院输出",
9
+ description: "审刑院终局回执,状态为 pass、bounce 或 escalate。",
10
+ promptSnippet: "审刑院终局回执",
11
+ parameters: auditorOutputSchema,
12
+ };
@@ -0,0 +1,64 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, resolve } from "node:path";
4
+ import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
5
+ export function captureCanonicalSkillExpansion(name, snapshot, configuredPath, evidence, originalRequest) {
6
+ const matchedPath = evidence?.location === configuredPath
7
+ ? configuredPath
8
+ : evidence?.location === snapshot.path
9
+ ? snapshot.path
10
+ : undefined;
11
+ const expectedContent = matchedPath === undefined
12
+ ? undefined
13
+ : `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`;
14
+ if (evidence?.name !== name
15
+ || matchedPath === undefined
16
+ || evidence.content !== expectedContent
17
+ || evidence.userMessage !== originalRequest) {
18
+ return undefined;
19
+ }
20
+ return Object.freeze({ ...evidence, name });
21
+ }
22
+ export class CanonicalSkillUnavailableError extends Error {
23
+ skillName;
24
+ code = "canonical-skill-unavailable";
25
+ constructor(skillName, path, cause) {
26
+ super(`Canonical ${skillName} Skill is unavailable at ${path}`, { cause });
27
+ this.skillName = skillName;
28
+ this.name = "CanonicalSkillUnavailableError";
29
+ }
30
+ }
31
+ export async function loadCanonicalSkillBinding(name) {
32
+ const configuredPath = resolve(homedir(), `.agents/skills/${name}/SKILL.md`);
33
+ let path;
34
+ let raw;
35
+ try {
36
+ path = await realpath(configuredPath);
37
+ raw = await readFile(path, "utf8");
38
+ }
39
+ catch (error) {
40
+ throw new CanonicalSkillUnavailableError(name, configuredPath, error);
41
+ }
42
+ const body = stripFrontmatter(raw).trim();
43
+ if (body.length === 0) {
44
+ throw new Error(`Canonical ${name} Skill is empty at ${path}`);
45
+ }
46
+ const snapshot = Object.freeze({
47
+ raw,
48
+ path,
49
+ baseDir: dirname(path),
50
+ body,
51
+ snapshotIdentity: Object.freeze({ text: raw }),
52
+ });
53
+ const binding = {
54
+ name,
55
+ snapshot,
56
+ invocation(originalRequest) {
57
+ return `/skill:${name} ${originalRequest}`;
58
+ },
59
+ captureExpansion(evidence, originalRequest) {
60
+ return captureCanonicalSkillExpansion(name, snapshot, configuredPath, evidence, originalRequest);
61
+ },
62
+ };
63
+ return Object.freeze(binding);
64
+ }
@@ -0,0 +1,151 @@
1
+ import { CorrectableSubmissionError } from "./submission-correctable-error.js";
2
+ function identityKey(identity) {
3
+ if (identity === null)
4
+ return "unassigned";
5
+ // GitHub omits App metadata on some surfaces (notably review comments).
6
+ // The stable user ID is the grouping identity; richer observed structure is
7
+ // merged below and must not split one actor across transport surfaces.
8
+ return String(identity.userId);
9
+ }
10
+ function mergeMachineIdentity(current, observed) {
11
+ if (current === null)
12
+ return observed;
13
+ if (observed === null)
14
+ return current;
15
+ if (current.appId === undefined && observed.appId !== undefined)
16
+ return observed;
17
+ if (current.appId !== undefined && observed.appId === undefined)
18
+ return current;
19
+ return observed.userType < current.userType ? observed : current;
20
+ }
21
+ function headRelationFor(record, targetHead) {
22
+ return record.commitOid === undefined || record.commitOid === null
23
+ ? "unbound"
24
+ : record.commitOid === targetHead ? "current" : "prior";
25
+ }
26
+ /** Receipt adapter consuming the typed facts retained by transport normalization. */
27
+ export function extractCollectorEvidenceIdentityGroups(records, targetHead) {
28
+ const groups = new Map();
29
+ for (const record of records) {
30
+ if (record.kind !== "review" && record.kind !== "issue_comment" && record.kind !== "review_comment" && record.kind !== "reaction")
31
+ continue;
32
+ if (record.githubId === undefined)
33
+ continue;
34
+ const identity = record.machineIdentity ?? null;
35
+ const kind = record.kind;
36
+ const source = {
37
+ kind,
38
+ id: record.githubId,
39
+ evidenceId: record.evidenceId,
40
+ headRelation: headRelationFor(record, targetHead),
41
+ };
42
+ const key = identityKey(identity);
43
+ let group = groups.get(key);
44
+ if (group === undefined) {
45
+ group = {
46
+ identity,
47
+ ...(record.authorLogin === undefined ? {} : { displayLogin: record.authorLogin }),
48
+ attendance: true,
49
+ findings: [],
50
+ materials: [],
51
+ };
52
+ groups.set(key, group);
53
+ }
54
+ else {
55
+ group.identity = mergeMachineIdentity(group.identity, identity);
56
+ }
57
+ group.materials.push(source);
58
+ }
59
+ return [...groups.values()];
60
+ }
61
+ /**
62
+ * #641 chain①: pointer-open failures are model misuse, not host failures. The
63
+ * seat rejects them as correctable so the model can retry with a stored
64
+ * evidenceId — on Pi and Grok/ACP alike (第 0 条: 模型提交方式可纠正).
65
+ */
66
+ export class CollectorUnknownEvidenceError extends CorrectableSubmissionError {
67
+ constructor(evidenceId) {
68
+ super(`未在本局已观测材料中找到 evidenceId ${evidenceId};请用 observe 返回的指针重试。`);
69
+ this.name = "CollectorUnknownEvidenceError";
70
+ }
71
+ }
72
+ /**
73
+ * #641 chain①: any malformed findings submission is a model misuse the model
74
+ * can correct and resubmit — a branded correctable rejection on every supported
75
+ * engine, never a round infrastructure failure.
76
+ */
77
+ export class CollectorFindingsValidationError extends CorrectableSubmissionError {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = "CollectorFindingsValidationError";
81
+ }
82
+ }
83
+ /**
84
+ * #641 chain①: turn model-submitted finding pointer refs into receipt findings.
85
+ * Each pointer must resolve to a stored text-bearing evidence record; the
86
+ * machine locator is enriched from the same record so receipt and volume agree
87
+ * (指针可解析、开卷相符) by construction. Throws branded correctable
88
+ * (non-fatal, model-visible) errors for unresolvable or mis-typed findings.
89
+ */
90
+ export function enrichCollectorFindings(input) {
91
+ const candidate = input.candidate;
92
+ if (candidate === undefined || candidate === null)
93
+ return;
94
+ if (typeof candidate !== "object" || Array.isArray(candidate)) {
95
+ throw new CollectorFindingsValidationError("通进司交件参数必须为对象");
96
+ }
97
+ const rawFindings = candidate.findings;
98
+ if (rawFindings === undefined)
99
+ return;
100
+ if (!Array.isArray(rawFindings)) {
101
+ throw new CollectorFindingsValidationError("通进司 findings 必须为数组");
102
+ }
103
+ const byEvidenceId = new Map(input.records.map((record) => [record.evidenceId, record]));
104
+ for (const raw of rawFindings) {
105
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
106
+ throw new CollectorFindingsValidationError("通进司 finding 必须为对象");
107
+ }
108
+ const evidenceId = raw.evidenceId;
109
+ if (typeof evidenceId !== "string" || evidenceId.length === 0) {
110
+ throw new CollectorFindingsValidationError("通进司 finding 缺少可解析的 evidenceId 指针");
111
+ }
112
+ const record = byEvidenceId.get(evidenceId);
113
+ if (record === undefined) {
114
+ throw new CollectorUnknownEvidenceError(evidenceId);
115
+ }
116
+ if (record.kind !== "review" && record.kind !== "issue_comment" && record.kind !== "review_comment") {
117
+ throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${record.kind}`);
118
+ }
119
+ if (record.githubId === undefined) {
120
+ throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 缺少 GitHub id`);
121
+ }
122
+ const category = raw.category;
123
+ if (category !== undefined && (typeof category !== "string" || category.trim().length === 0)) {
124
+ throw new CollectorFindingsValidationError("通进司 finding category 必须为非空字符串");
125
+ }
126
+ const identity = record.machineIdentity ?? null;
127
+ const group = input.groups.find((candidateGroup) => identityKey(candidateGroup.identity) === identityKey(identity));
128
+ if (group === undefined) {
129
+ throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 无归属身份组`);
130
+ }
131
+ group.findings.push({
132
+ identity,
133
+ source: {
134
+ kind: record.kind,
135
+ id: record.githubId,
136
+ evidenceId: record.evidenceId,
137
+ headRelation: headRelationFor(record, input.targetHead),
138
+ },
139
+ ...(category === undefined ? {} : { category: category.trim() }),
140
+ pointer: {
141
+ repository: input.repository,
142
+ prNumber: input.prNumber,
143
+ commentId: record.githubId,
144
+ ...(record.htmlUrl === undefined ? {} : { htmlUrl: record.htmlUrl }),
145
+ ...(record.authorLogin === undefined ? {} : { authorLogin: record.authorLogin }),
146
+ kind: record.kind,
147
+ authoritativeTime: record.authoritativeTime ?? null,
148
+ },
149
+ });
150
+ }
151
+ }
@@ -0,0 +1,95 @@
1
+ import { COLLECTOR_HOST } from "./collector-config.js";
2
+ import { enrichCollectorFindings, extractCollectorEvidenceIdentityGroups, } from "./collector-identity.js";
3
+ import { validateAcceptedCollectorReceipt } from "./package-contracts/collector-output.js";
4
+ export { validateAcceptedCollectorReceipt };
5
+ function fail(message) { throw new Error(message); }
6
+ /** Runtime output assembles findings pointers from the model submission into the typed receipt. */
7
+ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
8
+ ledger.assertNotFatal();
9
+ if (ledger.unresolvedTransportFailure)
10
+ fail("Collector cannot output while a transport failure is unrecovered");
11
+ if (ledger.latestCompleteSnapshotId === undefined)
12
+ fail("Collector output requires a complete final snapshot");
13
+ if (ledger.activationTime === undefined || ledger.deadlineTime === undefined)
14
+ fail("Collector output requires activation timeline");
15
+ if (clock !== undefined)
16
+ ledger.assertOutputObservationLaw(clock);
17
+ else if (ledger.observedGeneration !== ledger.mutationGeneration ||
18
+ (ledger.finalObservationRequired && !ledger.finalObservationCompleted)) {
19
+ fail("Collector output requires a complete observe after the latest request/wait mutation");
20
+ }
21
+ const finalSnapshot = ledger.getSnapshot(ledger.latestCompleteSnapshotId);
22
+ if (finalSnapshot === undefined || !finalSnapshot.complete)
23
+ fail("Collector final snapshot is incomplete");
24
+ if (finalSnapshot.prState !== "OPEN")
25
+ fail("Collector final snapshot PR state is not OPEN");
26
+ const evidenceRecords = [...ledger.allEvidence()];
27
+ const snapshots = [...ledger.allSnapshots()];
28
+ const evidenceIndex = new Map(evidenceRecords.map((record) => [record.evidenceId, record]));
29
+ const snapshotIndex = new Map(snapshots.map((snapshot) => [snapshot.snapshotId, snapshot]));
30
+ if (evidenceIndex.size !== evidenceRecords.length)
31
+ fail("Collector receipt evidenceId collision");
32
+ if (snapshotIndex.size !== snapshots.length)
33
+ fail("Collector receipt snapshotId collision");
34
+ for (const id of evidenceIndex.keys())
35
+ if (snapshotIndex.has(id))
36
+ fail(`Collector receipt id "${id}" is ambiguous`);
37
+ if (!snapshotIndex.has(finalSnapshot.snapshotId))
38
+ fail("Collector receipt lacks final snapshot");
39
+ for (const snapshot of snapshots) {
40
+ for (const id of snapshot.evidenceIds)
41
+ if (!evidenceIndex.has(id))
42
+ fail(`Collector snapshot ref "${id}" does not resolve`);
43
+ }
44
+ const groups = extractCollectorEvidenceIdentityGroups(evidenceRecords, finalSnapshot.headOid);
45
+ // #641 chain①: the collector LLM submits findings as pointer refs; the runtime
46
+ // enriches each with the machine locator from the same stored record.
47
+ enrichCollectorFindings({
48
+ candidate: candidateRaw,
49
+ records: evidenceRecords,
50
+ groups,
51
+ targetHead: finalSnapshot.headOid,
52
+ repository: ledger.config.repository.canonical,
53
+ prNumber: ledger.config.prNumber,
54
+ });
55
+ for (const group of groups) {
56
+ if (group.attendance !== true)
57
+ fail("Collector group lacks attendance");
58
+ for (const material of group.materials) {
59
+ if (material.evidenceId === undefined || !evidenceIndex.has(material.evidenceId))
60
+ fail("Collector material lacks a receipt-local evidence ref");
61
+ }
62
+ for (const finding of group.findings) {
63
+ if (finding.source.evidenceId === undefined || !evidenceIndex.has(finding.source.evidenceId))
64
+ fail("Collector finding lacks a receipt-local evidence ref");
65
+ }
66
+ }
67
+ return {
68
+ host: COLLECTOR_HOST,
69
+ repository: ledger.config.repository.canonical,
70
+ prNumber: ledger.config.prNumber,
71
+ manifestDigest: ledger.config.manifest.digest,
72
+ activationTime: ledger.activationTime.toISOString(),
73
+ deadlineTime: ledger.deadlineTime.toISOString(),
74
+ finalObservationTime: finalSnapshot.completedAt ?? finalSnapshot.observedAt,
75
+ finalSnapshotId: finalSnapshot.snapshotId,
76
+ targetHead: finalSnapshot.headOid,
77
+ groups,
78
+ requestAttempts: [...ledger.requestAttempts()],
79
+ snapshots,
80
+ evidenceRecords: evidenceRecords.map(toReceiptEvidenceRecord),
81
+ };
82
+ }
83
+ function toReceiptEvidenceRecord(record) {
84
+ return {
85
+ evidenceId: record.evidenceId,
86
+ kind: record.kind,
87
+ versionId: record.versionId,
88
+ contentDigest: record.contentDigest,
89
+ firstObservedAt: record.firstObservedAt,
90
+ ...(record.githubId === undefined ? {} : { githubId: record.githubId }),
91
+ ...(record.authorLogin === undefined ? {} : { authorLogin: record.authorLogin }),
92
+ ...(record.htmlUrl === undefined ? {} : { htmlUrl: record.htmlUrl }),
93
+ ...(record.authoritativeTime === undefined ? {} : { authoritativeTime: record.authoritativeTime }),
94
+ };
95
+ }