@mgiles/perk 3.1.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 +369 -359
- 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 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- 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 +163 -265
- 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 +128 -60
- 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/{doors → pi/v1}/lifecycleGates.ts +14 -59
- 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/pi/v1/objectivePlanning.ts +776 -0
- 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 +152 -34
- 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 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- 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 +98 -10
- 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/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- 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 +7 -0
- 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/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- 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 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- 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/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- 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
- package/shared/contracts-history.md +0 -605
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// (`perk pr land`)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// The per-plan landing bindings: the terminating `land` tool + the `/land` command, the
|
|
2
|
+
// in-session twin of the Python cold door (`perk pr land`). A deliberately ZERO-POLICY adapter
|
|
3
|
+
// module (no feature operation backs it — zero-policy passthrough): the tool DELEGATES the
|
|
4
|
+
// GitHub merge (mutations canonical in Python), then mirrors the envelope's `pending_learn` for
|
|
5
|
+
// the in-session path — setting the `pending-learn` marker (an idempotent existence-semaphore;
|
|
6
|
+
// the worker sets it too on the cold path) unless the cold door reports the learn-docs
|
|
7
|
+
// exemption (`pending_learn: false` — no marker, no /learn nudge). Never throws; a verified
|
|
8
|
+
// land result survives every advisory failure (marker writes, malformed sub-objects) as a loud
|
|
9
|
+
// warning line, never a post-merge rejection.
|
|
7
10
|
|
|
8
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { reconcileGuidance } from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { reconcileGuidance } from "../../../authoring/objective/prose.ts";
|
|
13
|
+
import { planningStageRefusal } from "../../../session/lifecycleGates.ts";
|
|
14
|
+
import { bindingSuffix } from "../../../substrate/bindingDelivery.ts";
|
|
15
|
+
import { PENDING_LEARN, setMarker } from "../../../substrate/cache.ts";
|
|
12
16
|
import {
|
|
13
17
|
booleanField,
|
|
14
18
|
type ColdJson,
|
|
@@ -17,15 +21,21 @@ import {
|
|
|
17
21
|
objectField,
|
|
18
22
|
runColdDoor,
|
|
19
23
|
stringField,
|
|
20
|
-
} from "
|
|
21
|
-
import { registerPerkCommand } from "
|
|
22
|
-
import { failFor, ok, type Result } from "
|
|
23
|
-
import { report } from "
|
|
24
|
+
} from "../../../substrate/coldDoor.ts";
|
|
25
|
+
import { registerPerkCommand } from "../../../substrate/command.ts";
|
|
26
|
+
import { failFor, ok, type Result } from "../../../substrate/result.ts";
|
|
27
|
+
import { report } from "../../../surfaces/report.ts";
|
|
24
28
|
|
|
25
29
|
// Learn-consume skip reasons that are ordinary, not failures: non-factory plans carry no
|
|
26
30
|
// `consumed_learn` (`no_consumed_learn`), and a dry run reports `dry_run`. Anything else surfaces.
|
|
27
31
|
const BENIGN_LEARN_SKIPS = new Set(["no_consumed_learn", "dry_run"]);
|
|
28
32
|
|
|
33
|
+
/** The marker-safe id vocabulary the reconcile drive requires of the objective id — the
|
|
34
|
+
* interpolated guidance is a steering message rendering the id as an unquoted CLI argument,
|
|
35
|
+
* so an out-of-vocabulary id never drives and an option-shaped `-`-leading id never passes
|
|
36
|
+
* (alphanumeric-first). */
|
|
37
|
+
const OBJECTIVE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
38
|
+
|
|
29
39
|
export interface ObjectiveLandUpdate {
|
|
30
40
|
/** Opaque string objective id (GitHub "5", Linear "ENG-5") — §8.21. */
|
|
31
41
|
id: string | null;
|
|
@@ -42,7 +52,7 @@ export interface LearnConsumeUpdate {
|
|
|
42
52
|
|
|
43
53
|
/** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state.
|
|
44
54
|
* `pending_learn` mirrors the cold envelope: `false` is the learn-docs exemption (no marker). */
|
|
45
|
-
|
|
55
|
+
interface LandOk {
|
|
46
56
|
pr: { number: number; state: string };
|
|
47
57
|
branch?: string;
|
|
48
58
|
/** Opaque string plan-issue id (§8.21). */
|
|
@@ -52,8 +62,16 @@ export interface LandOk {
|
|
|
52
62
|
learn?: LearnConsumeUpdate;
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
type LandResult = Result<LandOk>;
|
|
66
|
+
type LandDetails = LandResult["details"];
|
|
67
|
+
|
|
68
|
+
/** An optional advisory sub-object's three-state decode: absent is ordinary; malformed is
|
|
69
|
+
* DISTINGUISHED (the merge already succeeded, so the success report survives — but the
|
|
70
|
+
* unverified state is reported loudly, never silently dropped). */
|
|
71
|
+
type AdvisoryDecode<T> =
|
|
72
|
+
| { state: "absent" }
|
|
73
|
+
| { state: "malformed" }
|
|
74
|
+
| { state: "present"; value: T };
|
|
57
75
|
|
|
58
76
|
/** The decoded `perk pr land --json` payload — the cold door owns `pending_learn` (the
|
|
59
77
|
* learn-docs-exemption decision point); decoded leniently so skew degrades to legacy. */
|
|
@@ -62,47 +80,59 @@ interface LandPayload {
|
|
|
62
80
|
branch?: string;
|
|
63
81
|
issue?: string;
|
|
64
82
|
pending_learn: boolean;
|
|
65
|
-
objective
|
|
66
|
-
learn
|
|
83
|
+
objective: AdvisoryDecode<ObjectiveLandUpdate>;
|
|
84
|
+
learn: AdvisoryDecode<LearnConsumeUpdate>;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
/** Validate the optional `objective` sub-object
|
|
70
|
-
function decodeObjective(payload: ColdJson): ObjectiveLandUpdate
|
|
87
|
+
/** Validate the optional `objective` sub-object — three-state (absent/malformed/present). */
|
|
88
|
+
function decodeObjective(payload: ColdJson): AdvisoryDecode<ObjectiveLandUpdate> {
|
|
89
|
+
if (payload.objective === undefined) return { state: "absent" };
|
|
71
90
|
const obj = objectField(payload, "objective");
|
|
72
|
-
if (obj === undefined) return
|
|
91
|
+
if (obj === undefined) return { state: "malformed" };
|
|
73
92
|
const id = obj.id;
|
|
74
|
-
if (typeof id !== "string" && id !== null) return
|
|
93
|
+
if (typeof id !== "string" && id !== null) return { state: "malformed" };
|
|
75
94
|
const nodesMarked = obj.nodes_marked;
|
|
76
95
|
if (!Array.isArray(nodesMarked) || !nodesMarked.every((n) => typeof n === "string")) {
|
|
77
|
-
return
|
|
96
|
+
return { state: "malformed" };
|
|
78
97
|
}
|
|
79
98
|
const skippedReason = nullableStringField(obj, "skipped_reason");
|
|
80
|
-
if (skippedReason === undefined && obj.skipped_reason !== undefined)
|
|
99
|
+
if (skippedReason === undefined && obj.skipped_reason !== undefined) {
|
|
100
|
+
return { state: "malformed" };
|
|
101
|
+
}
|
|
81
102
|
// `closed` is an advisory display detail: decode leniently (missing/malformed → false) rather
|
|
82
103
|
// than dropping the whole sub-object.
|
|
83
104
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
state: "present",
|
|
106
|
+
value: {
|
|
107
|
+
id,
|
|
108
|
+
nodes_marked: nodesMarked,
|
|
109
|
+
skipped_reason: skippedReason ?? null,
|
|
110
|
+
closed: obj.closed === true,
|
|
111
|
+
},
|
|
88
112
|
};
|
|
89
113
|
}
|
|
90
114
|
|
|
91
|
-
/** Validate the optional `learn` sub-object
|
|
92
|
-
function decodeLearn(payload: ColdJson): LearnConsumeUpdate
|
|
115
|
+
/** Validate the optional `learn` sub-object — three-state (absent/malformed/present). */
|
|
116
|
+
function decodeLearn(payload: ColdJson): AdvisoryDecode<LearnConsumeUpdate> {
|
|
117
|
+
if (payload.learn === undefined) return { state: "absent" };
|
|
93
118
|
const learn = objectField(payload, "learn");
|
|
94
|
-
if (learn === undefined) return
|
|
119
|
+
if (learn === undefined) return { state: "malformed" };
|
|
95
120
|
const closed = learn.closed;
|
|
96
|
-
if (!Array.isArray(closed) || !closed.every((n) => typeof n === "string"))
|
|
121
|
+
if (!Array.isArray(closed) || !closed.every((n) => typeof n === "string")) {
|
|
122
|
+
return { state: "malformed" };
|
|
123
|
+
}
|
|
97
124
|
const skippedReason = nullableStringField(learn, "skipped_reason");
|
|
98
|
-
if (skippedReason === undefined && learn.skipped_reason !== undefined)
|
|
99
|
-
|
|
125
|
+
if (skippedReason === undefined && learn.skipped_reason !== undefined) {
|
|
126
|
+
return { state: "malformed" };
|
|
127
|
+
}
|
|
128
|
+
return { state: "present", value: { closed, skipped_reason: skippedReason ?? null } };
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
/**
|
|
103
132
|
* Narrow the `perk pr land --json` success payload. Strict on `pr` (malformed → bad_output);
|
|
104
|
-
* the optional `objective`/`learn` sub-objects
|
|
105
|
-
* merge already succeeded, so the success report must
|
|
133
|
+
* the optional `objective`/`learn` sub-objects decode three-state — a malformed advisory field
|
|
134
|
+
* is dropped from the details (the merge already succeeded, so the success report must
|
|
135
|
+
* survive it) but reported as an UNVERIFIED-state warning line, never silently.
|
|
106
136
|
*/
|
|
107
137
|
function decodeLand(payload: ColdJson): LandPayload | null {
|
|
108
138
|
const pr = objectField(payload, "pr");
|
|
@@ -123,32 +153,58 @@ function decodeLand(payload: ColdJson): LandPayload | null {
|
|
|
123
153
|
};
|
|
124
154
|
}
|
|
125
155
|
|
|
156
|
+
/** The malformed-advisory warning line (the `<objective|learn>` slots are the report name and
|
|
157
|
+
* its unverified-state name). */
|
|
158
|
+
function malformedAdvisoryWarning(field: "objective" | "learn"): string {
|
|
159
|
+
const stateName = field === "objective" ? "objective reconcile" : "learn";
|
|
160
|
+
return (
|
|
161
|
+
`Warning: the land envelope's ${field} report was malformed — ${stateName} state ` +
|
|
162
|
+
"UNVERIFIED; inspect the objective (or run /objective-reconcile) manually."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
126
166
|
/**
|
|
127
|
-
* The single land implementation both surfaces call. Delegates the merge to the Python cold
|
|
128
|
-
* then mirrors the envelope's `pending_learn` (in-session path): marker + /learn nudge on
|
|
129
|
-
* ordinary arm; no marker, no nudge on the learn-docs exemption.
|
|
167
|
+
* The single land implementation both surfaces call. Delegates the merge to the Python cold
|
|
168
|
+
* door, then mirrors the envelope's `pending_learn` (in-session path): marker + /learn nudge on
|
|
169
|
+
* the ordinary arm; no marker, no nudge on the learn-docs exemption. A marker WRITE failure is
|
|
170
|
+
* loud-not-fatal: the merge is already verified, so the success result (and the reconcile
|
|
171
|
+
* drive) survive with a warning line naming the /learn remediation. Returns a soft result
|
|
130
172
|
* (never throws).
|
|
131
173
|
*/
|
|
132
|
-
|
|
174
|
+
async function landPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<LandResult> {
|
|
133
175
|
const fail = failFor(ctx, "land");
|
|
134
176
|
|
|
177
|
+
// Planning sessions never legitimately land — the first check, before any cold-door
|
|
178
|
+
// delegation (a positioned stacked planning session's cwd binding is the PREDECESSOR).
|
|
179
|
+
const planningRefusal = planningStageRefusal(ctx, "land");
|
|
180
|
+
if (planningRefusal !== null) return fail(planningRefusal, "planning_session");
|
|
181
|
+
|
|
135
182
|
const r = await runColdDoor<LandPayload>(pi, ctx, ["pr", "land", "--json"], {
|
|
136
183
|
label: "perk pr land",
|
|
137
184
|
decode: decodeLand,
|
|
138
185
|
});
|
|
139
186
|
if (!r.ok) return fail(r.message, r.errorType);
|
|
140
187
|
|
|
141
|
-
if (r.data.pending_learn) {
|
|
142
|
-
// Set the semaphore for the in-session path (idempotent; the cold door also set it on disk).
|
|
143
|
-
setMarker(ctx.cwd, PENDING_LEARN);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
188
|
const lines = [
|
|
147
189
|
r.data.pending_learn
|
|
148
190
|
? `Landed PR #${r.data.pr.number}; run /learn to release the worktree.`
|
|
149
191
|
: `Landed PR #${r.data.pr.number}; learn-docs plan — no learn pass needed; the worktree is releasable.`,
|
|
150
192
|
];
|
|
151
|
-
|
|
193
|
+
if (r.data.pending_learn) {
|
|
194
|
+
// Set the semaphore for the in-session path (idempotent; the cold door also set it on
|
|
195
|
+
// disk). Guarded: a filesystem failure must never erase a VERIFIED land result — the
|
|
196
|
+
// success report + reconcile drive survive, with the /learn remediation named loudly.
|
|
197
|
+
try {
|
|
198
|
+
setMarker(ctx.cwd, PENDING_LEARN);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
201
|
+
lines.push(
|
|
202
|
+
`Warning: the pending-learn marker could not be written (${detail}); run /learn ` +
|
|
203
|
+
"before releasing the worktree.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const obj = r.data.objective.state === "present" ? r.data.objective.value : undefined;
|
|
152
208
|
if (obj?.nodes_marked.length && obj.id !== null) {
|
|
153
209
|
// The reconcile pass is auto-driven after land (see driveReconcileAfterLand); just report it.
|
|
154
210
|
lines.push(
|
|
@@ -159,7 +215,7 @@ export async function landPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<L
|
|
|
159
215
|
if (obj?.closed && obj.id !== null) {
|
|
160
216
|
lines.push(`Objective #${obj.id} complete — closed.`);
|
|
161
217
|
}
|
|
162
|
-
const learn = r.data.learn;
|
|
218
|
+
const learn = r.data.learn.state === "present" ? r.data.learn.value : undefined;
|
|
163
219
|
if (learn?.closed.length) {
|
|
164
220
|
// hop-2: the consumed perk:learn issues were closed + labelled perk:consolidated on land.
|
|
165
221
|
lines.push(
|
|
@@ -173,21 +229,31 @@ export async function landPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<L
|
|
|
173
229
|
if (learn?.skipped_reason && !BENIGN_LEARN_SKIPS.has(learn.skipped_reason)) {
|
|
174
230
|
lines.push(`Warning: learn consume incomplete — ${learn.skipped_reason}.`);
|
|
175
231
|
}
|
|
232
|
+
// A malformed advisory sub-object leaves the merge verified but its state UNKNOWN: the
|
|
233
|
+
// details omit the field (as before) and the drive stays suppressed, but the miss is loud.
|
|
234
|
+
if (r.data.objective.state === "malformed") lines.push(malformedAdvisoryWarning("objective"));
|
|
235
|
+
if (r.data.learn.state === "malformed") lines.push(malformedAdvisoryWarning("learn"));
|
|
176
236
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
237
|
+
const details: LandOk = {
|
|
238
|
+
pr: r.data.pr,
|
|
239
|
+
branch: r.data.branch,
|
|
240
|
+
issue: r.data.issue,
|
|
241
|
+
pending_learn: r.data.pending_learn,
|
|
242
|
+
objective: obj,
|
|
243
|
+
learn: learn,
|
|
244
|
+
};
|
|
245
|
+
return ok(lines.join("\n"), details, { terminate: true });
|
|
182
246
|
}
|
|
183
247
|
|
|
184
248
|
/**
|
|
185
|
-
* After a successful land that marked at least one objective node done, drive the session into
|
|
186
|
-
* reconcile pass by injecting the exact guidance `/objective-reconcile` injects (warm-door
|
|
187
|
-
* pattern). The terminating `land` tool stays terminating — terminate only skips the
|
|
188
|
-
* follow-up LLM call, while a `followUp` user message is a separate deliberate new
|
|
189
|
-
* circuits (sends nothing) unless the land succeeded with an objective node marked
|
|
190
|
-
*
|
|
249
|
+
* After a successful land that marked at least one objective node done, drive the session into
|
|
250
|
+
* the reconcile pass by injecting the exact guidance `/objective-reconcile` injects (warm-door
|
|
251
|
+
* driving pattern). The terminating `land` tool stays terminating — terminate only skips the
|
|
252
|
+
* *automatic* follow-up LLM call, while a `followUp` user message is a separate deliberate new
|
|
253
|
+
* turn. Short-circuits (sends nothing) unless the land succeeded with an objective node marked
|
|
254
|
+
* done AND the id passes the marker-safe vocabulary — the id is interpolated into a steering
|
|
255
|
+
* message, so a poisoned envelope id never drives. Exported for the offline suite (the
|
|
256
|
+
* streaming `followUp` branch is unreachable through the idle harness).
|
|
191
257
|
*/
|
|
192
258
|
export function driveReconcileAfterLand(
|
|
193
259
|
pi: ExtensionAPI,
|
|
@@ -197,6 +263,7 @@ export function driveReconcileAfterLand(
|
|
|
197
263
|
if (!details.ok) return;
|
|
198
264
|
const obj = details.objective;
|
|
199
265
|
if (!obj || obj.id === null || obj.nodes_marked.length === 0) return;
|
|
266
|
+
if (!OBJECTIVE_ID_RE.test(obj.id)) return;
|
|
200
267
|
const message = reconcileGuidance(obj.id) + bindingSuffix(ctx.cwd, "command:objective-reconcile");
|
|
201
268
|
if (ctx.isIdle()) {
|
|
202
269
|
// The `/land` command path (idle): inject an immediate turn.
|
|
@@ -213,8 +280,9 @@ const TOOL_GUIDELINES = [
|
|
|
213
280
|
"land refuses a stacked-delivery plan (`delivery_lineage`): stacked layers land as one atomic train, never individually.",
|
|
214
281
|
];
|
|
215
282
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
283
|
+
/** Install the per-plan landing bindings: the `land` terminating tool + the `/land` command
|
|
284
|
+
* twin. */
|
|
285
|
+
export function installLandBindings(pi: ExtensionAPI): void {
|
|
218
286
|
pi.registerTool({
|
|
219
287
|
name: "land",
|
|
220
288
|
label: "Land PR",
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// The ready + handoff bindings: the `ready` terminating tool + the `/ready` command twin,
|
|
2
|
+
// adapting the Pi-free ready operation in `delivery/ready.ts`. The in-session twin of the Python
|
|
3
|
+
// cold door (`perk pr ready`): a terminating surface that DELEGATES the mechanics (mutations
|
|
4
|
+
// canonical in Python). For an incremental plan this is the review gate — perk deliberately does
|
|
5
|
+
// NOT auto-publish on submit; `/ready` is the explicit gesture that opens the draft PR for
|
|
6
|
+
// review. For a STACKED layer it is the deliberate HUMAN handoff made after review + address: it
|
|
7
|
+
// stamps the exact verified published head into the delivery journal (draft AND non-draft PRs —
|
|
8
|
+
// mark-ready mechanics first, then the journal append), and every successful stacked stamp
|
|
9
|
+
// continues into the ready-time reconcile pass (`driveReadyContinuation` — the warm
|
|
10
|
+
// continuation, contracts.md §8.66). Write nothing, delegate via the cold-door seam, surface the
|
|
11
|
+
// structured result, never throw. This tier is pure decoding, rendering, process invocation, and
|
|
12
|
+
// Pi delivery — the arm order, the strict evidence vocabulary, and the continuation decision
|
|
13
|
+
// live in the feature op.
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { objectiveReadInstruction } from "../../../authoring/objective/prose.ts";
|
|
17
|
+
import {
|
|
18
|
+
type ReadyDeps,
|
|
19
|
+
type ReadyDriveEvidence,
|
|
20
|
+
type ReadyFacts,
|
|
21
|
+
type ReadyHandoff,
|
|
22
|
+
type ReadyOutcome,
|
|
23
|
+
type ReadyPr,
|
|
24
|
+
readyChange,
|
|
25
|
+
} from "../../../delivery/ready.ts";
|
|
26
|
+
import { bindingSuffix } from "../../../substrate/bindingDelivery.ts";
|
|
27
|
+
import {
|
|
28
|
+
booleanField,
|
|
29
|
+
type ColdJson,
|
|
30
|
+
numberField,
|
|
31
|
+
objectField,
|
|
32
|
+
runColdDoor,
|
|
33
|
+
stringField,
|
|
34
|
+
} from "../../../substrate/coldDoor.ts";
|
|
35
|
+
import { registerPerkCommand } from "../../../substrate/command.ts";
|
|
36
|
+
import { resolveIssueBackendId } from "../../../substrate/config.ts";
|
|
37
|
+
import { render } from "../../../substrate/prompts.ts";
|
|
38
|
+
import { failFor, ok } from "../../../substrate/result.ts";
|
|
39
|
+
import type { ToolGating } from "../../../substrate/toolGating.ts";
|
|
40
|
+
import { report } from "../../../surfaces/report.ts";
|
|
41
|
+
import { fetchObjectiveUrl } from "../objective.ts";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Narrow the `perk pr ready --json` success payload into the correlated facts variants; strict
|
|
45
|
+
* on `pr`, lenient on the rest. The stacked continuation decodes as ONE cohort: a
|
|
46
|
+
* partial/wrong-typed cohort is validated-and-dropped whole into `stacked_unverified` (the
|
|
47
|
+
* worker's own success already proved the mechanics — advisory detail is never half-rendered),
|
|
48
|
+
* keeping the routing fact as the visible mismatch signal. An absent-or-false `stacked` (an old
|
|
49
|
+
* worker included) is the `incremental` variant, false-vs-absent preserved for the wire.
|
|
50
|
+
*/
|
|
51
|
+
function decodeReady(payload: ColdJson): ReadyFacts | null {
|
|
52
|
+
const prField = objectField(payload, "pr");
|
|
53
|
+
if (prField === undefined) return null;
|
|
54
|
+
const number = numberField(prField, "number");
|
|
55
|
+
const url = stringField(prField, "url");
|
|
56
|
+
if (number === undefined || url === undefined) return null;
|
|
57
|
+
const pr: ReadyPr = { number, url };
|
|
58
|
+
const wasDraft = booleanField(payload, "was_draft");
|
|
59
|
+
const stacked = booleanField(payload, "stacked");
|
|
60
|
+
if (stacked !== true) {
|
|
61
|
+
return { route: "incremental", pr, was_draft: wasDraft, stacked };
|
|
62
|
+
}
|
|
63
|
+
const objective = stringField(payload, "objective");
|
|
64
|
+
const node = stringField(payload, "node");
|
|
65
|
+
const stampedHead = stringField(payload, "stamped_head");
|
|
66
|
+
const stampAdvanced = booleanField(payload, "stamp_advanced");
|
|
67
|
+
const plan = stringField(payload, "plan");
|
|
68
|
+
const parentCheckpoint = stringField(payload, "parent_checkpoint");
|
|
69
|
+
if (
|
|
70
|
+
objective === undefined ||
|
|
71
|
+
node === undefined ||
|
|
72
|
+
stampedHead === undefined ||
|
|
73
|
+
stampAdvanced === undefined ||
|
|
74
|
+
plan === undefined ||
|
|
75
|
+
parentCheckpoint === undefined
|
|
76
|
+
) {
|
|
77
|
+
return { route: "stacked_unverified", pr, was_draft: wasDraft };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
route: "stacked",
|
|
81
|
+
pr,
|
|
82
|
+
was_draft: wasDraft,
|
|
83
|
+
handoff: {
|
|
84
|
+
objective,
|
|
85
|
+
node,
|
|
86
|
+
stamped_head: stampedHead,
|
|
87
|
+
stamp_advanced: stampAdvanced,
|
|
88
|
+
plan,
|
|
89
|
+
parent_checkpoint: parentCheckpoint,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The ONE production `MarkReady` adapter: `perk pr ready --json` through the cold-door seam
|
|
95
|
+
* (cancellation, envelope validation, and version-skew diagnostics ride the seam).
|
|
96
|
+
* Module-private: production composes only through `readyDepsFor`. */
|
|
97
|
+
function createReadyMarker(pi: ExtensionAPI, ctx: ExtensionContext): ReadyDeps["markReady"] {
|
|
98
|
+
return async () => {
|
|
99
|
+
const r = await runColdDoor<ReadyFacts>(pi, ctx, ["pr", "ready", "--json"], {
|
|
100
|
+
label: "perk pr ready",
|
|
101
|
+
decode: decodeReady,
|
|
102
|
+
});
|
|
103
|
+
if (!r.ok) return { ok: false, message: r.message, errorType: r.errorType };
|
|
104
|
+
return { ok: true, facts: r.data };
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The one production `ReadyDeps` composition (module-private: the one-composition invariant
|
|
109
|
+
* is structural). The gate read is the injected capability — the feature op reads it only on
|
|
110
|
+
* the stamped-with-cohort path. */
|
|
111
|
+
function readyDepsFor(pi: ExtensionAPI, ctx: ExtensionContext, gating: ToolGating): ReadyDeps {
|
|
112
|
+
return {
|
|
113
|
+
markReady: createReadyMarker(pi, ctx),
|
|
114
|
+
sessionReadOnly: () => gating.isActive(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The wire-identical ok-arm details rebuilt from the facts variants: incremental ⇒ `stacked`
|
|
119
|
+
* passthrough (false or absent), no `handoff`; `stacked_unverified` ⇒ `stacked: true`, no
|
|
120
|
+
* `handoff`; `stacked` ⇒ `stacked: true` + the six-field cohort. Optional keys carry
|
|
121
|
+
* `undefined` exactly where the old decode left them absent (the JSON round-trip drops them). */
|
|
122
|
+
interface ReadyDetails {
|
|
123
|
+
pr: ReadyPr;
|
|
124
|
+
was_draft?: boolean;
|
|
125
|
+
stacked?: boolean;
|
|
126
|
+
handoff?: ReadyHandoff;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readyDetails(facts: ReadyFacts): ReadyDetails {
|
|
130
|
+
switch (facts.route) {
|
|
131
|
+
case "incremental":
|
|
132
|
+
return { pr: facts.pr, was_draft: facts.was_draft, stacked: facts.stacked };
|
|
133
|
+
case "stacked_unverified":
|
|
134
|
+
return { pr: facts.pr, was_draft: facts.was_draft, stacked: true };
|
|
135
|
+
case "stacked":
|
|
136
|
+
return { pr: facts.pr, was_draft: facts.was_draft, stacked: true, handoff: facts.handoff };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Render the success message: the ready line, plus — exactly when the facts route is
|
|
141
|
+
* `stacked` — the handoff-stamped line. Stamp facts only: the continuation is announced by
|
|
142
|
+
* `driveReadyContinuation`, and only once the feature op's refusal arms have accepted. */
|
|
143
|
+
function renderReadyMessage(facts: ReadyFacts): string {
|
|
144
|
+
const verb = facts.was_draft ? "Marked ready" : "Already ready";
|
|
145
|
+
let message = `${verb}: PR #${facts.pr.number} is open for review.`;
|
|
146
|
+
if (facts.route === "stacked") {
|
|
147
|
+
const handoff = facts.handoff;
|
|
148
|
+
const stamped = handoff.stamp_advanced ? "Handoff stamped" : "Handoff already stamped";
|
|
149
|
+
message +=
|
|
150
|
+
` ${stamped}: objective #${handoff.objective} node ${handoff.node} at ` +
|
|
151
|
+
`${handoff.stamped_head}.`;
|
|
152
|
+
}
|
|
153
|
+
return message;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** One loud skipped-pass warning (the stamp itself stands; re-running `/ready` re-enters). */
|
|
157
|
+
function warnSkippedPass(ctx: ExtensionContext, reason: string, retry: string): void {
|
|
158
|
+
report(
|
|
159
|
+
ctx,
|
|
160
|
+
"ready",
|
|
161
|
+
"warning",
|
|
162
|
+
`ready-time reconcile pass not driven — ${reason}. The handoff stamp stands; re-run ${retry} to enter the pass.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The retry gesture from the feature op's safe-interpolation policy: a marker-safe plan id
|
|
167
|
+
* interpolates; `null` renders the `<plan>` placeholder. */
|
|
168
|
+
function retryGesture(retryPlan: string | null): string {
|
|
169
|
+
return `\`perk ready ${retryPlan ?? "<plan>"}\``;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Inject the rendered ready-time reconcile pass (contracts.md §8.66) — the warm twin of the
|
|
173
|
+
* cold wrapper's seeded launch. Interpolates exclusively from the mint-only evidence. */
|
|
174
|
+
async function driveReconcilePass(
|
|
175
|
+
pi: ExtensionAPI,
|
|
176
|
+
ctx: ExtensionContext,
|
|
177
|
+
evidence: ReadyDriveEvidence,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const backend = resolveIssueBackendId(ctx.cwd);
|
|
180
|
+
const url = backend === "linear" ? await fetchObjectiveUrl(pi, ctx, evidence.objective) : "";
|
|
181
|
+
const readClause = objectiveReadInstruction(backend, evidence.objective, url);
|
|
182
|
+
const message =
|
|
183
|
+
render("stages/objective-reconcile-ready.md", {
|
|
184
|
+
objective: evidence.objective,
|
|
185
|
+
node: evidence.node,
|
|
186
|
+
plan: evidence.plan,
|
|
187
|
+
pr: String(evidence.pr),
|
|
188
|
+
parent_checkpoint: evidence.parent_checkpoint,
|
|
189
|
+
stamped_head: evidence.stamped_head,
|
|
190
|
+
read_clause: readClause,
|
|
191
|
+
}) + bindingSuffix(ctx.cwd, "command:objective-reconcile");
|
|
192
|
+
// Announce the continuation only HERE — after every refusal arm has accepted the drive.
|
|
193
|
+
report(
|
|
194
|
+
ctx,
|
|
195
|
+
"ready",
|
|
196
|
+
"info",
|
|
197
|
+
`continuing into the ready-time reconcile pass — objective #${evidence.objective}, ` +
|
|
198
|
+
`pinned range ${evidence.parent_checkpoint}..${evidence.stamped_head}`,
|
|
199
|
+
);
|
|
200
|
+
if (ctx.isIdle()) {
|
|
201
|
+
// The `/ready` command path (idle): inject an immediate turn.
|
|
202
|
+
pi.sendUserMessage(message);
|
|
203
|
+
} else {
|
|
204
|
+
// The `ready` tool path (streaming): deliver after the terminating ready batch.
|
|
205
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Translate the feature op's outcome into the warm continuation (contracts.md §8.66): the drive
|
|
211
|
+
* fires on EVERY accepted stacked stamp (`stamp_advanced: false` re-stamps included — re-running
|
|
212
|
+
* `/ready` re-enters reconciliation). The refusal arms are LOUD, never silent; the stamp itself
|
|
213
|
+
* always stands. `failed` and `completed` are explicit quiet arms. Exported as the one direct
|
|
214
|
+
* test seam — the streaming `followUp` branch is unreachable through the idle harness.
|
|
215
|
+
*/
|
|
216
|
+
export async function driveReadyContinuation(
|
|
217
|
+
pi: ExtensionAPI,
|
|
218
|
+
ctx: ExtensionContext,
|
|
219
|
+
outcome: ReadyOutcome,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
switch (outcome.kind) {
|
|
222
|
+
case "failed":
|
|
223
|
+
case "completed":
|
|
224
|
+
return; // exterior failure / incremental: nothing to drive, quietly
|
|
225
|
+
case "stamp_facts_unverified":
|
|
226
|
+
// A successful stacked stamp whose continuation cohort failed to decode — a
|
|
227
|
+
// malformed/mixed-version envelope must never fail silent.
|
|
228
|
+
warnSkippedPass(
|
|
229
|
+
ctx,
|
|
230
|
+
"the worker reported a stacked stamp but its continuation facts were malformed " +
|
|
231
|
+
"(a mixed-version envelope?)",
|
|
232
|
+
"/ready",
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
case "stamped": {
|
|
236
|
+
const continuation = outcome.continuation;
|
|
237
|
+
switch (continuation.kind) {
|
|
238
|
+
case "refused_read_only":
|
|
239
|
+
warnSkippedPass(
|
|
240
|
+
ctx,
|
|
241
|
+
"this session is read-only (the pass's write tools are gated off); exit the " +
|
|
242
|
+
"read-only session or run the pass from a terminal",
|
|
243
|
+
retryGesture(continuation.retryPlan),
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
case "evidence_invalid":
|
|
247
|
+
warnSkippedPass(
|
|
248
|
+
ctx,
|
|
249
|
+
"the stamp evidence failed strict validation (ids marker-safe; both diff-range " +
|
|
250
|
+
"endpoints full 40-hex lowercase)",
|
|
251
|
+
retryGesture(continuation.retryPlan),
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
case "drive":
|
|
255
|
+
await driveReconcilePass(pi, ctx, continuation.evidence);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Exhaustive over the continuation (no catch-all): union growth breaks the adapter here.
|
|
259
|
+
const exhaustiveContinuation: never = continuation;
|
|
260
|
+
throw new Error(`unreachable ready continuation: ${JSON.stringify(exhaustiveContinuation)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Exhaustive over the outcome (no catch-all): union growth breaks the adapter here.
|
|
264
|
+
const exhaustive: never = outcome;
|
|
265
|
+
throw new Error(`unreachable ready outcome: ${JSON.stringify(exhaustive)}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const TOOL_GUIDELINES = [
|
|
269
|
+
"For an incremental plan, call ready only when the PR is ready for human review; it marks the draft PR ready (the deliberate review gate). submit keeps the PR draft on purpose.",
|
|
270
|
+
"For a STACKED plan, /ready is the deliberate HUMAN handoff made AFTER review + address: it stamps the exact verified published head into the delivery journal (draft and non-draft PRs alike), and the recorded stamp unblocks planning of the layer's direct dependents. Never call it as routine post-submit choreography — review happens on the draft layer PR; only invoke it when the human explicitly asks.",
|
|
271
|
+
"ready operates on the active plan's worktree — it takes no arguments; the PR is discovered from the local plan-ref's branch. Idempotent: an already-ready PR is success, and a re-run converges on the same stamp.",
|
|
272
|
+
"A failed stamp (error_type ready_stamp_failed) names its own remediation: the ambiguous/transient arms converge on re-run; deterministic failures need their named repair first.",
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
/** Install the ready + handoff bindings: the `ready` terminating tool + the `/ready` command
|
|
276
|
+
* twin. */
|
|
277
|
+
export function installReadyBindings(pi: ExtensionAPI, gating: ToolGating): void {
|
|
278
|
+
pi.registerTool({
|
|
279
|
+
name: "ready",
|
|
280
|
+
label: "Mark PR ready",
|
|
281
|
+
description:
|
|
282
|
+
"Ready the active plan's PR. Incremental: mark the draft PR ready for review (the " +
|
|
283
|
+
"deliberate review gate; submit keeps the PR draft). Stacked: the deliberate post-review " +
|
|
284
|
+
"HUMAN handoff — stamps the exact verified published head (draft and non-draft PRs); " +
|
|
285
|
+
"never routine post-submit choreography, never auto-run. Terminating: ends the turn.",
|
|
286
|
+
promptSnippet:
|
|
287
|
+
"Ready the PR: open the draft for review (incremental) or record the post-review " +
|
|
288
|
+
"handoff stamp (stacked; human-asked only). Terminates the turn.",
|
|
289
|
+
promptGuidelines: TOOL_GUIDELINES,
|
|
290
|
+
executionMode: "sequential",
|
|
291
|
+
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
292
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
293
|
+
const fail = failFor(ctx, "ready");
|
|
294
|
+
const outcome = await readyChange(readyDepsFor(pi, ctx, gating));
|
|
295
|
+
if (outcome.kind === "failed") return fail(outcome.message, outcome.errorType);
|
|
296
|
+
const result = ok(renderReadyMessage(outcome.facts), readyDetails(outcome.facts), {
|
|
297
|
+
terminate: true,
|
|
298
|
+
});
|
|
299
|
+
await driveReadyContinuation(pi, ctx, outcome);
|
|
300
|
+
return result;
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
registerPerkCommand(pi, "ready", {
|
|
305
|
+
description:
|
|
306
|
+
"Ready the plan's PR: open the draft for review (incremental) or record the " +
|
|
307
|
+
"post-review handoff stamp (stacked).",
|
|
308
|
+
handler: async (_args, ctx) => {
|
|
309
|
+
const fail = failFor(ctx, "ready");
|
|
310
|
+
const outcome = await readyChange(readyDepsFor(pi, ctx, gating));
|
|
311
|
+
// Failure is reported loudly via failFor (the single error surface) — success only.
|
|
312
|
+
if (outcome.kind === "failed") {
|
|
313
|
+
fail(outcome.message, outcome.errorType);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Report-before-drive (load-bearing order): the success line lands before the injected
|
|
317
|
+
// reconcile turn.
|
|
318
|
+
report(ctx, "ready", "info", renderReadyMessage(outcome.facts));
|
|
319
|
+
await driveReadyContinuation(pi, ctx, outcome);
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|