@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,494 @@
|
|
|
1
|
+
// The PRIVATE SDK adapter behind the stage-execution seam (contracts.md §8.11).
|
|
2
|
+
//
|
|
3
|
+
// Every `@earendil-works/*` import on the drive path lives HERE — construction, raw session
|
|
4
|
+
// events (translated at the boundary into the perk-owned `StageEvent` union), and prompt/abort
|
|
5
|
+
// ownership are adapter-confined so the seam (`stageExecution.ts`) carries no SDK vocabulary on
|
|
6
|
+
// its caller surface and folds policy (budget, terminal capture, outcome) over perk shapes only. The only production
|
|
7
|
+
// importer is the seam itself (enforced by `extension/importDirectionGuard.test.ts` Rule F);
|
|
8
|
+
// tests import this module deliberately (to mint `WorkerModelSelection` and drive the handle).
|
|
9
|
+
//
|
|
10
|
+
// The opacity contract (narrow, stated exactly): `WorkerModelSelection` is *nominal* —
|
|
11
|
+
// `#private` fields make structural forgery impossible — and is minted only here (production
|
|
12
|
+
// imports of this module are guard-banned outside the seam). SDK types still appear on this
|
|
13
|
+
// adapter-owned class surface; the caller-side guarantee is the import-edge ban plus nominal
|
|
14
|
+
// minting, nothing stronger.
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
// pi-ai's `ModelThinkingLevel` (`"off" | minimal | … | xhigh`) is the union `resolveCliModel`
|
|
20
|
+
// returns and `createAgentSessionFromServices` accepts; the pi-coding-agent root does not
|
|
21
|
+
// re-export a thinking-level type (only `ThinkingLevelChangeEntry`).
|
|
22
|
+
import type { Api, Model, ModelThinkingLevel as ThinkingLevel } from "@earendil-works/pi-ai";
|
|
23
|
+
import {
|
|
24
|
+
type CreateAgentSessionRuntimeFactory,
|
|
25
|
+
createAgentSessionFromServices,
|
|
26
|
+
createAgentSessionRuntime,
|
|
27
|
+
createAgentSessionServices,
|
|
28
|
+
ModelRuntime,
|
|
29
|
+
resolveCliModel,
|
|
30
|
+
SessionManager,
|
|
31
|
+
SettingsManager,
|
|
32
|
+
} from "@earendil-works/pi-coding-agent";
|
|
33
|
+
|
|
34
|
+
// --- structural shapes (kept minimal so pure helpers stay offline-testable) ---------------------
|
|
35
|
+
|
|
36
|
+
/** The slice of an agent session event the worker reads (structural — see agent-session.d.ts). */
|
|
37
|
+
export interface DriveEvent {
|
|
38
|
+
type: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
result?: unknown;
|
|
41
|
+
isError?: boolean;
|
|
42
|
+
message?: {
|
|
43
|
+
role?: string;
|
|
44
|
+
stopReason?: string;
|
|
45
|
+
errorMessage?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Assistant token usage. `reasoning` is a provider-reported breakdown that is a **subset of
|
|
48
|
+
* `output`** on every pi-ai provider that populates it (anthropic `thinking_tokens`, google
|
|
49
|
+
* `thoughtsTokenCount` folded into `output`, openai `reasoning_tokens` inside completion/
|
|
50
|
+
* output tokens — verified @ pi-ai 0.80.5), so it is deliberately EXCLUDED from the budget
|
|
51
|
+
* sum: adding it would double-count.
|
|
52
|
+
*/
|
|
53
|
+
usage?: { input?: number; output?: number; reasoning?: number };
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The session surface the worker drives (structurally satisfied by pi's `AgentSession`). */
|
|
58
|
+
export interface DriveSessionLike {
|
|
59
|
+
bindExtensions(bindings: unknown): Promise<void>;
|
|
60
|
+
subscribe(listener: (event: DriveEvent) => void): () => void;
|
|
61
|
+
prompt(text: string): Promise<void>;
|
|
62
|
+
abort(): Promise<void>;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
sessionManager: { getBranch(): unknown[]; getSessionFile?(): string | null };
|
|
65
|
+
/**
|
|
66
|
+
* Optional (presence-gated): when the session exposes its extension runner, the seam
|
|
67
|
+
* preflights the stage's terminating perk tool post-bind and fails fast (zero-turn
|
|
68
|
+
* `no_extension_tools`) instead of burning the budget on a tool-less session.
|
|
69
|
+
*/
|
|
70
|
+
extensionRunner?: { getAllRegisteredTools(): { definition: { name: string } }[] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The runtime surface (structurally satisfied by pi's `AgentSessionRuntime`). */
|
|
74
|
+
export interface DriveRuntimeLike {
|
|
75
|
+
readonly session: DriveSessionLike;
|
|
76
|
+
dispose(): Promise<void> | void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Extract a tool's `details` object from a captured `tool_execution_end.result`; null if absent. */
|
|
80
|
+
function detailsOf(result: unknown): Record<string, unknown> | null {
|
|
81
|
+
if (result && typeof result === "object" && "details" in result) {
|
|
82
|
+
const details = (result as { details: unknown }).details;
|
|
83
|
+
if (details && typeof details === "object") return details as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- the perk-owned drive-event union -------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The perk-owned drive event union — the ONLY event vocabulary that crosses the handle boundary
|
|
92
|
+
* to the seam. The adapter translates raw SDK session events into these (`translateEvent`); all
|
|
93
|
+
* policy folding (budget counters, terminal capture, outcome classification) stays in the seam,
|
|
94
|
+
* so SDK event-shape churn is absorbed here and never reaches stage policy.
|
|
95
|
+
*/
|
|
96
|
+
export type StageEvent =
|
|
97
|
+
| {
|
|
98
|
+
kind: "turn_ended";
|
|
99
|
+
/**
|
|
100
|
+
* Fresh-work tokens for the turn: assistant `input + output` ONLY. `usage.reasoning` is a
|
|
101
|
+
* provider-reported breakdown that is a **subset of `output`** on every pi-ai provider that
|
|
102
|
+
* populates it (anthropic `thinking_tokens`, google `thoughtsTokenCount` folded into
|
|
103
|
+
* `output`, openai `reasoning_tokens` inside completion/output tokens — verified @ pi-ai
|
|
104
|
+
* 0.80.5), so it is deliberately EXCLUDED: adding it would double-count.
|
|
105
|
+
*/
|
|
106
|
+
freshTokens: number;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
kind: "tool_ended";
|
|
110
|
+
tool: string;
|
|
111
|
+
/** `details.ok` when the result carries the boolean, else `!isError`. */
|
|
112
|
+
ok: boolean;
|
|
113
|
+
/** The tool's structured `details` block (perk tools' result shape), null when absent. */
|
|
114
|
+
details: Record<string, unknown> | null;
|
|
115
|
+
/** Pre-cap error text for a failed tool (null when `ok`); the seam applies its cap. */
|
|
116
|
+
errorText: string | null;
|
|
117
|
+
}
|
|
118
|
+
| { kind: "model_errored"; message: string };
|
|
119
|
+
|
|
120
|
+
/** Best-effort error text for a failed tool (details.error | result string | a generic fallback). */
|
|
121
|
+
function toolErrorMessage(event: DriveEvent): string {
|
|
122
|
+
const details = detailsOf(event.result);
|
|
123
|
+
if (details && typeof details.error === "string" && details.error) return details.error;
|
|
124
|
+
if (typeof event.result === "string" && event.result) return event.result;
|
|
125
|
+
return `tool ${event.toolName ?? ""} failed`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Translate one raw agent-session event into the perk-owned union (pure); `null` for event types
|
|
130
|
+
* the drive does not observe. This is the entire SDK-event vocabulary the drive consumes: turn
|
|
131
|
+
* completion (with the fresh-work token sum — the `sumAssistantTokens` pattern in objective.ts),
|
|
132
|
+
* tool completion (with the parsed `details` block and pre-cap error text), and a
|
|
133
|
+
* post-acceptance model error (assistant `message_end` with `stopReason:"error"`, surfaced with
|
|
134
|
+
* retry off — audit §B #4).
|
|
135
|
+
*/
|
|
136
|
+
export function translateEvent(event: DriveEvent): StageEvent | null {
|
|
137
|
+
if (event.type === "turn_end") {
|
|
138
|
+
const usage = event.message?.usage;
|
|
139
|
+
const freshTokens = usage ? Math.max(0, usage.input ?? 0) + Math.max(0, usage.output ?? 0) : 0;
|
|
140
|
+
return { kind: "turn_ended", freshTokens };
|
|
141
|
+
}
|
|
142
|
+
if (event.type === "tool_execution_end") {
|
|
143
|
+
const details = detailsOf(event.result);
|
|
144
|
+
const ok = typeof details?.ok === "boolean" ? details.ok === true : !event.isError;
|
|
145
|
+
return {
|
|
146
|
+
kind: "tool_ended",
|
|
147
|
+
tool: event.toolName ?? "",
|
|
148
|
+
ok,
|
|
149
|
+
details,
|
|
150
|
+
errorText: ok ? null : toolErrorMessage(event),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
event.type === "message_end" &&
|
|
155
|
+
event.message?.role === "assistant" &&
|
|
156
|
+
event.message.stopReason === "error"
|
|
157
|
+
) {
|
|
158
|
+
return { kind: "model_errored", message: event.message.errorMessage ?? "model error" };
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- the drive-session handle --------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/** The binding the worker applies to every (re)bound session: headless (`hasUI === false`). */
|
|
166
|
+
function headlessBinding(): {
|
|
167
|
+
uiContext: undefined;
|
|
168
|
+
mode: "json";
|
|
169
|
+
onError: (err: unknown) => void;
|
|
170
|
+
} {
|
|
171
|
+
return {
|
|
172
|
+
uiContext: undefined,
|
|
173
|
+
mode: "json",
|
|
174
|
+
onError: (err: unknown) => console.error(`perk worker: extension error — ${String(err)}`),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create the drive-session handle over an already-created runtime — the seam's ONLY window onto
|
|
180
|
+
* the live session. Bind/subscribe, prompt, abort ownership, defensive rebind, and guarded
|
|
181
|
+
* disposal live behind it; the seam never touches `.session`/`.sessionManager`/`.extensionRunner`
|
|
182
|
+
* members directly. `listener` is the seam's policy fold and receives only the perk-owned
|
|
183
|
+
* `StageEvent` union — raw `DriveEvent`s are translated at this boundary and never cross it.
|
|
184
|
+
* Rebinding unsubscribes the prior raw listener first so events are never double-counted. The
|
|
185
|
+
* handle is private to the confined pair (seam ↔ adapter); the seam's fake-session injection
|
|
186
|
+
* seam (`deps.createRuntime` returning `DriveRuntimeLike`) is unchanged.
|
|
187
|
+
*/
|
|
188
|
+
export function createDriveSession(
|
|
189
|
+
runtime: DriveRuntimeLike,
|
|
190
|
+
listener: (event: StageEvent) => void,
|
|
191
|
+
) {
|
|
192
|
+
const binding = headlessBinding();
|
|
193
|
+
let bound: DriveSessionLike = runtime.session;
|
|
194
|
+
let unsubscribe: (() => void) | null = null;
|
|
195
|
+
let retainedAbort: Promise<void> | null = null;
|
|
196
|
+
const rawListener = (event: DriveEvent): void => {
|
|
197
|
+
const translated = translateEvent(event);
|
|
198
|
+
if (translated !== null) listener(translated);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
async function bindTo(target: DriveSessionLike): Promise<void> {
|
|
202
|
+
if (unsubscribe) unsubscribe();
|
|
203
|
+
await target.bindExtensions(binding);
|
|
204
|
+
unsubscribe = target.subscribe(rawListener);
|
|
205
|
+
bound = target;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
/** Headless bind + subscribe on the runtime's current session. */
|
|
210
|
+
async bind(): Promise<void> {
|
|
211
|
+
await bindTo(runtime.session);
|
|
212
|
+
},
|
|
213
|
+
/** The single driving prompt. */
|
|
214
|
+
async prompt(text: string): Promise<void> {
|
|
215
|
+
await bound.prompt(text);
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* OWNED + IDEMPOTENT: fires `session.abort()` on the runtime's live session exactly once —
|
|
219
|
+
* the drive can trip repeatedly (every post-trip `turn_ended` re-calls this), but later
|
|
220
|
+
* calls are no-ops, so no abort work can outlive `dispose()`'s drain. The rejection has an
|
|
221
|
+
* owner: the logging catch attaches immediately (never unhandled), and the caught chain is
|
|
222
|
+
* retained so `dispose()` drains it before returning.
|
|
223
|
+
*/
|
|
224
|
+
abort(): void {
|
|
225
|
+
if (retainedAbort !== null) return;
|
|
226
|
+
retainedAbort = runtime.session.abort().catch((err) => {
|
|
227
|
+
console.error(`perk worker: session.abort() rejected — ${String(err)}`);
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
/**
|
|
231
|
+
* The defensive-rebind arm: when the runtime replaced its session mid-drive, unsubscribe the
|
|
232
|
+
* prior listener, bind + subscribe the replacement, and return true (`false` = unchanged). A
|
|
233
|
+
* replacement is not expected on the happy path (the prompt instructs `/submit`, never
|
|
234
|
+
* `/implement`; `lifecycleGates.newSession` is `hasUI`-guarded; objective compaction is
|
|
235
|
+
* inert with no active objective) — the seam logs an observed rebind loudly.
|
|
236
|
+
*/
|
|
237
|
+
async rebindIfReplaced(): Promise<boolean> {
|
|
238
|
+
if (runtime.session === bound) return false;
|
|
239
|
+
await bindTo(runtime.session);
|
|
240
|
+
return true;
|
|
241
|
+
},
|
|
242
|
+
/** Preflight read (null when the session exposes no `extensionRunner`). */
|
|
243
|
+
registeredToolNames(): string[] | null {
|
|
244
|
+
const runner = bound.extensionRunner;
|
|
245
|
+
if (!runner) return null;
|
|
246
|
+
return runner.getAllRegisteredTools().map((t) => t.definition.name);
|
|
247
|
+
},
|
|
248
|
+
/** §8.35 pointer-capture read. */
|
|
249
|
+
sessionFile(): string | null {
|
|
250
|
+
return bound.sessionManager.getSessionFile?.() ?? null;
|
|
251
|
+
},
|
|
252
|
+
/** `sessionManager.getBranch()` for the seam's terminal classification. */
|
|
253
|
+
workflowBranch(): unknown[] {
|
|
254
|
+
return bound.sessionManager.getBranch();
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* Guarded cleanup: unsubscribe (caught) → runtime dispose (caught) → drain the retained
|
|
258
|
+
* abort promise (caught). NEVER throws — a throwing unsubscribe or a rejecting
|
|
259
|
+
* `runtime.dispose()` can never replace the seam's already-computed `RunOutcome` (the
|
|
260
|
+
* never-throws contract, contracts.md §8.11, holds under adversarial fakes).
|
|
261
|
+
*/
|
|
262
|
+
async dispose(): Promise<void> {
|
|
263
|
+
try {
|
|
264
|
+
unsubscribe?.();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(`perk worker: listener unsubscribe threw — ${String(err)}`);
|
|
267
|
+
}
|
|
268
|
+
unsubscribe = null;
|
|
269
|
+
try {
|
|
270
|
+
await runtime.dispose();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`perk worker: runtime dispose failed — ${String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
// Already a caught chain (see abort) — awaiting only drains it before return.
|
|
275
|
+
if (retainedAbort) await retainedAbort;
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The handle's nameable type, derived from its sole factory (no duplicate interface to drift):
|
|
282
|
+
* the object literal above carries the per-method contracts.
|
|
283
|
+
*/
|
|
284
|
+
export type DriveSessionHandle = ReturnType<typeof createDriveSession>;
|
|
285
|
+
|
|
286
|
+
// --- model/auth (Gap 5), unified around one nominal type ------------------------------------------
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The opaque model input the seam's `StageRunOptions.model` carries. NOMINAL: the `#private`
|
|
290
|
+
* fields make structural forgery impossible — a selection is minted only by this adapter
|
|
291
|
+
* (`resolveWorkerModel`/`resolveAuth`) and by tests that import the adapter deliberately. The
|
|
292
|
+
* SDK-typed reads below are adapter-internal by the import-edge ban (Rule F); they appear on
|
|
293
|
+
* this adapter-owned surface only.
|
|
294
|
+
*/
|
|
295
|
+
export class WorkerModelSelection {
|
|
296
|
+
// The ONE `#private` field supplies the nominal guarantee; the payload rides ordinary readonly
|
|
297
|
+
// fields. (Constructor parameter properties would be smaller still, but node's type-stripping
|
|
298
|
+
// test runner rejects non-erasable TS syntax.)
|
|
299
|
+
readonly #modelRuntime: ModelRuntime;
|
|
300
|
+
/** The EXPLICIT model only; `undefined` defers the pick to the SDK at session creation. */
|
|
301
|
+
readonly model: Model<Api> | undefined;
|
|
302
|
+
/**
|
|
303
|
+
* Thinking level parsed from the `--model <pattern>:<level>` suffix (`resolveWorkerModel`).
|
|
304
|
+
* `undefined` ⇒ the SDK's settings-default resolution — unchanged behavior.
|
|
305
|
+
*/
|
|
306
|
+
readonly thinkingLevel: ThinkingLevel | undefined;
|
|
307
|
+
|
|
308
|
+
constructor(modelRuntime: ModelRuntime, model?: Model<Api>, thinkingLevel?: ThinkingLevel) {
|
|
309
|
+
this.#modelRuntime = modelRuntime;
|
|
310
|
+
this.model = model;
|
|
311
|
+
this.thinkingLevel = thinkingLevel;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The canonical model/auth runtime (pi 0.84 `ModelRuntime`). */
|
|
315
|
+
get modelRuntime(): ModelRuntime {
|
|
316
|
+
return this.#modelRuntime;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** What a `--model` flag resolves to — discriminated so no contradictory state is expressible. */
|
|
321
|
+
export type ResolvedWorkerModel =
|
|
322
|
+
| { ok: true; selection: WorkerModelSelection; warning: string | undefined }
|
|
323
|
+
| { ok: false; error: string; warning: string | undefined };
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolve an explicit `--model` flag with pi's OWN CLI semantics (`resolveCliModel`): fuzzy
|
|
327
|
+
* matching, bare-id resolution, `provider/pattern`, and a `:thinking` suffix — the same chain the
|
|
328
|
+
* flag's string hits in an interactive pi launch, closing the warm/cold parity gap (cf.
|
|
329
|
+
* docs/learned/workflow/execution-path-parity.md).
|
|
330
|
+
*
|
|
331
|
+
* `raw` absent **or `""`** ⇒ `ok: true` with a selection carrying only a default-created
|
|
332
|
+
* `ModelRuntime` (model/thinking undefined — the SDK's own initial-model resolution at session
|
|
333
|
+
* creation stays the default). The `""` ≡ omitted equivalence is deliberate: workerMain's flag
|
|
334
|
+
* grammar produces `""` for a bare `--model`, and the tolerance is pinned by a test. A
|
|
335
|
+
* resolution that yields neither a model nor an error is normalized to the worker's not-found
|
|
336
|
+
* error (`ok: false` — fail fast, never guess). `warning` is a non-fatal resolution diagnostic
|
|
337
|
+
* (e.g. an invalid `:thinking` suffix) — the caller surfaces it only when proceeding.
|
|
338
|
+
*
|
|
339
|
+
* The optional `modelRuntime` param is the test-injection seam (deterministic `stubRuntime`
|
|
340
|
+
* tests); `ModelRuntime.create()` runs only when it is absent.
|
|
341
|
+
*/
|
|
342
|
+
export async function resolveWorkerModel(
|
|
343
|
+
raw: string | undefined,
|
|
344
|
+
modelRuntime?: ModelRuntime,
|
|
345
|
+
): Promise<ResolvedWorkerModel> {
|
|
346
|
+
const runtime = modelRuntime ?? (await ModelRuntime.create());
|
|
347
|
+
if (!raw) {
|
|
348
|
+
return { ok: true, selection: new WorkerModelSelection(runtime), warning: undefined };
|
|
349
|
+
}
|
|
350
|
+
const result = resolveCliModel({ cliModel: raw, modelRuntime: runtime });
|
|
351
|
+
if (result.model === undefined && result.error === undefined) {
|
|
352
|
+
return {
|
|
353
|
+
ok: false,
|
|
354
|
+
error: `model '${raw}' not found in the registry.`,
|
|
355
|
+
warning: result.warning,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (result.error !== undefined) {
|
|
359
|
+
return { ok: false, error: result.error, warning: result.warning };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
selection: new WorkerModelSelection(runtime, result.model, result.thinkingLevel),
|
|
364
|
+
warning: result.warning,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Normalize the seam's optional model input for the production drive path; returns null (never
|
|
370
|
+
* throws a domain error) when no model is available at all. `selection` absent ⇒ a
|
|
371
|
+
* default-runtime selection (async because pi 0.84's `ModelRuntime.create` is async; the default
|
|
372
|
+
* creation stays offline — `allowModelNetwork` defaults false). `null` iff there is no explicit
|
|
373
|
+
* model AND `getAvailableSnapshot()` is empty — the `no_model` fail-fast, unchanged. The model is
|
|
374
|
+
* NOT pre-pinned from the runtime: an `undefined` model lets `createAgentSession` run its own
|
|
375
|
+
* initial-model resolution (settings `defaultModel` → pi's curated per-provider defaults → first
|
|
376
|
+
* available), which picks a current-generation model instead of the catalogue's
|
|
377
|
+
* alphabetically-first (= oldest) entry.
|
|
378
|
+
*/
|
|
379
|
+
export async function resolveAuth(
|
|
380
|
+
selection: WorkerModelSelection | undefined,
|
|
381
|
+
): Promise<WorkerModelSelection | null> {
|
|
382
|
+
const effective = selection ?? new WorkerModelSelection(await ModelRuntime.create());
|
|
383
|
+
if (!effective.model && effective.modelRuntime.getAvailableSnapshot().length === 0) return null;
|
|
384
|
+
return effective;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// --- the production runtime factory ---------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Build the asymmetric runtime: `cwd = worktree` (project tier — perk's `@mgiles/perk` extension via the
|
|
391
|
+
* managed `.pi/settings.json`, the managed `AGENTS.md`/`APPEND_SYSTEM.md`) and `agentDir = throwaway`
|
|
392
|
+
* (user-global tier OUT — the throwaway dir has no `settings.json`, so the global tier is empty),
|
|
393
|
+
* env-var/registry auth+model (Gap 5). Settings are DISK-LAYERED (`SettingsManager.create` +
|
|
394
|
+
* `applyOverrides`, the SDK's sanctioned "with overrides" shape — docs/sdk.md "Settings
|
|
395
|
+
* Management"): the project tier resolves the managed `packages` list, while the compaction-off/
|
|
396
|
+
* retry-off determinism overrides ride the merged view only (package resolution reads the
|
|
397
|
+
* per-scope raws — overrides cannot leak into it). Missing `npm:` packages auto-install into
|
|
398
|
+
* `.pi/npm` during the loader's reload (skipped under `PI_OFFLINE`); an install failure throws →
|
|
399
|
+
* the seam's catch arm → a loud `failed`/`runtime_init`. No `tools` allowlist — read-write
|
|
400
|
+
* defaults + extension tools. The `createAgentSessionServices` factory builds the
|
|
401
|
+
* `DefaultResourceLoader` internally from `cwd`/`agentDir` (recipe correction #1).
|
|
402
|
+
*
|
|
403
|
+
* Adapter-owned inputs only (`worktree` + the nominal selection): no seam type appears in the
|
|
404
|
+
* signature, so a reverse seam←adapter type edge is impossible by construction. The throwaway
|
|
405
|
+
* `mkdtempSync` agentDir is best-effort removed (fail-soft `rm`; a removal failure logs and
|
|
406
|
+
* never affects the outcome) at exactly two moments — dispose, and a construction failure that
|
|
407
|
+
* would otherwise orphan it — the isolation invariant is untouched: no removal while the
|
|
408
|
+
* session lives.
|
|
409
|
+
*/
|
|
410
|
+
export async function defaultCreateRuntime(
|
|
411
|
+
worktree: string,
|
|
412
|
+
selection: WorkerModelSelection,
|
|
413
|
+
): Promise<DriveRuntimeLike> {
|
|
414
|
+
const agentDir = mkdtempSync(join(tmpdir(), "perk-worker-agent-"));
|
|
415
|
+
const removeAgentDir = (): void => {
|
|
416
|
+
try {
|
|
417
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error(`perk worker: throwaway agentDir removal failed — ${String(err)}`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
return await constructRuntime(worktree, selection, agentDir, removeAgentDir);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
// Construction failed before the disposer-wrapping runtime existed — without this arm every
|
|
426
|
+
// failed worker invocation would leak its `perk-worker-agent-*` directory.
|
|
427
|
+
removeAgentDir();
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The construction body behind `defaultCreateRuntime`'s failure-cleanup guard. */
|
|
433
|
+
async function constructRuntime(
|
|
434
|
+
worktree: string,
|
|
435
|
+
selection: WorkerModelSelection,
|
|
436
|
+
agentDir: string,
|
|
437
|
+
removeAgentDir: () => void,
|
|
438
|
+
): Promise<DriveRuntimeLike> {
|
|
439
|
+
const settingsManager = SettingsManager.create(worktree, agentDir);
|
|
440
|
+
settingsManager.applyOverrides({ compaction: { enabled: false }, retry: { enabled: false } });
|
|
441
|
+
const factory: CreateAgentSessionRuntimeFactory = async (factoryOpts) => {
|
|
442
|
+
const services = await createAgentSessionServices({
|
|
443
|
+
cwd: factoryOpts.cwd,
|
|
444
|
+
agentDir: factoryOpts.agentDir,
|
|
445
|
+
settingsManager,
|
|
446
|
+
modelRuntime: selection.modelRuntime,
|
|
447
|
+
});
|
|
448
|
+
const result = await createAgentSessionFromServices({
|
|
449
|
+
services,
|
|
450
|
+
sessionManager: factoryOpts.sessionManager,
|
|
451
|
+
sessionStartEvent: factoryOpts.sessionStartEvent,
|
|
452
|
+
// `undefined` ⇒ the SDK's initial-model resolution picks the model (see `resolveAuth`);
|
|
453
|
+
// an `undefined` thinkingLevel likewise defers to the settings default.
|
|
454
|
+
model: selection.model,
|
|
455
|
+
thinkingLevel: selection.thinkingLevel,
|
|
456
|
+
});
|
|
457
|
+
// Name the model that will actually drive (the SDK may have picked it) — the remote step
|
|
458
|
+
// log is otherwise silent about it until a provider error.
|
|
459
|
+
const chosen = result.session.model;
|
|
460
|
+
console.error(
|
|
461
|
+
`perk worker: model ${chosen ? `${chosen.provider}/${chosen.id}` : "unresolved"}`,
|
|
462
|
+
);
|
|
463
|
+
// Loud construction diagnostics (the CAUSE behind a later `no_extension_tools` symptom):
|
|
464
|
+
// settings I/O errors and extension load errors are recorded, not raised, by the SDK —
|
|
465
|
+
// surfacing them is the app layer's job. Fail-soft reporting only; never throws.
|
|
466
|
+
for (const entry of result.extensionsResult.errors) {
|
|
467
|
+
console.error(`perk worker: extension load error — ${entry.path}: ${entry.error}`);
|
|
468
|
+
}
|
|
469
|
+
for (const entry of settingsManager.drainErrors()) {
|
|
470
|
+
console.error(`perk worker: settings error (${entry.scope}) — ${String(entry.error)}`);
|
|
471
|
+
}
|
|
472
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
473
|
+
};
|
|
474
|
+
const runtime = await createAgentSessionRuntime(factory, {
|
|
475
|
+
cwd: worktree,
|
|
476
|
+
agentDir,
|
|
477
|
+
sessionManager: SessionManager.create(worktree),
|
|
478
|
+
});
|
|
479
|
+
const inner = runtime as unknown as DriveRuntimeLike;
|
|
480
|
+
return {
|
|
481
|
+
get session(): DriveSessionLike {
|
|
482
|
+
return inner.session;
|
|
483
|
+
},
|
|
484
|
+
async dispose(): Promise<void> {
|
|
485
|
+
try {
|
|
486
|
+
await inner.dispose();
|
|
487
|
+
} finally {
|
|
488
|
+
// Close the throwaway-agentDir leak at the one safe moment (post-dispose); a removal
|
|
489
|
+
// failure logs and never affects the outcome.
|
|
490
|
+
removeAgentDir();
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|