@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,352 @@
|
|
|
1
|
+
// The fixed automated review flow (contracts §8.4) — the typed feature operations behind the
|
|
2
|
+
// warm `/pr-review` door's `run_pr_review_wave` + `post_pr_review` tools. This module owns the
|
|
3
|
+
// POLICY: the per-activation review-pass state machine (`null` permits the backwards-compatible
|
|
4
|
+
// standalone post; a valid new pass moves to `pending` BEFORE target resolution; a normalized
|
|
5
|
+
// outcome records `{pr, complete, attempted, covered, minimumVerdict}` PR-bound and single-use;
|
|
6
|
+
// one successful post consumes it; incomplete coverage refuses a clean verdict; effective
|
|
7
|
+
// actionable evidence refuses a clean verdict; a mutation-time PR mismatch demotes back to
|
|
8
|
+
// `pending`), plus the `last_pr_review` record a real post applies through the session seam
|
|
9
|
+
// (classification ignored — the seam owns loudness).
|
|
10
|
+
//
|
|
11
|
+
// The recorded `minimumVerdict` is a code-owned floor projected ONCE from the reviewer's final
|
|
12
|
+
// effective reports (post-retry, post-blocked-reclassification): parent reconciliation can raise
|
|
13
|
+
// a verdict to actionable but never lower it below the evidence. It is private activation state
|
|
14
|
+
// — never part of the durable `last_pr_review` record, the tool aggregate, or the attempt
|
|
15
|
+
// receipts (§8.35), and never a second schema validator (the engine owns report validation).
|
|
16
|
+
//
|
|
17
|
+
// Pi-free by construction (importDirectionGuard Rule D): the Pi adapter
|
|
18
|
+
// (`pi/v1/codeReview/automated.ts`) owns registration, tool-boundary decode, the cold-door and
|
|
19
|
+
// RPC-wave compositions, and Result rendering; this module sees only the ports below. The wave
|
|
20
|
+
// vocabulary arrives type-only from `waves/prReviewWave.ts` (a legal feature→mechanism type
|
|
21
|
+
// edge) — `ChangeReviewOutcome` is an alias of `PrReviewWaveOutcome`, never a second
|
|
22
|
+
// hand-mirrored vocabulary.
|
|
23
|
+
|
|
24
|
+
import type { PrReviewRecord, WorkflowSession } from "../session/workflowSession.ts";
|
|
25
|
+
import type { PrReviewAngle, PrReviewWaveOutcome } from "../waves/prReviewWave.ts";
|
|
26
|
+
|
|
27
|
+
// ------------------------------------------------------------------------ ports
|
|
28
|
+
|
|
29
|
+
/** The resolved active-plan PR every lane in a pass reviews. */
|
|
30
|
+
export interface ReviewTarget {
|
|
31
|
+
number: number;
|
|
32
|
+
url: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The target-resolution role (production: the `perk pr url --json` cold-door composition). */
|
|
36
|
+
export interface ReviewTargetResolver {
|
|
37
|
+
resolve(): Promise<
|
|
38
|
+
{ ok: true; target: ReviewTarget } | { ok: false; message: string; errorType: string }
|
|
39
|
+
>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The external reviewer's request. Cancellation ownership is explicit: the tool's
|
|
44
|
+
* execute-callback signal rides the request; the production adapter forwards it into
|
|
45
|
+
* `runPrReviewWave`'s `opts.signal` (cancellation normalizes into the outcome, never a throw).
|
|
46
|
+
*/
|
|
47
|
+
export interface ChangeReviewRequest {
|
|
48
|
+
pr: number;
|
|
49
|
+
angles: readonly PrReviewAngle[];
|
|
50
|
+
directive?: string;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A type-only alias of the wave outcome — never a second hand-mirrored vocabulary. */
|
|
55
|
+
export type ChangeReviewOutcome = PrReviewWaveOutcome;
|
|
56
|
+
|
|
57
|
+
/** The external reviewer role (production: RPC adapter + `runPrReviewWave` + configured model). */
|
|
58
|
+
export interface ChangeReviewer {
|
|
59
|
+
review(request: ChangeReviewRequest): Promise<ChangeReviewOutcome>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One reconciled inline finding (the exact `review-post --batch` `comments[]` row). */
|
|
63
|
+
export interface ReviewComment {
|
|
64
|
+
path: string;
|
|
65
|
+
line: number;
|
|
66
|
+
body: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The publishing port's input (`expected_pr` threading stays feature policy). */
|
|
70
|
+
export interface AutomatedReviewBatch {
|
|
71
|
+
verdict: "clean" | "actionable";
|
|
72
|
+
summary: string;
|
|
73
|
+
comments?: ReviewComment[];
|
|
74
|
+
fyi?: string[];
|
|
75
|
+
expectedPr?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The cold door's ok-arm fields (the `review-post --json` surface). */
|
|
79
|
+
export interface PostOk {
|
|
80
|
+
pr: number;
|
|
81
|
+
mode?: string;
|
|
82
|
+
verdict?: string;
|
|
83
|
+
comment_count?: number;
|
|
84
|
+
next_command?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The publishing outcome — `errorType` required (the cold-door seam always supplies it; the op
|
|
89
|
+
* branches on the `"review_target_changed"` literal).
|
|
90
|
+
*/
|
|
91
|
+
export type PublishOutcome =
|
|
92
|
+
| { ok: true; data: PostOk }
|
|
93
|
+
| { ok: false; message: string; errorType: string };
|
|
94
|
+
|
|
95
|
+
/** The publishing role (production: the `perk pr review-post --json --batch` composition). */
|
|
96
|
+
export interface ReviewPublisher {
|
|
97
|
+
publish(batch: AutomatedReviewBatch): Promise<PublishOutcome>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ------------------------------------------------------------------------ the pass state
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The review-pass post state: `null` preserves standalone posting before any valid wave attempt;
|
|
104
|
+
* a decoded new pass invalidates old evidence immediately (`pending`); only one `recorded`
|
|
105
|
+
* outcome can post, after which `consumed` refuses duplicates until another valid pass starts.
|
|
106
|
+
*/
|
|
107
|
+
export type ReviewPassState =
|
|
108
|
+
| null
|
|
109
|
+
| { state: "pending" }
|
|
110
|
+
| {
|
|
111
|
+
state: "recorded";
|
|
112
|
+
pr: number;
|
|
113
|
+
complete: boolean;
|
|
114
|
+
attempted: readonly string[];
|
|
115
|
+
covered: readonly string[];
|
|
116
|
+
/**
|
|
117
|
+
* The lowest verdict the effective evidence admits: `actionable` iff any effective report
|
|
118
|
+
* is actionable or carries surviving findings. A primitive snapshot — mutating the source
|
|
119
|
+
* outcome after recording never changes it.
|
|
120
|
+
*/
|
|
121
|
+
minimumVerdict: AutomatedReviewBatch["verdict"];
|
|
122
|
+
}
|
|
123
|
+
| { state: "consumed" };
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A plain per-activation holder (the installer creates one per activation; the two feature ops
|
|
127
|
+
* own every transition — no module-level state, ever).
|
|
128
|
+
*/
|
|
129
|
+
export interface ReviewPassHolder {
|
|
130
|
+
current: ReviewPassState;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ------------------------------------------------------------------------ run the wave
|
|
134
|
+
|
|
135
|
+
/** The decoded wave selection (strict decoding stays at the adapter boundary). */
|
|
136
|
+
export interface ReviewSelection {
|
|
137
|
+
angles: readonly PrReviewAngle[];
|
|
138
|
+
directive?: string;
|
|
139
|
+
signal?: AbortSignal;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type RunAutomatedReviewOutcome =
|
|
143
|
+
| { kind: "no_target"; message: string; errorType: string }
|
|
144
|
+
| {
|
|
145
|
+
kind: "reviewed";
|
|
146
|
+
pr: number;
|
|
147
|
+
outcome: ChangeReviewOutcome;
|
|
148
|
+
attempted: readonly string[];
|
|
149
|
+
/** Non-null ⟺ the wave is incomplete — the adapter renders the loud degrade from it. */
|
|
150
|
+
incompleteWarning: { uncovered: string[]; reasons: string } | null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Project the minimum postable verdict from the reviewer's EFFECTIVE reports (the final
|
|
155
|
+
* post-retry set — `outcomeOf`'s ordered reports, never attempt receipts or superseded attempts).
|
|
156
|
+
* `actionable` iff any assignment's report is a non-null, non-array object whose exact
|
|
157
|
+
* `verdict` is `"actionable"` OR whose `findings` is a nonempty array; otherwise `clean`.
|
|
158
|
+
* The findings arm is a conservative safeguard against a contradictory report object reaching
|
|
159
|
+
* the feature below schema validation — not permission for the engine schema to accept one.
|
|
160
|
+
* FYI/summary prose is never consulted: diagnostics are DATA, not a verdict.
|
|
161
|
+
*/
|
|
162
|
+
function minimumVerdictOf(
|
|
163
|
+
reports: ChangeReviewOutcome["reports"],
|
|
164
|
+
): AutomatedReviewBatch["verdict"] {
|
|
165
|
+
for (const assignment of reports) {
|
|
166
|
+
const report: unknown = assignment.report;
|
|
167
|
+
if (typeof report !== "object" || report === null || Array.isArray(report)) continue;
|
|
168
|
+
if ("verdict" in report && report.verdict === "actionable") return "actionable";
|
|
169
|
+
if ("findings" in report && Array.isArray(report.findings) && report.findings.length > 0) {
|
|
170
|
+
return "actionable";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return "clean";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run one automated review pass: invalidate old evidence (`pending`) → resolve the target
|
|
178
|
+
* (failure leaves the state pending) → run the reviewer → record the PR-bound manifest with
|
|
179
|
+
* `attempted = [...angles, "ponytail"]` plus the minimum verdict projected from the effective
|
|
180
|
+
* reports. Recording COPIES the arrays and stores only the primitive verdict — the holder owns
|
|
181
|
+
* its evidence, never aliasing the outcome (or its reports) returned to the adapter.
|
|
182
|
+
*/
|
|
183
|
+
export async function runAutomatedReview(
|
|
184
|
+
selection: ReviewSelection,
|
|
185
|
+
deps: { resolver: ReviewTargetResolver; reviewer: ChangeReviewer; state: ReviewPassHolder },
|
|
186
|
+
): Promise<RunAutomatedReviewOutcome> {
|
|
187
|
+
deps.state.current = { state: "pending" };
|
|
188
|
+
const resolved = await deps.resolver.resolve();
|
|
189
|
+
if (!resolved.ok) {
|
|
190
|
+
return { kind: "no_target", message: resolved.message, errorType: resolved.errorType };
|
|
191
|
+
}
|
|
192
|
+
const pr = resolved.target.number;
|
|
193
|
+
// Cancellation normalizes into the outcome (`cancelled`, no retry) — never a throw.
|
|
194
|
+
const outcome = await deps.reviewer.review({
|
|
195
|
+
pr,
|
|
196
|
+
angles: selection.angles,
|
|
197
|
+
...(selection.directive !== undefined ? { directive: selection.directive } : {}),
|
|
198
|
+
...(selection.signal !== undefined ? { signal: selection.signal } : {}),
|
|
199
|
+
});
|
|
200
|
+
const attempted = [...selection.angles, "ponytail"];
|
|
201
|
+
deps.state.current = {
|
|
202
|
+
state: "recorded",
|
|
203
|
+
pr,
|
|
204
|
+
complete: outcome.complete,
|
|
205
|
+
attempted: [...attempted],
|
|
206
|
+
covered: [...outcome.covered],
|
|
207
|
+
minimumVerdict: minimumVerdictOf(outcome.reports),
|
|
208
|
+
};
|
|
209
|
+
// Loud degrade — the `unavailable` arm surfaces here too, never a silent fallback.
|
|
210
|
+
const incompleteWarning = outcome.complete
|
|
211
|
+
? null
|
|
212
|
+
: {
|
|
213
|
+
uncovered: attempted.filter((angle) => !outcome.covered.includes(angle)),
|
|
214
|
+
reasons: outcome.failures
|
|
215
|
+
.map((f) => `${f.key ?? "wave"}: ${f.reason} — ${f.detail}`)
|
|
216
|
+
.join("; "),
|
|
217
|
+
};
|
|
218
|
+
return { kind: "reviewed", pr, outcome, attempted, incompleteWarning };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ------------------------------------------------------------------------ publish the outcome
|
|
222
|
+
|
|
223
|
+
/** The decoded post params (strict decoding stays at the adapter boundary). */
|
|
224
|
+
export interface AutomatedPost {
|
|
225
|
+
verdict: "clean" | "actionable";
|
|
226
|
+
summary: string;
|
|
227
|
+
comments?: ReviewComment[];
|
|
228
|
+
fyi?: string[];
|
|
229
|
+
/** Standalone fallback only; recorded-wave calls use the authoritative attempted manifest. */
|
|
230
|
+
angles?: string[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type PublishAutomatedReviewOutcome =
|
|
234
|
+
| {
|
|
235
|
+
kind: "ineligible";
|
|
236
|
+
errorType:
|
|
237
|
+
| "review_wave_unavailable"
|
|
238
|
+
| "review_wave_consumed"
|
|
239
|
+
| "incomplete_coverage"
|
|
240
|
+
| "review_verdict_conflict";
|
|
241
|
+
message: string;
|
|
242
|
+
}
|
|
243
|
+
| { kind: "stale"; errorType: "stale_review_wave"; message: string }
|
|
244
|
+
| { kind: "publish_failed"; message: string; errorType: string }
|
|
245
|
+
| { kind: "posted"; data: PostOk; record: PrReviewRecord };
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Publish the reconciled outcome to the PR: the eligibility ladder (pending ⇒
|
|
249
|
+
* `review_wave_unavailable`; consumed ⇒ `review_wave_consumed`; a clean verdict over an
|
|
250
|
+
* incomplete recorded wave ⇒ `incomplete_coverage`; a clean verdict over a complete recorded
|
|
251
|
+
* wave whose minimum is actionable ⇒ `review_verdict_conflict` — state untouched and nothing
|
|
252
|
+
* recorded on all four; the record survives for a reconciled actionable post) → the
|
|
253
|
+
* publisher (a `review_target_changed` failure while a recorded state exists demotes to
|
|
254
|
+
* `pending`; any other failure passes through verbatim, state untouched) → on success apply
|
|
255
|
+
* `record-pr-review` (classification ignored — the seam owns loudness) and consume iff
|
|
256
|
+
* recorded. The standalone-post arm (a `null` state) is preserved: caller-supplied angles fill
|
|
257
|
+
* both manifests.
|
|
258
|
+
*/
|
|
259
|
+
export async function publishAutomatedReview(
|
|
260
|
+
post: AutomatedPost,
|
|
261
|
+
deps: { publisher: ReviewPublisher; state: ReviewPassHolder; session: WorkflowSession },
|
|
262
|
+
): Promise<PublishAutomatedReviewOutcome> {
|
|
263
|
+
const current = deps.state.current;
|
|
264
|
+
if (current?.state === "pending") {
|
|
265
|
+
return {
|
|
266
|
+
kind: "ineligible",
|
|
267
|
+
errorType: "review_wave_unavailable",
|
|
268
|
+
message: "the latest review pass has no recorded outcome; rerun /pr-review before posting",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (current?.state === "consumed") {
|
|
272
|
+
return {
|
|
273
|
+
kind: "ineligible",
|
|
274
|
+
errorType: "review_wave_consumed",
|
|
275
|
+
message:
|
|
276
|
+
"the recorded review outcome has already been posted; rerun /pr-review before posting again",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// Incomplete coverage is never a clean review. An actionable post may still record the
|
|
280
|
+
// findings plus the coverage caveat, consuming that recorded outcome on success.
|
|
281
|
+
if (post.verdict === "clean" && current?.state === "recorded" && !current.complete) {
|
|
282
|
+
return {
|
|
283
|
+
kind: "ineligible",
|
|
284
|
+
errorType: "incomplete_coverage",
|
|
285
|
+
message:
|
|
286
|
+
"incomplete coverage is never a clean review — the recorded review wave left angle(s) " +
|
|
287
|
+
"uncovered; post the actionable findings with a coverage note, or post nothing and " +
|
|
288
|
+
"suggest re-running /pr-review",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Complete coverage is necessary but insufficient for clean: the recorded floor (any effective
|
|
293
|
+
// actionable assessment — even with empty findings — or surviving finding) contradicts a clean
|
|
294
|
+
// post. No coercion, no override, no automatic post: the parent posts a reconciled actionable
|
|
295
|
+
// review against the same record or posts nothing.
|
|
296
|
+
if (
|
|
297
|
+
post.verdict === "clean" &&
|
|
298
|
+
current?.state === "recorded" &&
|
|
299
|
+
current.minimumVerdict === "actionable"
|
|
300
|
+
) {
|
|
301
|
+
return {
|
|
302
|
+
kind: "ineligible",
|
|
303
|
+
errorType: "review_verdict_conflict",
|
|
304
|
+
message:
|
|
305
|
+
"the recorded review outcome contains an actionable assessment or surviving findings; a " +
|
|
306
|
+
"clean verdict would contradict that evidence — post a reconciled actionable review or " +
|
|
307
|
+
"post nothing",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A recorded wave binds the Python mutation to the PR that every child reviewed. Standalone
|
|
312
|
+
// calls intentionally omit `expected_pr` for backwards-compatible direct posting.
|
|
313
|
+
const recorded = current?.state === "recorded" ? current : null;
|
|
314
|
+
const batch: AutomatedReviewBatch = { verdict: post.verdict, summary: post.summary };
|
|
315
|
+
if (post.comments !== undefined) batch.comments = post.comments;
|
|
316
|
+
if (post.fyi !== undefined) batch.fyi = post.fyi;
|
|
317
|
+
if (recorded !== null) batch.expectedPr = recorded.pr;
|
|
318
|
+
|
|
319
|
+
const published = await deps.publisher.publish(batch);
|
|
320
|
+
if (!published.ok) {
|
|
321
|
+
if (recorded !== null && published.errorType === "review_target_changed") {
|
|
322
|
+
deps.state.current = { state: "pending" };
|
|
323
|
+
return {
|
|
324
|
+
kind: "stale",
|
|
325
|
+
errorType: "stale_review_wave",
|
|
326
|
+
message:
|
|
327
|
+
"the active PR changed after this review wave; the recorded reports are stale — rerun " +
|
|
328
|
+
"/pr-review before posting",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { kind: "publish_failed", message: published.message, errorType: published.errorType };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const data = published.data;
|
|
335
|
+
// Record the outcome (best-effort tier, seam-reported): the classification is ignored — the
|
|
336
|
+
// post already succeeded and the seam owns the loud read-back warning.
|
|
337
|
+
const standaloneAngles = post.angles ?? [];
|
|
338
|
+
const attempted = recorded?.attempted ?? standaloneAngles;
|
|
339
|
+
const covered = recorded?.covered ?? standaloneAngles;
|
|
340
|
+
const record: PrReviewRecord = {
|
|
341
|
+
pr: data.pr,
|
|
342
|
+
verdict: post.verdict,
|
|
343
|
+
angles: [...attempted],
|
|
344
|
+
covered_angles: [...covered],
|
|
345
|
+
comment_count: data.comment_count ?? null,
|
|
346
|
+
mode: data.mode ?? null,
|
|
347
|
+
at: new Date().toISOString(),
|
|
348
|
+
};
|
|
349
|
+
deps.session.apply({ kind: "record-pr-review", record });
|
|
350
|
+
if (recorded !== null) deps.state.current = { state: "consumed" };
|
|
351
|
+
return { kind: "posted", data, record };
|
|
352
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// The curated-submission flow (contracts §8.4's per-door posting contract) — the typed feature
|
|
2
|
+
// operation behind the warm `submit_pr_review` tool: nothing perk-driven reaches GitHub before
|
|
3
|
+
// the human triage; the verdict lands last, atomically with the comments. This module owns the
|
|
4
|
+
// POLICY: the enforced `already_posted` resume guard over the `review_posts` ledger (presence
|
|
5
|
+
// refuses; absence is NOT proof — the ledger is best-effort), the formal-event gate ladder
|
|
6
|
+
// (headless refuses, interactive confirms, `comment` posts on the conversational go-ahead
|
|
7
|
+
// alone), the `dry_run` anchor-repair loop's bypass of both gates, and the two session records
|
|
8
|
+
// a real success always attempts (`record-review` then `append-review-post`, unconditionally in
|
|
9
|
+
// that order, ignoring both classifications — the session seam owns loudness).
|
|
10
|
+
//
|
|
11
|
+
// Pi-free by construction (importDirectionGuard Rule D): the Pi adapter
|
|
12
|
+
// (`pi/v1/codeReview/submit.ts`) owns registration, tool-boundary decode, the cold-door
|
|
13
|
+
// composition, and Result rendering; this module sees only the ports below.
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ReviewPostRow,
|
|
17
|
+
ReviewSubmissionRecord,
|
|
18
|
+
WorkflowSession,
|
|
19
|
+
} from "../session/workflowSession.ts";
|
|
20
|
+
|
|
21
|
+
// ------------------------------------------------------------------------ vocabulary
|
|
22
|
+
|
|
23
|
+
export type ReviewEvent = "approve" | "request-changes" | "comment";
|
|
24
|
+
|
|
25
|
+
/** One curated inline comment (the exact `review-submit --batch` `comments[]` row). */
|
|
26
|
+
export interface SubmitComment {
|
|
27
|
+
path: string;
|
|
28
|
+
line: number;
|
|
29
|
+
side?: "LEFT" | "RIGHT";
|
|
30
|
+
body: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The cold door's ok-arm fields (the `review-submit --json` surface; render-only → lenient). */
|
|
34
|
+
export interface SubmitOk {
|
|
35
|
+
dry_run?: boolean;
|
|
36
|
+
pr?: number;
|
|
37
|
+
event?: string;
|
|
38
|
+
mode?: string;
|
|
39
|
+
comment_count?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One `bad_anchors` `invalid[]` row (the cold door's per-comment repair detail). */
|
|
43
|
+
export interface InvalidAnchor {
|
|
44
|
+
index: number;
|
|
45
|
+
path: string;
|
|
46
|
+
line: number;
|
|
47
|
+
side: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The feature input (booleans normalized at the adapter decode). */
|
|
52
|
+
export interface CuratedSubmission {
|
|
53
|
+
pr: number;
|
|
54
|
+
event: ReviewEvent;
|
|
55
|
+
body: string;
|
|
56
|
+
comments?: SubmitComment[];
|
|
57
|
+
dryRun: boolean;
|
|
58
|
+
allowRepost: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The posting port's input — deliberately WITHOUT `allowRepost`: that flag is feature ledger
|
|
63
|
+
* policy and never crosses the posting boundary.
|
|
64
|
+
*/
|
|
65
|
+
export interface SubmitBatch {
|
|
66
|
+
pr: number;
|
|
67
|
+
event: ReviewEvent;
|
|
68
|
+
body: string;
|
|
69
|
+
comments?: SubmitComment[];
|
|
70
|
+
dryRun: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ------------------------------------------------------------------------ ports
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The posting outcome: `bad_anchors` carries the strict-decoded repair rows (decode drift ⇒ the
|
|
77
|
+
* `failed` arm — uncertainty renders plain, never a half table). `errorType` is required on the
|
|
78
|
+
* `failed` arm — the cold-door seam always supplies it.
|
|
79
|
+
*/
|
|
80
|
+
export type ReviewSubmitOutcome =
|
|
81
|
+
| { ok: true; data: SubmitOk }
|
|
82
|
+
| { ok: false; kind: "bad_anchors"; invalid: InvalidAnchor[]; message: string }
|
|
83
|
+
| { ok: false; kind: "failed"; message: string; errorType: string };
|
|
84
|
+
|
|
85
|
+
/** The external posting role (production: the `perk pr review-submit` cold-door composition). */
|
|
86
|
+
export interface ReviewSubmitter {
|
|
87
|
+
submit(batch: SubmitBatch): Promise<ReviewSubmitOutcome>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The formal-event human gate — a discriminated union, so the unsafe call is unrepresentable:
|
|
92
|
+
* a headless gate HAS no confirm to invoke. Production: `ctx.hasUI` selects the arm; the
|
|
93
|
+
* interactive arm wraps `ctx.ui.confirm`.
|
|
94
|
+
*/
|
|
95
|
+
export type FormalEventGate =
|
|
96
|
+
| { kind: "headless" }
|
|
97
|
+
| { kind: "interactive"; confirm(question: string, summary: string): Promise<boolean> };
|
|
98
|
+
|
|
99
|
+
// ------------------------------------------------------------------------ the result union
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The enumerated curated-submission outcome; every arm carries its policy-owned message text
|
|
103
|
+
* (the refusal arms carry ONLY the message — the prior-row / invalid-anchor detail is already
|
|
104
|
+
* rendered into it, and no production caller reads it structurally).
|
|
105
|
+
*/
|
|
106
|
+
export type SubmitCuratedOutcome =
|
|
107
|
+
| { kind: "already_posted"; message: string }
|
|
108
|
+
| { kind: "headless_formal_event"; message: string }
|
|
109
|
+
| { kind: "user_declined"; message: string }
|
|
110
|
+
| { kind: "bad_anchors"; message: string }
|
|
111
|
+
| { kind: "submit_failed"; message: string; errorType: string }
|
|
112
|
+
| { kind: "dry_run_ok"; data: SubmitOk }
|
|
113
|
+
| { kind: "posted"; data: SubmitOk; record: ReviewSubmissionRecord };
|
|
114
|
+
|
|
115
|
+
/** Flag spelling → the REST wire spelling shown in the human confirm. */
|
|
116
|
+
const WIRE_EVENT: Record<ReviewEvent, string> = {
|
|
117
|
+
approve: "APPROVE",
|
|
118
|
+
"request-changes": "REQUEST_CHANGES",
|
|
119
|
+
comment: "COMMENT",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** The body's first line, truncated for the confirm-dialog summary. */
|
|
123
|
+
function bodyFirstLine(body: string): string {
|
|
124
|
+
const line = body.split("\n", 1)[0] ?? "";
|
|
125
|
+
return line.length > 120 ? `${line.slice(0, 117)}…` : line;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ------------------------------------------------------------------------ the operation
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Submit the human-curated review batch to the foreign PR. Policy order (exactly): the resume
|
|
132
|
+
* guard (skipped on `dryRun`/`allowRepost`; refusal on a prior ledger row for the PR — absence
|
|
133
|
+
* is NOT proof of no post) → the formal-event gate (skipped on `dryRun` and for `comment`; the
|
|
134
|
+
* headless arm refuses; a decline executes nothing) → the posting port → on a real success the
|
|
135
|
+
* two session records, `record-review` then `append-review-post`, unconditionally in that order,
|
|
136
|
+
* ignoring both classifications (the seam owns loudness).
|
|
137
|
+
*/
|
|
138
|
+
export async function submitCuratedReview(
|
|
139
|
+
input: CuratedSubmission,
|
|
140
|
+
deps: { submitter: ReviewSubmitter; gate: FormalEventGate; session: WorkflowSession },
|
|
141
|
+
): Promise<SubmitCuratedOutcome> {
|
|
142
|
+
const commentCount = input.comments?.length ?? 0;
|
|
143
|
+
|
|
144
|
+
// The enforced resume guard (before the confirm AND the cold-door mutation): a PR that
|
|
145
|
+
// already has a review_posts ledger row in this session is a confirmed success — a repeat
|
|
146
|
+
// real post is refused unless explicitly deliberate. A ledger row can only be MISSING
|
|
147
|
+
// spuriously (best-effort tier), never present spuriously — so the guard refuses on
|
|
148
|
+
// presence and stays silent on absence (a missing row still means: verify posted-vs-pending
|
|
149
|
+
// against GitHub before re-posting).
|
|
150
|
+
if (!input.dryRun && !input.allowRepost) {
|
|
151
|
+
const prior = deps.session.reviewPosts().filter((row) => row.pr === input.pr);
|
|
152
|
+
const last = prior.at(-1);
|
|
153
|
+
if (last !== undefined) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "already_posted",
|
|
156
|
+
message:
|
|
157
|
+
`a ${last.event} review was already posted to PR #${input.pr} in this session ` +
|
|
158
|
+
`(review_posts row at ${last.at}) — on a stack resume skip this member; pass ` +
|
|
159
|
+
"allow_repost: true only for a deliberate second review of the same PR",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!input.dryRun && input.event !== "comment") {
|
|
165
|
+
if (deps.gate.kind === "headless") {
|
|
166
|
+
return {
|
|
167
|
+
kind: "headless_formal_event",
|
|
168
|
+
message:
|
|
169
|
+
"headless sessions cannot post formal review verdicts — re-run interactively or use " +
|
|
170
|
+
"event: comment",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const wire = WIRE_EVENT[input.event];
|
|
174
|
+
const firstLine = bodyFirstLine(input.body);
|
|
175
|
+
const summary =
|
|
176
|
+
`event: ${wire} · ${commentCount} inline comment(s)` +
|
|
177
|
+
(firstLine.length > 0 ? `\nbody: ${firstLine}` : "");
|
|
178
|
+
const yes = await deps.gate.confirm(`Post ${wire} review to PR #${input.pr}?`, summary);
|
|
179
|
+
if (!yes) {
|
|
180
|
+
return {
|
|
181
|
+
kind: "user_declined",
|
|
182
|
+
message: `user declined the ${input.event} review — nothing was submitted`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const batch: SubmitBatch = {
|
|
188
|
+
pr: input.pr,
|
|
189
|
+
event: input.event,
|
|
190
|
+
body: input.body,
|
|
191
|
+
...(input.comments !== undefined ? { comments: input.comments } : {}),
|
|
192
|
+
dryRun: input.dryRun,
|
|
193
|
+
};
|
|
194
|
+
const outcome = await deps.submitter.submit(batch);
|
|
195
|
+
|
|
196
|
+
if (!outcome.ok) {
|
|
197
|
+
if (outcome.kind === "bad_anchors") {
|
|
198
|
+
// The repair-loop arm: render the per-comment invalid[] detail (rows already strict-
|
|
199
|
+
// decoded by the adapter — a drifting payload arrived as the `failed` arm instead).
|
|
200
|
+
const table = outcome.invalid
|
|
201
|
+
.map(
|
|
202
|
+
(row) => ` comment[${row.index}] ${row.path}:${row.line} (${row.side}) — ${row.reason}`,
|
|
203
|
+
)
|
|
204
|
+
.join("\n");
|
|
205
|
+
return {
|
|
206
|
+
kind: "bad_anchors",
|
|
207
|
+
message: `${outcome.message}\n${table}\nrepair these anchors and re-run with dry_run: true`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { kind: "submit_failed", message: outcome.message, errorType: outcome.errorType };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (input.dryRun) return { kind: "dry_run_ok", data: outcome.data };
|
|
214
|
+
|
|
215
|
+
// Record the outcome (best-effort tier, seam-reported): record-review then append-review-post,
|
|
216
|
+
// BOTH always attempted, in that order, ignoring both classifications — the submission already
|
|
217
|
+
// succeeded and the seam owns the loud read-back warnings.
|
|
218
|
+
const record: ReviewSubmissionRecord = {
|
|
219
|
+
pr: outcome.data.pr ?? input.pr,
|
|
220
|
+
event: input.event,
|
|
221
|
+
comment_count: outcome.data.comment_count ?? null,
|
|
222
|
+
mode: outcome.data.mode ?? null,
|
|
223
|
+
at: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
const row: ReviewPostRow = { pr: record.pr, event: input.event, at: record.at };
|
|
226
|
+
deps.session.apply({ kind: "record-review", record });
|
|
227
|
+
deps.session.apply({ kind: "append-review-post", row });
|
|
228
|
+
return { kind: "posted", data: outcome.data, record };
|
|
229
|
+
}
|