@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2535
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/README.md +5 -2
- package/dist/inspector-contracts.js +1 -0
- package/dist/package-contracts/terminating-tools.js +9 -1
- package/dist/packaged-role-registry.js +3 -1
- package/dist/public-cli/config.js +9 -9
- package/dist/public-cli/main.js +247 -109
- package/dist/public-cli/registry.js +2 -4
- package/extensions/role-runtime.ts +2 -1
- package/package.json +1 -1
- package/src/gatekeeper-role.ts +2 -1
- package/src/inspector-contracts.ts +1 -0
- package/src/inspector-role.ts +20 -0
- package/src/package-contracts/terminating-tools.ts +11 -2
- package/src/packaged-role-registry.ts +2 -0
- package/src/public-cli/cli.ts +20 -0
- package/src/public-cli/config.ts +11 -11
- package/src/public-cli/inspector-run.ts +61 -0
- package/src/public-cli/invocation.ts +64 -86
- package/src/public-cli/option-definitions.ts +14 -0
- package/src/public-cli/registry.ts +2 -4
- package/src/public-cli/run-lifecycle.ts +27 -11
- package/src/public-cli/settlement.ts +94 -35
- package/src/public-cli/terminal.ts +2 -1
- package/src/role-runtime.ts +44 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
|
|
4
|
+
import { openToolObject } from "./open-tool-schema.ts";
|
|
5
|
+
import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
|
|
6
|
+
|
|
7
|
+
export { INSPECTOR_OUTPUT_TOOL_NAME as INSPECTOR_OUTPUT_TOOL };
|
|
8
|
+
|
|
9
|
+
export const inspectorOutputSchema = withInfrastructureFailureDeclaration(openToolObject(Type.Object({
|
|
10
|
+
status: Type.Unknown({ description: "pass | bounce — 取值形状指引,不作拒收依据" }),
|
|
11
|
+
findings: Type.Unknown({ description: "随交卷留存的问题记录" }),
|
|
12
|
+
})));
|
|
13
|
+
|
|
14
|
+
export function projectInspectorReceipt(parameters: unknown) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text" as const, text: "给事中回执已受理" }],
|
|
17
|
+
details: parameters,
|
|
18
|
+
terminate: true as const,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type RuntimeReviewerReceiptV2,
|
|
26
26
|
} from "./reviewer-output.ts";
|
|
27
27
|
import { isAuditEscalationResult } from "../audit-escalation.ts";
|
|
28
|
+
import { INSPECTOR_OUTPUT_TOOL_NAME } from "../inspector-contracts.ts";
|
|
28
29
|
import { DOCTOR_ACCEPTED_TEXT, DOCTOR_OUTPUT_TOOL_NAME, validateDoctorSubmissionShape, validateRecordedDoctorOutput, type DoctorOutput, type DoctorSubmission } from "../doctor-contracts.ts";
|
|
29
30
|
import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, type MergerOutput } from "../merger-contracts.ts";
|
|
30
31
|
import { NOTARY_ACCEPTED_TEXT, NOTARY_OUTPUT_TOOL_NAME, validateRecordedNotaryOutput, type NotaryOutput } from "../notary-contracts.ts";
|
|
@@ -82,6 +83,7 @@ export const TERMINATING_TOOL_NAMES = [
|
|
|
82
83
|
DOCTOR_OUTPUT_TOOL_NAME,
|
|
83
84
|
MERGER_OUTPUT_TOOL_NAME,
|
|
84
85
|
NOTARY_OUTPUT_TOOL_NAME,
|
|
86
|
+
INSPECTOR_OUTPUT_TOOL_NAME,
|
|
85
87
|
] as const;
|
|
86
88
|
|
|
87
89
|
export type TerminatingToolName = (typeof TERMINATING_TOOL_NAMES)[number];
|
|
@@ -93,7 +95,8 @@ export type AcceptedDetails =
|
|
|
93
95
|
| CollectorReceipt
|
|
94
96
|
| DoctorOutput
|
|
95
97
|
| MergerOutput
|
|
96
|
-
| NotaryOutput
|
|
98
|
+
| NotaryOutput
|
|
99
|
+
| Readonly<Record<string, unknown>>;
|
|
97
100
|
|
|
98
101
|
export function isTerminatingToolName(
|
|
99
102
|
name: string,
|
|
@@ -119,6 +122,8 @@ export function acceptedTextFor(toolName: TerminatingToolName): string {
|
|
|
119
122
|
return MERGER_ACCEPTED_TEXT;
|
|
120
123
|
case NOTARY_OUTPUT_TOOL_NAME:
|
|
121
124
|
return NOTARY_ACCEPTED_TEXT;
|
|
125
|
+
case INSPECTOR_OUTPUT_TOOL_NAME:
|
|
126
|
+
return "给事中回执已受理";
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
@@ -165,6 +170,7 @@ export function validateAcceptedDetails(
|
|
|
165
170
|
[DOCTOR_OUTPUT_TOOL_NAME]: ["completed", "refused"],
|
|
166
171
|
[MERGER_OUTPUT_TOOL_NAME]: ["completed", "escalate"],
|
|
167
172
|
[NOTARY_OUTPUT_TOOL_NAME]: ["pass", "bounce"],
|
|
173
|
+
[INSPECTOR_OUTPUT_TOOL_NAME]: ["pass", "bounce"],
|
|
168
174
|
};
|
|
169
175
|
const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
|
|
170
176
|
const baseDiscriminator = discriminator;
|
|
@@ -195,6 +201,8 @@ export function validateAcceptedDetails(
|
|
|
195
201
|
return validateMergerOutput(details);
|
|
196
202
|
case NOTARY_OUTPUT_TOOL_NAME:
|
|
197
203
|
return validateRecordedNotaryOutput(details);
|
|
204
|
+
case INSPECTOR_OUTPUT_TOOL_NAME:
|
|
205
|
+
return candidate as Readonly<Record<string, unknown>>;
|
|
198
206
|
}
|
|
199
207
|
} catch (error) {
|
|
200
208
|
if (error instanceof Error && error.constructor === Error) throw new AcceptedDetailsContractError(error.message, { cause: error });
|
|
@@ -239,7 +247,8 @@ export function acceptedFacts(toolName: TerminatingToolName, details: AcceptedDe
|
|
|
239
247
|
case FIXER_OUTPUT_TOOL_NAME:
|
|
240
248
|
case REVIEWER_OUTPUT_TOOL_NAME:
|
|
241
249
|
case DOCTOR_OUTPUT_TOOL_NAME:
|
|
242
|
-
case NOTARY_OUTPUT_TOOL_NAME:
|
|
250
|
+
case NOTARY_OUTPUT_TOOL_NAME:
|
|
251
|
+
case INSPECTOR_OUTPUT_TOOL_NAME: return { status: (details as { status: string }).status };
|
|
243
252
|
case JUDGE_OUTPUT_TOOL_NAME: return { status: (details as { judgeStatus: string }).judgeStatus };
|
|
244
253
|
case MERGER_OUTPUT_TOOL_NAME: {
|
|
245
254
|
const output = details as unknown as Record<string, unknown>;
|
|
@@ -6,6 +6,7 @@ import { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME } from "./package-contra
|
|
|
6
6
|
import { DOCTOR_OUTPUT_TOOL_NAME } from "./doctor-contracts.ts";
|
|
7
7
|
import { MERGER_OUTPUT_TOOL_NAME } from "./merger-contracts.ts";
|
|
8
8
|
import { NOTARY_OUTPUT_TOOL_NAME } from "./notary-contracts.ts";
|
|
9
|
+
import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
|
|
9
10
|
|
|
10
11
|
export const PACKAGED_ROLE_REGISTRY = [
|
|
11
12
|
{ role: "judge", phases: [null], outputTool: JUDGE_OUTPUT_TOOL_NAME, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
|
|
@@ -17,6 +18,7 @@ export const PACKAGED_ROLE_REGISTRY = [
|
|
|
17
18
|
{ role: "doctor", phases: [null], outputTool: DOCTOR_OUTPUT_TOOL_NAME, inputFlag: "ak-doctor-case", phaseFlag: undefined, activationStage: "load-and-install" },
|
|
18
19
|
{ role: "merger", phases: [null], outputTool: MERGER_OUTPUT_TOOL_NAME, inputFlag: "ak-merger-input", phaseFlag: undefined, activationStage: "prepare-git-and-install" },
|
|
19
20
|
{ role: "notary", phases: [null], outputTool: NOTARY_OUTPUT_TOOL_NAME, inputFlag: "ak-notary-source-run", phaseFlag: undefined, activationStage: "load-and-install" },
|
|
21
|
+
{ role: "inspector", phases: [null], outputTool: INSPECTOR_OUTPUT_TOOL_NAME, inputFlag: undefined, phaseFlag: undefined, activationStage: "load-and-install" },
|
|
20
22
|
] as const;
|
|
21
23
|
|
|
22
24
|
export type PackagedRole = (typeof PACKAGED_ROLE_REGISTRY)[number]["role"];
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
parseDoctorArgv,
|
|
41
41
|
parseFixerArgv,
|
|
42
42
|
parseJudgeArgv,
|
|
43
|
+
parseInspectorArgv,
|
|
43
44
|
parseMergerArgv,
|
|
44
45
|
parseNotaryArgv,
|
|
45
46
|
parseReviewerArgv,
|
|
@@ -60,6 +61,7 @@ import { runPublicCollector } from "./collector-run.ts";
|
|
|
60
61
|
import { runPublicDoctor } from "./doctor-run.ts";
|
|
61
62
|
import { runPublicFixer, runPublicFixerResume } from "./fixer-run.ts";
|
|
62
63
|
import { runPublicNotary } from "./notary-run.ts";
|
|
64
|
+
import { runPublicInspector } from "./inspector-run.ts";
|
|
63
65
|
import { runPublicJudge, runPublicResume } from "./judge-run.ts";
|
|
64
66
|
import { runPublicMerger, runPublicMergerResume } from "./merger-run.ts";
|
|
65
67
|
import { runPublicReviewer, runPublicReviewerResume } from "./reviewer-run.ts";
|
|
@@ -104,6 +106,7 @@ export const PUBLIC_ROLE_ARGV = {
|
|
|
104
106
|
doctor: { parse: parseDoctorArgv, options: optionsForOwner("doctor") },
|
|
105
107
|
merger: { parse: parseMergerArgv, options: optionsForOwner("merger") },
|
|
106
108
|
notary: { parse: parseNotaryArgv, options: optionsForOwner("notary") },
|
|
109
|
+
inspector: { parse: parseInspectorArgv, options: optionsForOwner("inspector") },
|
|
107
110
|
reviewer: { parse: parseReviewerArgv, options: optionsForOwner("reviewer") },
|
|
108
111
|
/** Deterministic analysis seat (#336) — argv parse only; no LLM admission. */
|
|
109
112
|
analyst: { parse: parseAnalystArgv, options: optionsForOwner("analyst") },
|
|
@@ -1317,6 +1320,23 @@ export async function runAkRole(
|
|
|
1317
1320
|
};
|
|
1318
1321
|
}
|
|
1319
1322
|
|
|
1323
|
+
if (parsed.command === "inspector") {
|
|
1324
|
+
const agentDir = resolveAgentDir(env, home);
|
|
1325
|
+
const cwd = env.cwd ?? process.cwd();
|
|
1326
|
+
const config = await loadAndValidateConfig(home, env.packageRoot);
|
|
1327
|
+
const credentials = env.credentials ?? (await loadCredentialProviders(agentDir));
|
|
1328
|
+
const seat = resolveEffectiveSeat(config, "inspector", credentials, invocationFromParsed(parsed));
|
|
1329
|
+
const result = await runPublicInspector(parsed.args, {
|
|
1330
|
+
home, agentDir, packageRoot: env.packageRoot, cwd, credentials,
|
|
1331
|
+
...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
|
|
1332
|
+
...(env.piRunner === undefined ? {} : { piRunner: env.piRunner }),
|
|
1333
|
+
...(seat.selection === undefined ? {} : { model: seat.selection }),
|
|
1334
|
+
...projectSeatEngine(seat),
|
|
1335
|
+
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
1336
|
+
}, io, PUBLIC_ROLE_ARGV.inspector.parse);
|
|
1337
|
+
return { exitCode: result.exitCode, ...(result.terminal === undefined ? {} : { terminal: result.terminal }) };
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1320
1340
|
// Merger public run path: derive active-merge envelope + forced merge-only method (#114).
|
|
1321
1341
|
if (parsed.command === "merger") {
|
|
1322
1342
|
const agentDir = resolveAgentDir(env, home);
|
package/src/public-cli/config.ts
CHANGED
|
@@ -42,8 +42,8 @@ export type SeatModelConfig = ModelRef;
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Persistent seat row (#356/#384/#453): model fields and engine are independent
|
|
45
|
-
* axes.
|
|
46
|
-
*
|
|
45
|
+
* axes. Direct Inspector and Notary seats may retain an engine after model clear so
|
|
46
|
+
* officer activation keeps its labor engine while province inheritance resumes.
|
|
47
47
|
* All other seats keep the baseline provider/model required contract.
|
|
48
48
|
*/
|
|
49
49
|
export type PersistentSeatConfig = {
|
|
@@ -154,9 +154,9 @@ export function setPersistentSeatConfig(
|
|
|
154
154
|
/**
|
|
155
155
|
* Clear a gate officer's persistent model override (#453).
|
|
156
156
|
* Scope is GateOfficerSeat only — non-province seats have no destructive clear seam.
|
|
157
|
-
*
|
|
157
|
+
* Directly callable gate officers may retain an engine-only residual so activation keeps
|
|
158
158
|
* its labor engine while model resolution returns to startup / province inheritance.
|
|
159
|
-
*
|
|
159
|
+
* Gatekeeper drops the whole row. Already-absent seats are a no-op.
|
|
160
160
|
*/
|
|
161
161
|
export function clearPersistentSeatConfig(
|
|
162
162
|
config: PublicCliConfig,
|
|
@@ -164,13 +164,13 @@ export function clearPersistentSeatConfig(
|
|
|
164
164
|
): PublicCliConfig {
|
|
165
165
|
const previous = config.seats[seat];
|
|
166
166
|
if (previous === undefined) return config;
|
|
167
|
-
//
|
|
168
|
-
if (seat === "notary" && previous.engine !== undefined) {
|
|
167
|
+
// Direct officer engine ownership survives an independent model clear.
|
|
168
|
+
if ((seat === "notary" || seat === "inspector") && previous.engine !== undefined) {
|
|
169
169
|
return {
|
|
170
170
|
...config,
|
|
171
171
|
seats: {
|
|
172
172
|
...config.seats,
|
|
173
|
-
|
|
173
|
+
[seat]: { engine: previous.engine },
|
|
174
174
|
},
|
|
175
175
|
};
|
|
176
176
|
}
|
|
@@ -204,8 +204,8 @@ export function isEngineAxisSeat(seat: string): seat is PublicCallableRole {
|
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* Set or clear persistent engine on a callable role seat (#356 / #378 / #391 / #453).
|
|
207
|
-
* First engine still requires an existing seat row (model, or
|
|
208
|
-
* Clearing engine from
|
|
207
|
+
* First engine still requires an existing seat row (model, or direct-officer engine residual).
|
|
208
|
+
* Clearing engine from an engine-only residual drops the empty row; clearing
|
|
209
209
|
* engine from a model+engine row leaves model-only. Seat type is PublicCallableRole
|
|
210
210
|
* (navigator excluded at the type boundary).
|
|
211
211
|
*/
|
|
@@ -427,10 +427,10 @@ function parseSeatModelConfig(value: unknown, seat: string): PersistentSeatConfi
|
|
|
427
427
|
// validatePublicCliConfigEngines → assertLegalEngineName (single authority).
|
|
428
428
|
throw new Error(`config seat ${seat} engine must be a string`);
|
|
429
429
|
}
|
|
430
|
-
//
|
|
430
|
+
// Direct officer engine-only residual remains legal after model clear.
|
|
431
431
|
// All other seats keep the baseline provider/model required contract.
|
|
432
432
|
if (!hasProvider) {
|
|
433
|
-
if (seat === "notary" && typeof raw.engine === "string") {
|
|
433
|
+
if ((seat === "notary" || seat === "inspector") && typeof raw.engine === "string") {
|
|
434
434
|
if (raw.thinking !== undefined) {
|
|
435
435
|
throw new Error(`config seat ${seat} thinking requires provider/model`);
|
|
436
436
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
|
|
2
|
+
import { CliUsageError } from "./cli-errors.ts";
|
|
3
|
+
import {
|
|
4
|
+
admitInspectorInvocation,
|
|
5
|
+
buildInspectorTransportPrompt,
|
|
6
|
+
type AdmittedInspectorInvocation,
|
|
7
|
+
type ParseInspectorArgvResult,
|
|
8
|
+
} from "./invocation.ts";
|
|
9
|
+
import { buildSeatModelCliArgs, type SeatModelConfig } from "./config.ts";
|
|
10
|
+
import { runAdmittedOneShotRole, type OneShotRunEnv } from "./one-shot-dispatch.ts";
|
|
11
|
+
import { presentStructuralRejection, trySettleInspectorTerminalResult } from "./settlement.ts";
|
|
12
|
+
import type { CliIo } from "./cli-io.ts";
|
|
13
|
+
import type { TerminalResult } from "./terminal.ts";
|
|
14
|
+
|
|
15
|
+
export type InspectorRunEnv = OneShotRunEnv & { createRunId?: () => string; extraPiArgs?: readonly string[] };
|
|
16
|
+
|
|
17
|
+
export function buildInspectorActivationExtraArgs(
|
|
18
|
+
admitted: AdmittedInspectorInvocation,
|
|
19
|
+
options: { model?: SeatModelConfig; engine?: string; packageRoot?: string; extraPiArgs?: readonly string[] } = {},
|
|
20
|
+
): string[] {
|
|
21
|
+
return [
|
|
22
|
+
"--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files",
|
|
23
|
+
"--session", admitted.sessionFile, "--session-dir", admitted.sessionDirectory,
|
|
24
|
+
...(options.extraPiArgs ?? []), "--ak-role", "inspector", "--mode", "json",
|
|
25
|
+
...buildSeatModelCliArgs(options.model),
|
|
26
|
+
buildInspectorTransportPrompt(admitted, engineSessionMaterialFromOptions(options)),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runPublicInspector(
|
|
31
|
+
argv: readonly string[], env: InspectorRunEnv, io: CliIo,
|
|
32
|
+
parse: (args: readonly string[]) => ParseInspectorArgvResult,
|
|
33
|
+
): Promise<{ exitCode: number; admitted?: AdmittedInspectorInvocation; terminal?: TerminalResult }> {
|
|
34
|
+
let admitted: AdmittedInspectorInvocation;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = parse(argv);
|
|
37
|
+
admitted = await admitInspectorInvocation({
|
|
38
|
+
home: env.home, cwd: env.cwd, instruction: parsed.instruction, attachmentPaths: parsed.attachmentPaths,
|
|
39
|
+
...(parsed.project === undefined ? {} : { project: parsed.project }),
|
|
40
|
+
...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
|
|
41
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof CliUsageError) {
|
|
45
|
+
presentStructuralRejection(error, io);
|
|
46
|
+
return { exitCode: 2 };
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const extraArgs = buildInspectorActivationExtraArgs(admitted, {
|
|
51
|
+
packageRoot: env.packageRoot,
|
|
52
|
+
...(env.model === undefined ? {} : { model: env.model }),
|
|
53
|
+
...(env.engine === undefined ? {} : { engine: env.engine }),
|
|
54
|
+
...(env.extraPiArgs === undefined ? {} : { extraPiArgs: env.extraPiArgs }),
|
|
55
|
+
});
|
|
56
|
+
return runAdmittedOneShotRole({
|
|
57
|
+
admitted, env, io, extraArgs,
|
|
58
|
+
adapters: { trySettle: trySettleInspectorTerminalResult, shouldPresentSettled: () => true },
|
|
59
|
+
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -111,6 +111,10 @@ export type AdmittedJudgeInvocation = AdmittedRoleInvocationBase & {
|
|
|
111
111
|
readonly role: "judge";
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
export type AdmittedInspectorInvocation = AdmittedRoleInvocationBase & {
|
|
115
|
+
readonly role: "inspector";
|
|
116
|
+
};
|
|
117
|
+
|
|
114
118
|
export type CoderPhase = "plan" | "apply";
|
|
115
119
|
|
|
116
120
|
export type AdmittedCoderInvocation = AdmittedRoleInvocationBase & {
|
|
@@ -189,6 +193,7 @@ export type AdmittedMergerInvocation = AdmittedRoleInvocationBase & {
|
|
|
189
193
|
|
|
190
194
|
export type AdmittedRoleInvocation =
|
|
191
195
|
| AdmittedJudgeInvocation
|
|
196
|
+
| AdmittedInspectorInvocation
|
|
192
197
|
| AdmittedCoderInvocation
|
|
193
198
|
| AdmittedFixerInvocation
|
|
194
199
|
| AdmittedCollectorInvocation
|
|
@@ -377,6 +382,8 @@ export type ParseJudgeArgvResult = {
|
|
|
377
382
|
project?: string;
|
|
378
383
|
};
|
|
379
384
|
|
|
385
|
+
export type ParseInspectorArgvResult = ParseJudgeArgvResult;
|
|
386
|
+
|
|
380
387
|
export type ParseCoderArgvResult = {
|
|
381
388
|
phase: CoderPhase;
|
|
382
389
|
instruction: string;
|
|
@@ -536,51 +543,41 @@ export function requireAuthorityRef(value: string | undefined): string {
|
|
|
536
543
|
* Parse Judge-specific argv after the `judge` token.
|
|
537
544
|
* Spellings from PUBLIC_OPTION_TABLE.judge; rejects burden family (#342).
|
|
538
545
|
*/
|
|
539
|
-
|
|
546
|
+
function parseStandardMaterialArgv(
|
|
547
|
+
owner: "judge" | "inspector",
|
|
548
|
+
args: readonly string[],
|
|
549
|
+
): ParseJudgeArgvResult {
|
|
540
550
|
const attachmentPaths: string[] = [];
|
|
541
551
|
let project: string | undefined;
|
|
542
552
|
const positional: string[] = [];
|
|
543
553
|
const tokens = [...args];
|
|
544
|
-
const
|
|
545
|
-
const options = createTypedOptionConsumer(definitions);
|
|
546
|
-
|
|
554
|
+
const options = createTypedOptionConsumer(roleOptions(owner));
|
|
547
555
|
while (tokens.length > 0) {
|
|
548
|
-
if (tokens[0] === "--") {
|
|
549
|
-
tokens.shift();
|
|
550
|
-
positional.push(...tokens);
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
556
|
+
if (tokens[0] === "--") { tokens.shift(); positional.push(...tokens); break; }
|
|
553
557
|
const taken = options.takeDashed(tokens);
|
|
554
558
|
if (taken !== undefined) {
|
|
555
|
-
if (taken.def.id === "attach")
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (taken.def.id === "project") {
|
|
560
|
-
project = requireOptionPath(taken.def.canonical, taken.value);
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
throw new CliUsageError(`unknown judge option: ${taken.def.canonical}`);
|
|
559
|
+
if (taken.def.id === "attach") attachmentPaths.push(requireOptionPath(taken.def.canonical, taken.value));
|
|
560
|
+
else if (taken.def.id === "project") project = requireOptionPath(taken.def.canonical, taken.value);
|
|
561
|
+
else throw new CliUsageError(`unknown ${owner} option: ${taken.def.canonical}`);
|
|
562
|
+
continue;
|
|
564
563
|
}
|
|
565
564
|
const token = tokens.shift()!;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
throw new CliUsageError(
|
|
569
|
-
"judge does not accept a public burden selector; Judge infers its own burden",
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
if (token.startsWith("-") && token !== "-") {
|
|
573
|
-
throw new CliUsageError(`unknown judge option: ${token}`);
|
|
565
|
+
if (owner === "judge" && isRejectedPublicSpelling("judge", token)) {
|
|
566
|
+
throw new CliUsageError("judge does not accept a public burden selector; Judge infers its own burden");
|
|
574
567
|
}
|
|
568
|
+
if (token.startsWith("-") && token !== "-") throw new CliUsageError(`unknown ${owner} option: ${token}`);
|
|
575
569
|
positional.push(token);
|
|
576
570
|
}
|
|
577
|
-
|
|
578
571
|
options.assertRequired();
|
|
579
|
-
return {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
572
|
+
return { instruction: positional.join(" "), attachmentPaths, ...(project === undefined ? {} : { project }) };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function parseJudgeArgv(args: readonly string[]): ParseJudgeArgvResult {
|
|
576
|
+
return parseStandardMaterialArgv("judge", args);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function parseInspectorArgv(args: readonly string[]): ParseInspectorArgvResult {
|
|
580
|
+
return parseStandardMaterialArgv("inspector", args);
|
|
584
581
|
}
|
|
585
582
|
|
|
586
583
|
/**
|
|
@@ -771,84 +768,65 @@ export type AdmitJudgeInvocationOptions = {
|
|
|
771
768
|
* Atomically admit a Judge Role run: freeze Attachments, persist the request,
|
|
772
769
|
* and reserve session placement under the #78 ledger book.
|
|
773
770
|
*/
|
|
774
|
-
|
|
771
|
+
async function admitStandardMaterialInvocation<R extends "judge" | "inspector">(
|
|
772
|
+
role: R,
|
|
775
773
|
options: AdmitJudgeInvocationOptions,
|
|
776
|
-
): Promise<
|
|
777
|
-
|
|
778
|
-
if (options.project !== undefined) {
|
|
779
|
-
requireOptionPath("--project", options.project);
|
|
780
|
-
}
|
|
774
|
+
): Promise<AdmittedRoleInvocationBase & { readonly role: R }> {
|
|
775
|
+
if (options.project !== undefined) requireOptionPath("--project", options.project);
|
|
781
776
|
const projectRoot = resolve(options.project ?? options.cwd);
|
|
782
777
|
const runId = (options.createRunId ?? uuidv7)();
|
|
783
778
|
const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } =
|
|
784
|
-
roleRunSessionCoordinates({ cwd: projectRoot, runId, role
|
|
779
|
+
roleRunSessionCoordinates({ cwd: projectRoot, runId, role, home: options.home });
|
|
785
780
|
const attachmentsDirectory = join(runDirectory, "attachments");
|
|
786
781
|
ensureRealDirectoryTree(ledgerHome, sessionDirectory);
|
|
787
782
|
ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
|
|
788
|
-
|
|
789
|
-
const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
|
|
790
|
-
options.attachmentPaths,
|
|
791
|
-
attachmentsDirectory,
|
|
792
|
-
);
|
|
783
|
+
const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(options.attachmentPaths, attachmentsDirectory);
|
|
793
784
|
const ticketFields = ticketAdmissionFields(ticketNumber);
|
|
794
|
-
|
|
795
785
|
const instruction = options.instruction;
|
|
796
|
-
const instructionEmpty = instruction.trim() === "";
|
|
797
786
|
const admitted = {
|
|
798
|
-
role
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
projectRoot,
|
|
802
|
-
runDirectory,
|
|
803
|
-
sessionDirectory,
|
|
804
|
-
sessionFile,
|
|
805
|
-
...ticketFields,
|
|
806
|
-
instruction,
|
|
807
|
-
instructionEmpty,
|
|
808
|
-
attachments: attachments.map((a) => ({
|
|
809
|
-
provenancePath: a.provenancePath,
|
|
810
|
-
frozenPath: a.frozenPath,
|
|
811
|
-
byteLength: a.byteLength,
|
|
812
|
-
sha256: a.sha256,
|
|
813
|
-
mediaKind: a.mediaKind,
|
|
814
|
-
})),
|
|
787
|
+
role, runId, bookKey, projectRoot, runDirectory, sessionDirectory, sessionFile,
|
|
788
|
+
...ticketFields, instruction, instructionEmpty: instruction.trim() === "",
|
|
789
|
+
attachments: attachments.map((attachment) => ({ ...attachment })),
|
|
815
790
|
};
|
|
816
791
|
const admittedRequestPath = join(runDirectory, "admitted-request.json");
|
|
817
792
|
await writeFile(admittedRequestPath, `${JSON.stringify(admitted, null, 2)}\n`, "utf8");
|
|
818
|
-
await writeRoleInvocationLedger(admitted,
|
|
793
|
+
await writeRoleInvocationLedger(admitted, role, options.model);
|
|
794
|
+
return { ...admitted, attachments, admittedRequestPath };
|
|
795
|
+
}
|
|
819
796
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
runId,
|
|
823
|
-
bookKey,
|
|
824
|
-
projectRoot,
|
|
825
|
-
instruction,
|
|
826
|
-
instructionEmpty,
|
|
827
|
-
attachments,
|
|
828
|
-
runDirectory,
|
|
829
|
-
sessionDirectory,
|
|
830
|
-
sessionFile,
|
|
831
|
-
admittedRequestPath,
|
|
832
|
-
...ticketFields,
|
|
833
|
-
};
|
|
797
|
+
export async function admitJudgeInvocation(options: AdmitJudgeInvocationOptions): Promise<AdmittedJudgeInvocation> {
|
|
798
|
+
return admitStandardMaterialInvocation("judge", options);
|
|
834
799
|
}
|
|
835
800
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
801
|
+
export type AdmitInspectorInvocationOptions = AdmitJudgeInvocationOptions;
|
|
802
|
+
|
|
803
|
+
export async function admitInspectorInvocation(options: AdmitInspectorInvocationOptions): Promise<AdmittedInspectorInvocation> {
|
|
804
|
+
return admitStandardMaterialInvocation("inspector", options);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function buildStandardMaterialTransportPrompt(
|
|
808
|
+
admitted: AdmittedJudgeInvocation | AdmittedInspectorInvocation,
|
|
839
809
|
engineMaterial?: EngineSessionMaterial,
|
|
840
810
|
): string {
|
|
841
811
|
const lines: string[] = [admitted.instructionEmpty ? "" : admitted.instruction];
|
|
842
812
|
if (admitted.attachments.length > 0) {
|
|
843
|
-
lines.push("");
|
|
844
|
-
lines.push("已受理附件(冻结快照路径):");
|
|
845
|
-
for (const attachment of admitted.attachments) {
|
|
846
|
-
lines.push(`- ${attachment.frozenPath}`);
|
|
847
|
-
}
|
|
813
|
+
lines.push("", "已受理附件(冻结快照路径):", ...admitted.attachments.map((attachment) => `- ${attachment.frozenPath}`));
|
|
848
814
|
}
|
|
849
815
|
return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
|
|
850
816
|
}
|
|
851
817
|
|
|
818
|
+
export function buildInspectorTransportPrompt(admitted: AdmittedInspectorInvocation, engineMaterial?: EngineSessionMaterial): string {
|
|
819
|
+
return buildStandardMaterialTransportPrompt(admitted, engineMaterial);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Build the Pi prompt transport for an admitted Judge request. */
|
|
823
|
+
export function buildJudgeTransportPrompt(
|
|
824
|
+
admitted: AdmittedJudgeInvocation,
|
|
825
|
+
engineMaterial?: EngineSessionMaterial,
|
|
826
|
+
): string {
|
|
827
|
+
return buildStandardMaterialTransportPrompt(admitted, engineMaterial);
|
|
828
|
+
}
|
|
829
|
+
|
|
852
830
|
/** Load admitted-request.json written at admission (Navigator work-context seam). */
|
|
853
831
|
export async function loadAdmittedJudgeRequest(
|
|
854
832
|
runDirectory: string,
|
|
@@ -29,6 +29,7 @@ export type OptionOwner =
|
|
|
29
29
|
| "doctor"
|
|
30
30
|
| "merger"
|
|
31
31
|
| "notary"
|
|
32
|
+
| "inspector"
|
|
32
33
|
| "analyst";
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -369,6 +370,11 @@ const JUDGE_OPTIONS = [
|
|
|
369
370
|
bindOwner("judge", SHARED_ATTACH_SEMANTICS),
|
|
370
371
|
] as const satisfies readonly PublicOptionDefinition[];
|
|
371
372
|
|
|
373
|
+
const INSPECTOR_OPTIONS = [
|
|
374
|
+
bindOwner("inspector", SHARED_PROJECT_SEMANTICS),
|
|
375
|
+
bindOwner("inspector", SHARED_ATTACH_SEMANTICS),
|
|
376
|
+
] as const satisfies readonly PublicOptionDefinition[];
|
|
377
|
+
|
|
372
378
|
const CODER_OPTIONS = [
|
|
373
379
|
{
|
|
374
380
|
id: "phase",
|
|
@@ -721,6 +727,7 @@ export const PUBLIC_OPTION_TABLE = {
|
|
|
721
727
|
doctor: DOCTOR_OPTIONS,
|
|
722
728
|
merger: MERGER_OPTIONS,
|
|
723
729
|
notary: NOTARY_OPTIONS,
|
|
730
|
+
inspector: INSPECTOR_OPTIONS,
|
|
724
731
|
analyst: ANALYST_OPTIONS,
|
|
725
732
|
} as const satisfies Record<OptionOwner, readonly PublicOptionDefinition[]>;
|
|
726
733
|
|
|
@@ -736,6 +743,7 @@ export const PUBLIC_ROLE_OPTION_OWNERS = [
|
|
|
736
743
|
"doctor",
|
|
737
744
|
"merger",
|
|
738
745
|
"notary",
|
|
746
|
+
"inspector",
|
|
739
747
|
"analyst",
|
|
740
748
|
] as const satisfies readonly PublicRoleOptionOwner[];
|
|
741
749
|
|
|
@@ -1097,6 +1105,12 @@ const ROLE_COMMAND_HELP = {
|
|
|
1097
1105
|
"ak-role notary --source-run 01a034f1-75bf-71a6-bcf5-d1299145b1a5@judge",
|
|
1098
1106
|
],
|
|
1099
1107
|
},
|
|
1108
|
+
inspector: {
|
|
1109
|
+
command: "inspector",
|
|
1110
|
+
summary: "Direct complexity and test-quality inspection.",
|
|
1111
|
+
usage: ["ak-role inspector [options] [instruction]"],
|
|
1112
|
+
examples: ['ak-role inspector --attach ./change.patch "Review this material."'],
|
|
1113
|
+
},
|
|
1100
1114
|
analyst: {
|
|
1101
1115
|
command: "analyst",
|
|
1102
1116
|
summary: "Deterministic analysis seat (issue / sweep / cohort).",
|
|
@@ -19,14 +19,12 @@ export type PublicCallableRole = (typeof PUBLIC_CALLABLE_ROLES)[number];
|
|
|
19
19
|
/** Automatic attendance seat — configurable, never a caller-selected command. */
|
|
20
20
|
export const AUTOMATIC_NAVIGATOR_SEAT = "navigator" as const;
|
|
21
21
|
|
|
22
|
-
/** Automatic
|
|
22
|
+
/** Automatic province seat — configurable model only; never a caller command. */
|
|
23
23
|
export const AUTOMATIC_GATEKEEPER_SEAT = "gatekeeper" as const;
|
|
24
|
-
export const AUTOMATIC_INSPECTOR_SEAT = "inspector" as const;
|
|
25
24
|
|
|
26
|
-
/**
|
|
25
|
+
/** Automatic-only configurable seats. Inspector remains automatic-capable via its callable seat. */
|
|
27
26
|
export const AUTOMATIC_CONFIGURABLE_SEATS = [
|
|
28
27
|
AUTOMATIC_GATEKEEPER_SEAT,
|
|
29
|
-
AUTOMATIC_INSPECTOR_SEAT,
|
|
30
28
|
AUTOMATIC_NAVIGATOR_SEAT,
|
|
31
29
|
] as const;
|
|
32
30
|
|
|
@@ -68,7 +68,8 @@ export type RoleRunRecord = {
|
|
|
68
68
|
| "doctor"
|
|
69
69
|
| "reviewer"
|
|
70
70
|
| "merger"
|
|
71
|
-
| "notary"
|
|
71
|
+
| "notary"
|
|
72
|
+
| "inspector";
|
|
72
73
|
readonly state: RoleRunState;
|
|
73
74
|
readonly bookKey: string;
|
|
74
75
|
readonly projectRoot: string;
|
|
@@ -179,7 +180,8 @@ export async function readRoleRunState(
|
|
|
179
180
|
record.role !== "doctor" &&
|
|
180
181
|
record.role !== "reviewer" &&
|
|
181
182
|
record.role !== "merger" &&
|
|
182
|
-
record.role !== "notary"
|
|
183
|
+
record.role !== "notary" &&
|
|
184
|
+
record.role !== "inspector"
|
|
183
185
|
) {
|
|
184
186
|
return undefined;
|
|
185
187
|
}
|
|
@@ -523,7 +525,8 @@ const WRITER_LEASE_RECLAIM_ROUNDS = 3;
|
|
|
523
525
|
* race repeatedly lost) surfaces the same typed error instead of spinning.
|
|
524
526
|
*
|
|
525
527
|
* `onCleanupFailure` receives a non-terminal diagnostic line when release-time
|
|
526
|
-
* lock cleanup fails
|
|
528
|
+
* lock cleanup fails, a contested lock cannot be read, or a stale lock is
|
|
529
|
+
* reclaimed (the #556 orphan-pi residual declaration). Release stays
|
|
527
530
|
* best-effort (a stale lock is reclaimed by the next acquire's autopsy), but
|
|
528
531
|
* the true error identity must still land somewhere observable — silent
|
|
529
532
|
* swallowing is forbidden.
|
|
@@ -532,18 +535,24 @@ export async function acquireRunWriterLease(
|
|
|
532
535
|
runDirectory: string,
|
|
533
536
|
onCleanupFailure?: (diagnostic: string) => void,
|
|
534
537
|
): Promise<RunWriterLease> {
|
|
535
|
-
const
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
538
|
+
const reportDiagnostic = (diagnostic: string): void => {
|
|
539
|
+
// Single sink exit for every non-terminal writer-lease diagnostic (#556):
|
|
540
|
+
// a throwing onCleanupFailure must not propagate through release() or
|
|
541
|
+
// acquire() — both are best-effort by contract. Template wording stays at
|
|
542
|
+
// the call sites, never inside this wrapper. The default io.stderr writes
|
|
543
|
+
// raw, so this exit guarantees the line delimiter.
|
|
544
|
+
const line = diagnostic.endsWith("\n") ? diagnostic : `${diagnostic}\n`;
|
|
539
545
|
try {
|
|
540
|
-
onCleanupFailure?.(
|
|
541
|
-
`writer lease lock cleanup failed (best-effort continue; stale lock is reclaimed by the next acquire's holder autopsy) at ${join(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`,
|
|
542
|
-
);
|
|
546
|
+
onCleanupFailure?.(line);
|
|
543
547
|
} catch {
|
|
544
|
-
// diagnostic-sink failure is itself best-effort; never break release().
|
|
548
|
+
// diagnostic-sink failure is itself best-effort; never break acquire()/release().
|
|
545
549
|
}
|
|
546
550
|
};
|
|
551
|
+
const reportCleanupFailure = (error: unknown): void => {
|
|
552
|
+
reportDiagnostic(
|
|
553
|
+
`writer lease lock cleanup failed (best-effort continue; stale lock is reclaimed by the next acquire's holder autopsy) at ${join(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`,
|
|
554
|
+
);
|
|
555
|
+
};
|
|
547
556
|
const lockPath = join(runDirectory, WRITER_LOCK_FILE);
|
|
548
557
|
let lastAutopsy: WriterLockAutopsy = { verdict: "absent" };
|
|
549
558
|
// The reclaim budget: every successful reclaim is immediately followed by a
|
|
@@ -585,6 +594,12 @@ export async function acquireRunWriterLease(
|
|
|
585
594
|
`stale writer lease reclaim failed at ${lockPath} (autopsy: ${describeAutopsy(lastAutopsy)}): ${describeErrorIdentity(reclaimError)}`,
|
|
586
595
|
);
|
|
587
596
|
}
|
|
597
|
+
// #556 residual declaration, facts only: the pid-only autopsy proves the
|
|
598
|
+
// CLI holder died; its pi child may outlive it and keep writing this run.
|
|
599
|
+
// Declare — do not guard.
|
|
600
|
+
reportDiagnostic(
|
|
601
|
+
`stale writer lease reclaimed at ${lockPath} (holder pid ${lastAutopsy.pid} verified dead): the killed holder may have left an orphaned pi child still writing this run — check for a surviving pi process on this run before continuing`,
|
|
602
|
+
);
|
|
588
603
|
}
|
|
589
604
|
throw new RunWriterLeaseHeldError(
|
|
590
605
|
`role run writer lease stayed contested at ${lockPath} after ${WRITER_LEASE_RECLAIM_ROUNDS} reclaims (last autopsy: ${describeAutopsy(lastAutopsy)})`,
|
|
@@ -1150,6 +1165,7 @@ export async function peekRoleRunRole(
|
|
|
1150
1165
|
| "reviewer"
|
|
1151
1166
|
| "merger"
|
|
1152
1167
|
| "notary"
|
|
1168
|
+
| "inspector"
|
|
1153
1169
|
| undefined
|
|
1154
1170
|
> {
|
|
1155
1171
|
const runDirectory = await findRunDirectoryById(home, runId);
|