@akagilnc/pi-workflow-roles 0.1.4528 → 0.1.4586
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 +3 -2
- package/README.zh-CN.md +4 -3
- package/dist/acp-host/production-host.js +854 -111
- package/dist/headless-host/production-host.js +850 -107
- package/dist/migrate-book-topology.js +5 -5
- package/dist/public-cli/invocation.js +10 -4
- package/dist/public-cli/main.js +733 -239
- package/dist/public-cli/option-definitions.js +5 -5
- package/dist/public-cli/post-admission.js +23 -8
- package/dist/public-cli/reviewer-run.js +339 -0
- package/dist/public-cli/terminal.js +9 -0
- package/dist/public-cli/turn-request.js +1 -1
- package/dist/public-role-summons.js +348 -16
- package/package.json +1 -1
- package/src/host-contracts.ts +0 -1
- package/src/public-cli/invocation.ts +19 -9
- package/src/public-cli/option-definitions.ts +5 -5
- package/src/public-cli/post-admission.ts +39 -9
- package/src/public-cli/reviewer-run.ts +238 -53
- package/src/public-cli/settlement.ts +0 -1
- package/src/public-cli/terminal.ts +27 -0
- package/src/public-cli/turn-request.ts +7 -1
- package/src/public-role-summons.ts +510 -17
- package/src/reviewer-role.ts +1 -2
- package/src/role-runtime.ts +2 -2
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
+
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, relative, sep } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
3
7
|
import { pickEngineAxis } from "./package-resources/engine-material.js";
|
|
4
8
|
const AK_ROLE_PACKAGE_ROOT_ENV = "AK_ROLE_PACKAGE_ROOT";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
5
10
|
function createCapturingIo() {
|
|
6
11
|
const chunks = [];
|
|
7
12
|
return {
|
|
@@ -89,13 +94,14 @@ async function createSummonEnv(options, afterHost) {
|
|
|
89
94
|
import("./public-cli/role-turn-host-resolution.js"),
|
|
90
95
|
import("./public-cli/config.js")
|
|
91
96
|
]);
|
|
92
|
-
const principalAuthority = piDurablePrincipalAuthority;
|
|
97
|
+
const principalAuthority = options.principalAuthority ?? piDurablePrincipalAuthority;
|
|
93
98
|
const roleTurnHost = resolveRoleTurnHost(
|
|
94
99
|
{
|
|
95
100
|
packageRoot: options.packageRoot,
|
|
96
101
|
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
97
102
|
...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
|
|
98
|
-
...options.extraPiArgs === void 0 || options.extraPiArgs.length === 0 ? {} : { extraPiArgs: options.extraPiArgs }
|
|
103
|
+
...options.extraPiArgs === void 0 || options.extraPiArgs.length === 0 ? {} : { extraPiArgs: options.extraPiArgs },
|
|
104
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
99
105
|
},
|
|
100
106
|
{ role: options.role, seat: options.seat, principalAuthority }
|
|
101
107
|
);
|
|
@@ -108,13 +114,16 @@ async function createSummonEnv(options, afterHost) {
|
|
|
108
114
|
};
|
|
109
115
|
}
|
|
110
116
|
const hostName = seatWithModel.host ?? "pi";
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
let hostFacingSelection = options.hostFacingModel;
|
|
118
|
+
if (hostFacingSelection === void 0) {
|
|
119
|
+
const { loadHostProvidersTable, projectHostFacingProvider } = await import("./public-cli/host-providers.js");
|
|
120
|
+
hostFacingSelection = projectHostFacingProvider(
|
|
121
|
+
seatWithModel.selection,
|
|
122
|
+
hostName,
|
|
123
|
+
loadHostProvidersTable(options.home),
|
|
124
|
+
options.home
|
|
125
|
+
);
|
|
126
|
+
}
|
|
118
127
|
return {
|
|
119
128
|
ok: true,
|
|
120
129
|
env: {
|
|
@@ -128,7 +137,8 @@ async function createSummonEnv(options, afterHost) {
|
|
|
128
137
|
credentials: options.credentials,
|
|
129
138
|
...hostFacingSelection === void 0 ? {} : { model: hostFacingSelection },
|
|
130
139
|
...projectSeatEngine(seatWithModel),
|
|
131
|
-
...projectSeatHost(seatWithModel)
|
|
140
|
+
...projectSeatHost(seatWithModel),
|
|
141
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
132
142
|
}
|
|
133
143
|
};
|
|
134
144
|
}
|
|
@@ -143,7 +153,19 @@ async function summonPublicRole(options) {
|
|
|
143
153
|
} = await import("./public-cli/config.js");
|
|
144
154
|
const credentials = options.credentials ?? await loadCredentialProviders(agentDir);
|
|
145
155
|
const config = await loadPublicCliConfig(home);
|
|
146
|
-
const
|
|
156
|
+
const resolvedSeat = resolveEffectiveSeat(config, options.role, credentials);
|
|
157
|
+
const inheritedSelection = options.model === void 0 ? void 0 : {
|
|
158
|
+
provider: options.model.provider,
|
|
159
|
+
model: options.model.model,
|
|
160
|
+
...options.model.thinking === void 0 ? {} : { thinking: options.model.thinking }
|
|
161
|
+
};
|
|
162
|
+
const seat = {
|
|
163
|
+
...resolvedSeat,
|
|
164
|
+
...inheritedSelection === void 0 ? {} : { selection: inheritedSelection, source: "invocation" },
|
|
165
|
+
...options.host === void 0 ? {} : { host: options.host, hostSource: "invocation" },
|
|
166
|
+
...options.engine === void 0 ? {} : { engine: options.engine, engineSource: "invocation" },
|
|
167
|
+
...options.engineModel === void 0 ? {} : { engineModel: options.engineModel }
|
|
168
|
+
};
|
|
147
169
|
const captured = options.io === void 0 ? createCapturingIo() : void 0;
|
|
148
170
|
const io = options.io ?? captured.io;
|
|
149
171
|
const summonEnvInput = {
|
|
@@ -156,7 +178,11 @@ async function summonPublicRole(options) {
|
|
|
156
178
|
seat,
|
|
157
179
|
...options.extraPiArgs === void 0 ? {} : { extraPiArgs: options.extraPiArgs },
|
|
158
180
|
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
159
|
-
...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters }
|
|
181
|
+
...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
|
|
182
|
+
...options.principalAuthority === void 0 ? {} : { principalAuthority: options.principalAuthority },
|
|
183
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
184
|
+
// Parent model is already host-facing; child must not project again.
|
|
185
|
+
...options.model === void 0 ? {} : { hostFacingModel: options.model }
|
|
160
186
|
};
|
|
161
187
|
async function prepareSummonEnv(afterHost) {
|
|
162
188
|
try {
|
|
@@ -168,7 +194,9 @@ async function summonPublicRole(options) {
|
|
|
168
194
|
ok: true,
|
|
169
195
|
env: {
|
|
170
196
|
...built.env,
|
|
171
|
-
|
|
197
|
+
// Nested court stations keep station-child semantics; dual-lens
|
|
198
|
+
// ordinary Reviewer axes opt out (#946 / ADR 0082).
|
|
199
|
+
...options.stationChild === false ? {} : { stationChild: true },
|
|
172
200
|
// Forward composition-root adapters so nested court stations (e.g.
|
|
173
201
|
// countersign → diarist) select the same faux/production table (#924).
|
|
174
202
|
...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
|
|
@@ -178,7 +206,8 @@ async function summonPublicRole(options) {
|
|
|
178
206
|
...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
|
|
179
207
|
...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
|
|
180
208
|
...options.correlationId === void 0 || options.correlationId.trim() === "" ? {} : { correlationId: options.correlationId },
|
|
181
|
-
...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
|
|
209
|
+
...options.createRunId === void 0 ? {} : { createRunId: options.createRunId },
|
|
210
|
+
...options.executionCwd === void 0 ? {} : { executionCwd: options.executionCwd }
|
|
182
211
|
}
|
|
183
212
|
};
|
|
184
213
|
} catch (error) {
|
|
@@ -290,6 +319,16 @@ async function summonPublicRole(options) {
|
|
|
290
319
|
result = stepped.ok;
|
|
291
320
|
break;
|
|
292
321
|
}
|
|
322
|
+
case "reviewer": {
|
|
323
|
+
const [{ runPublicReviewer }, { parseReviewerArgv }] = await Promise.all([
|
|
324
|
+
import("./public-cli/reviewer-run.js"),
|
|
325
|
+
import("./public-cli/invocation.js")
|
|
326
|
+
]);
|
|
327
|
+
const stepped = await runPrepared(parseReviewerArgv, (env, once) => runPublicReviewer(options.argv, env, io, once));
|
|
328
|
+
if ("fail" in stepped) return stepped.fail;
|
|
329
|
+
result = stepped.ok;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
293
332
|
}
|
|
294
333
|
const stderr = captured?.stderrText();
|
|
295
334
|
const runDirectory = result.admitted?.runDirectory;
|
|
@@ -301,6 +340,296 @@ async function summonPublicRole(options) {
|
|
|
301
340
|
...stderr === void 0 || stderr === "" ? {} : { stderr }
|
|
302
341
|
};
|
|
303
342
|
}
|
|
343
|
+
function withReviewerLens(argv, lens) {
|
|
344
|
+
const separator = argv.indexOf("--");
|
|
345
|
+
if (separator === -1) return [...argv, "--lens", lens];
|
|
346
|
+
return [...argv.slice(0, separator), "--lens", lens, ...argv.slice(separator)];
|
|
347
|
+
}
|
|
348
|
+
async function resolveReviewerWorktreeRoots(projectRoot) {
|
|
349
|
+
const callerProjectRoot = await realpath(projectRoot);
|
|
350
|
+
const sourceProjectRoot = await realpath((await execFileAsync(
|
|
351
|
+
"git",
|
|
352
|
+
["rev-parse", "--show-toplevel"],
|
|
353
|
+
{ cwd: callerProjectRoot }
|
|
354
|
+
)).stdout.trim());
|
|
355
|
+
const callerRelative = relative(sourceProjectRoot, callerProjectRoot);
|
|
356
|
+
const projectRelative = callerRelative === "" || callerRelative === "." || callerRelative.startsWith(`..${sep}`) || callerRelative === ".." ? "" : callerRelative;
|
|
357
|
+
return { callerProjectRoot, sourceProjectRoot, projectRelative };
|
|
358
|
+
}
|
|
359
|
+
function reviewerSandboxPath(worktreeAxis, projectRelative) {
|
|
360
|
+
return projectRelative === "" ? worktreeAxis : join(worktreeAxis, projectRelative);
|
|
361
|
+
}
|
|
362
|
+
async function openEphemeralReviewerWorktree(options) {
|
|
363
|
+
const { sourceProjectRoot, projectRelative } = await resolveReviewerWorktreeRoots(
|
|
364
|
+
options.projectRoot
|
|
365
|
+
);
|
|
366
|
+
const { stdout: headStdout } = await execFileAsync(
|
|
367
|
+
"git",
|
|
368
|
+
["rev-parse", "--verify", "HEAD^{commit}"],
|
|
369
|
+
{ cwd: sourceProjectRoot }
|
|
370
|
+
);
|
|
371
|
+
const targetCommit = headStdout.trim();
|
|
372
|
+
const root = await mkdtemp(join(tmpdir(), "ak-reviewer-sandbox-"));
|
|
373
|
+
const worktreeRoot = join(root, "work");
|
|
374
|
+
let registered = false;
|
|
375
|
+
const rollback = async (cause) => {
|
|
376
|
+
const cleanupErrors = [];
|
|
377
|
+
if (registered) {
|
|
378
|
+
try {
|
|
379
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreeRoot], {
|
|
380
|
+
cwd: sourceProjectRoot
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
cleanupErrors.push(error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
await rm(root, { recursive: true, force: true });
|
|
388
|
+
} catch (error) {
|
|
389
|
+
cleanupErrors.push(error);
|
|
390
|
+
}
|
|
391
|
+
if (cleanupErrors.length > 0) {
|
|
392
|
+
options.onCleanupDiagnostic?.([
|
|
393
|
+
"reviewer worktree cleanup failed",
|
|
394
|
+
...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
|
|
395
|
+
].join("\n"));
|
|
396
|
+
}
|
|
397
|
+
throw cause;
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
await execFileAsync("git", ["worktree", "add", "--detach", worktreeRoot, targetCommit], {
|
|
401
|
+
cwd: sourceProjectRoot
|
|
402
|
+
});
|
|
403
|
+
registered = true;
|
|
404
|
+
if (projectRelative !== "") {
|
|
405
|
+
await mkdir(reviewerSandboxPath(worktreeRoot, projectRelative), { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
await rollback(error);
|
|
409
|
+
}
|
|
410
|
+
let closed = false;
|
|
411
|
+
return {
|
|
412
|
+
executionCwd: reviewerSandboxPath(worktreeRoot, projectRelative),
|
|
413
|
+
close: async () => {
|
|
414
|
+
if (closed) return;
|
|
415
|
+
closed = true;
|
|
416
|
+
const cleanupErrors = [];
|
|
417
|
+
try {
|
|
418
|
+
await execFileAsync("git", ["worktree", "remove", worktreeRoot], {
|
|
419
|
+
cwd: sourceProjectRoot
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
cleanupErrors.push(error);
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
await rm(root, { recursive: true, force: true });
|
|
426
|
+
} catch (error) {
|
|
427
|
+
cleanupErrors.push(error);
|
|
428
|
+
}
|
|
429
|
+
if (cleanupErrors.length > 0) {
|
|
430
|
+
options.onCleanupDiagnostic?.([
|
|
431
|
+
"reviewer worktree cleanup failed",
|
|
432
|
+
...cleanupErrors.map((error) => error instanceof Error ? error.message : String(error))
|
|
433
|
+
].join("\n"));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function withEphemeralReviewerWorktree(options) {
|
|
439
|
+
const sandbox = await openEphemeralReviewerWorktree({
|
|
440
|
+
projectRoot: options.projectRoot,
|
|
441
|
+
...options.onCleanupDiagnostic === void 0 ? {} : { onCleanupDiagnostic: options.onCleanupDiagnostic }
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
return await options.run(sandbox.executionCwd);
|
|
445
|
+
} finally {
|
|
446
|
+
await sandbox.close();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function summonParallelReviewerLenses(options) {
|
|
450
|
+
const describeFailure = (error) => {
|
|
451
|
+
if (error instanceof AggregateError) {
|
|
452
|
+
return [error.message, ...error.errors.map(describeFailure)].join("\n");
|
|
453
|
+
}
|
|
454
|
+
return error instanceof Error ? error.message : String(error);
|
|
455
|
+
};
|
|
456
|
+
const failedResult = (error) => ({
|
|
457
|
+
exitCode: 1,
|
|
458
|
+
stderr: describeFailure(error)
|
|
459
|
+
});
|
|
460
|
+
const dualFailure = (error) => {
|
|
461
|
+
const failure = failedResult(error);
|
|
462
|
+
return { completeness: failure, correctness: failure };
|
|
463
|
+
};
|
|
464
|
+
let sourceProjectRoot;
|
|
465
|
+
let projectRelative;
|
|
466
|
+
let targetCommit;
|
|
467
|
+
const statusArgs = [
|
|
468
|
+
"status",
|
|
469
|
+
"--porcelain=v1",
|
|
470
|
+
"--untracked-files=all",
|
|
471
|
+
"--",
|
|
472
|
+
":/",
|
|
473
|
+
":(top,exclude).claude/worktrees/**"
|
|
474
|
+
];
|
|
475
|
+
try {
|
|
476
|
+
const roots = await resolveReviewerWorktreeRoots(options.projectRoot);
|
|
477
|
+
sourceProjectRoot = roots.sourceProjectRoot;
|
|
478
|
+
projectRelative = roots.projectRelative;
|
|
479
|
+
const { stdout: statusStdout } = await execFileAsync("git", statusArgs, {
|
|
480
|
+
cwd: sourceProjectRoot
|
|
481
|
+
});
|
|
482
|
+
if (statusStdout !== "") {
|
|
483
|
+
const diagnostic = [
|
|
484
|
+
"Reviewer target status gate failed:",
|
|
485
|
+
"git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
|
|
486
|
+
statusStdout
|
|
487
|
+
].join("\n");
|
|
488
|
+
return dualFailure(new Error(diagnostic));
|
|
489
|
+
}
|
|
490
|
+
const { stdout: targetStdout } = await execFileAsync(
|
|
491
|
+
"git",
|
|
492
|
+
["rev-parse", "--verify", "HEAD^{commit}"],
|
|
493
|
+
{ cwd: sourceProjectRoot }
|
|
494
|
+
);
|
|
495
|
+
targetCommit = targetStdout.trim();
|
|
496
|
+
await execFileAsync(
|
|
497
|
+
"git",
|
|
498
|
+
["rev-parse", "--verify", `${options.baseRevision}^{commit}`],
|
|
499
|
+
{ cwd: sourceProjectRoot }
|
|
500
|
+
);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return dualFailure(error);
|
|
503
|
+
}
|
|
504
|
+
const root = await mkdtemp(join(tmpdir(), "ak-reviewer-lenses-"));
|
|
505
|
+
const completenessRoot = join(root, "completeness");
|
|
506
|
+
const correctnessRoot = join(root, "correctness");
|
|
507
|
+
const worktrees = [completenessRoot, correctnessRoot];
|
|
508
|
+
const created = /* @__PURE__ */ new Set();
|
|
509
|
+
let results;
|
|
510
|
+
const creation = await Promise.allSettled(
|
|
511
|
+
worktrees.map(async (path) => {
|
|
512
|
+
await execFileAsync("git", ["worktree", "add", "--detach", path, targetCommit], {
|
|
513
|
+
cwd: sourceProjectRoot
|
|
514
|
+
});
|
|
515
|
+
created.add(path);
|
|
516
|
+
if (projectRelative !== "") {
|
|
517
|
+
await mkdir(reviewerSandboxPath(path, projectRelative), { recursive: true });
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
const creationFailures = creation.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
522
|
+
if (creationFailures.length > 0) {
|
|
523
|
+
const failure = failedResult(new AggregateError(
|
|
524
|
+
creationFailures,
|
|
525
|
+
"parallel reviewer worktree creation failed"
|
|
526
|
+
));
|
|
527
|
+
results = { completeness: failure, correctness: failure };
|
|
528
|
+
} else {
|
|
529
|
+
const summon = (lens, worktreeAxis) => {
|
|
530
|
+
const sandbox = reviewerSandboxPath(worktreeAxis, projectRelative);
|
|
531
|
+
return summonPublicRole({
|
|
532
|
+
role: "reviewer",
|
|
533
|
+
argv: withReviewerLens(options.argv, lens),
|
|
534
|
+
cwd: options.cwd,
|
|
535
|
+
executionCwd: sandbox,
|
|
536
|
+
// Ordinary single-axis public semantics — not a nested court station.
|
|
537
|
+
stationChild: false,
|
|
538
|
+
home: options.home,
|
|
539
|
+
...options.agentDir === void 0 ? {} : { agentDir: options.agentDir },
|
|
540
|
+
...options.credentials === void 0 ? {} : { credentials: options.credentials },
|
|
541
|
+
...options.model === void 0 ? {} : { model: options.model },
|
|
542
|
+
...options.host === void 0 ? {} : { host: options.host },
|
|
543
|
+
...options.engine === void 0 ? {} : { engine: options.engine },
|
|
544
|
+
...options.engineModel === void 0 ? {} : { engineModel: options.engineModel },
|
|
545
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
546
|
+
...options.correlationId === void 0 ? {} : { correlationId: options.correlationId },
|
|
547
|
+
...options.packageRoot === void 0 ? {} : { packageRoot: options.packageRoot },
|
|
548
|
+
...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
|
|
549
|
+
...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
|
|
550
|
+
...options.principalAuthority === void 0 ? {} : { principalAuthority: options.principalAuthority },
|
|
551
|
+
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
const settled = await Promise.allSettled([
|
|
555
|
+
summon("completeness", completenessRoot),
|
|
556
|
+
summon("correctness", correctnessRoot)
|
|
557
|
+
]);
|
|
558
|
+
results = {
|
|
559
|
+
completeness: settled[0].status === "fulfilled" ? settled[0].value : failedResult(settled[0].reason),
|
|
560
|
+
correctness: settled[1].status === "fulfilled" ? settled[1].value : failedResult(settled[1].reason)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
let sealDiagnostic;
|
|
564
|
+
try {
|
|
565
|
+
const { stdout: headBeforeStdout } = await execFileAsync(
|
|
566
|
+
"git",
|
|
567
|
+
["rev-parse", "--verify", "HEAD^{commit}"],
|
|
568
|
+
{ cwd: sourceProjectRoot }
|
|
569
|
+
);
|
|
570
|
+
const { stdout: sealedStatusStdout } = await execFileAsync("git", statusArgs, {
|
|
571
|
+
cwd: sourceProjectRoot
|
|
572
|
+
});
|
|
573
|
+
const { stdout: headAfterStdout } = await execFileAsync(
|
|
574
|
+
"git",
|
|
575
|
+
["rev-parse", "--verify", "HEAD^{commit}"],
|
|
576
|
+
{ cwd: sourceProjectRoot }
|
|
577
|
+
);
|
|
578
|
+
const headBefore = headBeforeStdout.trim();
|
|
579
|
+
const headAfter = headAfterStdout.trim();
|
|
580
|
+
if (headBefore !== targetCommit || headAfter !== targetCommit || sealedStatusStdout !== "") {
|
|
581
|
+
sealDiagnostic = [
|
|
582
|
+
"Reviewer target final seal failed:",
|
|
583
|
+
`HEAD before status => ${headBefore}`,
|
|
584
|
+
`HEAD after status => ${headAfter}`,
|
|
585
|
+
`expected PRE_HEAD ${targetCommit}`,
|
|
586
|
+
"git status --porcelain=v1 --untracked-files=all -- :/ ':(top,exclude).claude/worktrees/**'",
|
|
587
|
+
sealedStatusStdout
|
|
588
|
+
].join("\n");
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
sealDiagnostic = `Reviewer target final seal failed:
|
|
592
|
+
${describeFailure(error)}`;
|
|
593
|
+
}
|
|
594
|
+
if (sealDiagnostic !== void 0) {
|
|
595
|
+
const withSealFailure = (result) => ({
|
|
596
|
+
...result,
|
|
597
|
+
exitCode: 1,
|
|
598
|
+
stderr: [result.stderr, sealDiagnostic].filter(
|
|
599
|
+
(text) => typeof text === "string" && text !== ""
|
|
600
|
+
).join("\n")
|
|
601
|
+
});
|
|
602
|
+
results = {
|
|
603
|
+
completeness: withSealFailure(results.completeness),
|
|
604
|
+
correctness: withSealFailure(results.correctness)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const cleanup = await Promise.allSettled(
|
|
608
|
+
[...created].map((path) => execFileAsync("git", ["worktree", "remove", path], { cwd: sourceProjectRoot }))
|
|
609
|
+
);
|
|
610
|
+
const cleanupFailures = cleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
611
|
+
if (cleanupFailures.length === 0) {
|
|
612
|
+
const rootCleanup = await Promise.allSettled([rm(root, { recursive: true, force: true })]);
|
|
613
|
+
cleanupFailures.push(...rootCleanup.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
|
|
614
|
+
}
|
|
615
|
+
if (cleanupFailures.length > 0) {
|
|
616
|
+
const diagnostic = [
|
|
617
|
+
"parallel reviewer worktree cleanup failed",
|
|
618
|
+
...cleanupFailures.map((failure) => failure instanceof Error ? failure.message : String(failure))
|
|
619
|
+
].join("\n");
|
|
620
|
+
const withCleanupDiagnostic = (result) => ({
|
|
621
|
+
...result,
|
|
622
|
+
stderr: [result.stderr, diagnostic].filter(
|
|
623
|
+
(text) => typeof text === "string" && text !== ""
|
|
624
|
+
).join("\n")
|
|
625
|
+
});
|
|
626
|
+
results = {
|
|
627
|
+
completeness: withCleanupDiagnostic(results.completeness),
|
|
628
|
+
correctness: withCleanupDiagnostic(results.correctness)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return results;
|
|
632
|
+
}
|
|
304
633
|
async function summonGateOfficer(options) {
|
|
305
634
|
let home = options.home;
|
|
306
635
|
if (home === void 0) {
|
|
@@ -352,7 +681,10 @@ async function summonGateOfficer(options) {
|
|
|
352
681
|
}
|
|
353
682
|
export {
|
|
354
683
|
AK_ROLE_PACKAGE_ROOT_ENV,
|
|
684
|
+
openEphemeralReviewerWorktree,
|
|
355
685
|
resolveSummonsPackageRoot,
|
|
356
686
|
summonGateOfficer,
|
|
357
|
-
|
|
687
|
+
summonParallelReviewerLenses,
|
|
688
|
+
summonPublicRole,
|
|
689
|
+
withEphemeralReviewerWorktree
|
|
358
690
|
};
|
package/package.json
CHANGED
package/src/host-contracts.ts
CHANGED
|
@@ -100,7 +100,6 @@ export type RoleTurnActivation =
|
|
|
100
100
|
| {
|
|
101
101
|
readonly role: "reviewer";
|
|
102
102
|
readonly baseRevision: string;
|
|
103
|
-
/** Caller-selected single lens; no default (ADR 0082 reviewer-method-lens). */
|
|
104
103
|
readonly lens: "completeness" | "correctness";
|
|
105
104
|
readonly authorityRefs: readonly string[];
|
|
106
105
|
readonly ticketNumber?: number;
|
|
@@ -239,14 +239,14 @@ export type AdmittedNotaryInvocation = AdmittedRoleInvocationBase & {
|
|
|
239
239
|
readonly sourceRun: NotarySourceRunLocator;
|
|
240
240
|
};
|
|
241
241
|
|
|
242
|
-
/**
|
|
242
|
+
/** Durable single-axis Reviewer lens. Parallel default is parse-only omission, never admitted. */
|
|
243
243
|
export type ReviewerLens = "completeness" | "correctness";
|
|
244
244
|
|
|
245
245
|
export type AdmittedReviewerInvocation = AdmittedRoleInvocationBase & {
|
|
246
246
|
readonly role: "reviewer";
|
|
247
247
|
/** Required fixed base revision for the pinned review target (ADR 0037). */
|
|
248
248
|
readonly baseRevision: string;
|
|
249
|
-
/**
|
|
249
|
+
/** Frozen single-axis shape for an ordinary Reviewer run; reused on resume. */
|
|
250
250
|
readonly lens: ReviewerLens;
|
|
251
251
|
/**
|
|
252
252
|
* Required durable authority references/URLs frozen at admission.
|
|
@@ -929,8 +929,11 @@ export type ParseReviewerArgvResult = {
|
|
|
929
929
|
attachmentPaths: string[];
|
|
930
930
|
/** Required fixed base revision for the pinned review target. */
|
|
931
931
|
baseRevision: string;
|
|
932
|
-
/**
|
|
933
|
-
|
|
932
|
+
/**
|
|
933
|
+
* Explicit single-axis override. Omitted public `--lens` is the internal
|
|
934
|
+
* parallel two-axis branch mark only — never persisted on an admitted run.
|
|
935
|
+
*/
|
|
936
|
+
lens?: ReviewerLens;
|
|
934
937
|
/** Repeatable durable authority references/URLs (exact order preserved; at least one). */
|
|
935
938
|
authorityRefs: string[];
|
|
936
939
|
project?: string;
|
|
@@ -1048,7 +1051,7 @@ export function isReviewerLens(value: unknown): value is ReviewerLens {
|
|
|
1048
1051
|
return value === "completeness" || value === "correctness";
|
|
1049
1052
|
}
|
|
1050
1053
|
|
|
1051
|
-
/**
|
|
1054
|
+
/** Admitted single-axis lens enum; public `--lens all` and bare omission are not admitted values. */
|
|
1052
1055
|
export function requireReviewerLens(value: string | undefined): ReviewerLens {
|
|
1053
1056
|
const trimmed = (value ?? "").trim();
|
|
1054
1057
|
if (!isReviewerLens(trimmed)) {
|
|
@@ -3073,7 +3076,7 @@ export function parseReviewerArgv(
|
|
|
3073
3076
|
continue;
|
|
3074
3077
|
}
|
|
3075
3078
|
if (taken.def.id === "lens") {
|
|
3076
|
-
//
|
|
3079
|
+
// Public override is single-axis only; omission stays undefined for the parallel branch.
|
|
3077
3080
|
lens = requireReviewerLens(taken.value);
|
|
3078
3081
|
continue;
|
|
3079
3082
|
}
|
|
@@ -3090,13 +3093,13 @@ export function parseReviewerArgv(
|
|
|
3090
3093
|
positional.push(token);
|
|
3091
3094
|
}
|
|
3092
3095
|
|
|
3093
|
-
//
|
|
3096
|
+
// Base and authority are required; omitted lens selects the parallel two-axis mode.
|
|
3094
3097
|
options.assertRequired();
|
|
3095
3098
|
return {
|
|
3096
3099
|
instruction: positional.join(" "),
|
|
3097
3100
|
attachmentPaths,
|
|
3098
3101
|
baseRevision: baseRevision!,
|
|
3099
|
-
lens: lens
|
|
3102
|
+
...(lens === undefined ? {} : { lens }),
|
|
3100
3103
|
authorityRefs,
|
|
3101
3104
|
...(project === undefined ? {} : { project }),
|
|
3102
3105
|
};
|
|
@@ -3110,12 +3113,13 @@ export type AdmitReviewerInvocationOptions = {
|
|
|
3110
3113
|
instruction: string;
|
|
3111
3114
|
attachmentPaths: readonly string[];
|
|
3112
3115
|
baseRevision: string;
|
|
3113
|
-
/**
|
|
3116
|
+
/** Explicit single-axis shape, frozen unchanged at admission and resume. */
|
|
3114
3117
|
lens: ReviewerLens;
|
|
3115
3118
|
/** Required durable authority references/URLs; frozen unchanged at admission. */
|
|
3116
3119
|
authorityRefs: readonly string[];
|
|
3117
3120
|
project?: string;
|
|
3118
3121
|
createRunId?: () => string;
|
|
3122
|
+
correlationId?: string;
|
|
3119
3123
|
/** Effective model for this invocation — written onto invocation.json. */
|
|
3120
3124
|
model?: InvocationEffectiveModel;
|
|
3121
3125
|
};
|
|
@@ -3175,6 +3179,9 @@ export async function admitReviewerInvocation(
|
|
|
3175
3179
|
baseRevision,
|
|
3176
3180
|
lens,
|
|
3177
3181
|
authorityRefs: [...authorityRefs],
|
|
3182
|
+
...(options.correlationId === undefined
|
|
3183
|
+
? {}
|
|
3184
|
+
: { correlationId: options.correlationId }),
|
|
3178
3185
|
attachments: attachments.map((a) => ({
|
|
3179
3186
|
provenancePath: a.provenancePath,
|
|
3180
3187
|
frozenPath: a.frozenPath,
|
|
@@ -3204,6 +3211,9 @@ export async function admitReviewerInvocation(
|
|
|
3204
3211
|
baseRevision,
|
|
3205
3212
|
lens,
|
|
3206
3213
|
authorityRefs,
|
|
3214
|
+
...(options.correlationId === undefined
|
|
3215
|
+
? {}
|
|
3216
|
+
: { correlationId: options.correlationId }),
|
|
3207
3217
|
};
|
|
3208
3218
|
}
|
|
3209
3219
|
|
|
@@ -552,12 +552,12 @@ const REVIEWER_OPTIONS = [
|
|
|
552
552
|
canonical: "--lens",
|
|
553
553
|
aliases: [],
|
|
554
554
|
valueMetavar: "completeness|correctness",
|
|
555
|
-
required:
|
|
555
|
+
required: false,
|
|
556
556
|
repeatable: false,
|
|
557
557
|
form: "option",
|
|
558
558
|
description: {
|
|
559
|
-
en: "
|
|
560
|
-
zh: "
|
|
559
|
+
en: "Optional single-lens override: completeness or correctness; omitted runs both in parallel.",
|
|
560
|
+
zh: "可选单 lens 覆盖:completeness 或 correctness;省略时并行运行两轴。",
|
|
561
561
|
},
|
|
562
562
|
},
|
|
563
563
|
{
|
|
@@ -1227,9 +1227,9 @@ const ROLE_COMMAND_HELP = {
|
|
|
1227
1227
|
},
|
|
1228
1228
|
reviewer: {
|
|
1229
1229
|
command: "reviewer",
|
|
1230
|
-
summary: "Fixed-target
|
|
1230
|
+
summary: "Fixed-target parallel two-lens review, with an optional single-lens override.",
|
|
1231
1231
|
usage: [
|
|
1232
|
-
"ak-role reviewer --base <revision> --lens completeness|correctness --authority-ref <ref> [options] <instruction>",
|
|
1232
|
+
"ak-role reviewer --base <revision> [--lens completeness|correctness] --authority-ref <ref> [options] <instruction>",
|
|
1233
1233
|
],
|
|
1234
1234
|
examples: [
|
|
1235
1235
|
'ak-role reviewer --base main --lens completeness --authority-ref docs/adr/0001.md "Review the branch."',
|