@mgiles/perk 1.0.1 → 2.0.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 +67 -57
- package/extension/adapters/planAdapterPlannotator.ts +27 -41
- package/extension/adapters/planAdapterTombell.ts +15 -28
- package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
- package/extension/checkpoints/checkpoints.ts +21 -13
- package/extension/doors/address.ts +6 -5
- package/extension/doors/askUser.ts +12 -8
- package/extension/doors/ciExecutor.ts +23 -15
- package/extension/doors/hunkHandoff.ts +202 -0
- package/extension/doors/land.ts +33 -10
- package/extension/doors/learn.ts +240 -34
- package/extension/doors/learnFactory.ts +144 -0
- package/extension/doors/lifecycleGates.ts +2 -1
- package/extension/doors/plannotatorHandoff.ts +509 -0
- package/extension/doors/prReview.ts +22 -37
- package/extension/doors/prReviewBrowser.ts +341 -0
- package/extension/doors/prReviewTerminal.ts +267 -0
- package/extension/doors/ready.ts +2 -1
- package/extension/doors/selfcheck.ts +240 -6
- package/extension/doors/submit.ts +37 -19
- package/extension/doors/submitPrReview.ts +408 -0
- package/extension/factories/implementHere.ts +116 -0
- package/extension/factories/objective.ts +17 -6
- package/extension/factories/objectiveAuthor.ts +15 -32
- package/extension/factories/objectiveDraft.ts +1 -1
- package/extension/factories/objectivePlan.ts +16 -34
- package/extension/factories/objectiveSave.ts +7 -17
- package/extension/factories/planMode.ts +24 -38
- package/extension/factories/planReview.ts +291 -177
- package/extension/factories/planSave.ts +23 -8
- package/extension/index.ts +117 -20
- package/extension/substrate/bindingDelivery.ts +32 -10
- package/extension/substrate/bindings.ts +4 -2
- package/extension/substrate/cache.ts +36 -9
- package/extension/substrate/clipboard.ts +81 -0
- package/extension/substrate/command.ts +25 -0
- package/extension/substrate/config.ts +98 -76
- package/extension/substrate/consoleCapture.ts +90 -0
- package/extension/substrate/git.ts +77 -0
- package/extension/substrate/miniJinja.ts +480 -0
- package/extension/substrate/paths.ts +38 -0
- package/extension/substrate/prompts.ts +15 -24
- package/extension/substrate/providers.ts +62 -8
- package/extension/substrate/sessionData.ts +1 -1
- package/extension/substrate/sessionPointers.ts +184 -0
- package/extension/substrate/structuredOutput.ts +3 -1
- package/extension/substrate/terminalLaunch.ts +178 -0
- package/extension/substrate/toolGating.ts +332 -73
- package/extension/substrate/toolParams.ts +7 -0
- package/extension/substrate/workflowState.ts +54 -2
- package/extension/surfaces/footerProvider.ts +8 -4
- package/extension/surfaces/surfaces.ts +338 -13
- package/extension/vendor/btw/btw.ts +10 -0
- package/extension/worker/readOnlySession.ts +19 -6
- package/extension/worker/worker.ts +187 -31
- package/extension/workerMain.ts +14 -13
- package/package.json +3 -7
- package/prompts/README.md +56 -5
- package/prompts/_fixtures/cases.yaml +52 -131
- package/prompts/_fixtures/golden/cond_elif-a.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-b.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-c.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-false.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-true.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-1.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-2.txt +3 -0
- package/prompts/_fixtures/golden/no_trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trim_block.txt +3 -0
- package/prompts/_fixtures/golden/trim_inline.txt +1 -0
- package/prompts/_fixtures/live.yaml +396 -0
- package/prompts/_fixtures/templates/cond_elif.md +9 -0
- package/prompts/_fixtures/templates/cond_if.md +7 -0
- package/prompts/_fixtures/templates/cond_ops.md +3 -0
- package/prompts/_fixtures/templates/no_trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trim_block.md +5 -0
- package/prompts/_fixtures/templates/trim_inline.md +1 -0
- package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -0
- package/prompts/contexts/adapters/plannotator-plan.md +6 -0
- package/prompts/contexts/adapters/tombell-plan.md +17 -0
- package/prompts/contexts/objective-authoring.md +20 -0
- package/prompts/contexts/plan-authoring.md +24 -0
- package/prompts/contexts/read-only.md +10 -0
- package/prompts/stages/conflict-resolution.md +4 -0
- package/prompts/stages/learn-code.md +8 -0
- package/prompts/stages/learn-docs.md +7 -6
- package/prompts/stages/learn-orchestrate.md +6 -0
- package/prompts/stages/learn.md +1 -1
- package/prompts/stages/objective-author/adopt.md +12 -0
- package/prompts/stages/objective-author/file.md +9 -0
- package/prompts/stages/objective-author/seed.md +9 -0
- package/prompts/stages/objective-plan/guidance.md +1 -1
- package/prompts/stages/objective-plan/seed.md +3 -2
- package/prompts/stages/objective-reconcile.md +7 -0
- package/prompts/stages/objective-replan.md +14 -0
- package/prompts/stages/objective-save.md +9 -0
- package/prompts/stages/plan-from/adopt.md +10 -0
- package/prompts/stages/plan-from/file.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +11 -0
- package/prompts/stages/pr-review-browser/foreign.md +11 -0
- package/prompts/stages/pr-review-terminal/active.md +12 -0
- package/prompts/stages/pr-review-terminal/foreign.md +13 -0
- package/prompts/stages/pr-review-terminal/local.md +4 -0
- package/prompts/stages/pr-review.md +6 -0
- package/prompts/stages/replan.md +13 -0
- package/prompts/stages/skills/create-from.md +15 -0
- package/prompts/stages/skills/create.md +9 -0
- package/prompts/stages/skills/refine.md +9 -0
- package/shared/README.md +23 -13
- package/shared/bindings.yaml +22 -2
- package/shared/contracts-history.md +191 -0
- package/shared/contracts.md +2258 -1638
- package/shared/providers.yaml +8 -1
- package/shared/registry.yaml +10 -11
- package/shared/schemas/contracts/bindings.schema.json +38 -0
- package/shared/schemas/contracts/providers.schema.json +89 -0
- package/shared/schemas/contracts/registry.schema.json +98 -0
- package/shared/schemas/inputs/handoff-arg.schema.json +6 -0
- package/shared/schemas/inputs/resolve-threads-batch.schema.json +37 -0
- package/shared/schemas/inputs/review-post-batch.schema.json +84 -0
- package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
- package/shared/schemas/inputs/structured-roadmap-node.schema.json +102 -0
- package/shared/schemas/outputs/doctor-report.schema.json +236 -0
- package/shared/schemas/outputs/init-report.schema.json +419 -0
- package/shared/schemas/outputs/learn-capture.schema.json +90 -0
- package/shared/schemas/outputs/learn-skip.schema.json +59 -0
- package/shared/schemas/outputs/plan-save.schema.json +209 -0
- package/shared/schemas/outputs/pr-feedback.schema.json +334 -0
- package/shared/schemas/outputs/pr-land.schema.json +187 -0
- package/shared/schemas/outputs/pr-ready.schema.json +75 -0
- package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
- package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
- package/shared/schemas/outputs/pr-review-context.schema.json +86 -0
- package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
- package/shared/schemas/outputs/pr-submit.schema.json +147 -0
- package/extension/doors/learnDocs.ts +0 -99
- package/prompts/_fixtures/golden/address-action-model.txt +0 -10
- package/prompts/_fixtures/golden/address-action.txt +0 -10
- package/prompts/_fixtures/golden/address-preview-model.txt +0 -6
- package/prompts/_fixtures/golden/address-preview.txt +0 -6
- package/prompts/_fixtures/golden/implement-github.txt +0 -8
- package/prompts/_fixtures/golden/learn-docs.txt +0 -8
- package/prompts/_fixtures/golden/learn-github.txt +0 -11
- package/prompts/_fixtures/golden/learn-linear.txt +0 -11
- package/prompts/_fixtures/golden/learn-no-ref.txt +0 -8
- package/prompts/_fixtures/golden/learn-other.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +0 -20
- package/prompts/_fixtures/golden/objective-plan-seed.txt +0 -15
- package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +0 -1
- package/prompts/_fixtures/golden/objective-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-github.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-other.txt +0 -1
|
@@ -12,24 +12,33 @@
|
|
|
12
12
|
// /plan-body materialization, `run_id` mint) is the cold-door/runner's job and is a PREPARED-
|
|
13
13
|
// WORKTREE input (audit Gap 7): the worker inherits `PERK_RUN_ID` from the env and never re-mints.
|
|
14
14
|
//
|
|
15
|
+
// Budget semantics: `budget.tokens` counts FRESH WORK only — assistant `input + output` per
|
|
16
|
+
// `turn_end`. Cache reads/writes and the provider `reasoning` breakdown (a subset of `output` in
|
|
17
|
+
// pi-ai's normalization) are excluded by design; see `applyEvent`.
|
|
18
|
+
//
|
|
15
19
|
// Inverse of `extension/worker/readOnlySession.ts`: that builds a fully-isolated READ-ONLY child (loads
|
|
16
20
|
// nothing, `["read","grep","find","ls"]`); the worker is the OPPOSITE — read-write defaults + the
|
|
17
|
-
// real perk extension loaded from the worktree's `.pi/settings.json` (
|
|
21
|
+
// real perk extension loaded from the worktree's `.pi/settings.json` (disk-layered settings:
|
|
22
|
+
// `SettingsManager.create(worktree, throwawayAgentDir)` resolves the managed project-tier
|
|
23
|
+
// `packages` list — perk + the borrowed set, the same package set as a warm session), with the
|
|
18
24
|
// user-global tier locked out via a throwaway `agentDir`.
|
|
19
25
|
|
|
20
26
|
import { appendFileSync, mkdtempSync } from "node:fs";
|
|
21
27
|
import { tmpdir } from "node:os";
|
|
22
28
|
import { join } from "node:path";
|
|
23
29
|
import { env } from "node:process";
|
|
24
|
-
|
|
30
|
+
// pi-ai's `ModelThinkingLevel` (`"off" | minimal | … | xhigh`) is the union `resolveCliModel`
|
|
31
|
+
// returns and `createAgentSessionFromServices` accepts; the pi-coding-agent root does not
|
|
32
|
+
// re-export a thinking-level type (only `ThinkingLevelChangeEntry`).
|
|
33
|
+
import type { Api, Model, ModelThinkingLevel as ThinkingLevel } from "@earendil-works/pi-ai";
|
|
25
34
|
import {
|
|
26
35
|
AuthStorage,
|
|
27
36
|
type CreateAgentSessionRuntimeFactory,
|
|
28
|
-
type CreateAgentSessionServicesOptions,
|
|
29
37
|
createAgentSessionFromServices,
|
|
30
38
|
createAgentSessionRuntime,
|
|
31
39
|
createAgentSessionServices,
|
|
32
40
|
ModelRegistry,
|
|
41
|
+
resolveCliModel,
|
|
33
42
|
SessionManager,
|
|
34
43
|
SettingsManager,
|
|
35
44
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -37,6 +46,7 @@ import { planReadInstruction } from "../doors/lifecycleGates.ts";
|
|
|
37
46
|
import { ensureRunScratch, type PlanRef, readPlanRef, runEventsPath } from "../substrate/cache.ts";
|
|
38
47
|
import { loadPerkConfig } from "../substrate/config.ts";
|
|
39
48
|
import { render } from "../substrate/prompts.ts";
|
|
49
|
+
import { captureSessionPointer } from "../substrate/sessionPointers.ts";
|
|
40
50
|
import { rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
41
51
|
import { capForModel } from "./readOnlySession.ts";
|
|
42
52
|
|
|
@@ -115,8 +125,18 @@ export interface DriveStageOptions {
|
|
|
115
125
|
stage: DriveStage;
|
|
116
126
|
/** The seeded first prompt (see `initialPromptFor`). */
|
|
117
127
|
initialPrompt: string;
|
|
118
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Explicit model; else the SDK's own default resolution picks one at session creation
|
|
130
|
+
* (settings `defaultModel` → pi's per-provider defaults → first available — Gap 5). Never
|
|
131
|
+
* pre-pinned here: `getAvailable()` sorts alphabetically, so `[0]` is the *oldest* model of
|
|
132
|
+
* the first provider (a since-removed `claude-3-5-haiku` date-pin 404'd a whole remote drive).
|
|
133
|
+
*/
|
|
119
134
|
model?: Model<Api>;
|
|
135
|
+
/**
|
|
136
|
+
* Thinking level parsed from the `--model <pattern>:<level>` suffix (`resolveWorkerModel`).
|
|
137
|
+
* `undefined` ⇒ the SDK's settings-default resolution — unchanged behavior.
|
|
138
|
+
*/
|
|
139
|
+
thinkingLevel?: ThinkingLevel;
|
|
120
140
|
authStorage?: AuthStorage;
|
|
121
141
|
modelRegistry?: ModelRegistry;
|
|
122
142
|
budget: DriveBudget;
|
|
@@ -126,12 +146,11 @@ export interface DriveStageOptions {
|
|
|
126
146
|
|
|
127
147
|
/**
|
|
128
148
|
* The offline seam (mirrors `readOnlySession.test.ts`'s `runTask` injection). `createRuntime`
|
|
129
|
-
* overrides the production runtime factory so tests drive synthetic sessions; `
|
|
130
|
-
*
|
|
149
|
+
* overrides the production runtime factory so tests drive synthetic sessions; `now` injects the
|
|
150
|
+
* clock for deterministic `elapsed_ms`.
|
|
131
151
|
*/
|
|
132
152
|
export interface DriveStageDeps {
|
|
133
153
|
createRuntime?: (opts: DriveStageOptions) => Promise<DriveRuntimeLike>;
|
|
134
|
-
resourceLoaderOptions?: CreateAgentSessionServicesOptions["resourceLoaderOptions"];
|
|
135
154
|
now?: () => number;
|
|
136
155
|
/** The structured run-event sink. Absent ⇒ the default run-scoped NDJSON file sink. */
|
|
137
156
|
eventSink?: RunEventSink;
|
|
@@ -149,7 +168,14 @@ export interface DriveEvent {
|
|
|
149
168
|
role?: string;
|
|
150
169
|
stopReason?: string;
|
|
151
170
|
errorMessage?: string;
|
|
152
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Assistant token usage. `reasoning` is a provider-reported breakdown that is a **subset of
|
|
173
|
+
* `output`** on every pi-ai provider that populates it (anthropic `thinking_tokens`, google
|
|
174
|
+
* `thoughtsTokenCount` folded into `output`, openai `reasoning_tokens` inside completion/
|
|
175
|
+
* output tokens — verified @ pi-ai 0.80.5), so it is deliberately EXCLUDED from the budget
|
|
176
|
+
* sum: adding it would double-count.
|
|
177
|
+
*/
|
|
178
|
+
usage?: { input?: number; output?: number; reasoning?: number };
|
|
153
179
|
/** Assistant text/content blocks (where `[WIP:n]`/`[DONE:n]` markers live). */
|
|
154
180
|
content?: unknown;
|
|
155
181
|
};
|
|
@@ -162,7 +188,13 @@ export interface DriveSessionLike {
|
|
|
162
188
|
prompt(text: string): Promise<void>;
|
|
163
189
|
abort(): Promise<void>;
|
|
164
190
|
dispose(): void;
|
|
165
|
-
sessionManager: { getBranch(): unknown[] };
|
|
191
|
+
sessionManager: { getBranch(): unknown[]; getSessionFile?(): string | null };
|
|
192
|
+
/**
|
|
193
|
+
* Optional (presence-gated): when the session exposes its extension runner, `driveStage`
|
|
194
|
+
* preflights the stage's terminating perk tool post-bind and fails fast (zero-turn
|
|
195
|
+
* `no_extension_tools`) instead of burning the budget on a tool-less session.
|
|
196
|
+
*/
|
|
197
|
+
extensionRunner?: { getAllRegisteredTools(): { definition: { name: string } }[] };
|
|
166
198
|
}
|
|
167
199
|
|
|
168
200
|
/** The runtime surface (structurally satisfied by pi's `AgentSessionRuntime`). */
|
|
@@ -209,6 +241,10 @@ function detailsOf(result: unknown): Record<string, unknown> | null {
|
|
|
209
241
|
* assistant token usage (the `sumAssistantTokens` pattern in objective.ts), captures the `submit`
|
|
210
242
|
* /`resolve_review_threads` terminal tool details, and records a post-acceptance model error
|
|
211
243
|
* (assistant `message_end` with `stopReason:"error"`, surfaced with retry off — audit §B #4).
|
|
244
|
+
*
|
|
245
|
+
* The token sum is `input + output` ONLY: `usage.reasoning` is a subset of `output` on every
|
|
246
|
+
* pi-ai provider that reports it (see the `DriveEvent.usage` doc), so summing it would
|
|
247
|
+
* double-count.
|
|
212
248
|
*/
|
|
213
249
|
export function applyEvent(counters: DriveCounters, event: DriveEvent): void {
|
|
214
250
|
if (event.type === "turn_end") {
|
|
@@ -314,6 +350,17 @@ export function evaluateTerminal(args: {
|
|
|
314
350
|
};
|
|
315
351
|
}
|
|
316
352
|
|
|
353
|
+
/**
|
|
354
|
+
* The post-bind preflight rule (pure): the stage's terminating perk tool must be registered —
|
|
355
|
+
* `implement` → `submit`, `address` → `resolve_review_threads`. Returns the required tool name
|
|
356
|
+
* when absent, else `null`. Deliberately does NOT require the `subagent` tool for `address` — the
|
|
357
|
+
* subagent-under-worker live smoke stays the §8.11 carried risk.
|
|
358
|
+
*/
|
|
359
|
+
export function missingTerminatingTool(stage: DriveStage, toolNames: string[]): string | null {
|
|
360
|
+
const required = stage === "implement" ? "submit" : "resolve_review_threads";
|
|
361
|
+
return toolNames.includes(required) ? null : required;
|
|
362
|
+
}
|
|
363
|
+
|
|
317
364
|
/** Pull a `{ number, url }` PR from a captured `submit` details block; null when malformed. */
|
|
318
365
|
function extractPr(
|
|
319
366
|
details: Record<string, unknown> | null,
|
|
@@ -465,9 +512,11 @@ export function defaultEventSink(worktree: string, runId: string): RunEventSink
|
|
|
465
512
|
/**
|
|
466
513
|
* Re-derive the stage's initial prompt from the plan-ref — the TS twin of
|
|
467
514
|
* `perk/run/launch.py._implement_prompt`/`_address_prompt`. INVARIANT: textual parity with the Python
|
|
468
|
-
* plane (asserted reciprocally in `worker.test.ts` + `tests/test_worker_prompt_parity.py`)
|
|
469
|
-
*
|
|
470
|
-
*
|
|
515
|
+
* plane (asserted reciprocally in `worker.test.ts` + `tests/test_worker_prompt_parity.py`). No
|
|
516
|
+
* skill-binding suffix is appended here: in the driven session the bindings arrive via Mechanism A
|
|
517
|
+
* (bindingDelivery.ts injects the handoff stage's render because this prompt carries no
|
|
518
|
+
* `BINDING_HEADER`) — content byte-identical to the cold door's suffix (contracts.md §8.38).
|
|
519
|
+
* Returns `null` when there is no plan-ref (nothing to prime).
|
|
471
520
|
*
|
|
472
521
|
* The implement primer's wording lives in the canonical template `prompts/stages/implement.md`,
|
|
473
522
|
* rendered by the shared seam (contracts.md §8.31); branching stays in code — only the `read_cmd`
|
|
@@ -493,7 +542,7 @@ export function initialPromptFor(
|
|
|
493
542
|
}
|
|
494
543
|
// address
|
|
495
544
|
const modelClause = classifierModel
|
|
496
|
-
? `, passing \`model: "${classifierModel}"\` on that call (the configured [subagents] review-classifier model)`
|
|
545
|
+
? `, passing \`model: "${classifierModel}"\` on that call (the configured [models.subagents] review-classifier model)`
|
|
497
546
|
: "";
|
|
498
547
|
return render("stages/address/action.md", {
|
|
499
548
|
provider,
|
|
@@ -548,20 +597,24 @@ export function createBindManager(binding: unknown, listener: (event: DriveEvent
|
|
|
548
597
|
/**
|
|
549
598
|
* Build the asymmetric runtime: `cwd = worktree` (project tier — perk's `@mgiles/perk` extension via the
|
|
550
599
|
* managed `.pi/settings.json`, the managed `AGENTS.md`/`APPEND_SYSTEM.md`) and `agentDir = throwaway`
|
|
551
|
-
* (user-global tier OUT
|
|
552
|
-
*
|
|
553
|
-
*
|
|
600
|
+
* (user-global tier OUT — the throwaway dir has no `settings.json`, so the global tier is empty),
|
|
601
|
+
* env-var/registry auth+model (Gap 5). Settings are DISK-LAYERED (`SettingsManager.create` +
|
|
602
|
+
* `applyOverrides`, the SDK's sanctioned "with overrides" shape — docs/sdk.md "Settings
|
|
603
|
+
* Management"): the project tier resolves the managed `packages` list, while the compaction-off/
|
|
604
|
+
* retry-off determinism overrides ride the merged view only (package resolution reads the
|
|
605
|
+
* per-scope raws — overrides cannot leak into it). Missing `npm:` packages auto-install into
|
|
606
|
+
* `.pi/npm` during the loader's reload (skipped under `PI_OFFLINE`); an install failure throws →
|
|
607
|
+
* `driveStage`'s catch arm → a loud `failed`/`drive_error`. No `tools` allowlist — read-write
|
|
608
|
+
* defaults + extension tools. The `createAgentSessionServices` factory builds the
|
|
609
|
+
* `DefaultResourceLoader` internally from `cwd`/`agentDir` (recipe correction #1).
|
|
554
610
|
*/
|
|
555
611
|
async function defaultCreateRuntime(
|
|
556
612
|
opts: DriveStageOptions,
|
|
557
|
-
|
|
558
|
-
resolved: { authStorage: AuthStorage; modelRegistry: ModelRegistry; model: Model<Api> },
|
|
613
|
+
resolved: ResolvedAuth,
|
|
559
614
|
): Promise<DriveRuntimeLike> {
|
|
560
615
|
const agentDir = mkdtempSync(join(tmpdir(), "perk-worker-agent-"));
|
|
561
|
-
const settingsManager = SettingsManager.
|
|
562
|
-
|
|
563
|
-
retry: { enabled: false },
|
|
564
|
-
});
|
|
616
|
+
const settingsManager = SettingsManager.create(opts.worktree, agentDir);
|
|
617
|
+
settingsManager.applyOverrides({ compaction: { enabled: false }, retry: { enabled: false } });
|
|
565
618
|
const factory: CreateAgentSessionRuntimeFactory = async (factoryOpts) => {
|
|
566
619
|
const services = await createAgentSessionServices({
|
|
567
620
|
cwd: factoryOpts.cwd,
|
|
@@ -569,14 +622,31 @@ async function defaultCreateRuntime(
|
|
|
569
622
|
authStorage: resolved.authStorage,
|
|
570
623
|
settingsManager,
|
|
571
624
|
modelRegistry: resolved.modelRegistry,
|
|
572
|
-
resourceLoaderOptions: deps.resourceLoaderOptions,
|
|
573
625
|
});
|
|
574
626
|
const result = await createAgentSessionFromServices({
|
|
575
627
|
services,
|
|
576
628
|
sessionManager: factoryOpts.sessionManager,
|
|
577
629
|
sessionStartEvent: factoryOpts.sessionStartEvent,
|
|
630
|
+
// `undefined` ⇒ the SDK's initial-model resolution picks the model (see `resolveAuth`);
|
|
631
|
+
// an `undefined` thinkingLevel likewise defers to the settings default.
|
|
578
632
|
model: resolved.model,
|
|
633
|
+
thinkingLevel: opts.thinkingLevel,
|
|
579
634
|
});
|
|
635
|
+
// Name the model that will actually drive (the SDK may have picked it) — the remote step
|
|
636
|
+
// log is otherwise silent about it until a provider error.
|
|
637
|
+
const chosen = result.session.model;
|
|
638
|
+
console.error(
|
|
639
|
+
`perk worker: model ${chosen ? `${chosen.provider}/${chosen.id}` : "unresolved"}`,
|
|
640
|
+
);
|
|
641
|
+
// Loud construction diagnostics (the CAUSE behind a later `no_extension_tools` symptom):
|
|
642
|
+
// settings I/O errors and extension load errors are recorded, not raised, by the SDK —
|
|
643
|
+
// surfacing them is the app layer's job. Fail-soft reporting only; never throws.
|
|
644
|
+
for (const entry of result.extensionsResult.errors) {
|
|
645
|
+
console.error(`perk worker: extension load error — ${entry.path}: ${entry.error}`);
|
|
646
|
+
}
|
|
647
|
+
for (const entry of settingsManager.drainErrors()) {
|
|
648
|
+
console.error(`perk worker: settings error (${entry.scope}) — ${String(entry.error)}`);
|
|
649
|
+
}
|
|
580
650
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
581
651
|
};
|
|
582
652
|
const runtime = await createAgentSessionRuntime(factory, {
|
|
@@ -589,19 +659,67 @@ async function defaultCreateRuntime(
|
|
|
589
659
|
|
|
590
660
|
// --- model/auth resolution (Gap 5) --------------------------------------------------------------
|
|
591
661
|
|
|
592
|
-
interface ResolvedAuth {
|
|
662
|
+
export interface ResolvedAuth {
|
|
593
663
|
authStorage: AuthStorage;
|
|
594
664
|
modelRegistry: ModelRegistry;
|
|
595
|
-
model
|
|
665
|
+
/** The EXPLICIT model only; `undefined` defers the pick to the SDK at session creation. */
|
|
666
|
+
model: Model<Api> | undefined;
|
|
596
667
|
}
|
|
597
668
|
|
|
598
|
-
/**
|
|
599
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Resolve auth; returns null (never throws) when no model is available at all. The model is NOT
|
|
671
|
+
* pre-pinned from the registry: an `undefined` model lets `createAgentSession` run its own
|
|
672
|
+
* initial-model resolution (settings `defaultModel` → pi's curated per-provider defaults → first
|
|
673
|
+
* available), which picks a current-generation model instead of the registry's
|
|
674
|
+
* alphabetically-first (= oldest) entry.
|
|
675
|
+
*/
|
|
676
|
+
export function resolveAuth(opts: DriveStageOptions): ResolvedAuth | null {
|
|
600
677
|
const authStorage = opts.authStorage ?? AuthStorage.create();
|
|
601
678
|
const modelRegistry = opts.modelRegistry ?? ModelRegistry.create(authStorage);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
679
|
+
if (!opts.model && modelRegistry.getAvailable().length === 0) return null;
|
|
680
|
+
return { authStorage, modelRegistry, model: opts.model };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** What an explicit `--model` flag resolves to (a thin projection of `ResolveCliModelResult`). */
|
|
684
|
+
export interface ResolvedWorkerModel {
|
|
685
|
+
model: Model<Api> | undefined;
|
|
686
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
687
|
+
/** Non-fatal resolution diagnostic (e.g. an invalid `:thinking` suffix) — surface, continue. */
|
|
688
|
+
warning: string | undefined;
|
|
689
|
+
/** Fatal: the pattern resolved to no model — fail fast, never guess. */
|
|
690
|
+
error: string | undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolve an explicit `--model` flag with pi's OWN CLI semantics (`resolveCliModel`): fuzzy
|
|
695
|
+
* matching, bare-id resolution, `provider/pattern`, and a `:thinking` suffix — the same chain the
|
|
696
|
+
* flag's string hits in an interactive pi launch, closing the warm/cold parity gap (cf.
|
|
697
|
+
* docs/learned/workflow/execution-path-parity.md). `raw` falsy ⇒ all-undefined (the SDK's own
|
|
698
|
+
* default resolution picks the model at session creation — see `resolveAuth`). A resolution that
|
|
699
|
+
* yields neither a model nor an error is normalized to the worker's not-found error.
|
|
700
|
+
*/
|
|
701
|
+
export function resolveWorkerModel(
|
|
702
|
+
raw: string | undefined,
|
|
703
|
+
modelRegistry: ModelRegistry,
|
|
704
|
+
): ResolvedWorkerModel {
|
|
705
|
+
if (!raw) {
|
|
706
|
+
return { model: undefined, thinkingLevel: undefined, warning: undefined, error: undefined };
|
|
707
|
+
}
|
|
708
|
+
const result = resolveCliModel({ cliModel: raw, modelRegistry });
|
|
709
|
+
if (result.model === undefined && result.error === undefined) {
|
|
710
|
+
return {
|
|
711
|
+
model: undefined,
|
|
712
|
+
thinkingLevel: undefined,
|
|
713
|
+
warning: result.warning,
|
|
714
|
+
error: `model '${raw}' not found in the registry.`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
model: result.model,
|
|
719
|
+
thinkingLevel: result.thinkingLevel,
|
|
720
|
+
warning: result.warning,
|
|
721
|
+
error: result.error,
|
|
722
|
+
};
|
|
605
723
|
}
|
|
606
724
|
|
|
607
725
|
// --- the drive primitive ------------------------------------------------------------------------
|
|
@@ -677,12 +795,50 @@ export async function driveStage(
|
|
|
677
795
|
runtime = deps.createRuntime
|
|
678
796
|
? await deps.createRuntime(opts)
|
|
679
797
|
: // biome-ignore lint/style/noNonNullAssertion: resolved is non-null on the production path.
|
|
680
|
-
await defaultCreateRuntime(opts,
|
|
798
|
+
await defaultCreateRuntime(opts, resolved!);
|
|
681
799
|
|
|
682
800
|
let boundSession = runtime.session;
|
|
683
801
|
await bindManager.bind(boundSession);
|
|
684
802
|
emitter.emit({ kind: "run_started", run_id: runId, stage: opts.stage });
|
|
685
803
|
|
|
804
|
+
// Terminating-tool preflight (presence-gated on `extensionRunner`): disk discovery has a
|
|
805
|
+
// silent-zero arm — a missing/unparseable `.pi/settings.json` or an unresolvable local-path
|
|
806
|
+
// package yields ZERO extension tools without throwing — so fail fast (zero turns) instead of
|
|
807
|
+
// burning the whole budget on a drive that can never call its terminating tool. Reuses the
|
|
808
|
+
// `model_error` terminal signal with a distinct `error.type` (the `no_model` precedent).
|
|
809
|
+
if (boundSession.extensionRunner) {
|
|
810
|
+
const toolNames = boundSession.extensionRunner
|
|
811
|
+
.getAllRegisteredTools()
|
|
812
|
+
.map((t) => t.definition.name);
|
|
813
|
+
const missing = missingTerminatingTool(opts.stage, toolNames);
|
|
814
|
+
if (missing !== null) {
|
|
815
|
+
return finish({
|
|
816
|
+
status: "failed",
|
|
817
|
+
terminal_signal: "model_error",
|
|
818
|
+
pr: null,
|
|
819
|
+
errorType: "no_extension_tools",
|
|
820
|
+
errorMessage:
|
|
821
|
+
`perk extension tools did not register — the ${opts.stage} stage's terminating ` +
|
|
822
|
+
`tool \`${missing}\` is missing. Check the worktree's .pi/settings.json packages ` +
|
|
823
|
+
"list (perk init converges it); construction diagnostics are on stderr.",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Implementation/worker session pointer (contracts.md §8.35): the headless drive records the
|
|
829
|
+
// inner driven session's file under THIS run id into the shared main checkout (the worktree's
|
|
830
|
+
// `mainCheckoutRoot`), labelled `.worker` by capture site. The inner session's own
|
|
831
|
+
// `session_start` records the matching `.main`. Best-effort + non-fatal (carrier warns).
|
|
832
|
+
if (opts.stage === "implement") {
|
|
833
|
+
captureSessionPointer({
|
|
834
|
+
cwd: opts.worktree,
|
|
835
|
+
runId,
|
|
836
|
+
klass: "implementation",
|
|
837
|
+
site: "worker",
|
|
838
|
+
sessionFile: boundSession.sessionManager.getSessionFile?.() ?? null,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
686
842
|
// Budget/abort wiring (Gap 2): wall-clock timer + external signal both trip → session.abort().
|
|
687
843
|
const timer = setTimeout(() => trip("budget"), opts.budget.wallClockMs);
|
|
688
844
|
if (opts.signal) {
|
package/extension/workerMain.ts
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
// stderr), and exits 0 on `completed` else non-zero. Runs as `.ts` under node 22 type-stripping.
|
|
10
10
|
|
|
11
11
|
import { argv, env, exit, stderr, stdout } from "node:process";
|
|
12
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
13
12
|
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { runEventsPath } from "./substrate/cache.ts";
|
|
13
|
+
import { runEventsPath, workflowDir } from "./substrate/cache.ts";
|
|
15
14
|
import {
|
|
16
15
|
type DriveBudget,
|
|
17
16
|
type DriveStage,
|
|
18
17
|
driveStage,
|
|
19
18
|
initialPromptForWorktree,
|
|
20
19
|
type RunOutcome,
|
|
20
|
+
resolveWorkerModel,
|
|
21
21
|
} from "./worker/worker.ts";
|
|
22
22
|
|
|
23
23
|
/** Documented defaults for the budget watchdog (overridable via flags). */
|
|
@@ -90,23 +90,23 @@ async function main(): Promise<number> {
|
|
|
90
90
|
const initialPrompt = initialPromptForWorktree(parsed.worktree, parsed.stage);
|
|
91
91
|
if (initialPrompt === null) {
|
|
92
92
|
stderr.write(
|
|
93
|
-
`perk worker: no plan-ref under ${parsed.worktree}
|
|
93
|
+
`perk worker: no plan-ref under ${workflowDir(parsed.worktree)} — cannot seed the ${parsed.stage} prompt.\n`,
|
|
94
94
|
);
|
|
95
95
|
return 2;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Headless auth/model (Gap 5): env-var key resolution; `--model
|
|
98
|
+
// Headless auth/model (Gap 5): env-var key resolution; an explicit `--model` resolves with
|
|
99
|
+
// pi's CLI semantics (fuzzy matching, `provider/pattern`, a `:thinking` suffix —
|
|
100
|
+
// `resolveWorkerModel`), else the SDK's default resolution at session creation (settings
|
|
101
|
+
// default → pi's per-provider defaults → first available) — the deferral is unchanged.
|
|
99
102
|
const authStorage = AuthStorage.create();
|
|
100
103
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!model) {
|
|
106
|
-
stderr.write(`perk worker: model '${parsed.model}' not found in the registry.\n`);
|
|
107
|
-
return 2;
|
|
108
|
-
}
|
|
104
|
+
const resolved = resolveWorkerModel(parsed.model, modelRegistry);
|
|
105
|
+
if (resolved.error) {
|
|
106
|
+
stderr.write(`perk worker: ${resolved.error}\n`);
|
|
107
|
+
return 2;
|
|
109
108
|
}
|
|
109
|
+
if (resolved.warning) stderr.write(`perk worker: ${resolved.warning}\n`);
|
|
110
110
|
|
|
111
111
|
const controller = new AbortController();
|
|
112
112
|
const onSignal = (): void => controller.abort();
|
|
@@ -119,7 +119,8 @@ async function main(): Promise<number> {
|
|
|
119
119
|
worktree: parsed.worktree,
|
|
120
120
|
stage: parsed.stage,
|
|
121
121
|
initialPrompt,
|
|
122
|
-
model,
|
|
122
|
+
model: resolved.model,
|
|
123
|
+
thinkingLevel: resolved.thinkingLevel,
|
|
123
124
|
authStorage,
|
|
124
125
|
modelRegistry,
|
|
125
126
|
budget: parsed.budget,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgiles/perk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "perk Pi extension (session interior) for the plan-oriented workflow.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"lint": "biome check extension",
|
|
35
35
|
"format": "biome format --write extension"
|
|
36
36
|
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"nunjucks": "3.2.4"
|
|
39
|
-
},
|
|
40
37
|
"peerDependencies": {
|
|
41
38
|
"@earendil-works/pi-ai": "*",
|
|
42
39
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -45,10 +42,9 @@
|
|
|
45
42
|
},
|
|
46
43
|
"devDependencies": {
|
|
47
44
|
"@biomejs/biome": "2.4.16",
|
|
48
|
-
"@earendil-works/pi-ai": "0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
45
|
+
"@earendil-works/pi-ai": "0.80.5",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.80.5",
|
|
50
47
|
"@types/node": "22.19.19",
|
|
51
|
-
"@types/nunjucks": "3.2.6",
|
|
52
48
|
"typescript": "6.0.3",
|
|
53
49
|
"yaml": "2.9.0"
|
|
54
50
|
}
|
package/prompts/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
# prompts —
|
|
1
|
+
# prompts — perk's externalized prompt prose
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory is the canonical home for **all** of perk's externalized prompt prose. A
|
|
4
|
+
template may be **cross-plane** (rendered in production by both planes) or **single-plane**
|
|
5
|
+
(rendered by only one plane — e.g. a warm-door-only or cold-door-only seed/guidance prompt);
|
|
6
|
+
either way it is authored within the frozen mini-jinja subset below and listed in
|
|
7
|
+
`_fixtures/live.yaml`, where it is rendered on **both** engines and asserted byte-equal
|
|
8
|
+
regardless of which plane consumes it in production (a free cross-engine portability guarantee,
|
|
9
|
+
costing nothing since the subset is shared).
|
|
10
|
+
|
|
11
|
+
The templates are authored once and **bundled into every build artifact**
|
|
4
12
|
(the Python wheel as package data `perk/_prompts/`; the npm package under `prompts/`),
|
|
5
13
|
exactly like `shared/`. Each plane locates this directory at runtime through its own
|
|
6
14
|
resolver — `prompts_dir()` (`perk/_resources.py`) and `promptsDir()`
|
|
@@ -10,6 +18,49 @@ Templates are rendered by jinja2 (Python) and a vendored TS subset (the extensio
|
|
|
10
18
|
are loaded by explicit name through the resolver — never by scanning the directory, so
|
|
11
19
|
this README is a durable doc, not a template.
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
## Frozen template grammar
|
|
22
|
+
|
|
23
|
+
The templates use a deliberately tiny, **frozen** subset of jinja syntax — the canonical
|
|
24
|
+
"mini-jinja" surface. jinja2 is the reference engine; the extension renders the same subset. A
|
|
25
|
+
**cross-plane conformance guard** (`tests/test_prompt_grammar.py` +
|
|
26
|
+
`extension/substrate/promptGrammar.test.ts`) fails CI if any template uses a construct outside the
|
|
27
|
+
subset.
|
|
28
|
+
|
|
29
|
+
## Render parity — two decoupled tiers
|
|
30
|
+
|
|
31
|
+
The two render seams (jinja2 on Python, the vendored mini-jinja on TS) are kept byte-identical by
|
|
32
|
+
two tiers that separate the frozen render **contract** from real prompt **prose**:
|
|
33
|
+
|
|
34
|
+
- **Tier A — contract snapshots.** `_fixtures/cases.yaml` lists purpose-built fixture templates
|
|
35
|
+
under `_fixtures/templates/` (one per render feature) with committed goldens under
|
|
36
|
+
`_fixtures/golden/` (jinja2's output). Both planes assert `render == golden`
|
|
37
|
+
(`tests/test_prompts.py` + `extension/substrate/prompts.test.ts`). These goldens change only when
|
|
38
|
+
the render contract changes — never when a real prompt's prose changes.
|
|
39
|
+
- **Tier B — live cross-engine equality.** `_fixtures/live.yaml` lists every real template with
|
|
40
|
+
representative vars and **no** golden. `tests/test_prompt_parity.py` renders each real template
|
|
41
|
+
with jinja2, shells out once to `extension/testing/renderLive.ts` (mini-jinja), and asserts the
|
|
42
|
+
two outputs are byte-equal — so editing a real prompt's prose touches no fixture. A coverage
|
|
43
|
+
guard asserts every real template is listed in `live.yaml`.
|
|
44
|
+
|
|
45
|
+
The subset is exactly four categories:
|
|
46
|
+
|
|
47
|
+
1. **Variable substitution** — `{{ name }}`, where the contents are a single bare identifier
|
|
48
|
+
(`[A-Za-z_][A-Za-z0-9_]*`). Nothing else: no filters, no dotted access, no parentheses, no
|
|
49
|
+
literals, no operators.
|
|
50
|
+
2. **Include** — `{% include "path/to/file.md" %}`, a double-quoted root-relative path only.
|
|
51
|
+
3. **Conditionals** — `{% if cond %}` / `{% elif cond %}` / `{% else %}` / `{% endif %}`, where
|
|
52
|
+
`cond` uses only bare identifiers (truthiness), double-quoted string literals, `==`, and the
|
|
53
|
+
keywords `and` / `or` / `not`. For example: `{% if provider == "github" or provider == "linear" %}`,
|
|
54
|
+
`{% if not pr_id %}`.
|
|
55
|
+
4. **Plain tags** — `{% %}` only. The whitespace-control markers `{%- … -%}` / `{{- … -}}` are
|
|
56
|
+
**not allowed**; tag-line stripping is handled by the render env's `trim_blocks` setting.
|
|
57
|
+
|
|
58
|
+
**Not allowed** (the guard fails on these): `{% for x in y %}` / `{% endfor %}`, `{% set %}`,
|
|
59
|
+
`{% macro %}` / `{% block %}` / `{% extends %}` / `{% raw %}`, `{# comments #}`, filters
|
|
60
|
+
(`{{ x | upper }}`), attribute access (`{{ user.name }}`), `!=` / `<` / `>`, `in`, `is`,
|
|
61
|
+
parentheses, and numeric literals.
|
|
62
|
+
|
|
63
|
+
This README is **excluded** from the guard's scan — it is documentation, never rendered, and the
|
|
64
|
+
out-of-subset examples above are shown deliberately as prose. The canonical spec lives in
|
|
65
|
+
[`shared/contracts.md` §8.31](../shared/contracts.md). Widening the subset is a deliberate
|
|
66
|
+
decision that amends §8.31 **and** both guards.
|