@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,362 +0,0 @@
|
|
|
1
|
-
// The SECOND 3rd-party plan adapter — and the first with the AUGMENT posture. A perk-owned shim
|
|
2
|
-
// that enables `@plannotator/pi-extension` as a REAL, selectable plan provider: unlike the tombell
|
|
3
|
-
// adapter (REPLACE posture — perk's plan surface fully vacates), plannotator AUGMENTS perk's plan
|
|
4
|
-
// flow. perk's `/plan` mode, authoring injection, and read-only gate STAY (planMode skips only the
|
|
5
|
-
// `--plan` flag + `Ctrl+Alt+P` shortcut — the two real registration collisions).
|
|
6
|
-
//
|
|
7
|
-
// INJECTION + BRIDGE ONLY: the `plan_review` TOOL lives in `extension/factories/planReview.ts`
|
|
8
|
-
// (perk's backend-neutral review door); this module is the injection-only adapter shape. It owns
|
|
9
|
-
// (1) the plannotator review-step authoring context (injected while the gate is active AND
|
|
10
|
-
// plannotator is selected — THREE content flavors, one customType, each once-only: branch-scan
|
|
11
|
-
// dedup'd on the flavor's marker: the plan bridge context, the objective flavor when the stage
|
|
12
|
-
// is `objective-author` or `objective-save` (both objective stages route to the objective
|
|
13
|
-
// review arm), or the gist flavor when the stage is `gist-author`) and (2) the pure
|
|
14
|
-
// event-bus bridge
|
|
15
|
-
// (`requestPlannotatorPlanReview`; `createPlannotatorBridge` is its thin structural wrapper)
|
|
16
|
-
// that planReview.ts dispatches to when plannotator is the selected plan provider and the
|
|
17
|
-
// plan-review browser open (plannotatorHandoff.ts) launches. The bridge speaks plannotator's
|
|
18
|
-
// published `plannotator:request` event API (in-process `pi.events` bus); the decision wait is
|
|
19
|
-
// a per-review `plannotator:review-result` listener disposed via the unsubscribe pi's
|
|
20
|
-
// `EventBus.on` returns.
|
|
21
|
-
//
|
|
22
|
-
// INERT BY DEFAULT. The shim is ALWAYS registered in index.ts but the injection fires only when
|
|
23
|
-
// the resolved `[providers] plan` selection is `plannotator-plan` (read fresh per-event, same
|
|
24
|
-
// shape as planMode/planAdapterTombell). On any other selection the context handler only strips
|
|
25
|
-
// its own stale marker — zero behavior change on the default path.
|
|
26
|
-
//
|
|
27
|
-
// INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
|
|
28
|
-
// restamps `cache.plan-ref.provider` (stays `"github"`). The adapter is INJECTION-ONLY again
|
|
29
|
-
// (Invariant 1: composes, never owns) — the review tool, the `approvalSave` composition, and the
|
|
30
|
-
// gate exit all live behind planReview.ts's seams; the injection's gate-active check reads the
|
|
31
|
-
// persisted `perk:workflow-state.mode`, the gate's own state twin.
|
|
32
|
-
//
|
|
33
|
-
// EVENT ENVELOPE (pinned against `@plannotator/pi-extension@0.20.0`, `plannotator-events.ts` —
|
|
34
|
-
// verified unchanged through 0.26.1):
|
|
35
|
-
// request — pi.events.emit("plannotator:request", { requestId, action: "plan-review",
|
|
36
|
-
// payload: { planContent, origin? }, respond }) // respond = in-payload callback
|
|
37
|
-
// handshake — respond({ status: "handled", result: { status: "pending", reviewId } })
|
|
38
|
-
// | respond({ status: "unavailable", error? }) | respond({ status: "error", error })
|
|
39
|
-
// decision — pi.events.on("plannotator:review-result", { reviewId, approved, feedback?, ... })
|
|
40
|
-
//
|
|
41
|
-
// DIRECT EDITS FEEDBACK FORMAT (pinned against plannotator `packages/editor/directEdits.ts`,
|
|
42
|
-
// `buildDirectEditsSection` / `composeFeedbackWithDirectEdits`, at v0.26.1). The browser's
|
|
43
|
-
// direct-edit mode arrives as PROSE inside the existing `feedback` string, never a new envelope
|
|
44
|
-
// field: `# Direct Edits\n` + blank line + a one-sentence preamble (two wording variants — never
|
|
45
|
-
// couple to it) + blank line + a ```diff fence containing
|
|
46
|
-
// `createTwoFilesPatch('plan.md (original)', 'plan.md (edited)', base, edited, undefined,
|
|
47
|
-
// undefined, { context: 3 }).trimEnd()` against the exact bytes perk submitted. The section is
|
|
48
|
-
// composed FIRST; non-sentinel annotation feedback follows after `\n\n---\n\n`; edits-only
|
|
49
|
-
// feedback is just the section. `extractDirectEdits` below parses it strictly (fail-open — a
|
|
50
|
-
// null degrades to today's verbatim behavior).
|
|
51
|
-
|
|
52
|
-
import { randomUUID } from "node:crypto";
|
|
53
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
54
|
-
import { GIST_AUTHOR_STAGE } from "../factories/gistAuthor.ts";
|
|
55
|
-
import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
|
|
56
|
-
import { OBJECTIVE_SAVE_STAGE } from "../factories/objectiveSave.ts";
|
|
57
|
-
import { resolvedPlanProviderId } from "../factories/planMode.ts";
|
|
58
|
-
// Type-only (erased at runtime — no cycle): the outcome vocabulary lives with the review door.
|
|
59
|
-
import type { ReviewOutcome } from "../factories/planReview.ts";
|
|
60
|
-
import { render } from "../substrate/prompts.ts";
|
|
61
|
-
import { PLANNOTATOR_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
|
|
62
|
-
import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
63
|
-
|
|
64
|
-
/** The plannotator plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
|
|
65
|
-
export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE = "perk:plan-adapter-plannotator";
|
|
66
|
-
const PLAN_ADAPTER_PLANNOTATOR_MARKER = "[PLAN ADAPTER: PLANNOTATOR]";
|
|
67
|
-
const OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER = "[OBJECTIVE ADAPTER: PLANNOTATOR]";
|
|
68
|
-
const GIST_ADAPTER_PLANNOTATOR_MARKER = "[GIST ADAPTER: PLANNOTATOR]";
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The handshake timeout for plannotator's immediate `respond` callback (mirrors plannotator's own
|
|
72
|
-
* `PLANNOTATOR_TIMEOUT_MS = 5_000`). Overridable for tests via PERK_PLANNOTATOR_HANDSHAKE_MS.
|
|
73
|
-
*/
|
|
74
|
-
export const PLANNOTATOR_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
75
|
-
|
|
76
|
-
function handshakeTimeoutMs(): number {
|
|
77
|
-
const raw = Number(process.env.PERK_PLANNOTATOR_HANDSHAKE_MS ?? "");
|
|
78
|
-
return Number.isFinite(raw) && raw > 0 ? raw : PLANNOTATOR_HANDSHAKE_TIMEOUT_MS;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The augment-posture bridge prompt: perk's plan-authoring discipline plus the plannotator review
|
|
83
|
-
* step. Prompting, NOT enforcement (perk's own gate is the read-only authority). Durable anchors
|
|
84
|
-
* only — mirrors PLAN_AUTHORING_CONTEXT, which is also injected (perk's plan mode stays).
|
|
85
|
-
*/
|
|
86
|
-
export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-plan.md", {
|
|
87
|
-
marker: PLAN_ADAPTER_PLANNOTATOR_MARKER,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The objective flavor of the bridge prompt, injected in an objective-authoring session
|
|
92
|
-
* (stage `objective-author` or `objective-save`) instead of the plan flavor. An APPROVED review auto-saves the
|
|
93
|
-
* objective via the `objectiveApprovalSave` seam; `/objective-save` is the manual failsafe on
|
|
94
|
-
* the skipped/unavailable arms.
|
|
95
|
-
*/
|
|
96
|
-
export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = render(
|
|
97
|
-
"contexts/adapters/plannotator-objective.md",
|
|
98
|
-
{ marker: OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER },
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* The gist flavor of the bridge prompt, injected in a `gist-author` session instead of the
|
|
103
|
-
* plan/objective flavors. The review surface renders the gist draft (title + scope line +
|
|
104
|
-
* prose); an approval carrying `# Direct Edits` does NOT auto-save — the model folds the diff
|
|
105
|
-
* into the matching `gist_draft` fields and re-reviews (contracts.md §8.23's gist arm).
|
|
106
|
-
*/
|
|
107
|
-
export const GIST_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-gist.md", {
|
|
108
|
-
marker: GIST_ADAPTER_PLANNOTATOR_MARKER,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
/** Whether the foreign `plannotator-plan` provider is the selected plan provider for `cwd`. */
|
|
112
|
-
export function isPlannotatorPlanSelected(cwd: string): boolean {
|
|
113
|
-
return resolvedPlanProviderId(cwd) === PLANNOTATOR_PLAN_PROVIDER_ID;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ------------------------------------------------------------------ the event-bus bridge core
|
|
117
|
-
|
|
118
|
-
/** The minimal `pi.events` surface the bridge needs (mirrors pi's EventBus, whose `on` returns an unsubscribe function). */
|
|
119
|
-
export interface PlannotatorBus {
|
|
120
|
-
emit(channel: string, data: unknown): void;
|
|
121
|
-
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Plannotator's immediate `respond` handshake payload (pinned envelope, see header). */
|
|
125
|
-
interface HandshakeResponse {
|
|
126
|
-
status?: string;
|
|
127
|
-
error?: string;
|
|
128
|
-
result?: { status?: string; reviewId?: string };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** The human decision arriving on `plannotator:review-result`. */
|
|
132
|
-
interface ReviewDecision {
|
|
133
|
-
approved: boolean;
|
|
134
|
-
feedback?: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* The pure, offline-testable plan-review bridge (the ergonomic mirror of
|
|
139
|
-
* `requestPlannotatorCodeReview` in plannotatorHandoff.ts): emit ONE `plannotator:request` with
|
|
140
|
-
* `action: "plan-review"`, await the bounded `respond` handshake, then await the human decision
|
|
141
|
-
* on a PER-REVIEW `plannotator:review-result` listener — filtered on the handshake's `reviewId`
|
|
142
|
-
* and disposed via the unsubscribe `bus.on` returns when the decision arrives or the turn
|
|
143
|
-
* aborts. Pure over the bus → unit-testable offline with a fake plannotator listener.
|
|
144
|
-
*/
|
|
145
|
-
export async function requestPlannotatorPlanReview(
|
|
146
|
-
bus: PlannotatorBus,
|
|
147
|
-
plan: string,
|
|
148
|
-
signal?: AbortSignal,
|
|
149
|
-
): Promise<ReviewOutcome> {
|
|
150
|
-
if (signal?.aborted) return { status: "aborted" };
|
|
151
|
-
|
|
152
|
-
// 1. Emit the request and await the immediate `respond` handshake (bounded — fail-open).
|
|
153
|
-
const requestId = randomUUID();
|
|
154
|
-
let respondResolve: (response: HandshakeResponse) => void = () => {};
|
|
155
|
-
const handshake = new Promise<HandshakeResponse | "timeout">((resolve) => {
|
|
156
|
-
respondResolve = resolve;
|
|
157
|
-
});
|
|
158
|
-
const timer = setTimeout(() => respondResolve("timeout" as never), handshakeTimeoutMs());
|
|
159
|
-
bus.emit("plannotator:request", {
|
|
160
|
-
requestId,
|
|
161
|
-
action: "plan-review",
|
|
162
|
-
payload: { planContent: plan, origin: "perk" },
|
|
163
|
-
respond: (response: unknown) => respondResolve(response as HandshakeResponse),
|
|
164
|
-
});
|
|
165
|
-
const response = await handshake;
|
|
166
|
-
clearTimeout(timer);
|
|
167
|
-
|
|
168
|
-
if (response === "timeout") {
|
|
169
|
-
return {
|
|
170
|
-
status: "unavailable",
|
|
171
|
-
warning: "plannotator did not respond to the review request (handshake timeout)",
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (response?.status !== "handled") {
|
|
175
|
-
const detail = response?.error ? `: ${response.error}` : "";
|
|
176
|
-
return {
|
|
177
|
-
status: "unavailable",
|
|
178
|
-
warning: `plannotator reported ${response?.status ?? "an invalid response"}${detail}`,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const reviewId = response.result?.reviewId;
|
|
182
|
-
if (response.result?.status !== "pending" || typeof reviewId !== "string") {
|
|
183
|
-
return {
|
|
184
|
-
status: "unavailable",
|
|
185
|
-
warning: "plannotator handshake returned no pending reviewId",
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// A turn aborted DURING the handshake wait must not wedge: `addEventListener("abort", …)` on
|
|
190
|
-
// an already-aborted signal never fires, so re-check before registering the decision wait.
|
|
191
|
-
if (signal?.aborted) return { status: "aborted" };
|
|
192
|
-
|
|
193
|
-
// 2. Await the human decision (no timeout — the reviewer takes as long as they take), but
|
|
194
|
-
// honor a turn abort so an interrupted session never leaks a wedged promise. Either exit
|
|
195
|
-
// disposes the result listener via the unsubscribe.
|
|
196
|
-
return await new Promise<ReviewOutcome>((resolve) => {
|
|
197
|
-
let settled = false;
|
|
198
|
-
const finish = (outcome: ReviewOutcome): void => {
|
|
199
|
-
if (settled) return;
|
|
200
|
-
settled = true;
|
|
201
|
-
unsubscribe();
|
|
202
|
-
signal?.removeEventListener("abort", onAbort);
|
|
203
|
-
resolve(outcome);
|
|
204
|
-
};
|
|
205
|
-
const onAbort = (): void => finish({ status: "aborted" });
|
|
206
|
-
const unsubscribe = bus.on("plannotator:review-result", (data) => {
|
|
207
|
-
const d = data as { reviewId?: unknown; approved?: unknown; feedback?: unknown };
|
|
208
|
-
if (d?.reviewId !== reviewId) return;
|
|
209
|
-
const decision: ReviewDecision = {
|
|
210
|
-
approved: d.approved === true,
|
|
211
|
-
feedback: typeof d.feedback === "string" && d.feedback.trim() ? d.feedback : undefined,
|
|
212
|
-
};
|
|
213
|
-
finish({ status: "completed", reviewId, ...decision });
|
|
214
|
-
});
|
|
215
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Create the plannotator bridge over an event bus — the thin structural slice
|
|
221
|
-
* (`{ review(plan, signal) }`) that `registerPlanReview` injects into the review door; the body
|
|
222
|
-
* lives in `requestPlannotatorPlanReview`.
|
|
223
|
-
*/
|
|
224
|
-
export function createPlannotatorBridge(bus: PlannotatorBus): {
|
|
225
|
-
review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome>;
|
|
226
|
-
} {
|
|
227
|
-
return { review: (plan, signal) => requestPlannotatorPlanReview(bus, plan, signal) };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ------------------------------------------------------------------ Direct Edits extraction
|
|
231
|
-
|
|
232
|
-
const DIRECT_EDITS_HEADING = "# Direct Edits";
|
|
233
|
-
const DIFF_FENCE_OPEN = "```diff\n";
|
|
234
|
-
const REMAINDER_SEPARATOR = "\n\n---\n\n";
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Whether `feedback` OPENS with the Direct Edits heading (plan-review feedback composes the
|
|
238
|
-
* section first — a heading anywhere else is quoted prose, not a section). Callers pair this
|
|
239
|
-
* with `extractDirectEdits`: heading present but extraction null means the section was seen but
|
|
240
|
-
* could not be honored (the fail-open ladder's loud-warning arm).
|
|
241
|
-
*/
|
|
242
|
-
export function hasDirectEditsHeading(feedback: string): boolean {
|
|
243
|
-
return feedback === DIRECT_EDITS_HEADING || feedback.startsWith(`${DIRECT_EDITS_HEADING}\n`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Strictly extract the Direct Edits unified diff from a plannotator review-result `feedback`
|
|
248
|
-
* string (the format pin lives in the module header). Returns the fence body as `diff` plus the
|
|
249
|
-
* annotation `remainder` after the section (one leading `\n\n---\n\n` separator stripped;
|
|
250
|
-
* `undefined` when blank). Null means "no extractable Direct Edits section" — both the
|
|
251
|
-
* no-section case AND a present-heading-but-unparseable body (callers distinguish the two via
|
|
252
|
-
* `hasDirectEditsHeading`). The preamble prose between the heading and the fence is skipped
|
|
253
|
-
* without inspecting its wording (plannotator ships two variants).
|
|
254
|
-
*/
|
|
255
|
-
export function extractDirectEdits(feedback: string): { diff: string; remainder?: string } | null {
|
|
256
|
-
if (!hasDirectEditsHeading(feedback)) return null;
|
|
257
|
-
const openIdx = feedback.indexOf(`\n${DIFF_FENCE_OPEN}`, DIRECT_EDITS_HEADING.length);
|
|
258
|
-
if (openIdx === -1) return null;
|
|
259
|
-
const bodyStart = openIdx + 1 + DIFF_FENCE_OPEN.length;
|
|
260
|
-
// The closing fence is the first line that is exactly ``` — unambiguous inside the body,
|
|
261
|
-
// because every diff body line carries a prefix char (` `/`-`/`+`/`\`/`@`), so no body line
|
|
262
|
-
// can start with a backtick.
|
|
263
|
-
let close = -1;
|
|
264
|
-
let searchFrom = bodyStart;
|
|
265
|
-
while (close === -1) {
|
|
266
|
-
const idx = feedback.indexOf("\n```", searchFrom);
|
|
267
|
-
if (idx === -1) return null;
|
|
268
|
-
const after = feedback[idx + 4];
|
|
269
|
-
if (after === undefined || after === "\n") {
|
|
270
|
-
close = idx;
|
|
271
|
-
} else {
|
|
272
|
-
searchFrom = idx + 4;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const diff = feedback.slice(bodyStart, close);
|
|
276
|
-
if (diff.trim() === "") return null;
|
|
277
|
-
let rest = feedback.slice(close + 4);
|
|
278
|
-
if (rest.startsWith(REMAINDER_SEPARATOR)) rest = rest.slice(REMAINDER_SEPARATOR.length);
|
|
279
|
-
return { diff, remainder: rest.trim() === "" ? undefined : rest };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// ----------------------------------------------------------------------------- registration
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Register the plannotator plan adapter: the augment-posture authoring-context injection, inert
|
|
286
|
-
* unless `[providers] plan = "plannotator-plan"`. INJECTION-ONLY (Invariant 1: composes, never
|
|
287
|
-
* owns) — the `plan_review` tool lives in planReview.ts (the backend-neutral review door), which
|
|
288
|
-
* dispatches to this module's bridge when plannotator is selected; the adapter itself never
|
|
289
|
-
* arbitrates tools and needs no gating.
|
|
290
|
-
*/
|
|
291
|
-
export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
|
|
292
|
-
// Inject the bridge context while the read-only gate is active AND plannotator is selected.
|
|
293
|
-
// Three content flavors, one customType: an objective-authoring session (also read-only —
|
|
294
|
-
// BOTH objective stages: `plan_review` routes objective-author AND objective-save to the
|
|
295
|
-
// objective review arm) gets the objective flavor (the review surface renders the objective
|
|
296
|
-
// draft), a gist-author session gets the gist flavor (the rendered gist draft); any other
|
|
297
|
-
// gated stage gets the plan flavor. The gate-active check reads the persisted
|
|
298
|
-
// `perk:workflow-state.mode` (the gate's state twin) — never the gate itself.
|
|
299
|
-
pi.on("before_agent_start", async (_event, ctx) => {
|
|
300
|
-
if (!isPlannotatorPlanSelected(ctx.cwd)) return;
|
|
301
|
-
const branch = branchOf(ctx);
|
|
302
|
-
const state = rebuildWorkflowState(branch);
|
|
303
|
-
if (state.mode !== "read-only") return;
|
|
304
|
-
const flavor =
|
|
305
|
-
state.stage === OBJECTIVE_AUTHOR_STAGE || state.stage === OBJECTIVE_SAVE_STAGE
|
|
306
|
-
? "objective"
|
|
307
|
-
: state.stage === GIST_AUTHOR_STAGE
|
|
308
|
-
? "gist"
|
|
309
|
-
: "plan";
|
|
310
|
-
const content =
|
|
311
|
-
flavor === "objective"
|
|
312
|
-
? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
|
|
313
|
-
: flavor === "gist"
|
|
314
|
-
? GIST_ADAPTER_PLANNOTATOR_CONTEXT
|
|
315
|
-
: PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
|
|
316
|
-
// Once-only PER FLAVOR: the dedup key is the flavor's marker (not the shared customType), so
|
|
317
|
-
// a stage change still delivers the missing flavor while a prior copy of another flavor
|
|
318
|
-
// sits on the branch. Injected customs persist, so a live copy suppresses re-injection;
|
|
319
|
-
// compaction dropping it makes the scan come up clean and the next turn re-injects.
|
|
320
|
-
const marker =
|
|
321
|
-
flavor === "objective"
|
|
322
|
-
? OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER
|
|
323
|
-
: flavor === "gist"
|
|
324
|
-
? GIST_ADAPTER_PLANNOTATOR_MARKER
|
|
325
|
-
: PLAN_ADAPTER_PLANNOTATOR_MARKER;
|
|
326
|
-
if (branchCarries(branch, marker)) return;
|
|
327
|
-
return {
|
|
328
|
-
message: {
|
|
329
|
-
customType: PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE,
|
|
330
|
-
content,
|
|
331
|
-
display: false,
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// Strip the stale bridge markers (ALL THREE flavors) from context when plannotator-plan is no
|
|
337
|
-
// longer selected (same hygiene as the tombell shim), so they never linger across a deselect.
|
|
338
|
-
const hasMarker = (text: string): boolean =>
|
|
339
|
-
text.includes(PLAN_ADAPTER_PLANNOTATOR_MARKER) ||
|
|
340
|
-
text.includes(OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER) ||
|
|
341
|
-
text.includes(GIST_ADAPTER_PLANNOTATOR_MARKER);
|
|
342
|
-
pi.on("context", async (event, ctx) => {
|
|
343
|
-
if (isPlannotatorPlanSelected(ctx.cwd)) return;
|
|
344
|
-
return {
|
|
345
|
-
messages: event.messages.filter((m) => {
|
|
346
|
-
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
347
|
-
if (msg.customType === PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE) return false;
|
|
348
|
-
if (msg.role !== "user") return true;
|
|
349
|
-
const content = msg.content;
|
|
350
|
-
if (typeof content === "string") return !hasMarker(content);
|
|
351
|
-
if (Array.isArray(content)) {
|
|
352
|
-
return !content.some(
|
|
353
|
-
(c) =>
|
|
354
|
-
(c as { type?: string; text?: string }).type === "text" &&
|
|
355
|
-
hasMarker((c as { text?: string }).text ?? ""),
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
return true;
|
|
359
|
-
}),
|
|
360
|
-
};
|
|
361
|
-
});
|
|
362
|
-
}
|