@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,2225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Invocation request admission: optional opaque instruction, frozen
|
|
3
|
+
* Attachments, project default/override (ADR 0052 / #106).
|
|
4
|
+
*/
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { lstat, mkdir, readFile, realpath, writeFile, } from "node:fs/promises";
|
|
7
|
+
import { basename, isAbsolute, join, resolve, sep } from "node:path";
|
|
8
|
+
import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
|
|
9
|
+
import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
|
|
10
|
+
import { readRunTicketNumber } from "../run-ticket-number.js";
|
|
11
|
+
import { loadDoctorCase, } from "../doctor-evidence.js";
|
|
12
|
+
import { COLLECTOR_FIXED_KICKOFF, emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
|
|
13
|
+
import { FixerPacketValidationError, parseFixerPrerequisites, } from "../package-contracts/fixer-packet.js";
|
|
14
|
+
import { createProductionMergerGitState } from "../merger-git-state.js";
|
|
15
|
+
import { validateMergerInput, } from "../merger-contracts.js";
|
|
16
|
+
import { sha256Hex } from "../sha256.js";
|
|
17
|
+
import { uuidv7 } from "../uuidv7.js";
|
|
18
|
+
import { NOTARY_FIXED_KICKOFF, } from "../notary-contracts.js";
|
|
19
|
+
import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-source-run.js";
|
|
20
|
+
import { appendEngineSessionMaterial, } from "../package-resources/engine-material.js";
|
|
21
|
+
import { CliUsageError } from "./cli-errors.js";
|
|
22
|
+
import { REJECTED_PUBLIC_SPELLINGS, createTypedOptionConsumer, evaluateAnalystModeOptionContract, optionsForOwner, resolveAnalystMode, } from "./option-definitions.js";
|
|
23
|
+
/** Issue principal + derive ledger placement through the injected authority only. */
|
|
24
|
+
export function issueAdmissionPlacement(authority, request) {
|
|
25
|
+
const principal = authority.issue(request);
|
|
26
|
+
const { sessionDirectory, sessionFile } = authority.decode(principal);
|
|
27
|
+
const runDirectory = join(sessionDirectory, "..");
|
|
28
|
+
const ledgerHome = resolveActivationLedgerHome(request.home);
|
|
29
|
+
const bookKey = resolveBookKeyFromGit(request.cwd);
|
|
30
|
+
return {
|
|
31
|
+
principal,
|
|
32
|
+
sessionDirectory,
|
|
33
|
+
sessionFile,
|
|
34
|
+
runDirectory,
|
|
35
|
+
ledgerHome,
|
|
36
|
+
bookKey,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Unique admitted-request.json persistence projection: top-level sessionDirectory/sessionFile
|
|
41
|
+
* (base wire shape). Memory Admitted keeps only the opaque principal — never dual-carry.
|
|
42
|
+
*/
|
|
43
|
+
async function writeAdmittedRequestPersistence(admittedRequestPath, body, coordinates) {
|
|
44
|
+
const { principal: _omitPrincipal, ...rest } = body;
|
|
45
|
+
const projection = {
|
|
46
|
+
...rest,
|
|
47
|
+
sessionDirectory: coordinates.sessionDirectory,
|
|
48
|
+
sessionFile: coordinates.sessionFile,
|
|
49
|
+
};
|
|
50
|
+
await writeFile(admittedRequestPath, `${JSON.stringify(projection, null, 2)}\n`, "utf8");
|
|
51
|
+
}
|
|
52
|
+
/** Project effective model onto ledger fields; absent thinking stays absent. */
|
|
53
|
+
function effectiveModelLedgerFields(model) {
|
|
54
|
+
if (model === undefined)
|
|
55
|
+
return {};
|
|
56
|
+
return {
|
|
57
|
+
provider: model.provider,
|
|
58
|
+
model: model.model,
|
|
59
|
+
...(model.thinking === undefined ? {} : { thinking: model.thinking }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export { homeFromRunDirectory };
|
|
63
|
+
/**
|
|
64
|
+
* Persist one `invocation.json` identity page for the public run.
|
|
65
|
+
* Admission is the sole source for every field; this is the only identity
|
|
66
|
+
* projection and callers never provide an independent ledger shape.
|
|
67
|
+
* When an effective model is known at admission, provider/model (and thinking
|
|
68
|
+
* only when supplied) are written onto the same page.
|
|
69
|
+
*/
|
|
70
|
+
async function writeRoleInvocationLedger(source, role, effectiveModel) {
|
|
71
|
+
const identity = {
|
|
72
|
+
role,
|
|
73
|
+
runId: source.runId,
|
|
74
|
+
bookKey: source.bookKey,
|
|
75
|
+
projectRoot: source.projectRoot,
|
|
76
|
+
runDirectory: source.runDirectory,
|
|
77
|
+
sessionDirectory: source.sessionDirectory,
|
|
78
|
+
sessionFile: source.sessionFile,
|
|
79
|
+
...(source.correlationId === undefined ? {} : { correlationId: source.correlationId }),
|
|
80
|
+
...(source.ticketNumber === undefined ? {} : { ticketNumber: source.ticketNumber }),
|
|
81
|
+
...effectiveModelLedgerFields(effectiveModel),
|
|
82
|
+
};
|
|
83
|
+
await writeFile(join(source.runDirectory, "invocation.json"), `${JSON.stringify(identity, null, 2)}\n`, "utf8");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Merge the effective launch model / engine / host onto the existing invocation
|
|
87
|
+
* identity page (resume / temporary override path — same field shape as admission).
|
|
88
|
+
* Bare model clears any prior thinking key so absence stays honest.
|
|
89
|
+
*
|
|
90
|
+
* Engine axis (#617 Scope 1): `string` writes, `null` deletes (authoritative seat
|
|
91
|
+
* projection when the live table has no engine), `undefined` preserves any existing
|
|
92
|
+
* key for non-authoritative partial updates.
|
|
93
|
+
* Host stays write-if-present (`string` only).
|
|
94
|
+
*/
|
|
95
|
+
export async function recordEffectiveInvocationModel(runDirectory, model, engine, host) {
|
|
96
|
+
const ledgerPath = join(runDirectory, "invocation.json");
|
|
97
|
+
const current = JSON.parse(await readFile(ledgerPath, "utf8"));
|
|
98
|
+
const next = { ...current };
|
|
99
|
+
if (model !== undefined) {
|
|
100
|
+
next.provider = model.provider;
|
|
101
|
+
next.model = model.model;
|
|
102
|
+
if (model.thinking === undefined) {
|
|
103
|
+
delete next.thinking;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
next.thinking = model.thinking;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (engine === null) {
|
|
110
|
+
delete next.engine;
|
|
111
|
+
}
|
|
112
|
+
else if (engine !== undefined) {
|
|
113
|
+
next.engine = engine;
|
|
114
|
+
}
|
|
115
|
+
if (host !== undefined) {
|
|
116
|
+
next.host = host;
|
|
117
|
+
}
|
|
118
|
+
await writeFile(ledgerPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
119
|
+
}
|
|
120
|
+
/** Merge observed launch-time fields into the single existing invocation.json identity page. */
|
|
121
|
+
async function mergeInvocationIdentityPage(runDirectory, fields) {
|
|
122
|
+
const ledgerPath = join(runDirectory, "invocation.json");
|
|
123
|
+
const current = JSON.parse(await readFile(ledgerPath, "utf8"));
|
|
124
|
+
await writeFile(ledgerPath, `${JSON.stringify({
|
|
125
|
+
...current,
|
|
126
|
+
...fields,
|
|
127
|
+
}, null, 2)}\n`, "utf8");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Bind a post-admission resolved ticketNumber onto the in-memory admitted
|
|
131
|
+
* object and both durable pages (invocation.json + admitted-request.json).
|
|
132
|
+
* Used by seat LLM ticket binding when admission was unbound
|
|
133
|
+
* (#635 / #582 diarist-resolves-ticket-llm-layer). Never clears an existing binding.
|
|
134
|
+
*/
|
|
135
|
+
export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
|
|
136
|
+
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
|
|
137
|
+
throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
|
|
138
|
+
}
|
|
139
|
+
if (admitted.ticketResolution === "true-unbound") {
|
|
140
|
+
throw new Error("bindAdmittedTicketNumber refuses to replace durable true-unbound ticket resolution");
|
|
141
|
+
}
|
|
142
|
+
if (admitted.ticketNumber !== undefined) {
|
|
143
|
+
if (admitted.ticketNumber === ticketNumber)
|
|
144
|
+
return;
|
|
145
|
+
throw new Error(`bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`);
|
|
146
|
+
}
|
|
147
|
+
admitted.ticketNumber = ticketNumber;
|
|
148
|
+
await mergeInvocationIdentityPage(admitted.runDirectory, { ticketNumber });
|
|
149
|
+
const admittedPath = admitted.admittedRequestPath;
|
|
150
|
+
const current = JSON.parse(await readFile(admittedPath, "utf8"));
|
|
151
|
+
await writeFile(admittedPath, `${JSON.stringify({ ...current, ticketNumber }, null, 2)}\n`, "utf8");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Persist seat LLM true-unbound conclusion onto in-memory admitted + both durable
|
|
155
|
+
* pages so later auto-resume attempts and manual resume never re-enter hermes (#635).
|
|
156
|
+
*/
|
|
157
|
+
export async function recordTrueUnboundTicketResolution(admitted) {
|
|
158
|
+
if (admitted.ticketNumber !== undefined) {
|
|
159
|
+
throw new Error(`recordTrueUnboundTicketResolution refuses to overwrite ticket #${admitted.ticketNumber}`);
|
|
160
|
+
}
|
|
161
|
+
if (admitted.ticketResolution === "true-unbound")
|
|
162
|
+
return;
|
|
163
|
+
admitted.ticketResolution =
|
|
164
|
+
"true-unbound";
|
|
165
|
+
await mergeInvocationIdentityPage(admitted.runDirectory, {
|
|
166
|
+
ticketResolution: "true-unbound",
|
|
167
|
+
});
|
|
168
|
+
const admittedPath = admitted.admittedRequestPath;
|
|
169
|
+
const current = JSON.parse(await readFile(admittedPath, "utf8"));
|
|
170
|
+
await writeFile(admittedPath, `${JSON.stringify({ ...current, ticketResolution: "true-unbound" }, null, 2)}\n`, "utf8");
|
|
171
|
+
}
|
|
172
|
+
/** Add the identity returned by the production Pi launch seam to its existing ledger page. */
|
|
173
|
+
export async function recordLaunchedPiIdentity(runDirectory, identity) {
|
|
174
|
+
await mergeInvocationIdentityPage(runDirectory, {
|
|
175
|
+
piExecutable: identity.executable,
|
|
176
|
+
piVersion: identity.version,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Read the package root that is about to serve this public run (observed values only). */
|
|
180
|
+
export async function observeLaunchedRolePackageIdentity(packageRoot, selectedRoleEntry) {
|
|
181
|
+
const rolePackageRoot = packageRoot;
|
|
182
|
+
const raw = JSON.parse(await readFile(join(rolePackageRoot, "package.json"), "utf8"));
|
|
183
|
+
if (typeof raw.version !== "string" || raw.version.trim() === "") {
|
|
184
|
+
throw new Error(`role package.json at ${rolePackageRoot} does not declare a nonblank version`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
roleEntry: selectedRoleEntry,
|
|
188
|
+
rolePackageRoot,
|
|
189
|
+
rolePackageVersion: raw.version,
|
|
190
|
+
entryMode: "public-cli",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Add the role-package identity resolved at the public launch seam to its existing ledger page. */
|
|
194
|
+
export async function recordLaunchedRolePackageIdentity(runDirectory, identity) {
|
|
195
|
+
await mergeInvocationIdentityPage(runDirectory, {
|
|
196
|
+
roleEntry: identity.roleEntry,
|
|
197
|
+
rolePackageRoot: identity.rolePackageRoot,
|
|
198
|
+
rolePackageVersion: identity.rolePackageVersion,
|
|
199
|
+
entryMode: identity.entryMode,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Positive ticket number for analyst query-scope face (and shared integer parse). */
|
|
203
|
+
export function parsePositiveTicketNumber(raw, flag) {
|
|
204
|
+
const trimmed = raw.trim();
|
|
205
|
+
if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
|
|
206
|
+
throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
|
|
207
|
+
}
|
|
208
|
+
const value = Number(trimmed);
|
|
209
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
210
|
+
throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
/** 共享解析体:同形 owner 的 argv → instruction/attachments/project。 */
|
|
215
|
+
function parseInstructionArgv(args, owner) {
|
|
216
|
+
const attachmentPaths = [];
|
|
217
|
+
let project;
|
|
218
|
+
let subject;
|
|
219
|
+
let sourceRun;
|
|
220
|
+
const positional = [];
|
|
221
|
+
const tokens = [...args];
|
|
222
|
+
const definitions = roleOptions(owner);
|
|
223
|
+
const options = createTypedOptionConsumer(definitions);
|
|
224
|
+
while (tokens.length > 0) {
|
|
225
|
+
if (tokens[0] === "--") {
|
|
226
|
+
tokens.shift();
|
|
227
|
+
positional.push(...tokens);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
const taken = options.takeDashed(tokens);
|
|
231
|
+
if (taken !== undefined) {
|
|
232
|
+
if (taken.def.id === "attach") {
|
|
233
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (taken.def.id === "project") {
|
|
237
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (taken.def.id === "subject") {
|
|
241
|
+
const raw = typeof taken.value === "string" ? taken.value.trim() : "";
|
|
242
|
+
if (raw !== "judge" && raw !== "doctor") {
|
|
243
|
+
throw new CliUsageError(`auditor --subject must be judge|doctor, got ${taken.value ?? "(missing)"}`);
|
|
244
|
+
}
|
|
245
|
+
subject = raw;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (taken.def.id === "source-run") {
|
|
249
|
+
const raw = typeof taken.value === "string" ? taken.value.trim() : "";
|
|
250
|
+
if (raw === "") {
|
|
251
|
+
throw new CliUsageError("auditor --source-run requires a run locator");
|
|
252
|
+
}
|
|
253
|
+
sourceRun = raw;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
throw new CliUsageError(`unknown ${owner} option: ${taken.def.canonical}`);
|
|
257
|
+
}
|
|
258
|
+
const token = tokens.shift();
|
|
259
|
+
if (token.startsWith("-") && token !== "-") {
|
|
260
|
+
throw new CliUsageError(`unknown ${owner} option: ${token}`);
|
|
261
|
+
}
|
|
262
|
+
positional.push(token);
|
|
263
|
+
}
|
|
264
|
+
options.assertRequired();
|
|
265
|
+
return {
|
|
266
|
+
instruction: positional.join(" "),
|
|
267
|
+
attachmentPaths,
|
|
268
|
+
...(project === undefined ? {} : { project }),
|
|
269
|
+
...(subject === undefined ? {} : { subject }),
|
|
270
|
+
...(sourceRun === undefined ? {} : { sourceRun }),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Honest activation-class failure while deriving the active-merge envelope. */
|
|
274
|
+
export class MergerEnvelopeDerivationError extends Error {
|
|
275
|
+
code = "merger-envelope-derivation";
|
|
276
|
+
/** Typed cause for #107 classifyPostAdmissionFailure (isTypedActivationError). */
|
|
277
|
+
knownCause = "activation";
|
|
278
|
+
constructor(message, options) {
|
|
279
|
+
super(message, options);
|
|
280
|
+
this.name = "MergerEnvelopeDerivationError";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Reject missing/blank path values so empty overrides cannot silently degrade. */
|
|
284
|
+
function requireOptionPath(flag, value) {
|
|
285
|
+
if (value === undefined || value.trim() === "") {
|
|
286
|
+
throw new CliUsageError(flag === "--base"
|
|
287
|
+
? `${flag} requires a nonempty revision`
|
|
288
|
+
: `${flag} requires a path`);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
/** True when token is a retained rejected spelling for the owner (#342). */
|
|
293
|
+
function isRejectedPublicSpelling(owner, token) {
|
|
294
|
+
for (const entry of REJECTED_PUBLIC_SPELLINGS) {
|
|
295
|
+
if (entry.owner !== owner)
|
|
296
|
+
continue;
|
|
297
|
+
for (const spelling of entry.spellings) {
|
|
298
|
+
if (token === spelling || token.startsWith(`${spelling}=`))
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
/** Role option definitions — sole spelling source for the matching parser. */
|
|
305
|
+
function roleOptions(owner) {
|
|
306
|
+
return optionsForOwner(owner);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Public --authority-ref admission grammar (refs-only).
|
|
310
|
+
* Unique owner for fresh argv and durable resume restore — no string-only parallel.
|
|
311
|
+
* Accepts durable reference tokens as-is; rejects blank and inline Spec prose
|
|
312
|
+
* (whitespace-bearing sentences). Does not fetch, normalize, or judge content.
|
|
313
|
+
*/
|
|
314
|
+
export function requireAuthorityRef(value) {
|
|
315
|
+
if (value === undefined || value.trim() === "") {
|
|
316
|
+
throw new CliUsageError("--authority-ref requires a nonempty durable reference");
|
|
317
|
+
}
|
|
318
|
+
// Spec prose sentences contain whitespace; durable public refs are single tokens.
|
|
319
|
+
if (/\s/.test(value)) {
|
|
320
|
+
throw new CliUsageError("--authority-ref requires a durable reference, not inline Spec prose");
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Parse Judge-specific argv after the `judge` token.
|
|
326
|
+
* Spellings from PUBLIC_OPTION_TABLE.judge; rejects burden family (#342).
|
|
327
|
+
*/
|
|
328
|
+
export function parseJudgeArgv(args) {
|
|
329
|
+
// Judge owns burden inference — rejected spellings checked per-token.
|
|
330
|
+
// `--` ends flag parsing: everything after is opaque instruction.
|
|
331
|
+
const dd = args.indexOf("--");
|
|
332
|
+
const preDd = dd === -1 ? args : args.slice(0, dd);
|
|
333
|
+
for (const token of preDd) {
|
|
334
|
+
if (isRejectedPublicSpelling("judge", token)) {
|
|
335
|
+
throw new CliUsageError("judge does not accept a public burden selector; Judge infers its own burden");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return parseInstructionArgv(args, "judge");
|
|
339
|
+
}
|
|
340
|
+
export function parseCountersignArgv(args) {
|
|
341
|
+
return parseInstructionArgv(args, "countersign");
|
|
342
|
+
}
|
|
343
|
+
export function parseInspectorArgv(args) {
|
|
344
|
+
return parseInstructionArgv(args, "inspector");
|
|
345
|
+
}
|
|
346
|
+
export function parseGatekeeperArgv(args) {
|
|
347
|
+
return parseInstructionArgv(args, "gatekeeper");
|
|
348
|
+
}
|
|
349
|
+
export function parseNavigatorArgv(args) {
|
|
350
|
+
return parseInstructionArgv(args, "navigator");
|
|
351
|
+
}
|
|
352
|
+
export function parseAuditorArgv(args) {
|
|
353
|
+
return parseInstructionArgv(args, "auditor");
|
|
354
|
+
}
|
|
355
|
+
export function parseEvidenceChildArgv(args) {
|
|
356
|
+
return parseInstructionArgv(args, "evidence-child");
|
|
357
|
+
}
|
|
358
|
+
export function parseDiaristArgv(args) {
|
|
359
|
+
return parseInstructionArgv(args, "diarist");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Parse Coder-specific argv after the `coder` token.
|
|
363
|
+
* Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.coder (#342).
|
|
364
|
+
*/
|
|
365
|
+
export function parseCoderArgv(args) {
|
|
366
|
+
const attachmentPaths = [];
|
|
367
|
+
let project;
|
|
368
|
+
const positional = [];
|
|
369
|
+
const tokens = [...args];
|
|
370
|
+
const definitions = roleOptions("coder");
|
|
371
|
+
const options = createTypedOptionConsumer(definitions);
|
|
372
|
+
while (tokens.length > 0) {
|
|
373
|
+
if (tokens[0] === "--") {
|
|
374
|
+
tokens.shift();
|
|
375
|
+
positional.push(...tokens);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
const taken = options.takeDashed(tokens);
|
|
379
|
+
if (taken !== undefined) {
|
|
380
|
+
if (taken.def.id === "attach") {
|
|
381
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (taken.def.id === "project") {
|
|
385
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
throw new CliUsageError(`unknown coder option: ${taken.def.canonical}`);
|
|
389
|
+
}
|
|
390
|
+
const token = tokens.shift();
|
|
391
|
+
if (token.startsWith("-") && token !== "-") {
|
|
392
|
+
throw new CliUsageError(`unknown coder option: ${token}`);
|
|
393
|
+
}
|
|
394
|
+
positional.push(token);
|
|
395
|
+
}
|
|
396
|
+
// Phase aliases + default come solely from the typed coder phase row (#342).
|
|
397
|
+
const phase = options.consumeLeadingPhase(positional);
|
|
398
|
+
options.assertRequired();
|
|
399
|
+
return {
|
|
400
|
+
phase,
|
|
401
|
+
instruction: positional.join(" "),
|
|
402
|
+
attachmentPaths,
|
|
403
|
+
...(project === undefined ? {} : { project }),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Parse Fixer-specific argv after the `fixer` token.
|
|
408
|
+
* Phase defaults to apply; spellings from PUBLIC_OPTION_TABLE.fixer (#342).
|
|
409
|
+
*/
|
|
410
|
+
export function parseFixerArgv(args) {
|
|
411
|
+
const attachmentPaths = [];
|
|
412
|
+
let project;
|
|
413
|
+
let prerequisitesPath;
|
|
414
|
+
const positional = [];
|
|
415
|
+
const tokens = [...args];
|
|
416
|
+
const definitions = roleOptions("fixer");
|
|
417
|
+
const options = createTypedOptionConsumer(definitions);
|
|
418
|
+
while (tokens.length > 0) {
|
|
419
|
+
if (tokens[0] === "--") {
|
|
420
|
+
tokens.shift();
|
|
421
|
+
positional.push(...tokens);
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
const taken = options.takeDashed(tokens);
|
|
425
|
+
if (taken !== undefined) {
|
|
426
|
+
if (taken.def.id === "attach") {
|
|
427
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (taken.def.id === "project") {
|
|
431
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (taken.def.id === "prerequisites") {
|
|
435
|
+
prerequisitesPath = requireOptionPath(taken.def.canonical, taken.value);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
throw new CliUsageError(`unknown fixer option: ${taken.def.canonical}`);
|
|
439
|
+
}
|
|
440
|
+
const token = tokens.shift();
|
|
441
|
+
if (token.startsWith("-") && token !== "-") {
|
|
442
|
+
throw new CliUsageError(`unknown fixer option: ${token}`);
|
|
443
|
+
}
|
|
444
|
+
positional.push(token);
|
|
445
|
+
}
|
|
446
|
+
// Phase aliases + default come solely from the typed fixer phase row (#342).
|
|
447
|
+
const phase = options.consumeLeadingPhase(positional);
|
|
448
|
+
options.assertRequired();
|
|
449
|
+
return {
|
|
450
|
+
phase,
|
|
451
|
+
instruction: positional.join(" "),
|
|
452
|
+
attachmentPaths,
|
|
453
|
+
...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
|
|
454
|
+
...(project === undefined ? {} : { project }),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function freezeRegularFileAttachment(sourcePath, destinationDir, index) {
|
|
458
|
+
const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
|
|
459
|
+
let st;
|
|
460
|
+
try {
|
|
461
|
+
st = await lstat(absolute);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
throw new CliUsageError(`attachment is not a readable regular file: ${sourcePath}`, { cause: error });
|
|
465
|
+
}
|
|
466
|
+
if (!st.isFile() || st.isSymbolicLink()) {
|
|
467
|
+
throw new CliUsageError(`attachment must be a regular file (not a directory or symlink): ${sourcePath}`);
|
|
468
|
+
}
|
|
469
|
+
const bytes = await readFile(absolute);
|
|
470
|
+
const name = `${String(index).padStart(2, "0")}-${basename(absolute)}`;
|
|
471
|
+
const frozenPath = join(destinationDir, name);
|
|
472
|
+
await writeFile(frozenPath, bytes);
|
|
473
|
+
return {
|
|
474
|
+
attachment: {
|
|
475
|
+
provenancePath: absolute,
|
|
476
|
+
frozenPath,
|
|
477
|
+
byteLength: bytes.byteLength,
|
|
478
|
+
sha256: sha256Hex(bytes),
|
|
479
|
+
mediaKind: "regular-file",
|
|
480
|
+
},
|
|
481
|
+
body: bytes,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
|
|
485
|
+
async function freezeAttachments(attachmentPaths, attachmentsDirectory) {
|
|
486
|
+
const attachments = [];
|
|
487
|
+
for (let i = 0; i < attachmentPaths.length; i += 1) {
|
|
488
|
+
const frozen = await freezeRegularFileAttachment(attachmentPaths[i], attachmentsDirectory, i);
|
|
489
|
+
attachments.push(frozen.attachment);
|
|
490
|
+
}
|
|
491
|
+
return attachments;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Freeze summons attachments into an already-retained run (#637 same-ticket resume).
|
|
495
|
+
* Writes under attachments/summons-<key>/ so prior freeze names stay intact.
|
|
496
|
+
* Manual resume never calls this — birth attachments keep their original semantics.
|
|
497
|
+
*/
|
|
498
|
+
export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, summonsKey = `s-${Date.now().toString(36)}`) {
|
|
499
|
+
if (attachmentPaths.length === 0)
|
|
500
|
+
return [];
|
|
501
|
+
const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
|
|
502
|
+
const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
|
|
503
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
504
|
+
return freezeAttachments(attachmentPaths, attachmentsDirectory);
|
|
505
|
+
}
|
|
506
|
+
function ticketAdmissionFields(ticketNumber) {
|
|
507
|
+
return ticketNumber === undefined ? {} : { ticketNumber };
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Shared instruction-seat admission for Judge and Inspector: project check,
|
|
511
|
+
* principal/placement issue, attachment freeze, admitted-request and invocation
|
|
512
|
+
* ledger write. CorrelationId is projected only when supplied (Inspector).
|
|
513
|
+
* Ticket binding is post-admission via shared seat LLM path (#635).
|
|
514
|
+
*/
|
|
515
|
+
async function admitStandardMaterialInvocation(role, options) {
|
|
516
|
+
// Empty project override must not reach resolve("") → cwd (silent default).
|
|
517
|
+
if (options.project !== undefined) {
|
|
518
|
+
requireOptionPath("--project", options.project);
|
|
519
|
+
}
|
|
520
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
521
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
522
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
523
|
+
cwd: projectRoot,
|
|
524
|
+
runId,
|
|
525
|
+
role,
|
|
526
|
+
home: options.home,
|
|
527
|
+
});
|
|
528
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
529
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
530
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
531
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
532
|
+
const correlationFields = options.correlationId === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { correlationId: options.correlationId };
|
|
535
|
+
const instruction = options.instruction;
|
|
536
|
+
const instructionEmpty = instruction.trim() === "";
|
|
537
|
+
const admitted = {
|
|
538
|
+
role,
|
|
539
|
+
runId,
|
|
540
|
+
bookKey,
|
|
541
|
+
projectRoot,
|
|
542
|
+
runDirectory,
|
|
543
|
+
principal,
|
|
544
|
+
...correlationFields,
|
|
545
|
+
instruction,
|
|
546
|
+
instructionEmpty,
|
|
547
|
+
attachments: attachments.map((a) => ({
|
|
548
|
+
provenancePath: a.provenancePath,
|
|
549
|
+
frozenPath: a.frozenPath,
|
|
550
|
+
byteLength: a.byteLength,
|
|
551
|
+
sha256: a.sha256,
|
|
552
|
+
mediaKind: a.mediaKind,
|
|
553
|
+
})),
|
|
554
|
+
};
|
|
555
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
556
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
557
|
+
sessionDirectory,
|
|
558
|
+
sessionFile,
|
|
559
|
+
});
|
|
560
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
561
|
+
return {
|
|
562
|
+
role,
|
|
563
|
+
runId,
|
|
564
|
+
bookKey,
|
|
565
|
+
projectRoot,
|
|
566
|
+
instruction,
|
|
567
|
+
instructionEmpty,
|
|
568
|
+
attachments,
|
|
569
|
+
runDirectory,
|
|
570
|
+
principal,
|
|
571
|
+
admittedRequestPath,
|
|
572
|
+
...correlationFields,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Atomically admit a Judge Role run: freeze Attachments, persist the request,
|
|
577
|
+
* and reserve session placement under the #78 ledger book.
|
|
578
|
+
*/
|
|
579
|
+
export async function admitJudgeInvocation(options) {
|
|
580
|
+
return admitStandardMaterialInvocation("judge", options);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Admit a direct Inspector (察院) run: freeze attachments, persist the request,
|
|
584
|
+
* and reserve session placement. Same instruction-seat face as Judge (#568).
|
|
585
|
+
*/
|
|
586
|
+
export async function admitInspectorInvocation(options) {
|
|
587
|
+
return admitStandardMaterialInvocation("inspector", options);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Admit a direct Gatekeeper (门下省) run (#639): freeze attachments, persist
|
|
591
|
+
* the request, reserve session placement — same instruction-seat face.
|
|
592
|
+
*/
|
|
593
|
+
export async function admitGatekeeperInvocation(options) {
|
|
594
|
+
return admitStandardMaterialInvocation("gatekeeper", options);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Admit a direct Navigator (游奕使) run (#639): freeze attachments, persist
|
|
598
|
+
* the request, reserve session placement — same instruction-seat face.
|
|
599
|
+
*/
|
|
600
|
+
export async function admitNavigatorInvocation(options) {
|
|
601
|
+
return admitStandardMaterialInvocation("navigator", options);
|
|
602
|
+
}
|
|
603
|
+
/** Admit a public 审刑院 run (#675). */
|
|
604
|
+
export async function admitAuditorInvocation(options) {
|
|
605
|
+
return admitStandardMaterialInvocation("auditor", options);
|
|
606
|
+
}
|
|
607
|
+
/** Admit a public evidence-child run (#675). */
|
|
608
|
+
export async function admitEvidenceChildInvocation(options) {
|
|
609
|
+
return admitStandardMaterialInvocation("evidence-child", options);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Admit a direct Diarist (起居郎) run (#708): freeze attachments, persist the
|
|
613
|
+
* request, reserve session placement — same instruction-seat face.
|
|
614
|
+
*/
|
|
615
|
+
export async function admitDiaristInvocation(options) {
|
|
616
|
+
return admitStandardMaterialInvocation("diarist", options);
|
|
617
|
+
}
|
|
618
|
+
/** Shared prompt transport for instruction-seat roles (judge/countersign/inspector). */
|
|
619
|
+
export function buildInstructionTransportPrompt(admitted, engineMaterial) {
|
|
620
|
+
const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
|
|
621
|
+
if (admitted.attachments.length > 0) {
|
|
622
|
+
lines.push("");
|
|
623
|
+
lines.push("已受理附件(冻结快照路径):");
|
|
624
|
+
for (const attachment of admitted.attachments) {
|
|
625
|
+
lines.push(`- ${attachment.frozenPath}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
629
|
+
}
|
|
630
|
+
/** Build the Pi prompt transport for an admitted Judge request. */
|
|
631
|
+
export function buildJudgeTransportPrompt(admitted, engineMaterial) {
|
|
632
|
+
return buildInstructionTransportPrompt(admitted, engineMaterial);
|
|
633
|
+
}
|
|
634
|
+
export function buildInspectorTransportPrompt(admitted, engineMaterial) {
|
|
635
|
+
return buildInstructionTransportPrompt(admitted, engineMaterial);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Atomically admit a Countersign Role run: freeze Attachments, persist the
|
|
639
|
+
* request, write the invocation ledger (#572 / ADR 0074).
|
|
640
|
+
*/
|
|
641
|
+
export async function admitCountersignInvocation(options) {
|
|
642
|
+
// Same shared freeze/coordinate body as judge but role: "countersign" so
|
|
643
|
+
// the ledger and session coordinates use the correct role from the start.
|
|
644
|
+
if (options.project !== undefined) {
|
|
645
|
+
requireOptionPath("--project", options.project);
|
|
646
|
+
}
|
|
647
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
648
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
649
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
650
|
+
cwd: projectRoot,
|
|
651
|
+
runId,
|
|
652
|
+
role: "countersign",
|
|
653
|
+
home: options.home,
|
|
654
|
+
});
|
|
655
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
656
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
657
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
658
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
659
|
+
// Ticket binding is LLM-only post-admission (#635); admission stays unbound.
|
|
660
|
+
const instruction = options.instruction;
|
|
661
|
+
const instructionEmpty = instruction.trim() === "";
|
|
662
|
+
const admitted = {
|
|
663
|
+
role: "countersign",
|
|
664
|
+
runId,
|
|
665
|
+
bookKey,
|
|
666
|
+
projectRoot,
|
|
667
|
+
runDirectory,
|
|
668
|
+
principal,
|
|
669
|
+
...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }),
|
|
670
|
+
instruction,
|
|
671
|
+
instructionEmpty,
|
|
672
|
+
attachments: attachments.map((a) => ({
|
|
673
|
+
provenancePath: a.provenancePath,
|
|
674
|
+
frozenPath: a.frozenPath,
|
|
675
|
+
byteLength: a.byteLength,
|
|
676
|
+
sha256: a.sha256,
|
|
677
|
+
mediaKind: a.mediaKind,
|
|
678
|
+
})),
|
|
679
|
+
};
|
|
680
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
681
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
682
|
+
sessionDirectory,
|
|
683
|
+
sessionFile,
|
|
684
|
+
});
|
|
685
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
686
|
+
return {
|
|
687
|
+
role: "countersign",
|
|
688
|
+
runId,
|
|
689
|
+
bookKey,
|
|
690
|
+
projectRoot,
|
|
691
|
+
instruction,
|
|
692
|
+
instructionEmpty,
|
|
693
|
+
attachments,
|
|
694
|
+
runDirectory,
|
|
695
|
+
principal,
|
|
696
|
+
admittedRequestPath,
|
|
697
|
+
...(options.correlationId === undefined ? {} : { correlationId: options.correlationId }),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/** Build the Pi prompt transport for an admitted Countersign request. */
|
|
701
|
+
export function buildCountersignTransportPrompt(admitted, engineMaterial) {
|
|
702
|
+
return buildInstructionTransportPrompt(admitted, engineMaterial);
|
|
703
|
+
}
|
|
704
|
+
/** Load admitted-request.json written at admission (Navigator work-context seam). */
|
|
705
|
+
export async function loadAdmittedJudgeRequest(runDirectory) {
|
|
706
|
+
try {
|
|
707
|
+
const raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
|
|
708
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
709
|
+
return undefined;
|
|
710
|
+
const record = raw;
|
|
711
|
+
if (record.role !== "judge")
|
|
712
|
+
return undefined;
|
|
713
|
+
if (typeof record.instruction !== "string")
|
|
714
|
+
return undefined;
|
|
715
|
+
if (typeof record.instructionEmpty !== "boolean")
|
|
716
|
+
return undefined;
|
|
717
|
+
if (!Array.isArray(record.attachments))
|
|
718
|
+
return undefined;
|
|
719
|
+
return {
|
|
720
|
+
instruction: record.instruction,
|
|
721
|
+
instructionEmpty: record.instructionEmpty,
|
|
722
|
+
attachments: record.attachments,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
export async function ensureRunArtifactsDir(runDirectory) {
|
|
730
|
+
const dir = join(runDirectory, "artifacts");
|
|
731
|
+
await mkdir(dir, { recursive: true });
|
|
732
|
+
return dir;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Admit a Coder Role run on the common Invocation request.
|
|
736
|
+
* Nonblank task remains authoritative: blank instruction is a structural reject.
|
|
737
|
+
* Phase (default apply / explicit plan) is frozen into the admitted request.
|
|
738
|
+
*/
|
|
739
|
+
export async function admitCoderInvocation(options) {
|
|
740
|
+
if (options.project !== undefined) {
|
|
741
|
+
requireOptionPath("--project", options.project);
|
|
742
|
+
}
|
|
743
|
+
const instruction = options.instruction;
|
|
744
|
+
if (instruction.trim() === "") {
|
|
745
|
+
throw new CliUsageError("coder requires a nonblank task instruction");
|
|
746
|
+
}
|
|
747
|
+
if (options.phase !== "plan" && options.phase !== "apply") {
|
|
748
|
+
throw new CliUsageError("coder phase must be plan or apply");
|
|
749
|
+
}
|
|
750
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
751
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
752
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
753
|
+
cwd: projectRoot,
|
|
754
|
+
runId,
|
|
755
|
+
role: "coder",
|
|
756
|
+
home: options.home,
|
|
757
|
+
});
|
|
758
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
759
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
760
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
761
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
762
|
+
const taskPath = join(runDirectory, "task.md");
|
|
763
|
+
await writeFile(taskPath, instruction, "utf8");
|
|
764
|
+
const admitted = {
|
|
765
|
+
role: "coder",
|
|
766
|
+
phase: options.phase,
|
|
767
|
+
runId,
|
|
768
|
+
bookKey,
|
|
769
|
+
projectRoot,
|
|
770
|
+
runDirectory,
|
|
771
|
+
principal,
|
|
772
|
+
instruction,
|
|
773
|
+
instructionEmpty: false,
|
|
774
|
+
taskPath,
|
|
775
|
+
attachments: attachments.map((a) => ({
|
|
776
|
+
provenancePath: a.provenancePath,
|
|
777
|
+
frozenPath: a.frozenPath,
|
|
778
|
+
byteLength: a.byteLength,
|
|
779
|
+
sha256: a.sha256,
|
|
780
|
+
mediaKind: a.mediaKind,
|
|
781
|
+
})),
|
|
782
|
+
};
|
|
783
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
784
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
785
|
+
sessionDirectory,
|
|
786
|
+
sessionFile,
|
|
787
|
+
});
|
|
788
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
789
|
+
return {
|
|
790
|
+
role: "coder",
|
|
791
|
+
phase: options.phase,
|
|
792
|
+
runId,
|
|
793
|
+
bookKey,
|
|
794
|
+
projectRoot,
|
|
795
|
+
instruction,
|
|
796
|
+
instructionEmpty: false,
|
|
797
|
+
attachments,
|
|
798
|
+
runDirectory,
|
|
799
|
+
principal,
|
|
800
|
+
admittedRequestPath,
|
|
801
|
+
taskPath,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Build the Pi prompt transport for an admitted Coder request.
|
|
806
|
+
* Task bytes already live at taskPath for --ak-coder-task; the prompt carries
|
|
807
|
+
* the same instruction plus frozen Attachment paths.
|
|
808
|
+
*/
|
|
809
|
+
export function buildCoderTransportPrompt(admitted, engineMaterial) {
|
|
810
|
+
const lines = [admitted.instruction];
|
|
811
|
+
if (admitted.attachments.length > 0) {
|
|
812
|
+
lines.push("");
|
|
813
|
+
lines.push("已受理附件(冻结快照路径):");
|
|
814
|
+
for (const attachment of admitted.attachments) {
|
|
815
|
+
lines.push(`- ${attachment.frozenPath}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Admit a Fixer Role run on the common Invocation request plus optional prerequisites.
|
|
822
|
+
* Nonblank instruction remains authoritative. Phase defaults to apply at parse time.
|
|
823
|
+
* Prerequisite grammar is structural; unmet/insufficient prerequisites stay Fixer judgments.
|
|
824
|
+
*/
|
|
825
|
+
export async function admitFixerInvocation(options) {
|
|
826
|
+
if (options.project !== undefined) {
|
|
827
|
+
requireOptionPath("--project", options.project);
|
|
828
|
+
}
|
|
829
|
+
const instruction = options.instruction;
|
|
830
|
+
if (instruction.trim() === "") {
|
|
831
|
+
throw new CliUsageError("fixer requires a nonblank repair instruction");
|
|
832
|
+
}
|
|
833
|
+
if (options.phase !== "plan" && options.phase !== "apply") {
|
|
834
|
+
throw new CliUsageError("fixer phase must be plan or apply");
|
|
835
|
+
}
|
|
836
|
+
// Validate/read prerequisites before freezing request materials.
|
|
837
|
+
let prerequisites = Object.freeze([]);
|
|
838
|
+
let prerequisitesSource;
|
|
839
|
+
if (options.prerequisitesPath !== undefined) {
|
|
840
|
+
const absolutePrereq = isAbsolute(options.prerequisitesPath)
|
|
841
|
+
? options.prerequisitesPath
|
|
842
|
+
: resolve(options.prerequisitesPath);
|
|
843
|
+
try {
|
|
844
|
+
prerequisitesSource = await readFile(absolutePrereq, "utf8");
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
throw new CliUsageError(`fixer prerequisites path is unreadable: ${options.prerequisitesPath}`, { cause: error });
|
|
848
|
+
}
|
|
849
|
+
try {
|
|
850
|
+
prerequisites = parseFixerPrerequisites(prerequisitesSource);
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
if (error instanceof FixerPacketValidationError) {
|
|
854
|
+
throw new CliUsageError(error.message, { cause: error });
|
|
855
|
+
}
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
860
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
861
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
862
|
+
cwd: projectRoot,
|
|
863
|
+
runId,
|
|
864
|
+
role: "fixer",
|
|
865
|
+
home: options.home,
|
|
866
|
+
});
|
|
867
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
868
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
869
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
870
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
871
|
+
let prerequisitesPath;
|
|
872
|
+
if (prerequisitesSource !== undefined) {
|
|
873
|
+
prerequisitesPath = join(runDirectory, "prerequisites.json");
|
|
874
|
+
await writeFile(prerequisitesPath, `${JSON.stringify(prerequisites, null, 2)}\n`, "utf8");
|
|
875
|
+
}
|
|
876
|
+
const packetPath = join(runDirectory, "fix-packet.md");
|
|
877
|
+
await writeFile(packetPath, instruction, "utf8");
|
|
878
|
+
const admitted = {
|
|
879
|
+
role: "fixer",
|
|
880
|
+
phase: options.phase,
|
|
881
|
+
runId,
|
|
882
|
+
bookKey,
|
|
883
|
+
projectRoot,
|
|
884
|
+
runDirectory,
|
|
885
|
+
principal,
|
|
886
|
+
instruction,
|
|
887
|
+
instructionEmpty: false,
|
|
888
|
+
packetPath,
|
|
889
|
+
...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
|
|
890
|
+
prerequisites: prerequisites.map((entry) => ({
|
|
891
|
+
id: entry.id,
|
|
892
|
+
requirement: entry.requirement,
|
|
893
|
+
})),
|
|
894
|
+
attachments: attachments.map((a) => ({
|
|
895
|
+
provenancePath: a.provenancePath,
|
|
896
|
+
frozenPath: a.frozenPath,
|
|
897
|
+
byteLength: a.byteLength,
|
|
898
|
+
sha256: a.sha256,
|
|
899
|
+
mediaKind: a.mediaKind,
|
|
900
|
+
})),
|
|
901
|
+
};
|
|
902
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
903
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
904
|
+
sessionDirectory,
|
|
905
|
+
sessionFile,
|
|
906
|
+
});
|
|
907
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
908
|
+
return {
|
|
909
|
+
role: "fixer",
|
|
910
|
+
phase: options.phase,
|
|
911
|
+
runId,
|
|
912
|
+
bookKey,
|
|
913
|
+
projectRoot,
|
|
914
|
+
instruction,
|
|
915
|
+
instructionEmpty: false,
|
|
916
|
+
attachments,
|
|
917
|
+
runDirectory,
|
|
918
|
+
principal,
|
|
919
|
+
admittedRequestPath,
|
|
920
|
+
packetPath,
|
|
921
|
+
...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
|
|
922
|
+
prerequisites,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Build the Pi prompt transport for an admitted Fixer request.
|
|
927
|
+
* Instruction bytes live at packetPath; prerequisites at optional path.
|
|
928
|
+
* Diagnosis method is available via package --skill, not forced into this prompt.
|
|
929
|
+
*/
|
|
930
|
+
export function buildFixerTransportPrompt(admitted, engineMaterial) {
|
|
931
|
+
const lines = [admitted.instruction];
|
|
932
|
+
if (admitted.attachments.length > 0) {
|
|
933
|
+
lines.push("");
|
|
934
|
+
lines.push("已受理附件(冻结快照路径):");
|
|
935
|
+
for (const attachment of admitted.attachments) {
|
|
936
|
+
lines.push(`- ${attachment.frozenPath}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
940
|
+
}
|
|
941
|
+
function parsePositivePrOption(raw) {
|
|
942
|
+
if (raw === undefined || raw.trim() === "")
|
|
943
|
+
throw new CliUsageError("--pr requires a positive pull request number");
|
|
944
|
+
try {
|
|
945
|
+
return parseCollectorPrNumber(raw);
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function parseRepoOption(raw) {
|
|
952
|
+
if (raw === undefined || raw.trim() === "")
|
|
953
|
+
throw new CliUsageError("--repo requires owner/repo");
|
|
954
|
+
return raw;
|
|
955
|
+
}
|
|
956
|
+
export function parseCollectorArgv(args) {
|
|
957
|
+
// Spellings from PUBLIC_OPTION_TABLE.collector (#342).
|
|
958
|
+
const attachmentPaths = [];
|
|
959
|
+
let project;
|
|
960
|
+
let repo;
|
|
961
|
+
let prNumber;
|
|
962
|
+
let requestManifestPath;
|
|
963
|
+
const positional = [];
|
|
964
|
+
const tokens = [...args];
|
|
965
|
+
const definitions = roleOptions("collector");
|
|
966
|
+
const options = createTypedOptionConsumer(definitions);
|
|
967
|
+
while (tokens.length > 0) {
|
|
968
|
+
if (tokens[0] === "--") {
|
|
969
|
+
tokens.shift();
|
|
970
|
+
positional.push(...tokens);
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
const taken = options.takeDashed(tokens);
|
|
974
|
+
if (taken !== undefined) {
|
|
975
|
+
if (taken.def.id === "attach") {
|
|
976
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (taken.def.id === "project") {
|
|
980
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (taken.def.id === "pr") {
|
|
984
|
+
prNumber = parsePositivePrOption(taken.value);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (taken.def.id === "repo") {
|
|
988
|
+
repo = parseRepoOption(taken.value);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (taken.def.id === "request-manifest") {
|
|
992
|
+
requestManifestPath = requireOptionPath(taken.def.canonical, taken.value);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
throw new CliUsageError(`unknown collector option: ${taken.def.canonical}`);
|
|
996
|
+
}
|
|
997
|
+
const token = tokens.shift();
|
|
998
|
+
if (token.startsWith("-") && token !== "-") {
|
|
999
|
+
throw new CliUsageError(`unknown collector option: ${token}`);
|
|
1000
|
+
}
|
|
1001
|
+
positional.push(token);
|
|
1002
|
+
}
|
|
1003
|
+
// Unconditional required (e.g. --pr) from typed table via shared consumer (#342).
|
|
1004
|
+
options.assertRequired();
|
|
1005
|
+
return {
|
|
1006
|
+
prNumber: prNumber,
|
|
1007
|
+
instruction: positional.join(" "),
|
|
1008
|
+
attachmentPaths,
|
|
1009
|
+
...(project === undefined ? {} : { project }),
|
|
1010
|
+
...(repo === undefined ? {} : { repo }),
|
|
1011
|
+
...(requestManifestPath === undefined ? {} : { requestManifestPath }),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Resolve owner/repo from the project's `origin` remote (github.com only).
|
|
1016
|
+
* Supports https and SSH GitHub URL shapes; never scrapes instruction prose.
|
|
1017
|
+
*/
|
|
1018
|
+
export function resolveGitHubRemoteRepository(projectRoot) {
|
|
1019
|
+
let remoteUrl;
|
|
1020
|
+
try {
|
|
1021
|
+
remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1022
|
+
cwd: projectRoot,
|
|
1023
|
+
encoding: "utf8",
|
|
1024
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1025
|
+
}).trim();
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
throw new CliUsageError("collector requires a github.com origin remote or an explicit --repo owner/repo", { cause: error });
|
|
1029
|
+
}
|
|
1030
|
+
if (remoteUrl.length === 0) {
|
|
1031
|
+
throw new CliUsageError("collector requires a github.com origin remote or an explicit --repo owner/repo");
|
|
1032
|
+
}
|
|
1033
|
+
const ownerRepo = ownerRepoFromGitHubRemoteUrl(remoteUrl);
|
|
1034
|
+
if (ownerRepo === undefined) {
|
|
1035
|
+
throw new CliUsageError(`collector origin remote must be a github.com owner/repo URL, got ${remoteUrl}`);
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
return parseCollectorRepository(ownerRepo);
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1042
|
+
throw new CliUsageError(detail, { cause: error });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function ownerRepoFromGitHubRemoteUrl(remoteUrl) {
|
|
1046
|
+
const trimmed = remoteUrl.trim();
|
|
1047
|
+
// git@github.com:owner/repo.git — exact owner/repo identity only.
|
|
1048
|
+
const scp = /^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/i.exec(trimmed);
|
|
1049
|
+
if (scp) {
|
|
1050
|
+
return `${scp[1]}/${stripGitSuffix(scp[2])}`;
|
|
1051
|
+
}
|
|
1052
|
+
// ssh://git@github.com/owner/repo(.git) — exact owner/repo identity only.
|
|
1053
|
+
const ssh = /^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i.exec(trimmed);
|
|
1054
|
+
if (ssh) {
|
|
1055
|
+
return `${ssh[1]}/${stripGitSuffix(ssh[2])}`;
|
|
1056
|
+
}
|
|
1057
|
+
// https://github.com/owner/repo(.git) and git://github.com/... — exact two-segment path.
|
|
1058
|
+
let parsed;
|
|
1059
|
+
try {
|
|
1060
|
+
parsed = new URL(trimmed);
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
return undefined;
|
|
1064
|
+
}
|
|
1065
|
+
if (!/^github\.com$/i.test(parsed.hostname))
|
|
1066
|
+
return undefined;
|
|
1067
|
+
// Non-identity URL material (query/hash/extra path) is not a repository remote.
|
|
1068
|
+
if (parsed.search !== "" || parsed.hash !== "")
|
|
1069
|
+
return undefined;
|
|
1070
|
+
const parts = parsed.pathname.split("/").filter((p) => p.length > 0);
|
|
1071
|
+
if (parts.length !== 2)
|
|
1072
|
+
return undefined;
|
|
1073
|
+
return `${parts[0]}/${stripGitSuffix(parts[1])}`;
|
|
1074
|
+
}
|
|
1075
|
+
function stripGitSuffix(name) {
|
|
1076
|
+
return name.toLowerCase().endsWith(".git") ? name.slice(0, -4) : name;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Admit a Collector Role run: assemble the retained leg manifest from typed
|
|
1080
|
+
* declarations, resolve repository identity, and place the session under #78.
|
|
1081
|
+
* Does not preflight PR/author existence against GitHub.
|
|
1082
|
+
*/
|
|
1083
|
+
export async function admitCollectorInvocation(options) {
|
|
1084
|
+
if (options.project !== undefined) {
|
|
1085
|
+
requireOptionPath("--project", options.project);
|
|
1086
|
+
}
|
|
1087
|
+
let prNumber;
|
|
1088
|
+
try {
|
|
1089
|
+
prNumber = parseCollectorPrNumber(options.prNumber);
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1093
|
+
throw new CliUsageError(detail, { cause: error });
|
|
1094
|
+
}
|
|
1095
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1096
|
+
let repository;
|
|
1097
|
+
if (options.repo !== undefined) {
|
|
1098
|
+
try {
|
|
1099
|
+
repository = parseCollectorRepository(options.repo);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1103
|
+
throw new CliUsageError(detail, { cause: error });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
repository = resolveGitHubRemoteRepository(projectRoot);
|
|
1108
|
+
}
|
|
1109
|
+
// Validate optional request-manifest before freezing request materials.
|
|
1110
|
+
let manifest = emptyCollectorManifest();
|
|
1111
|
+
let manifestCanonicalJson;
|
|
1112
|
+
if (options.requestManifestPath !== undefined) {
|
|
1113
|
+
try {
|
|
1114
|
+
manifest = await loadCollectorManifest(options.requestManifestPath);
|
|
1115
|
+
manifestCanonicalJson = manifest.canonicalJson;
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const manifestDigest = manifest.digest;
|
|
1122
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
1123
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1124
|
+
cwd: projectRoot,
|
|
1125
|
+
runId,
|
|
1126
|
+
role: "collector",
|
|
1127
|
+
home: options.home,
|
|
1128
|
+
});
|
|
1129
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
1130
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1131
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
1132
|
+
const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
|
|
1133
|
+
let requestManifestPath;
|
|
1134
|
+
if (manifestCanonicalJson !== undefined) {
|
|
1135
|
+
requestManifestPath = join(runDirectory, "request-manifest.json");
|
|
1136
|
+
await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
|
|
1137
|
+
}
|
|
1138
|
+
const instruction = options.instruction ?? "";
|
|
1139
|
+
const instructionEmpty = instruction.trim() === "";
|
|
1140
|
+
const admitted = {
|
|
1141
|
+
role: "collector",
|
|
1142
|
+
runId,
|
|
1143
|
+
bookKey,
|
|
1144
|
+
projectRoot,
|
|
1145
|
+
runDirectory,
|
|
1146
|
+
principal,
|
|
1147
|
+
instruction,
|
|
1148
|
+
instructionEmpty,
|
|
1149
|
+
prNumber,
|
|
1150
|
+
repository: repository.canonical,
|
|
1151
|
+
repositoryDisplay: repository.display,
|
|
1152
|
+
...(requestManifestPath === undefined ? {} : { requestManifestPath }),
|
|
1153
|
+
manifestDigest,
|
|
1154
|
+
attachments: attachments.map((a) => ({
|
|
1155
|
+
provenancePath: a.provenancePath,
|
|
1156
|
+
frozenPath: a.frozenPath,
|
|
1157
|
+
byteLength: a.byteLength,
|
|
1158
|
+
sha256: a.sha256,
|
|
1159
|
+
mediaKind: a.mediaKind,
|
|
1160
|
+
})),
|
|
1161
|
+
};
|
|
1162
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1163
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1164
|
+
sessionDirectory,
|
|
1165
|
+
sessionFile,
|
|
1166
|
+
});
|
|
1167
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1168
|
+
return {
|
|
1169
|
+
role: "collector",
|
|
1170
|
+
runId,
|
|
1171
|
+
bookKey,
|
|
1172
|
+
projectRoot,
|
|
1173
|
+
instruction,
|
|
1174
|
+
instructionEmpty,
|
|
1175
|
+
attachments,
|
|
1176
|
+
runDirectory,
|
|
1177
|
+
principal,
|
|
1178
|
+
admittedRequestPath,
|
|
1179
|
+
prNumber,
|
|
1180
|
+
repository,
|
|
1181
|
+
...(requestManifestPath === undefined ? {} : { requestManifestPath }),
|
|
1182
|
+
manifestDigest,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Collector always consumes the fixed packaged kickoff (one-shot observation).
|
|
1187
|
+
* Optional public instruction is retained only in the admitted request.
|
|
1188
|
+
*/
|
|
1189
|
+
export function buildCollectorTransportPrompt(_admitted, engineMaterial) {
|
|
1190
|
+
// Exact historical fixed kickoff bytes (one-shot observation).
|
|
1191
|
+
return appendEngineSessionMaterial([COLLECTOR_FIXED_KICKOFF], engineMaterial).join("\n");
|
|
1192
|
+
}
|
|
1193
|
+
/** Positive Issue number grammar shared with Doctor case path identity. */
|
|
1194
|
+
const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
1195
|
+
/** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
|
|
1196
|
+
const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/;
|
|
1197
|
+
/**
|
|
1198
|
+
* Parse a positive Issue number for public Doctor admission.
|
|
1199
|
+
* Leading zeros and non-integers are structural rejects.
|
|
1200
|
+
*/
|
|
1201
|
+
export function parseDoctorIssueNumber(raw) {
|
|
1202
|
+
const trimmed = raw.trim();
|
|
1203
|
+
if (!DOCTOR_ISSUE_NUMBER_PATTERN.test(trimmed)) {
|
|
1204
|
+
throw new CliUsageError(`doctor --issue must be a positive integer, got ${raw}`);
|
|
1205
|
+
}
|
|
1206
|
+
return Number(trimmed);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Parse Doctor-specific argv after the `doctor` token.
|
|
1210
|
+
* Requires --issue; optional confined --runs override; common --attach/--project.
|
|
1211
|
+
*/
|
|
1212
|
+
export function parseDoctorArgv(args) {
|
|
1213
|
+
// Spellings from PUBLIC_OPTION_TABLE.doctor (#342).
|
|
1214
|
+
const attachmentPaths = [];
|
|
1215
|
+
let project;
|
|
1216
|
+
let issueRaw;
|
|
1217
|
+
let runs;
|
|
1218
|
+
const positional = [];
|
|
1219
|
+
const tokens = [...args];
|
|
1220
|
+
const definitions = roleOptions("doctor");
|
|
1221
|
+
const options = createTypedOptionConsumer(definitions);
|
|
1222
|
+
while (tokens.length > 0) {
|
|
1223
|
+
if (tokens[0] === "--") {
|
|
1224
|
+
tokens.shift();
|
|
1225
|
+
positional.push(...tokens);
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
const taken = options.takeDashed(tokens);
|
|
1229
|
+
if (taken !== undefined) {
|
|
1230
|
+
if (taken.def.id === "issue") {
|
|
1231
|
+
if (taken.value === undefined || taken.value.trim() === "") {
|
|
1232
|
+
throw new CliUsageError("doctor --issue requires a positive integer");
|
|
1233
|
+
}
|
|
1234
|
+
issueRaw = taken.value;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (taken.def.id === "runs") {
|
|
1238
|
+
if (taken.value === undefined || taken.value.trim() === "") {
|
|
1239
|
+
throw new CliUsageError("doctor --runs requires a path");
|
|
1240
|
+
}
|
|
1241
|
+
runs = taken.value;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (taken.def.id === "attach") {
|
|
1245
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (taken.def.id === "project") {
|
|
1249
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
throw new CliUsageError(`unknown doctor option: ${taken.def.canonical}`);
|
|
1253
|
+
}
|
|
1254
|
+
const token = tokens.shift();
|
|
1255
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1256
|
+
throw new CliUsageError(`unknown doctor option: ${token}`);
|
|
1257
|
+
}
|
|
1258
|
+
positional.push(token);
|
|
1259
|
+
}
|
|
1260
|
+
// Unconditional required (e.g. --issue) from typed table via shared consumer (#342).
|
|
1261
|
+
options.assertRequired();
|
|
1262
|
+
const issueNumber = parseDoctorIssueNumber(issueRaw);
|
|
1263
|
+
if (runs !== undefined && runs.trim() === "") {
|
|
1264
|
+
throw new CliUsageError("doctor --runs requires a path");
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
issueNumber,
|
|
1268
|
+
instruction: positional.join(" "),
|
|
1269
|
+
attachmentPaths,
|
|
1270
|
+
...(project === undefined ? {} : { project }),
|
|
1271
|
+
...(runs === undefined ? {} : { runs }),
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Resolve the retained Doctor case runs root from Issue identity.
|
|
1276
|
+
* Default is the #78 book locator; optional --runs must stay project-confined
|
|
1277
|
+
* and match Doctor case grammar for the same issue number.
|
|
1278
|
+
*/
|
|
1279
|
+
export async function resolveDoctorCaseRunsPath(options) {
|
|
1280
|
+
const ledgerHome = resolveActivationLedgerHome(options.home);
|
|
1281
|
+
const defaultRuns = join(activationBookDirectory(ledgerHome, options.bookKey), "issues", String(options.issueNumber), "runs");
|
|
1282
|
+
if (options.runs === undefined) {
|
|
1283
|
+
return defaultRuns;
|
|
1284
|
+
}
|
|
1285
|
+
const raw = options.runs.trim();
|
|
1286
|
+
if (raw === "") {
|
|
1287
|
+
throw new CliUsageError("doctor --runs requires a path");
|
|
1288
|
+
}
|
|
1289
|
+
// Project-relative only — absolute overrides would bypass confinement.
|
|
1290
|
+
if (isAbsolute(raw)) {
|
|
1291
|
+
throw new CliUsageError("doctor --runs must be a project-relative path");
|
|
1292
|
+
}
|
|
1293
|
+
const resolved = resolve(options.projectRoot, raw);
|
|
1294
|
+
if (resolved !== options.projectRoot &&
|
|
1295
|
+
!pathContainedIn(options.projectRoot, resolved)) {
|
|
1296
|
+
throw new CliUsageError("doctor --runs escapes the project root");
|
|
1297
|
+
}
|
|
1298
|
+
let real;
|
|
1299
|
+
try {
|
|
1300
|
+
real = await realpath(resolved);
|
|
1301
|
+
}
|
|
1302
|
+
catch (error) {
|
|
1303
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1304
|
+
throw new CliUsageError(`doctor --runs is not a readable retained runs root: ${detail}`, { cause: error });
|
|
1305
|
+
}
|
|
1306
|
+
const normalized = real.split(sep).join("/");
|
|
1307
|
+
const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
|
|
1308
|
+
if (!match) {
|
|
1309
|
+
throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/issues/<n>/runs directory");
|
|
1310
|
+
}
|
|
1311
|
+
if (Number(match[1]) !== options.issueNumber) {
|
|
1312
|
+
throw new CliUsageError(`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`);
|
|
1313
|
+
}
|
|
1314
|
+
return real;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Admit a Doctor Role run: resolve Issue → retained runs root via #78 (or a
|
|
1318
|
+
* confined override), construct the structurally exact case identity through
|
|
1319
|
+
* loadDoctorCase, and place the Doctor session under the book runs lane.
|
|
1320
|
+
* Does not copy session content into a second store.
|
|
1321
|
+
*/
|
|
1322
|
+
export async function admitDoctorInvocation(options) {
|
|
1323
|
+
if (options.project !== undefined) {
|
|
1324
|
+
requireOptionPath("--project", options.project);
|
|
1325
|
+
}
|
|
1326
|
+
if (!Number.isInteger(options.issueNumber) ||
|
|
1327
|
+
options.issueNumber < 1 ||
|
|
1328
|
+
!DOCTOR_ISSUE_NUMBER_PATTERN.test(String(options.issueNumber))) {
|
|
1329
|
+
throw new CliUsageError(`doctor --issue must be a positive integer, got ${options.issueNumber}`);
|
|
1330
|
+
}
|
|
1331
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1332
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
1333
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1334
|
+
cwd: projectRoot,
|
|
1335
|
+
runId,
|
|
1336
|
+
role: "doctor",
|
|
1337
|
+
home: options.home,
|
|
1338
|
+
});
|
|
1339
|
+
let caseRunsPath;
|
|
1340
|
+
try {
|
|
1341
|
+
caseRunsPath = await resolveDoctorCaseRunsPath({
|
|
1342
|
+
home: options.home,
|
|
1343
|
+
projectRoot,
|
|
1344
|
+
bookKey,
|
|
1345
|
+
issueNumber: options.issueNumber,
|
|
1346
|
+
...(options.runs === undefined ? {} : { runs: options.runs }),
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
if (error instanceof CliUsageError)
|
|
1351
|
+
throw error;
|
|
1352
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1353
|
+
throw new CliUsageError(detail, { cause: error });
|
|
1354
|
+
}
|
|
1355
|
+
// Default #78 locator may not exist yet — ensure the empty runs root so
|
|
1356
|
+
// loadDoctorCase can form an empty case and Doctor's refusal owns insufficiency.
|
|
1357
|
+
if (options.runs === undefined) {
|
|
1358
|
+
ensureRealDirectoryTree(ledgerHome, caseRunsPath);
|
|
1359
|
+
}
|
|
1360
|
+
let caseIdentity;
|
|
1361
|
+
try {
|
|
1362
|
+
const patient = await loadDoctorCase(caseRunsPath);
|
|
1363
|
+
if (patient.identity.issueNumber !== options.issueNumber) {
|
|
1364
|
+
throw new CliUsageError(`doctor case issue ${patient.identity.issueNumber} does not match --issue ${options.issueNumber}`);
|
|
1365
|
+
}
|
|
1366
|
+
caseIdentity = patient.identity;
|
|
1367
|
+
caseRunsPath = await realpath(caseRunsPath);
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
if (error instanceof CliUsageError)
|
|
1371
|
+
throw error;
|
|
1372
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1373
|
+
throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
|
|
1374
|
+
}
|
|
1375
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
1376
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1377
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
1378
|
+
const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
|
|
1379
|
+
const instruction = options.instruction ?? "";
|
|
1380
|
+
const instructionEmpty = instruction.trim() === "";
|
|
1381
|
+
const admitted = {
|
|
1382
|
+
role: "doctor",
|
|
1383
|
+
runId,
|
|
1384
|
+
bookKey,
|
|
1385
|
+
projectRoot,
|
|
1386
|
+
runDirectory,
|
|
1387
|
+
principal,
|
|
1388
|
+
instruction,
|
|
1389
|
+
instructionEmpty,
|
|
1390
|
+
issueNumber: options.issueNumber,
|
|
1391
|
+
caseRunsPath,
|
|
1392
|
+
caseIdentity,
|
|
1393
|
+
attachments: attachments.map((a) => ({
|
|
1394
|
+
provenancePath: a.provenancePath,
|
|
1395
|
+
frozenPath: a.frozenPath,
|
|
1396
|
+
byteLength: a.byteLength,
|
|
1397
|
+
sha256: a.sha256,
|
|
1398
|
+
mediaKind: a.mediaKind,
|
|
1399
|
+
})),
|
|
1400
|
+
};
|
|
1401
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1402
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1403
|
+
sessionDirectory,
|
|
1404
|
+
sessionFile,
|
|
1405
|
+
});
|
|
1406
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1407
|
+
return {
|
|
1408
|
+
role: "doctor",
|
|
1409
|
+
runId,
|
|
1410
|
+
bookKey,
|
|
1411
|
+
projectRoot,
|
|
1412
|
+
instruction,
|
|
1413
|
+
instructionEmpty,
|
|
1414
|
+
attachments,
|
|
1415
|
+
runDirectory,
|
|
1416
|
+
principal,
|
|
1417
|
+
admittedRequestPath,
|
|
1418
|
+
issueNumber: options.issueNumber,
|
|
1419
|
+
caseRunsPath,
|
|
1420
|
+
caseIdentity,
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/** Build the Pi prompt transport for an admitted Doctor request. */
|
|
1424
|
+
export function buildDoctorTransportPrompt(admitted, engineMaterial) {
|
|
1425
|
+
const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
|
|
1426
|
+
if (admitted.attachments.length > 0) {
|
|
1427
|
+
lines.push("");
|
|
1428
|
+
lines.push("已受理附件(冻结快照路径):");
|
|
1429
|
+
for (const attachment of admitted.attachments) {
|
|
1430
|
+
lines.push(`- ${attachment.frozenPath}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Parse Notary-specific argv after the `notary` token.
|
|
1437
|
+
* Input contract = zero prompt, zero attachment projection (#448 / #276).
|
|
1438
|
+
*/
|
|
1439
|
+
export function parseNotaryArgv(args) {
|
|
1440
|
+
let project;
|
|
1441
|
+
let sourceRun;
|
|
1442
|
+
const tokens = [...args];
|
|
1443
|
+
const definitions = roleOptions("notary");
|
|
1444
|
+
const options = createTypedOptionConsumer(definitions);
|
|
1445
|
+
while (tokens.length > 0) {
|
|
1446
|
+
if (tokens[0] === "--") {
|
|
1447
|
+
tokens.shift();
|
|
1448
|
+
if (tokens.length > 0) {
|
|
1449
|
+
throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
|
|
1450
|
+
}
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
const taken = options.takeDashed(tokens);
|
|
1454
|
+
if (taken !== undefined) {
|
|
1455
|
+
if (taken.def.id === "project") {
|
|
1456
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
if (taken.def.id === "source-run") {
|
|
1460
|
+
if (taken.value === undefined || taken.value.trim() === "") {
|
|
1461
|
+
throw new CliUsageError("notary --source-run requires a run locator");
|
|
1462
|
+
}
|
|
1463
|
+
sourceRun = taken.value;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
throw new CliUsageError(`unknown notary option: ${taken.def.canonical}`);
|
|
1467
|
+
}
|
|
1468
|
+
const token = tokens.shift();
|
|
1469
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1470
|
+
throw new CliUsageError(`unknown notary option: ${token}`);
|
|
1471
|
+
}
|
|
1472
|
+
throw new CliUsageError("notary rejects caller prompt/instruction; only --source-run locator is admitted");
|
|
1473
|
+
}
|
|
1474
|
+
options.assertRequired();
|
|
1475
|
+
if (sourceRun === undefined || sourceRun.trim() === "") {
|
|
1476
|
+
throw new CliUsageError("notary --source-run requires a run locator");
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
sourceRun,
|
|
1480
|
+
...(project === undefined ? {} : { project }),
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
export async function admitNotaryInvocation(options) {
|
|
1484
|
+
if (options.project !== undefined) {
|
|
1485
|
+
requireOptionPath("--project", options.project);
|
|
1486
|
+
}
|
|
1487
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1488
|
+
let sourceRun;
|
|
1489
|
+
try {
|
|
1490
|
+
sourceRun = await resolveNotarySourceRunLocator({
|
|
1491
|
+
projectRoot,
|
|
1492
|
+
sourceRun: options.sourceRun,
|
|
1493
|
+
home: options.home,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
if (error instanceof NotarySourceRunError) {
|
|
1498
|
+
throw new CliUsageError(error.message, { cause: error });
|
|
1499
|
+
}
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
const runId = options.createRunId?.() ?? uuidv7();
|
|
1503
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1504
|
+
cwd: projectRoot,
|
|
1505
|
+
runId,
|
|
1506
|
+
role: "notary",
|
|
1507
|
+
home: options.home,
|
|
1508
|
+
});
|
|
1509
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1510
|
+
const ticketFields = ticketAdmissionFields(await readRunTicketNumber(sourceRun.runDirectory));
|
|
1511
|
+
const admitted = {
|
|
1512
|
+
role: "notary",
|
|
1513
|
+
runId,
|
|
1514
|
+
bookKey,
|
|
1515
|
+
projectRoot,
|
|
1516
|
+
runDirectory,
|
|
1517
|
+
principal,
|
|
1518
|
+
instruction: "",
|
|
1519
|
+
instructionEmpty: true,
|
|
1520
|
+
attachments: [],
|
|
1521
|
+
sourceRunPath: sourceRun.runDirectory,
|
|
1522
|
+
sourceRun,
|
|
1523
|
+
...ticketFields,
|
|
1524
|
+
...(options.correlationId === undefined
|
|
1525
|
+
? {}
|
|
1526
|
+
: { correlationId: options.correlationId }),
|
|
1527
|
+
};
|
|
1528
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1529
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1530
|
+
sessionDirectory,
|
|
1531
|
+
sessionFile,
|
|
1532
|
+
});
|
|
1533
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1534
|
+
return {
|
|
1535
|
+
role: "notary",
|
|
1536
|
+
runId,
|
|
1537
|
+
bookKey,
|
|
1538
|
+
projectRoot,
|
|
1539
|
+
instruction: "",
|
|
1540
|
+
instructionEmpty: true,
|
|
1541
|
+
attachments: [],
|
|
1542
|
+
runDirectory,
|
|
1543
|
+
principal,
|
|
1544
|
+
admittedRequestPath,
|
|
1545
|
+
sourceRunPath: sourceRun.runDirectory,
|
|
1546
|
+
sourceRun,
|
|
1547
|
+
...ticketFields,
|
|
1548
|
+
...(options.correlationId === undefined
|
|
1549
|
+
? {}
|
|
1550
|
+
: { correlationId: options.correlationId }),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/** Package-owned fixed kickoff only — never caller instruction/attachments.
|
|
1554
|
+
* Ticket rides admitted → activation → agent-start typed bound (not free-text kickoff). */
|
|
1555
|
+
export function buildNotaryTransportPrompt(_admitted, engineMaterial) {
|
|
1556
|
+
return appendEngineSessionMaterial([NOTARY_FIXED_KICKOFF], engineMaterial).join("\n");
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Parse Reviewer-specific argv after the `reviewer` token.
|
|
1560
|
+
* Public flags: --project, required --base, optional repeatable --authority-ref.
|
|
1561
|
+
* Reviewer gathers its own evidence; users submit neither attachments nor capability packets.
|
|
1562
|
+
* Caller instruction remains scope/procedure provenance — not Spec authority.
|
|
1563
|
+
*/
|
|
1564
|
+
/**
|
|
1565
|
+
* Parse Gleaner-Left argv after the `gleaner-left` token.
|
|
1566
|
+
* Public flags: --project, required --base. No --attach / ticket face (unanchored self-fetch).
|
|
1567
|
+
* Instruction may be empty; callers must not pass directional instruction.
|
|
1568
|
+
*/
|
|
1569
|
+
export function parseGleanerLeftArgv(args) {
|
|
1570
|
+
let project;
|
|
1571
|
+
let baseRevision;
|
|
1572
|
+
const positional = [];
|
|
1573
|
+
const tokens = [...args];
|
|
1574
|
+
const definitions = roleOptions("gleaner-left");
|
|
1575
|
+
const options = createTypedOptionConsumer(definitions);
|
|
1576
|
+
while (tokens.length > 0) {
|
|
1577
|
+
if (tokens[0] === "--") {
|
|
1578
|
+
tokens.shift();
|
|
1579
|
+
positional.push(...tokens);
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
const taken = options.takeDashed(tokens);
|
|
1583
|
+
if (taken !== undefined) {
|
|
1584
|
+
if (taken.def.id === "project") {
|
|
1585
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (taken.def.id === "base") {
|
|
1589
|
+
baseRevision = requireOptionPath(taken.def.canonical, taken.value);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
throw new CliUsageError(`unknown gleaner-left option: ${taken.def.canonical}`);
|
|
1593
|
+
}
|
|
1594
|
+
const token = tokens.shift();
|
|
1595
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1596
|
+
throw new CliUsageError(`unknown gleaner-left option: ${token}`);
|
|
1597
|
+
}
|
|
1598
|
+
positional.push(token);
|
|
1599
|
+
}
|
|
1600
|
+
options.assertRequired();
|
|
1601
|
+
return {
|
|
1602
|
+
instruction: positional.join(" "),
|
|
1603
|
+
baseRevision: baseRevision,
|
|
1604
|
+
...(project === undefined ? {} : { project }),
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Admit a Gleaner-Left Role run on the fixed comparison base.
|
|
1609
|
+
* Empty instruction is the lawful path; no attachment/ticket admission face.
|
|
1610
|
+
*/
|
|
1611
|
+
export async function admitGleanerLeftInvocation(options) {
|
|
1612
|
+
if (options.project !== undefined) {
|
|
1613
|
+
requireOptionPath("--project", options.project);
|
|
1614
|
+
}
|
|
1615
|
+
if (options.baseRevision.trim() === "") {
|
|
1616
|
+
throw new CliUsageError("--base requires a nonempty revision");
|
|
1617
|
+
}
|
|
1618
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1619
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
1620
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1621
|
+
cwd: projectRoot,
|
|
1622
|
+
runId,
|
|
1623
|
+
role: "gleaner-left",
|
|
1624
|
+
home: options.home,
|
|
1625
|
+
});
|
|
1626
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1627
|
+
const instruction = options.instruction;
|
|
1628
|
+
const instructionEmpty = instruction.trim() === "";
|
|
1629
|
+
const admitted = {
|
|
1630
|
+
role: "gleaner-left",
|
|
1631
|
+
runId,
|
|
1632
|
+
bookKey,
|
|
1633
|
+
projectRoot,
|
|
1634
|
+
runDirectory,
|
|
1635
|
+
principal,
|
|
1636
|
+
instruction,
|
|
1637
|
+
instructionEmpty,
|
|
1638
|
+
baseRevision: options.baseRevision,
|
|
1639
|
+
attachments: [],
|
|
1640
|
+
...(options.correlationId === undefined
|
|
1641
|
+
? {}
|
|
1642
|
+
: { correlationId: options.correlationId }),
|
|
1643
|
+
};
|
|
1644
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1645
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1646
|
+
sessionDirectory,
|
|
1647
|
+
sessionFile,
|
|
1648
|
+
});
|
|
1649
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1650
|
+
return {
|
|
1651
|
+
role: "gleaner-left",
|
|
1652
|
+
runId,
|
|
1653
|
+
bookKey,
|
|
1654
|
+
projectRoot,
|
|
1655
|
+
instruction,
|
|
1656
|
+
instructionEmpty,
|
|
1657
|
+
attachments: [],
|
|
1658
|
+
runDirectory,
|
|
1659
|
+
principal,
|
|
1660
|
+
admittedRequestPath,
|
|
1661
|
+
baseRevision: options.baseRevision,
|
|
1662
|
+
...(options.correlationId === undefined
|
|
1663
|
+
? {}
|
|
1664
|
+
: { correlationId: options.correlationId }),
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/** Bound comparison base is a typed fact, not a directional instruction. */
|
|
1668
|
+
export function buildGleanerLeftTransportPrompt(admitted, engineMaterial) {
|
|
1669
|
+
const lines = [
|
|
1670
|
+
`左拾遗案已受理。比较基线:${admitted.baseRevision}`,
|
|
1671
|
+
];
|
|
1672
|
+
if (!admitted.instructionEmpty) {
|
|
1673
|
+
lines.push("", admitted.instruction);
|
|
1674
|
+
}
|
|
1675
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
1676
|
+
}
|
|
1677
|
+
export function parseReviewerArgv(args) {
|
|
1678
|
+
// Spellings from PUBLIC_OPTION_TABLE.reviewer (#342). No --attach face.
|
|
1679
|
+
const attachmentPaths = [];
|
|
1680
|
+
const authorityRefs = [];
|
|
1681
|
+
let project;
|
|
1682
|
+
let baseRevision;
|
|
1683
|
+
const positional = [];
|
|
1684
|
+
const tokens = [...args];
|
|
1685
|
+
const definitions = roleOptions("reviewer");
|
|
1686
|
+
const options = createTypedOptionConsumer(definitions);
|
|
1687
|
+
while (tokens.length > 0) {
|
|
1688
|
+
if (tokens[0] === "--") {
|
|
1689
|
+
tokens.shift();
|
|
1690
|
+
positional.push(...tokens);
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
const taken = options.takeDashed(tokens);
|
|
1694
|
+
if (taken !== undefined) {
|
|
1695
|
+
if (taken.def.id === "project") {
|
|
1696
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (taken.def.id === "base") {
|
|
1700
|
+
baseRevision = requireOptionPath(taken.def.canonical, taken.value);
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
if (taken.def.id === "authority-ref") {
|
|
1704
|
+
authorityRefs.push(requireAuthorityRef(taken.value));
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
throw new CliUsageError(`unknown reviewer option: ${taken.def.canonical}`);
|
|
1708
|
+
}
|
|
1709
|
+
const token = tokens.shift();
|
|
1710
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1711
|
+
throw new CliUsageError(`unknown reviewer option: ${token}`);
|
|
1712
|
+
}
|
|
1713
|
+
positional.push(token);
|
|
1714
|
+
}
|
|
1715
|
+
// Unconditional required (e.g. --base) from typed table via shared consumer (#342).
|
|
1716
|
+
options.assertRequired();
|
|
1717
|
+
return {
|
|
1718
|
+
instruction: positional.join(" "),
|
|
1719
|
+
attachmentPaths,
|
|
1720
|
+
baseRevision: baseRevision,
|
|
1721
|
+
authorityRefs,
|
|
1722
|
+
...(project === undefined ? {} : { project }),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Admit a Reviewer Role run on the fixed base only.
|
|
1727
|
+
* Caller instruction is optional provenance; Reviewer acquires issue/authority independently.
|
|
1728
|
+
* Optional authorityRefs are frozen as durable references only — not Spec prose.
|
|
1729
|
+
*/
|
|
1730
|
+
export async function admitReviewerInvocation(options) {
|
|
1731
|
+
if (options.project !== undefined) {
|
|
1732
|
+
requireOptionPath("--project", options.project);
|
|
1733
|
+
}
|
|
1734
|
+
if (options.baseRevision.trim() === "") {
|
|
1735
|
+
throw new CliUsageError("--base requires a nonempty revision");
|
|
1736
|
+
}
|
|
1737
|
+
const authorityRefs = Object.freeze((options.authorityRefs ?? []).map((ref) => requireAuthorityRef(ref)));
|
|
1738
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1739
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
1740
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1741
|
+
cwd: projectRoot,
|
|
1742
|
+
runId,
|
|
1743
|
+
role: "reviewer",
|
|
1744
|
+
home: options.home,
|
|
1745
|
+
});
|
|
1746
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
1747
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1748
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
1749
|
+
// Public parse already rejects attachments; keep freeze loop for structural symmetry.
|
|
1750
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
1751
|
+
const instruction = options.instruction;
|
|
1752
|
+
const instructionEmpty = instruction.trim() === "";
|
|
1753
|
+
const admitted = {
|
|
1754
|
+
role: "reviewer",
|
|
1755
|
+
runId,
|
|
1756
|
+
bookKey,
|
|
1757
|
+
projectRoot,
|
|
1758
|
+
runDirectory,
|
|
1759
|
+
principal,
|
|
1760
|
+
instruction,
|
|
1761
|
+
instructionEmpty,
|
|
1762
|
+
baseRevision: options.baseRevision,
|
|
1763
|
+
authorityRefs: [...authorityRefs],
|
|
1764
|
+
attachments: attachments.map((a) => ({
|
|
1765
|
+
provenancePath: a.provenancePath,
|
|
1766
|
+
frozenPath: a.frozenPath,
|
|
1767
|
+
byteLength: a.byteLength,
|
|
1768
|
+
sha256: a.sha256,
|
|
1769
|
+
mediaKind: a.mediaKind,
|
|
1770
|
+
})),
|
|
1771
|
+
};
|
|
1772
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1773
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1774
|
+
sessionDirectory,
|
|
1775
|
+
sessionFile,
|
|
1776
|
+
});
|
|
1777
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1778
|
+
return {
|
|
1779
|
+
role: "reviewer",
|
|
1780
|
+
runId,
|
|
1781
|
+
bookKey,
|
|
1782
|
+
projectRoot,
|
|
1783
|
+
instruction,
|
|
1784
|
+
instructionEmpty,
|
|
1785
|
+
attachments,
|
|
1786
|
+
runDirectory,
|
|
1787
|
+
principal,
|
|
1788
|
+
admittedRequestPath,
|
|
1789
|
+
baseRevision: options.baseRevision,
|
|
1790
|
+
authorityRefs,
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Build the Pi prompt transport for an admitted Reviewer request.
|
|
1795
|
+
* Semantic input is fixed base only — caller instruction stays provenance on disk.
|
|
1796
|
+
* Optional engine material follows the same dual-path coordinates as Judge (#376/#378).
|
|
1797
|
+
*/
|
|
1798
|
+
export function buildReviewerTransportPrompt(admitted, engineMaterial) {
|
|
1799
|
+
const lines = [
|
|
1800
|
+
`本次审查的固定基点:${admitted.baseRevision}`,
|
|
1801
|
+
];
|
|
1802
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Parse Merger-specific argv after the `merger` token.
|
|
1806
|
+
* Spellings from PUBLIC_OPTION_TABLE.merger; internal packet fields rejected (#342).
|
|
1807
|
+
*/
|
|
1808
|
+
export function parseMergerArgv(args) {
|
|
1809
|
+
const attachmentPaths = [];
|
|
1810
|
+
let project;
|
|
1811
|
+
const positional = [];
|
|
1812
|
+
const tokens = [...args];
|
|
1813
|
+
const definitions = roleOptions("merger");
|
|
1814
|
+
const options = createTypedOptionConsumer(definitions);
|
|
1815
|
+
while (tokens.length > 0) {
|
|
1816
|
+
if (tokens[0] === "--") {
|
|
1817
|
+
tokens.shift();
|
|
1818
|
+
positional.push(...tokens);
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
const taken = options.takeDashed(tokens);
|
|
1822
|
+
if (taken !== undefined) {
|
|
1823
|
+
if (taken.def.id === "attach") {
|
|
1824
|
+
attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (taken.def.id === "project") {
|
|
1828
|
+
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
throw new CliUsageError(`unknown merger option: ${taken.def.canonical}`);
|
|
1832
|
+
}
|
|
1833
|
+
const token = tokens.shift();
|
|
1834
|
+
// Rejected public spellings (#342) plus other internal packet field faces.
|
|
1835
|
+
if (isRejectedPublicSpelling("merger", token) ||
|
|
1836
|
+
token === "--targetObjectId" ||
|
|
1837
|
+
token.startsWith("--targetObjectId=") ||
|
|
1838
|
+
token === "--sourceObjectId" ||
|
|
1839
|
+
token.startsWith("--sourceObjectId=") ||
|
|
1840
|
+
token === "--expectedConflictPaths" ||
|
|
1841
|
+
token.startsWith("--expectedConflictPaths=") ||
|
|
1842
|
+
token === "--resolutionScope" ||
|
|
1843
|
+
token.startsWith("--resolutionScope=")) {
|
|
1844
|
+
throw new CliUsageError("merger does not accept public packet fields; the adapter derives the active-merge envelope");
|
|
1845
|
+
}
|
|
1846
|
+
if (token.startsWith("-") && token !== "-") {
|
|
1847
|
+
throw new CliUsageError(`unknown merger option: ${token}`);
|
|
1848
|
+
}
|
|
1849
|
+
positional.push(token);
|
|
1850
|
+
}
|
|
1851
|
+
options.assertRequired();
|
|
1852
|
+
return {
|
|
1853
|
+
instruction: positional.join(" "),
|
|
1854
|
+
attachmentPaths,
|
|
1855
|
+
...(project === undefined ? {} : { project }),
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
function mergerMaterialFromUtf8(text) {
|
|
1859
|
+
const bytes = Buffer.from(text, "utf8");
|
|
1860
|
+
return Object.freeze({
|
|
1861
|
+
bytesBase64: bytes.toString("base64"),
|
|
1862
|
+
sha256: sha256Hex(bytes),
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Derive the mechanical Merger envelope from an already-active ordinary merge.
|
|
1867
|
+
* Uses the production Git seam (HEAD, sole MERGE_HEAD, AUTO_MERGE, unmerged set).
|
|
1868
|
+
* Failures are activation-class facts — not CLI semantic guesses.
|
|
1869
|
+
*/
|
|
1870
|
+
export async function deriveMergerEnvelopeFromActiveMerge(projectRoot, gitState = createProductionMergerGitState(projectRoot)) {
|
|
1871
|
+
let state;
|
|
1872
|
+
try {
|
|
1873
|
+
state = await gitState.activeMerge();
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
const message = error instanceof Error && error.message.trim() !== ""
|
|
1877
|
+
? error.message
|
|
1878
|
+
: "Assigned repository does not have one ordinary in-progress merge";
|
|
1879
|
+
throw new MergerEnvelopeDerivationError(message, { cause: error });
|
|
1880
|
+
}
|
|
1881
|
+
if (state.unmergedPaths.length === 0) {
|
|
1882
|
+
throw new MergerEnvelopeDerivationError("Assigned repository does not have one ordinary in-progress merge with a complete conflict set");
|
|
1883
|
+
}
|
|
1884
|
+
const expectedConflictPaths = Object.freeze([...state.unmergedPaths]);
|
|
1885
|
+
// Scope is derived as the complete conflict set; the role may not broaden it.
|
|
1886
|
+
const resolutionScope = Object.freeze([...state.unmergedPaths]);
|
|
1887
|
+
return Object.freeze({
|
|
1888
|
+
targetObjectId: state.targetObjectId,
|
|
1889
|
+
sourceObjectId: state.sourceObjectId,
|
|
1890
|
+
automaticMergeTreeId: state.automaticMergeTreeId,
|
|
1891
|
+
expectedConflictPaths,
|
|
1892
|
+
resolutionScope,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Admit a Merger Role run on the common Invocation request.
|
|
1897
|
+
* Mechanical envelope (parents, AUTO_MERGE, conflicts, scope) is derived from
|
|
1898
|
+
* the active merge — callers never supply public packet fields for those facts.
|
|
1899
|
+
*/
|
|
1900
|
+
export async function admitMergerInvocation(options) {
|
|
1901
|
+
if (options.project !== undefined) {
|
|
1902
|
+
requireOptionPath("--project", options.project);
|
|
1903
|
+
}
|
|
1904
|
+
const instruction = options.instruction;
|
|
1905
|
+
if (instruction.trim() === "") {
|
|
1906
|
+
throw new CliUsageError("merger requires a nonblank task instruction");
|
|
1907
|
+
}
|
|
1908
|
+
const projectRoot = resolve(options.project ?? options.cwd);
|
|
1909
|
+
// Derive mechanical envelope before placing a run identity so no-merge/drift
|
|
1910
|
+
// fails honestly without orphan ledger rows or guessed packet fields.
|
|
1911
|
+
const derived = await deriveMergerEnvelopeFromActiveMerge(projectRoot, options.gitState ?? createProductionMergerGitState(projectRoot));
|
|
1912
|
+
const runId = (options.createRunId ?? uuidv7)();
|
|
1913
|
+
const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
|
|
1914
|
+
cwd: projectRoot,
|
|
1915
|
+
runId,
|
|
1916
|
+
role: "merger",
|
|
1917
|
+
home: options.home,
|
|
1918
|
+
});
|
|
1919
|
+
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
1920
|
+
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
1921
|
+
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
1922
|
+
const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
|
|
1923
|
+
// Intent materials seed primary-source investigation; the method owns the work.
|
|
1924
|
+
const targetIntent = mergerMaterialFromUtf8(`Investigate primary sources for target parent ${derived.targetObjectId}. Do not invent intent.`);
|
|
1925
|
+
const sourceIntent = mergerMaterialFromUtf8(`Investigate primary sources for source parent ${derived.sourceObjectId}. Do not invent intent.`);
|
|
1926
|
+
const taskMaterial = mergerMaterialFromUtf8(instruction);
|
|
1927
|
+
const authorityMaterial = mergerMaterialFromUtf8(instruction);
|
|
1928
|
+
// Validate merger envelope before placing admitted identity.
|
|
1929
|
+
const mergerInput = validateMergerInput({
|
|
1930
|
+
version: 1,
|
|
1931
|
+
attemptId: runId,
|
|
1932
|
+
targetObjectId: derived.targetObjectId,
|
|
1933
|
+
sourceObjectId: derived.sourceObjectId,
|
|
1934
|
+
materials: {
|
|
1935
|
+
task: taskMaterial,
|
|
1936
|
+
authority: authorityMaterial,
|
|
1937
|
+
targetIntent,
|
|
1938
|
+
sourceIntent,
|
|
1939
|
+
},
|
|
1940
|
+
expectedConflictPaths: [...derived.expectedConflictPaths],
|
|
1941
|
+
resolutionScope: [...derived.resolutionScope],
|
|
1942
|
+
// Authorized checks remain available on the assignment; default none.
|
|
1943
|
+
authorizedChecks: [],
|
|
1944
|
+
});
|
|
1945
|
+
const mergerInputPath = join(runDirectory, "merger-input.json");
|
|
1946
|
+
await writeFile(mergerInputPath, `${JSON.stringify(mergerInput, null, 2)}\n`, "utf8");
|
|
1947
|
+
const admitted = {
|
|
1948
|
+
role: "merger",
|
|
1949
|
+
runId,
|
|
1950
|
+
bookKey,
|
|
1951
|
+
projectRoot,
|
|
1952
|
+
runDirectory,
|
|
1953
|
+
principal,
|
|
1954
|
+
instruction,
|
|
1955
|
+
instructionEmpty: false,
|
|
1956
|
+
mergerInputPath,
|
|
1957
|
+
derived: {
|
|
1958
|
+
targetObjectId: derived.targetObjectId,
|
|
1959
|
+
sourceObjectId: derived.sourceObjectId,
|
|
1960
|
+
automaticMergeTreeId: derived.automaticMergeTreeId,
|
|
1961
|
+
expectedConflictPaths: [...derived.expectedConflictPaths],
|
|
1962
|
+
resolutionScope: [...derived.resolutionScope],
|
|
1963
|
+
},
|
|
1964
|
+
attachments: attachments.map((a) => ({
|
|
1965
|
+
provenancePath: a.provenancePath,
|
|
1966
|
+
frozenPath: a.frozenPath,
|
|
1967
|
+
byteLength: a.byteLength,
|
|
1968
|
+
sha256: a.sha256,
|
|
1969
|
+
mediaKind: a.mediaKind,
|
|
1970
|
+
})),
|
|
1971
|
+
};
|
|
1972
|
+
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
1973
|
+
await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
|
|
1974
|
+
sessionDirectory,
|
|
1975
|
+
sessionFile,
|
|
1976
|
+
});
|
|
1977
|
+
await writeRoleInvocationLedger({ ...admitted, sessionDirectory, sessionFile }, admitted.role, options.model);
|
|
1978
|
+
return {
|
|
1979
|
+
role: "merger",
|
|
1980
|
+
runId,
|
|
1981
|
+
bookKey,
|
|
1982
|
+
projectRoot,
|
|
1983
|
+
instruction,
|
|
1984
|
+
instructionEmpty: false,
|
|
1985
|
+
attachments,
|
|
1986
|
+
runDirectory,
|
|
1987
|
+
principal,
|
|
1988
|
+
admittedRequestPath,
|
|
1989
|
+
mergerInputPath,
|
|
1990
|
+
derived: admitted.derived,
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Build the Pi prompt transport for an admitted Merger request.
|
|
1995
|
+
* Every invocation forces package merge-only method expansion before conflict work.
|
|
1996
|
+
*/
|
|
1997
|
+
export function buildMergerTransportPrompt(admitted, engineMaterial) {
|
|
1998
|
+
const lines = [
|
|
1999
|
+
`/skill:resolving-merge-conflicts ${admitted.instruction}`,
|
|
2000
|
+
];
|
|
2001
|
+
if (admitted.attachments.length > 0) {
|
|
2002
|
+
lines.push("");
|
|
2003
|
+
lines.push("已受理附件(冻结快照路径):");
|
|
2004
|
+
for (const attachment of admitted.attachments) {
|
|
2005
|
+
lines.push(`- ${attachment.frozenPath}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
2009
|
+
}
|
|
2010
|
+
const ANALYST_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
2011
|
+
/**
|
|
2012
|
+
* Parse a positive ticket / issue number for public analyst admission.
|
|
2013
|
+
* Leading zeros and non-integers are structural rejects (same face as #176).
|
|
2014
|
+
* `flag` names the actual argv face in diagnostics (cohort group lists reuse this).
|
|
2015
|
+
*/
|
|
2016
|
+
export function parseAnalystTicketNumber(raw, flag = "--ticket") {
|
|
2017
|
+
const trimmed = raw.trim();
|
|
2018
|
+
if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
|
|
2019
|
+
throw new CliUsageError(`analyst ${flag} must be a positive integer, got ${raw}`);
|
|
2020
|
+
}
|
|
2021
|
+
const value = Number(trimmed);
|
|
2022
|
+
// Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
|
|
2023
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
2024
|
+
throw new CliUsageError(`analyst ${flag} must be a positive integer, got ${raw}`);
|
|
2025
|
+
}
|
|
2026
|
+
return value;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Parse one cohort issue token: bare positive integer or `book:N`.
|
|
2030
|
+
* Book keys may contain ":" (e.g. synthetic `root:<path>`) — split on the last
|
|
2031
|
+
* colon only when the RHS is a positive integer token.
|
|
2032
|
+
*/
|
|
2033
|
+
export function parseAnalystCohortIssueToken(raw, flag) {
|
|
2034
|
+
const trimmed = raw.trim();
|
|
2035
|
+
if (trimmed === "") {
|
|
2036
|
+
throw new CliUsageError(`${flag} requires a comma-separated list of N or book:N`);
|
|
2037
|
+
}
|
|
2038
|
+
const sep = trimmed.lastIndexOf(":");
|
|
2039
|
+
if (sep > 0) {
|
|
2040
|
+
const rhs = trimmed.slice(sep + 1);
|
|
2041
|
+
if (ANALYST_TICKET_NUMBER_PATTERN.test(rhs)) {
|
|
2042
|
+
const bookKey = trimmed.slice(0, sep);
|
|
2043
|
+
if (bookKey.trim() === "") {
|
|
2044
|
+
throw new CliUsageError(`${flag} book:N requires a non-empty book key, got ${raw}`);
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
kind: "book-qualified",
|
|
2048
|
+
bookKey,
|
|
2049
|
+
issueNumber: parseAnalystTicketNumber(rhs, flag),
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return {
|
|
2054
|
+
kind: "bare",
|
|
2055
|
+
issueNumber: parseAnalystTicketNumber(trimmed, flag),
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Sole cohort list grammar (#412): split on unescaped commas. `\,` is a literal
|
|
2060
|
+
* comma and `\\` a literal backslash — both round-trip, so any directory-name
|
|
2061
|
+
* book key (ADR 0048) is expressible. Any other `\x` stays literally `\x`, so
|
|
2062
|
+
* pre-existing unescaped input never changes meaning. Colons remain owned by
|
|
2063
|
+
* the token's lastIndexOf(':') rule.
|
|
2064
|
+
*/
|
|
2065
|
+
function splitAnalystCohortIssueListParts(raw) {
|
|
2066
|
+
const parts = [];
|
|
2067
|
+
let current = "";
|
|
2068
|
+
let escaped = false;
|
|
2069
|
+
for (const ch of raw) {
|
|
2070
|
+
if (escaped) {
|
|
2071
|
+
current += ch === "," || ch === "\\" ? ch : `\\${ch}`;
|
|
2072
|
+
escaped = false;
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
if (ch === "\\") {
|
|
2076
|
+
escaped = true;
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (ch === ",") {
|
|
2080
|
+
parts.push(current);
|
|
2081
|
+
current = "";
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
current += ch;
|
|
2085
|
+
}
|
|
2086
|
+
parts.push(escaped ? `${current}\\` : current);
|
|
2087
|
+
return parts;
|
|
2088
|
+
}
|
|
2089
|
+
function parseAnalystCohortIssueTokenList(raw, flag) {
|
|
2090
|
+
const trimmed = raw.trim();
|
|
2091
|
+
if (trimmed === "") {
|
|
2092
|
+
throw new CliUsageError(`${flag} requires a comma-separated list of N or book:N`);
|
|
2093
|
+
}
|
|
2094
|
+
const parts = splitAnalystCohortIssueListParts(trimmed).map((part) => part.trim());
|
|
2095
|
+
if (parts.some((part) => part === "")) {
|
|
2096
|
+
throw new CliUsageError(`${flag} requires a comma-separated list of N or book:N`);
|
|
2097
|
+
}
|
|
2098
|
+
return parts.map((part) => parseAnalystCohortIssueToken(part, flag));
|
|
2099
|
+
}
|
|
2100
|
+
function requireOptionValue(flag, value, what) {
|
|
2101
|
+
if (value === undefined || value.trim() === "") {
|
|
2102
|
+
throw new CliUsageError(`${flag} requires ${what}`);
|
|
2103
|
+
}
|
|
2104
|
+
return value;
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Parse analyst-specific argv after the `analyst` token (#336/#337/#338).
|
|
2108
|
+
* Spellings + mode relation contracts from PUBLIC_OPTION_TABLE.analyst / ANALYST_* (#342).
|
|
2109
|
+
* Mode exclusion, conditional requiredness, cardinality, and at-least-one are
|
|
2110
|
+
* table-driven — do not restate them as parallel handwritten branches here.
|
|
2111
|
+
* Unconditional required:true also goes through the shared consumer.
|
|
2112
|
+
*/
|
|
2113
|
+
export function parseAnalystArgv(args) {
|
|
2114
|
+
const valueLists = new Map();
|
|
2115
|
+
const tokens = [...args];
|
|
2116
|
+
const definitions = roleOptions("analyst");
|
|
2117
|
+
// Shared typed consumer: dashed + positional take, repeatable, required (#342).
|
|
2118
|
+
const options = createTypedOptionConsumer(definitions);
|
|
2119
|
+
const pushValue = (id, value) => {
|
|
2120
|
+
const existing = valueLists.get(id);
|
|
2121
|
+
if (existing === undefined)
|
|
2122
|
+
valueLists.set(id, [value]);
|
|
2123
|
+
else
|
|
2124
|
+
existing.push(value);
|
|
2125
|
+
};
|
|
2126
|
+
while (tokens.length > 0) {
|
|
2127
|
+
if (tokens[0] === "--") {
|
|
2128
|
+
tokens.shift();
|
|
2129
|
+
if (tokens.length > 0) {
|
|
2130
|
+
throw new CliUsageError(`unexpected analyst argument: ${tokens[0]}`);
|
|
2131
|
+
}
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
const taken = options.takeDashed(tokens);
|
|
2135
|
+
if (taken !== undefined) {
|
|
2136
|
+
if (taken.def.valueMetavar === null) {
|
|
2137
|
+
pushValue(taken.def.id, "");
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
if (taken.def.id === "ticket") {
|
|
2141
|
+
if (taken.value === undefined || taken.value.trim() === "") {
|
|
2142
|
+
throw new CliUsageError("analyst --ticket requires a positive integer");
|
|
2143
|
+
}
|
|
2144
|
+
pushValue("ticket", taken.value);
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
if (taken.def.id === "attach") {
|
|
2148
|
+
pushValue("attach", requireOptionPath(taken.def.canonical, taken.value));
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
if (taken.def.id === "group-a-label" || taken.def.id === "group-b-label") {
|
|
2152
|
+
pushValue(taken.def.id, requireOptionValue(taken.def.canonical, taken.value, "a label"));
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (taken.def.id === "group-a-issues" || taken.def.id === "group-b-issues") {
|
|
2156
|
+
pushValue(taken.def.id, requireOptionValue(taken.def.canonical, taken.value, "a comma-separated list of N or book:N"));
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
throw new CliUsageError(`unknown analyst option: ${taken.def.canonical}`);
|
|
2160
|
+
}
|
|
2161
|
+
const token = tokens.shift();
|
|
2162
|
+
// #399: deleted --project-root; disabled --model-groups public face.
|
|
2163
|
+
if (isRejectedPublicSpelling("analyst", token)) {
|
|
2164
|
+
if (token === "--project-root" || token.startsWith("--project-root=")) {
|
|
2165
|
+
throw new CliUsageError("analyst no longer accepts --project-root (deleted); use bare call for whole book or --ticket N (cwd git common-dir selects the book)");
|
|
2166
|
+
}
|
|
2167
|
+
if (token === "--model-groups" || token.startsWith("--model-groups=")) {
|
|
2168
|
+
throw new CliUsageError("analyst --model-groups public CLI face is disabled; input face is being redesigned for multi-issue comparison (see follow-up ticket)");
|
|
2169
|
+
}
|
|
2170
|
+
throw new CliUsageError(`unknown analyst option: ${token}`);
|
|
2171
|
+
}
|
|
2172
|
+
if (token.startsWith("-") && token !== "-") {
|
|
2173
|
+
throw new CliUsageError(`unknown analyst option: ${token}`);
|
|
2174
|
+
}
|
|
2175
|
+
// Positional selectors (e.g. sweep) via shared typed consumer — not a parallel list.
|
|
2176
|
+
const positional = options.takePositional(token);
|
|
2177
|
+
if (positional !== undefined) {
|
|
2178
|
+
pushValue(positional.id, "");
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
throw new CliUsageError(`unexpected analyst argument: ${token}`);
|
|
2182
|
+
}
|
|
2183
|
+
const counts = new Map();
|
|
2184
|
+
for (const [id, values] of valueLists) {
|
|
2185
|
+
counts.set(id, values.length);
|
|
2186
|
+
}
|
|
2187
|
+
options.assertRequired();
|
|
2188
|
+
const mode = resolveAnalystMode(new Set(counts.keys()));
|
|
2189
|
+
const verdict = evaluateAnalystModeOptionContract(mode, counts);
|
|
2190
|
+
if (!verdict.ok) {
|
|
2191
|
+
throw new CliUsageError(verdict.message);
|
|
2192
|
+
}
|
|
2193
|
+
if (mode === "cohort") {
|
|
2194
|
+
const groupALabel = valueLists.get("group-a-label")[0];
|
|
2195
|
+
const groupAIssuesRaw = valueLists.get("group-a-issues")[0];
|
|
2196
|
+
const groupBLabel = valueLists.get("group-b-label")[0];
|
|
2197
|
+
const groupBIssuesRaw = valueLists.get("group-b-issues")[0];
|
|
2198
|
+
return {
|
|
2199
|
+
query: "cohort",
|
|
2200
|
+
groups: [
|
|
2201
|
+
{
|
|
2202
|
+
groupLabel: groupALabel,
|
|
2203
|
+
issues: parseAnalystCohortIssueTokenList(groupAIssuesRaw, "--group-a-issues"),
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
groupLabel: groupBLabel,
|
|
2207
|
+
issues: parseAnalystCohortIssueTokenList(groupBIssuesRaw, "--group-b-issues"),
|
|
2208
|
+
},
|
|
2209
|
+
],
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
if (mode === "sweep") {
|
|
2213
|
+
return {
|
|
2214
|
+
query: "sweep",
|
|
2215
|
+
attachmentPaths: valueLists.get("attach") ?? [],
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const ticketRaw = valueLists.get("ticket")?.[0];
|
|
2219
|
+
return {
|
|
2220
|
+
query: "issue",
|
|
2221
|
+
...(ticketRaw === undefined
|
|
2222
|
+
? {}
|
|
2223
|
+
: { ticket: parseAnalystTicketNumber(ticketRaw) }),
|
|
2224
|
+
};
|
|
2225
|
+
}
|