@mgiles/perk 3.2.0 → 3.3.0
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 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +365 -380
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
- package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/pi/v1/lifecycleGates.ts +127 -0
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -21
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +5 -4
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +263 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +178 -163
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +87 -46
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +43 -18
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -6
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +1 -1
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review-browser/stack.md +5 -6
- package/shared/README.md +8 -0
- package/shared/bindings.yaml +3 -3
- package/shared/contracts.md +2601 -506
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +13 -0
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -489
- package/extension/doors/learn.ts +0 -668
- package/extension/doors/lifecycleGates.ts +0 -207
- package/extension/doors/objectiveStack.ts +0 -1543
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -279
- package/extension/doors/submit.ts +0 -373
- package/extension/doors/submitPrReview.ts +0 -505
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectiveSave.ts +0 -366
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Participating submit/address writers exclude each other by canonical worktree Git identity.
|
|
2
|
+
// There is NO reclamation policy. Death, age, reload and cancellation cannot prove quiescence.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readSync,
|
|
12
|
+
type Stats,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { worktreeGitDir } from "./git.ts";
|
|
18
|
+
|
|
19
|
+
const OWNER_LIMIT = 16 * 1024;
|
|
20
|
+
export const WORKTREE_RESOLVER_LOCK = "perk-submit-conflict.lock";
|
|
21
|
+
|
|
22
|
+
interface LockRecord {
|
|
23
|
+
schema: 1;
|
|
24
|
+
token: string;
|
|
25
|
+
pid: number;
|
|
26
|
+
parentSessionId: string;
|
|
27
|
+
ownerRunId: string;
|
|
28
|
+
requestId: string;
|
|
29
|
+
worktreeIdentity: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
export type LockOwner = Omit<LockRecord, "token" | "schema">;
|
|
33
|
+
export type LockFinish = {
|
|
34
|
+
kind: "released" | "retained" | "ownership-error" | "io-error";
|
|
35
|
+
path: string;
|
|
36
|
+
};
|
|
37
|
+
export interface WorktreeResolverClaim {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
check(): "owned" | "ownership-error" | "io-error";
|
|
40
|
+
/** First choice wins, locally idempotent. Retain closes resources but never removes metadata. */
|
|
41
|
+
finish(disposition: "release" | "retain"): LockFinish;
|
|
42
|
+
}
|
|
43
|
+
export type WorktreeResolverAcquisition =
|
|
44
|
+
| { kind: "acquired"; claim: WorktreeResolverClaim }
|
|
45
|
+
| { kind: "unavailable" }
|
|
46
|
+
| { kind: "busy"; path: string; owner?: LockOwner }
|
|
47
|
+
| { kind: "io-error"; path: string; residue: boolean };
|
|
48
|
+
|
|
49
|
+
/** Only deterministic filesystem fault tests substitute these operations. */
|
|
50
|
+
export interface WorktreeLockFs {
|
|
51
|
+
open: typeof openSync;
|
|
52
|
+
close: typeof closeSync;
|
|
53
|
+
fstat: (fd: number) => Stats;
|
|
54
|
+
lstat: (path: string) => Stats;
|
|
55
|
+
read: (fd: number, buffer: Buffer, offset: number, length: number, position: number) => number;
|
|
56
|
+
write: (fd: number, data: string) => void;
|
|
57
|
+
sync: (fd: number) => void;
|
|
58
|
+
unlink: (path: string) => void;
|
|
59
|
+
}
|
|
60
|
+
const realFs: WorktreeLockFs = {
|
|
61
|
+
open: openSync,
|
|
62
|
+
close: closeSync,
|
|
63
|
+
fstat: fstatSync,
|
|
64
|
+
lstat: lstatSync,
|
|
65
|
+
read: readSync,
|
|
66
|
+
write: (fd, data) => writeFileSync(fd, data, "utf8"),
|
|
67
|
+
sync: fsyncSync,
|
|
68
|
+
unlink: unlinkSync,
|
|
69
|
+
};
|
|
70
|
+
function code(error: unknown): unknown {
|
|
71
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
72
|
+
}
|
|
73
|
+
function sameFile(a: Stats, b: Stats): boolean {
|
|
74
|
+
return a.isFile() && b.isFile() && a.dev === b.dev && a.ino === b.ino;
|
|
75
|
+
}
|
|
76
|
+
function readRecord(fs: WorktreeLockFs, fd: number): LockRecord | null {
|
|
77
|
+
const size = fs.fstat(fd).size;
|
|
78
|
+
if (size <= 0 || size > OWNER_LIMIT) return null;
|
|
79
|
+
const bytes = Buffer.alloc(size);
|
|
80
|
+
let read = 0;
|
|
81
|
+
while (read < size) {
|
|
82
|
+
const n = fs.read(fd, bytes, read, size - read, read);
|
|
83
|
+
if (n === 0) return null;
|
|
84
|
+
read += n;
|
|
85
|
+
}
|
|
86
|
+
let raw: unknown;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(bytes.toString("utf8"));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
93
|
+
const r = raw as Record<string, unknown>;
|
|
94
|
+
if (
|
|
95
|
+
r.schema !== 1 ||
|
|
96
|
+
typeof r.token !== "string" ||
|
|
97
|
+
!r.token ||
|
|
98
|
+
typeof r.pid !== "number" ||
|
|
99
|
+
!Number.isInteger(r.pid) ||
|
|
100
|
+
r.pid <= 0
|
|
101
|
+
)
|
|
102
|
+
return null;
|
|
103
|
+
for (const field of [
|
|
104
|
+
"parentSessionId",
|
|
105
|
+
"ownerRunId",
|
|
106
|
+
"requestId",
|
|
107
|
+
"worktreeIdentity",
|
|
108
|
+
"createdAt",
|
|
109
|
+
] as const) {
|
|
110
|
+
if (typeof r[field] !== "string" || !r[field]) return null;
|
|
111
|
+
}
|
|
112
|
+
return r as unknown as LockRecord;
|
|
113
|
+
}
|
|
114
|
+
function ownerOf(r: LockRecord): LockOwner {
|
|
115
|
+
return {
|
|
116
|
+
pid: r.pid,
|
|
117
|
+
parentSessionId: r.parentSessionId,
|
|
118
|
+
ownerRunId: r.ownerRunId,
|
|
119
|
+
requestId: r.requestId,
|
|
120
|
+
worktreeIdentity: r.worktreeIdentity,
|
|
121
|
+
createdAt: r.createdAt,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function incumbent(fs: WorktreeLockFs, path: string): WorktreeResolverAcquisition {
|
|
126
|
+
let fd: number | undefined;
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.lstat(path);
|
|
129
|
+
if (!stat.isFile() || stat.size > OWNER_LIMIT) return { kind: "busy", path };
|
|
130
|
+
fd = fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
131
|
+
if (!sameFile(stat, fs.fstat(fd))) return { kind: "busy", path };
|
|
132
|
+
const record = readRecord(fs, fd);
|
|
133
|
+
return { kind: "busy", path, ...(record ? { owner: ownerOf(record) } : {}) };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Even a disappearing incumbent doesn't invite a retry. A racing external replacement is
|
|
136
|
+
// outside the protocol; ELOOP never follows the new symlink for diagnostic reads.
|
|
137
|
+
if (code(error) === "ENOENT" || code(error) === "ELOOP") return { kind: "busy", path };
|
|
138
|
+
return { kind: "io-error", path, residue: true };
|
|
139
|
+
} finally {
|
|
140
|
+
if (fd !== undefined) fs.close(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function acquireWorktreeResolverLock(
|
|
145
|
+
cwd: string,
|
|
146
|
+
parent: { sessionId: string; runId: string; requestId: string },
|
|
147
|
+
opts: {
|
|
148
|
+
fs?: Partial<WorktreeLockFs>;
|
|
149
|
+
gitDir?: (cwd: string) => string | null;
|
|
150
|
+
/** The token minter (default `randomUUID`) — only deterministic fault tests substitute it. */
|
|
151
|
+
token?: () => string;
|
|
152
|
+
} = {},
|
|
153
|
+
): WorktreeResolverAcquisition {
|
|
154
|
+
const identity = (opts.gitDir ?? worktreeGitDir)(cwd);
|
|
155
|
+
if (identity === null) return { kind: "unavailable" };
|
|
156
|
+
const path = join(identity, WORKTREE_RESOLVER_LOCK);
|
|
157
|
+
const fs = { ...realFs, ...opts.fs };
|
|
158
|
+
let fd: number;
|
|
159
|
+
try {
|
|
160
|
+
fd = fs.open(path, "wx", 0o600);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (code(error) === "EEXIST") {
|
|
163
|
+
try {
|
|
164
|
+
return incumbent(fs, path);
|
|
165
|
+
} catch {
|
|
166
|
+
return { kind: "io-error", path, residue: true };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { kind: "io-error", path, residue: false };
|
|
170
|
+
}
|
|
171
|
+
// Everything after the exclusive create runs inside the cleanup-protected block — the record
|
|
172
|
+
// construction included: a token-minting failure must take the same identity-fenced unlink
|
|
173
|
+
// path as a write failure, never escape with the descriptor open and an empty lock file that
|
|
174
|
+
// would wedge every later acquisition as busy.
|
|
175
|
+
let stat: Stats | undefined;
|
|
176
|
+
let token: string;
|
|
177
|
+
try {
|
|
178
|
+
stat = fs.fstat(fd);
|
|
179
|
+
if (!stat.isFile()) throw new Error("not a regular file");
|
|
180
|
+
token = (opts.token ?? randomUUID)();
|
|
181
|
+
const record: LockRecord = {
|
|
182
|
+
schema: 1,
|
|
183
|
+
token,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
parentSessionId: parent.sessionId,
|
|
186
|
+
ownerRunId: parent.runId,
|
|
187
|
+
requestId: parent.requestId,
|
|
188
|
+
worktreeIdentity: identity,
|
|
189
|
+
createdAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
fs.write(fd, `${JSON.stringify(record)}\n`);
|
|
192
|
+
fs.sync(fd);
|
|
193
|
+
} catch {
|
|
194
|
+
let residue = true;
|
|
195
|
+
try {
|
|
196
|
+
if (stat && sameFile(stat, fs.lstat(path))) {
|
|
197
|
+
fs.unlink(path);
|
|
198
|
+
residue = false;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
/* Report residue; never remove an unidentified or successor file. */
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
fs.close(fd);
|
|
205
|
+
} catch {
|
|
206
|
+
residue = true;
|
|
207
|
+
}
|
|
208
|
+
return { kind: "io-error", path, residue };
|
|
209
|
+
}
|
|
210
|
+
const acquiredStat = stat;
|
|
211
|
+
let finished: LockFinish | undefined;
|
|
212
|
+
function check(): "owned" | "ownership-error" | "io-error" {
|
|
213
|
+
if (finished) return "ownership-error";
|
|
214
|
+
try {
|
|
215
|
+
if (!sameFile(acquiredStat, fs.lstat(path)) || !sameFile(acquiredStat, fs.fstat(fd)))
|
|
216
|
+
return "ownership-error";
|
|
217
|
+
const reader = fs.open(
|
|
218
|
+
path,
|
|
219
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
if (!sameFile(acquiredStat, fs.fstat(reader))) return "ownership-error";
|
|
223
|
+
return readRecord(fs, reader)?.token === token ? "owned" : "ownership-error";
|
|
224
|
+
} finally {
|
|
225
|
+
fs.close(reader);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return code(error) === "ENOENT" ? "ownership-error" : "io-error";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind: "acquired",
|
|
233
|
+
claim: {
|
|
234
|
+
path,
|
|
235
|
+
check,
|
|
236
|
+
finish(disposition) {
|
|
237
|
+
if (finished) return finished;
|
|
238
|
+
let kind: LockFinish["kind"] = "retained";
|
|
239
|
+
try {
|
|
240
|
+
if (disposition === "release") {
|
|
241
|
+
const ownership = check();
|
|
242
|
+
if (ownership === "owned") {
|
|
243
|
+
fs.unlink(path);
|
|
244
|
+
kind = "released";
|
|
245
|
+
} else kind = ownership;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
kind = "io-error";
|
|
249
|
+
} finally {
|
|
250
|
+
try {
|
|
251
|
+
fs.close(fd);
|
|
252
|
+
} catch {
|
|
253
|
+
kind = "io-error";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finished = { kind, path };
|
|
257
|
+
return finished;
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// surfaces module" is surfaces.ts + report.ts for the surfaces guard).
|
|
7
7
|
//
|
|
8
8
|
// Perk status (charter §6 D2): perk presents ONE footer status under the single
|
|
9
|
-
// `perk` slot — the
|
|
10
|
-
//
|
|
11
|
-
// sanctioned). The perk-owned footer (`perkFooter`/`installPerkFooter` below)
|
|
12
|
-
// directly; the `perk` status slot keeps publishing — it is the RPC-visible
|
|
13
|
-
// is an RPC no-op).
|
|
9
|
+
// `perk` slot — the objective segment composed with the ref-counted activity facet
|
|
10
|
+
// (`<objective> · <activity>`, either half optional). The per-feature status slots and widgets
|
|
11
|
+
// are retired (D8 sanctioned). The perk-owned footer (`perkFooter`/`installPerkFooter` below)
|
|
12
|
+
// renders the value directly; the `perk` status slot keeps publishing — it is the RPC-visible
|
|
13
|
+
// surface (setFooter is an RPC no-op).
|
|
14
14
|
|
|
15
15
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
16
|
import type { ReportDetailSink } from "./report.ts";
|
|
@@ -35,6 +35,19 @@ export const STATUS_SLOT_PERK = "perk";
|
|
|
35
35
|
// --- footer identity marks (charter §5 / D3: emoji are footer-only identity, 2 cells wide) ---
|
|
36
36
|
export const MARK_OBJECTIVE = "🎯";
|
|
37
37
|
|
|
38
|
+
// --- activity vocabulary (the optional second half of the composed `perk` status) ---
|
|
39
|
+
// Plain text, no emoji (D3 keeps emoji as identity marks): a Perk-owned wait the operator
|
|
40
|
+
// cannot otherwise see.
|
|
41
|
+
export const ACTIVITY_BROWSER_REVIEW = "waiting on browser review";
|
|
42
|
+
/** The slice an activity owner takes: begin a wait with its text, receive the matching `end`. */
|
|
43
|
+
export type ActivitySink = (text: string) => () => void;
|
|
44
|
+
/**
|
|
45
|
+
* The least-capable slice a wiring layer threads toward an activity owner: begin a wait against
|
|
46
|
+
* the live target, nothing else. The full `PerkStatusHandle` stays at the composition root, the
|
|
47
|
+
* objective publisher and the footer.
|
|
48
|
+
*/
|
|
49
|
+
export type ActivityHandle = Pick<PerkStatusHandle, "beginActivity">;
|
|
50
|
+
|
|
38
51
|
// --- glyph vocabulary (charter §5 / D3) — charter-law data, pinned by tests ---
|
|
39
52
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
40
53
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
@@ -101,41 +114,80 @@ export function setWorkingMessage(target: WorkingMessageTarget, message?: string
|
|
|
101
114
|
// --- the perk status (charter D2) --------------------------------------------
|
|
102
115
|
|
|
103
116
|
/**
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
117
|
+
* The composed status handle: the objective publisher sets its half through `set`, activity
|
|
118
|
+
* owners hold ref-counted waits through `beginActivity`, and the handle republishes the single
|
|
119
|
+
* `perk` status slot with the composed value. The footer reads it back via `get` and repaints via
|
|
120
|
+
* `subscribe` — the slot's `setStatus` dual-publish is deliberate (RPC clients see the slot;
|
|
121
|
+
* setFooter is an RPC no-op).
|
|
108
122
|
*/
|
|
109
123
|
export interface PerkStatusHandle {
|
|
110
|
-
/** Set (or clear, with undefined) the
|
|
124
|
+
/** Set (or clear, with undefined) the objective half; publishes the slot. No-op headless. */
|
|
111
125
|
set(target: StandingTarget, text: string | undefined): void;
|
|
112
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Begin one activity wait: the newest live wait's text shows while any begun wait is unended;
|
|
128
|
+
* returns this wait's `end` (idempotent; inert after a reset — no publish). No-op headless
|
|
129
|
+
* (returns a no-op `end`, records nothing).
|
|
130
|
+
*/
|
|
131
|
+
beginActivity(target: StandingTarget, text: string): () => void;
|
|
132
|
+
/** Reset every wait (session shutdown); a late `end` of a reset wait is inert. No-op headless. */
|
|
133
|
+
clearActivity(target: StandingTarget): void;
|
|
134
|
+
/** The composed value (`<objective> · <activity>`, either half optional; undefined when neither). */
|
|
113
135
|
get(): string | undefined;
|
|
114
136
|
/**
|
|
115
|
-
* Subscribe to publishes: the listener fires after every headful `set
|
|
116
|
-
* calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
137
|
+
* Subscribe to publishes: the listener fires after every headful `set`, begin, end and clear
|
|
138
|
+
* (headless calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
117
139
|
*/
|
|
118
140
|
subscribe(listener: () => void): () => void;
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
/**
|
|
122
|
-
* Create the
|
|
123
|
-
* and passed to the objective publisher; no hidden module state).
|
|
124
|
-
* (never record
|
|
125
|
-
*
|
|
144
|
+
* Create the composed `perk` status handle (one per extension instance — created in index.ts
|
|
145
|
+
* and passed to the objective publisher and the activity owners; no hidden module state).
|
|
146
|
+
* Headless calls are full no-ops (never record text, so headless-era text can't resurrect in a
|
|
147
|
+
* later headful render). The activity is a SET of live wait tokens, not a set/clear: the browser
|
|
148
|
+
* doors accept a concurrent double-open, so an older wait's settle must not blank a newer one —
|
|
149
|
+
* the text clears only when the LAST wait ends (the newest live wait's text shows meanwhile, so
|
|
150
|
+
* ending one restores the remainder). An ended or reset token is simply absent, so its late `end`
|
|
151
|
+
* finds nothing to remove and publishes nothing — a wait begun after a reset can never be
|
|
152
|
+
* consumed by a stale `end`. No width handling: pi's footer truncates.
|
|
126
153
|
*/
|
|
127
154
|
export function createPerkStatus(): PerkStatusHandle {
|
|
128
|
-
let
|
|
155
|
+
let objective: string | undefined;
|
|
156
|
+
const live = new Set<{ text: string }>(); // insertion-ordered: the last entry is the newest
|
|
129
157
|
const listeners = new Set<() => void>();
|
|
158
|
+
const compose = (): string | undefined => {
|
|
159
|
+
const activity = [...live].at(-1)?.text;
|
|
160
|
+
const halves = [objective, activity].filter((half) => half !== undefined);
|
|
161
|
+
return halves.length === 0 ? undefined : halves.join(" · ");
|
|
162
|
+
};
|
|
163
|
+
const publish = (target: StandingTarget): void => {
|
|
164
|
+
target.ui.setStatus(STATUS_SLOT_PERK, compose());
|
|
165
|
+
for (const listener of listeners) listener();
|
|
166
|
+
};
|
|
130
167
|
return {
|
|
131
168
|
set(target, text) {
|
|
132
169
|
if (!target.hasUI) return;
|
|
133
|
-
|
|
134
|
-
target
|
|
135
|
-
|
|
170
|
+
objective = text;
|
|
171
|
+
publish(target);
|
|
172
|
+
},
|
|
173
|
+
beginActivity(target, text) {
|
|
174
|
+
if (!target.hasUI) return () => {};
|
|
175
|
+
const token = { text };
|
|
176
|
+
live.add(token);
|
|
177
|
+
publish(target);
|
|
178
|
+
return () => {
|
|
179
|
+
// Already ended, or reset by `clearActivity`: nothing to remove, nothing to publish.
|
|
180
|
+
if (!live.delete(token)) return;
|
|
181
|
+
publish(target);
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
clearActivity(target) {
|
|
185
|
+
if (!target.hasUI) return;
|
|
186
|
+
live.clear();
|
|
187
|
+
publish(target);
|
|
136
188
|
},
|
|
137
189
|
get() {
|
|
138
|
-
return
|
|
190
|
+
return compose();
|
|
139
191
|
},
|
|
140
192
|
subscribe(listener) {
|
|
141
193
|
listeners.add(listener);
|
|
@@ -155,7 +207,7 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
155
207
|
export interface FooterParts {
|
|
156
208
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
157
209
|
identity: string;
|
|
158
|
-
/** The
|
|
210
|
+
/** The composed perk status value (`handle.get()`), verbatim — it carries its own 🎯 mark. */
|
|
159
211
|
objective?: string;
|
|
160
212
|
/** Git branch (dim); omitted when not in a repo. */
|
|
161
213
|
branch?: string;
|
|
@@ -329,9 +381,9 @@ export type PerkFooterFactory = (
|
|
|
329
381
|
/**
|
|
330
382
|
* The perk-owned footer factory (charter D2): replaces pi's default footer wholesale with one
|
|
331
383
|
* line in the intended split layout. `render` gathers everything live per call (D10 stateless
|
|
332
|
-
* render): the
|
|
333
|
-
* own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the
|
|
334
|
-
* directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
384
|
+
* render): the composed status value via the handle, branch/guests via `footerData` (excluding
|
|
385
|
+
* perk's own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the
|
|
386
|
+
* value directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
335
387
|
* every handle recompose and on branch change; `dispose` detaches both. Lifecycle (pi ≥ 0.84,
|
|
336
388
|
* verified at 0.84.1): `setExtensionFooter` disposes a replaced factory's component, and pi's
|
|
337
389
|
* `resetExtensionUI` restores the built-in footer (disposing this one) on /reload and before
|
|
@@ -412,7 +464,7 @@ export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): s
|
|
|
412
464
|
}
|
|
413
465
|
|
|
414
466
|
// --- display-only transcript entry renderers -----------------------------------------------------
|
|
415
|
-
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's custom-entry families are
|
|
467
|
+
// The audit §2.3 verdict (docs/design/archive/pi-adoption-audit.md): perk's custom-entry families are
|
|
416
468
|
// display-only. Most render as durable one-line transition markers; report detail is the generic
|
|
417
469
|
// full-diagnostic family and always renders every logical row. Renderer BODIES live here (a
|
|
418
470
|
// transcript renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the
|
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
// The adversarial-review `
|
|
2
|
-
// human-in-the-loop review doors' (/pr-review-browser, /pr-review-terminal)
|
|
3
|
-
// code (sibling of `prReviewWave.ts`): the four door angles, the per-lane
|
|
4
|
-
// schema, and the lane/task composition are module-owned here, launched
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
1
|
+
// The adversarial-review `ReportWaveRequest`-building entrypoint over the shared report-wave
|
|
2
|
+
// module — the human-in-the-loop review doors' (/pr-review-browser, /pr-review-terminal)
|
|
3
|
+
// vocabulary as tested code (sibling of `prReviewWave.ts`): the four door angles, the per-lane
|
|
4
|
+
// completion-report schema, and the lane/task composition are module-owned here, launched
|
|
5
|
+
// NON-BLOCKING via `wave.start` so the parent ends its turn after launch and relays batches
|
|
6
|
+
// on native supervisor wakes before collecting on the matching workflow-completion notice.
|
|
7
7
|
//
|
|
8
8
|
// ZERO retries — deliberate: the doors' contract is honest incompleteness surfaced to the human
|
|
9
9
|
// during triage (an `ok: false` lane is reported, never papered over), so the pr-review
|
|
10
10
|
// bounded-retry policy does not carry over.
|
|
11
11
|
//
|
|
12
|
-
// The surface handle (URL/port) is STRUCTURALLY UNREPRESENTABLE:
|
|
13
|
-
// has no URL parameter at all, so the children can never
|
|
14
|
-
// construction, pinned by the suite.
|
|
12
|
+
// The surface handle (URL/port) is STRUCTURALLY UNREPRESENTABLE:
|
|
13
|
+
// `buildAdversarialReviewAssignments` has no URL parameter at all, so the children can never
|
|
14
|
+
// learn the review surface — enforced by construction, pinned by the suite.
|
|
15
15
|
//
|
|
16
16
|
// Driven live by the registered `start_review_wave` / `collect_review_wave` tool pair
|
|
17
|
-
// (`extension/
|
|
17
|
+
// (`extension/pi/v1/codeReview/reviewWave.ts`); the `agents/adversarial-reviewer.md` def completes via
|
|
18
18
|
// the `structured_output` tool this wave's `outputSchema` injects per lane.
|
|
19
19
|
|
|
20
|
+
import { reclassifyBlockedReports } from "./blockedReports.ts";
|
|
20
21
|
import { PONYTAIL_REVIEW_SKILL } from "./ponytail.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
import type {
|
|
23
|
+
CollectWaveResult,
|
|
24
|
+
ReportAssignment,
|
|
25
|
+
ReportWave,
|
|
26
|
+
ReportWaveRef,
|
|
27
|
+
ReportWaveRequest,
|
|
28
|
+
StartWaveResult,
|
|
27
29
|
} from "./reportWave.ts";
|
|
28
30
|
|
|
29
31
|
/** The four-slug adversarial-review angle allowlist (claimed-intent is mandatory at the tool boundary). */
|
|
@@ -50,22 +52,29 @@ export function isAdversarialReviewAngle(value: string): value is AdversarialRev
|
|
|
50
52
|
* The per-lane completion-report schema the wave enforces as its `outputSchema` — the engine
|
|
51
53
|
* injects a `structured_output` tool into each lane and fails any lane whose report is missing
|
|
52
54
|
* or schema-invalid. Transcribes the adversarial-reviewer's completion-report contract
|
|
53
|
-
* (contracts.md §8.4): closed shapes, `{angle, summary, findings, fyi}` all
|
|
54
|
-
* DELIBERATELY NO VERDICT FIELD — the human triages every finding, so there is no
|
|
55
|
-
* actionable derivation to make consistent
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* (contracts.md §8.4): closed shapes, `{angle, summary, findings, fyi, streamed, blocked}` all
|
|
56
|
+
* required, and DELIBERATELY NO VERDICT FIELD — the human triages every finding, so there is no
|
|
57
|
+
* clean/actionable derivation to make consistent. `blocked: true` is NOT a verdict: it marks a
|
|
58
|
+
* required review that could not complete (context fetch failed, a context file unreadable, the
|
|
59
|
+
* hunt stopped early) — `collectAdversarialReviewWave` normalizes it into an uncovered
|
|
60
|
+
* `lane-failed` BEFORE coverage is computed, so a schema-valid-but-empty report never counts as
|
|
61
|
+
* "no findings". The conditional mirrors `PR_REVIEW_REPORT_SCHEMA`'s blocked arm: empty
|
|
62
|
+
* `findings` and a nonblank `fyi` (the blocker first). Finding rows anchor candidate GitHub review
|
|
63
|
+
* comments: `line` is required-nullable (a real finding that cannot anchor to a diff line keeps
|
|
64
|
+
* `line: null`), `side` optional (omitted ⇒ RIGHT), and the severity/confidence enums match the
|
|
65
|
+
* agent def's triage tags.
|
|
59
66
|
*/
|
|
60
67
|
export const ADVERSARIAL_REVIEW_REPORT_SCHEMA = {
|
|
61
68
|
type: "object",
|
|
62
69
|
additionalProperties: false,
|
|
63
|
-
required: ["angle", "summary", "findings", "fyi"],
|
|
70
|
+
required: ["angle", "summary", "findings", "fyi", "streamed", "blocked"],
|
|
64
71
|
properties: {
|
|
65
72
|
angle: {
|
|
66
73
|
type: "string",
|
|
67
74
|
enum: ["claimed-intent", "correctness", "tests", "quality", "ponytail"],
|
|
68
75
|
},
|
|
76
|
+
streamed: { type: "boolean" },
|
|
77
|
+
blocked: { type: "boolean" },
|
|
69
78
|
summary: { type: "string" },
|
|
70
79
|
findings: {
|
|
71
80
|
type: "array",
|
|
@@ -88,12 +97,25 @@ export const ADVERSARIAL_REVIEW_REPORT_SCHEMA = {
|
|
|
88
97
|
items: { type: "string" },
|
|
89
98
|
},
|
|
90
99
|
},
|
|
100
|
+
allOf: [
|
|
101
|
+
{
|
|
102
|
+
// `blocked` is top-level required, so this `if` cannot vacuously match an absent key.
|
|
103
|
+
if: { properties: { blocked: { const: true } } },
|
|
104
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON-Schema conditional, not a thenable.
|
|
105
|
+
then: {
|
|
106
|
+
properties: {
|
|
107
|
+
findings: { maxItems: 0 },
|
|
108
|
+
fyi: { minItems: 1, items: { type: "string", pattern: "\\S" } },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
/**
|
|
94
|
-
* Build the reviewer
|
|
95
|
-
* task naming the angle, the PR number, and the head-worktree path — AND NOTHING ELSE: no
|
|
96
|
-
* parameter exists, so the surface handle is unrepresentable by construction (the children
|
|
116
|
+
* Build the reviewer assignments for a selection: key = label = slug, the fixed agent/phase,
|
|
117
|
+
* and a task naming the angle, the PR number, and the head-worktree path — AND NOTHING ELSE: no
|
|
118
|
+
* URL parameter exists, so the surface handle is unrepresentable by construction (the children
|
|
97
119
|
* re-derive everything else themselves via `perk pr review-context`).
|
|
98
120
|
*
|
|
99
121
|
* `stack` is a DISCRIMINATOR, not a member array: with `stack: true` the task names the stack
|
|
@@ -101,16 +123,16 @@ export const ADVERSARIAL_REVIEW_REPORT_SCHEMA = {
|
|
|
101
123
|
* children learn the authoritative ordered membership from the context worker, never from
|
|
102
124
|
* relayed prose. Without it, tasks are byte-identical to the single-PR form.
|
|
103
125
|
*/
|
|
104
|
-
export function
|
|
126
|
+
export function buildAdversarialReviewAssignments(opts: {
|
|
105
127
|
angles: AdversarialReviewAngle[];
|
|
106
128
|
pr: number;
|
|
107
129
|
worktree: string;
|
|
108
130
|
directive?: string;
|
|
109
131
|
stack?: boolean;
|
|
110
|
-
}):
|
|
111
|
-
// ONE uniform suffix on every
|
|
112
|
-
// judgment lever stays angle selection — the directive never re-scopes
|
|
113
|
-
// emphasis inside the assigned angle.
|
|
132
|
+
}): ReportAssignment[] {
|
|
133
|
+
// ONE uniform suffix on every assignment (the `buildPrReviewAssignments` byte-posture): the
|
|
134
|
+
// parent's judgment lever stays angle selection — the directive never re-scopes an angle, it
|
|
135
|
+
// only sets emphasis inside the assigned angle.
|
|
114
136
|
const suffix =
|
|
115
137
|
opts.directive === undefined
|
|
116
138
|
? ""
|
|
@@ -121,14 +143,14 @@ export function buildAdversarialReviewLanes(opts: {
|
|
|
121
143
|
? `Review the PR stack topped by PR #${opts.pr} (combined diff) at ${opts.worktree}. ` +
|
|
122
144
|
`Fetch context with \`perk pr review-context --pr ${opts.pr} --stack\`.`
|
|
123
145
|
: `Review PR #${opts.pr} at ${opts.worktree}.`;
|
|
124
|
-
const
|
|
146
|
+
const assignments: ReportAssignment[] = opts.angles.map((angle) => ({
|
|
125
147
|
key: angle,
|
|
126
148
|
label: angle,
|
|
127
149
|
agent: "perk.adversarial-reviewer",
|
|
128
150
|
phase: "review",
|
|
129
151
|
task: `${ADVERSARIAL_REVIEW_ANGLES[angle]} ${subject}${suffix}`,
|
|
130
152
|
}));
|
|
131
|
-
|
|
153
|
+
assignments.push({
|
|
132
154
|
key: "ponytail",
|
|
133
155
|
label: "ponytail",
|
|
134
156
|
agent: "perk.adversarial-reviewer",
|
|
@@ -137,7 +159,7 @@ export function buildAdversarialReviewLanes(opts: {
|
|
|
137
159
|
skill: "ponytail-review",
|
|
138
160
|
requiredSkill: PONYTAIL_REVIEW_SKILL,
|
|
139
161
|
});
|
|
140
|
-
return
|
|
162
|
+
return assignments;
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
export interface AdversarialReviewWaveOptions {
|
|
@@ -157,25 +179,44 @@ export interface AdversarialReviewWaveOptions {
|
|
|
157
179
|
/** Accepted for parity/tests only — the flow tool deliberately never threads its own signal. */
|
|
158
180
|
signal?: AbortSignal;
|
|
159
181
|
/** Test seam; production validates the exact source-bound Ponytail review skill. */
|
|
160
|
-
requiredSkillPreflight?:
|
|
182
|
+
requiredSkillPreflight?: ReportWaveRequest["requiredSkillPreflight"];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Only the typed flag classifies an adversarial report as blocked; diagnostic prose never does.
|
|
186
|
+
function isBlockedFlag(report: Record<string, unknown>): boolean {
|
|
187
|
+
return report.blocked === true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Collect the adversarial-review wave and apply the flow's lane semantics: a settled result has
|
|
192
|
+
* every `blocked: true` report reclassified into an uncovered assignment-keyed `lane-failed`
|
|
193
|
+
* (`reclassifyBlockedReports` — the same detail string as `prReviewWave.ts`) BEFORE the caller
|
|
194
|
+
* computes `covered`/`complete`; `none`/`running` pass through unchanged. The report stays
|
|
195
|
+
* verdict-free — only coverage changes.
|
|
196
|
+
*/
|
|
197
|
+
export async function collectAdversarialReviewWave(
|
|
198
|
+
wave: ReportWave,
|
|
199
|
+
ref: ReportWaveRef,
|
|
200
|
+
): Promise<CollectWaveResult> {
|
|
201
|
+
const collected = await wave.collect(ref);
|
|
202
|
+
if (collected.kind !== "settled") return collected;
|
|
203
|
+
return { ...collected, result: reclassifyBlockedReports(collected.result, isBlockedFlag) };
|
|
161
204
|
}
|
|
162
205
|
|
|
163
206
|
/**
|
|
164
|
-
* Start the adversarial-review wave NON-BLOCKING (the streaming
|
|
165
|
-
* the angle vocabulary and launch under the strict completeness policy — zero retries, so an
|
|
166
|
-
* uncovered angle stays an honest, human-visible incompleteness. Returns the `
|
|
167
|
-
* outcome: the
|
|
168
|
-
* failure.
|
|
207
|
+
* Start the adversarial-review wave NON-BLOCKING (the streaming split): build the assignments
|
|
208
|
+
* from the angle vocabulary and launch under the strict completeness policy — zero retries, so an
|
|
209
|
+
* uncovered angle stays an honest, human-visible incompleteness. Returns the `wave.start`
|
|
210
|
+
* outcome: the opaque ref + identity telemetry on success, or the normalized launch failure.
|
|
169
211
|
*/
|
|
170
212
|
export async function startAdversarialReviewWave(
|
|
171
|
-
|
|
213
|
+
wave: ReportWave,
|
|
172
214
|
opts: AdversarialReviewWaveOptions,
|
|
173
|
-
): Promise<
|
|
174
|
-
return await
|
|
175
|
-
adapter,
|
|
215
|
+
): Promise<StartWaveResult> {
|
|
216
|
+
return await wave.start(
|
|
176
217
|
{
|
|
177
218
|
flow: "adversarial-review",
|
|
178
|
-
|
|
219
|
+
assignments: buildAdversarialReviewAssignments({
|
|
179
220
|
angles: opts.angles,
|
|
180
221
|
pr: opts.pr,
|
|
181
222
|
worktree: opts.worktree,
|
|
@@ -190,6 +231,6 @@ export async function startAdversarialReviewWave(
|
|
|
190
231
|
? { requiredSkillPreflight: opts.requiredSkillPreflight }
|
|
191
232
|
: {}),
|
|
192
233
|
},
|
|
193
|
-
opts.signal,
|
|
234
|
+
{ signal: opts.signal },
|
|
194
235
|
);
|
|
195
236
|
}
|