@mgiles/perk 2.3.0 → 3.1.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 +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +135 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +6 -6
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
// perk-owned checkpoints. The implement-session progress tracker: seed an ordered step list from
|
|
2
|
-
// the plan body's `## Steps` numbered list, then advance it as the model emits `[DONE:n]` markers
|
|
3
|
-
// in its turns. State lives in a dedicated `perk:checkpoint` session entry — kept OFF the shared
|
|
4
|
-
// `perk:workflow-state` record (progress is high-churn; this avoids LWW-append smell).
|
|
5
|
-
//
|
|
6
|
-
// Opt-in + inert-by-default: perk plans are prose, so when no `## Steps` list is present the
|
|
7
|
-
// checkpoint degrades to inert (no crash, no nagging). The `perk-plan` skill documents the optional
|
|
8
|
-
// `## Steps` section as the forward path.
|
|
9
|
-
//
|
|
10
|
-
// The pure helpers (extractDoneSteps / markCompletedSteps + the step extractor) are perk-owned
|
|
11
|
-
// copies of pi's official `examples/extensions/plan-mode/utils.ts`, adapted to key off `## Steps`
|
|
12
|
-
// rather than plan-mode's `Plan:` header. Progress is surfaced headless-safe as the checkpoints
|
|
13
|
-
// segment of the composed `perk` status (a `📋 done/total · ▸n` segment published through the shared
|
|
14
|
-
// `PerkStatusHandle`) plus a themed `belowEditor` widget factory via `setStandingWidget` (stateless
|
|
15
|
-
// render, lines windowed to ≤4 steps and width-truncated); `/checkpoints` notifies a one-line
|
|
16
|
-
// summary. Accepted trade-off: pi's RPC mode drops factory widgets — the status chip +
|
|
17
|
-
// `/checkpoints` remain the RPC-visible surfaces (recorded in shared/contracts.md).
|
|
18
|
-
//
|
|
19
|
-
// TODO-PROVIDER DEFERRAL. perk's checkpoints are the *reference* todo provider (`perk-checkpoints`).
|
|
20
|
-
// They consume the resolved `[providers] todo` selection and **step the progress surface aside**
|
|
21
|
-
// when a foreign todo provider is selected — the todo-seam mirror of planMode.ts's plan-seam
|
|
22
|
-
// deferral. The four runtime surfaces guard on `isPerkCheckpointsReferenceSelected(ctx.cwd)` (read
|
|
23
|
-
// fresh per-event, fail-safe to the reference): `session_start`/`session_tree`/`turn_end`
|
|
24
|
-
// early-return **silently** (no seed, no advance, no render) so the foreign todo provider owns the
|
|
25
|
-
// surface uncontested; `/checkpoints` **announces** the deferral headless-safe. Runtime deferral
|
|
26
|
-
// only — registration-time vacating is the concrete foreign todo adapter's concern. Fail-safe: any
|
|
27
|
-
// config-read error → treated as the reference → zero change on the default selection.
|
|
28
|
-
|
|
29
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
|
-
import { readHandoff, readPlanBody } from "../substrate/cache.ts";
|
|
31
|
-
import { registerPerkCommand } from "../substrate/command.ts";
|
|
32
|
-
import { loadPerkConfig } from "../substrate/config.ts";
|
|
33
|
-
import {
|
|
34
|
-
loadProviders,
|
|
35
|
-
PERK_CHECKPOINTS_PROVIDER_ID,
|
|
36
|
-
resolveProviders,
|
|
37
|
-
} from "../substrate/providers.ts";
|
|
38
|
-
import {
|
|
39
|
-
digestSessionData,
|
|
40
|
-
readSessionArtifact,
|
|
41
|
-
writeSessionArtifact,
|
|
42
|
-
} from "../substrate/sessionData.ts";
|
|
43
|
-
import type { BranchEntry } from "../substrate/workflowState.ts";
|
|
44
|
-
import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
45
|
-
import {
|
|
46
|
-
checkpointEntryRenderer,
|
|
47
|
-
MARK_CHECKPOINTS,
|
|
48
|
-
type PerkStatusHandle,
|
|
49
|
-
progressLine,
|
|
50
|
-
registerTranscriptRenderer,
|
|
51
|
-
renderCoarsePlanLines,
|
|
52
|
-
renderProgressLines,
|
|
53
|
-
report,
|
|
54
|
-
setStandingWidget,
|
|
55
|
-
type ThemeLike,
|
|
56
|
-
WIDGET_SLOT_CHECKPOINTS,
|
|
57
|
-
} from "../surfaces/surfaces.ts";
|
|
58
|
-
import { generatePlanSteps } from "./planSteps.ts";
|
|
59
|
-
|
|
60
|
-
/** The dedicated checkpoint session entry type. */
|
|
61
|
-
export const CHECKPOINT_TYPE = "perk:checkpoint";
|
|
62
|
-
|
|
63
|
-
/** The once-only generated-checklist context injection's custom message type. */
|
|
64
|
-
export const STEPS_CONTEXT_TYPE = "perk:steps-context";
|
|
65
|
-
|
|
66
|
-
/** The generated-steps session artifact (written via the accessor seam). */
|
|
67
|
-
export const STEPS_ARTIFACT_NAME = "plan-steps.json";
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The resolved `[providers] todo` selection id for `cwd`, read fresh per-event (no static state —
|
|
71
|
-
* the same per-event-read shape `resolvedPlanProviderId` uses in planMode.ts). Fail-safe to the
|
|
72
|
-
* perk-checkpoints reference: any load/resolution failure returns the reference id so perk's own
|
|
73
|
-
* checkpoints keep working — the default path is the hard guarantee. The catch narrows to genuine
|
|
74
|
-
* file-read/parse failures (the resolver is per-seam fail-open) and is logged, never silent.
|
|
75
|
-
*/
|
|
76
|
-
export function resolvedTodoProviderId(cwd: string): string {
|
|
77
|
-
try {
|
|
78
|
-
return resolveProviders(loadPerkConfig(cwd).providers, loadProviders()).todo.id;
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error(
|
|
81
|
-
`perk: todo provider resolution failed — falling back to ${PERK_CHECKPOINTS_PROVIDER_ID}: ${error}`,
|
|
82
|
-
);
|
|
83
|
-
return PERK_CHECKPOINTS_PROVIDER_ID;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Whether perk's own checkpoints reference is the selected todo provider for `cwd`. When a foreign
|
|
89
|
-
* todo provider is selected via `[providers] todo`, perk's progress surface steps aside (defers).
|
|
90
|
-
*/
|
|
91
|
-
export function isPerkCheckpointsReferenceSelected(cwd: string): boolean {
|
|
92
|
-
return resolvedTodoProviderId(cwd) === PERK_CHECKPOINTS_PROVIDER_ID;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface CheckpointStep {
|
|
96
|
-
step: number;
|
|
97
|
-
text: string;
|
|
98
|
-
completed: boolean;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface CheckpointState {
|
|
102
|
-
steps: CheckpointStep[];
|
|
103
|
-
/** The in-progress step number (derived from `[WIP:n]` + completion), or `null`. */
|
|
104
|
-
current: number | null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** A step list with no items is "inert" — no `## Steps` was found in the plan body. */
|
|
108
|
-
export function isInert(state: CheckpointState): boolean {
|
|
109
|
-
return state.steps.length === 0;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// --- pure helpers (perk-owned copies of plan-mode/utils.ts) -------------------------------------
|
|
113
|
-
|
|
114
|
-
/** Extract `[DONE:n]` step numbers from a block of text (case-insensitive). */
|
|
115
|
-
export function extractDoneSteps(text: string): number[] {
|
|
116
|
-
const steps: number[] = [];
|
|
117
|
-
for (const match of text.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
118
|
-
const step = Number(match[1]);
|
|
119
|
-
if (Number.isFinite(step)) steps.push(step);
|
|
120
|
-
}
|
|
121
|
-
return steps;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Mark steps named by `[DONE:n]` in `text` as completed (mutates `steps`); returns the count. */
|
|
125
|
-
export function markCompletedSteps(text: string, steps: CheckpointStep[]): number {
|
|
126
|
-
const done = extractDoneSteps(text);
|
|
127
|
-
for (const n of done) {
|
|
128
|
-
const item = steps.find((s) => s.step === n);
|
|
129
|
-
if (item) item.completed = true;
|
|
130
|
-
}
|
|
131
|
-
return done.length;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Extract `[WIP:n]` step numbers from a block of text (case-insensitive). */
|
|
135
|
-
export function extractWipSteps(text: string): number[] {
|
|
136
|
-
const steps: number[] = [];
|
|
137
|
-
for (const match of text.matchAll(/\[WIP:(\d+)\]/gi)) {
|
|
138
|
-
const step = Number(match[1]);
|
|
139
|
-
if (Number.isFinite(step)) steps.push(step);
|
|
140
|
-
}
|
|
141
|
-
return steps;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** The last `[WIP:n]` in `text` whose step exists and is not completed, else `null`. */
|
|
145
|
-
export function latestWipStep(text: string, steps: CheckpointStep[]): number | null {
|
|
146
|
-
const wips = extractWipSteps(text);
|
|
147
|
-
for (let i = wips.length - 1; i >= 0; i--) {
|
|
148
|
-
const n = wips[i] as number;
|
|
149
|
-
const item = steps.find((s) => s.step === n);
|
|
150
|
-
if (item && !item.completed) return n;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Derive the in-progress step: `preferred` if it names an existing incomplete step; else the
|
|
157
|
-
* lowest-numbered incomplete step; else `null` (all complete / no steps).
|
|
158
|
-
*/
|
|
159
|
-
export function computeCurrent(steps: CheckpointStep[], preferred: number | null): number | null {
|
|
160
|
-
if (preferred != null) {
|
|
161
|
-
const item = steps.find((s) => s.step === preferred);
|
|
162
|
-
if (item && !item.completed) return preferred;
|
|
163
|
-
}
|
|
164
|
-
const lowest = steps.filter((s) => !s.completed).sort((a, b) => a.step - b.step)[0];
|
|
165
|
-
return lowest ? lowest.step : null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Parse the plan body's `## Steps` numbered list into checkpoint steps. Returns `[]` when there is
|
|
170
|
-
* no `## Steps` section (the inert path). Only a recognizable `<n>. text` / `<n>) text` list
|
|
171
|
-
* under the header is parsed; the section ends at the next `## ` heading.
|
|
172
|
-
*/
|
|
173
|
-
export function extractSteps(planBody: string | null | undefined): CheckpointStep[] {
|
|
174
|
-
if (!planBody) return [];
|
|
175
|
-
const lines = planBody.split(/\r?\n/);
|
|
176
|
-
let inSection = false;
|
|
177
|
-
const steps: CheckpointStep[] = [];
|
|
178
|
-
for (const raw of lines) {
|
|
179
|
-
const line = raw ?? "";
|
|
180
|
-
const header = line.match(/^\s*(#{1,6})\s+(.*\S)\s*$/);
|
|
181
|
-
if (header) {
|
|
182
|
-
// Enter the section on a `## Steps`-style heading; leave it on any other heading.
|
|
183
|
-
inSection = /^steps\b/i.test((header[2] ?? "").trim());
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (!inSection) continue;
|
|
187
|
-
const numbered = line.match(/^\s*(\d+)[.)]\s+(.+?)\s*$/);
|
|
188
|
-
if (numbered) {
|
|
189
|
-
const text = (numbered[2] ?? "").trim();
|
|
190
|
-
if (text) steps.push({ step: steps.length + 1, text, completed: false });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return steps;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// --- rebuild (the scan-after-marker discipline) -------------------------------------------------
|
|
197
|
-
|
|
198
|
-
function isAssistantText(entry: {
|
|
199
|
-
type?: string;
|
|
200
|
-
message?: { role?: string; content?: unknown };
|
|
201
|
-
}): string | null {
|
|
202
|
-
if (entry.type !== "message" || entry.message?.role !== "assistant") return null;
|
|
203
|
-
const content = entry.message.content;
|
|
204
|
-
if (typeof content === "string") return content;
|
|
205
|
-
if (Array.isArray(content)) {
|
|
206
|
-
return content
|
|
207
|
-
.map((b) => {
|
|
208
|
-
const block = b as { type?: string; text?: string };
|
|
209
|
-
return block.type === "text" && typeof block.text === "string" ? block.text : "";
|
|
210
|
-
})
|
|
211
|
-
.filter(Boolean)
|
|
212
|
-
.join("\n");
|
|
213
|
-
}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Rebuild checkpoint state from the branch. The latest `perk:checkpoint` entry is the **marker**
|
|
219
|
-
* (its `steps` carry completion persisted on prior turns); we then re-fold `[DONE:n]` from assistant
|
|
220
|
-
* messages **after** that marker only — so stale `[DONE:n]` from a previous execution can't
|
|
221
|
-
* resurrect a step (a subtlety: stale `[DONE:n]` must not resurrect a step). No checkpoint entry ⟹ inert.
|
|
222
|
-
*/
|
|
223
|
-
export function rebuildCheckpoint(branch: readonly BranchEntry[]): CheckpointState {
|
|
224
|
-
let markerIdx = -1;
|
|
225
|
-
let seed: CheckpointStep[] | null = null;
|
|
226
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
227
|
-
const e = branch[i] as BranchEntry & { data?: { steps?: CheckpointStep[] } };
|
|
228
|
-
if (e?.type === "custom" && e.customType === CHECKPOINT_TYPE) {
|
|
229
|
-
const stored = e.data?.steps;
|
|
230
|
-
if (Array.isArray(stored)) {
|
|
231
|
-
seed = stored.map((s) => ({ step: s.step, text: s.text, completed: !!s.completed }));
|
|
232
|
-
markerIdx = i;
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (seed === null) return { steps: [], current: null };
|
|
238
|
-
|
|
239
|
-
const after: string[] = [];
|
|
240
|
-
for (let i = markerIdx + 1; i < branch.length; i++) {
|
|
241
|
-
const text = isAssistantText(branch[i] as never);
|
|
242
|
-
if (text) after.push(text);
|
|
243
|
-
}
|
|
244
|
-
const afterText = after.join("\n");
|
|
245
|
-
markCompletedSteps(afterText, seed);
|
|
246
|
-
const current = computeCurrent(seed, latestWipStep(afterText, seed));
|
|
247
|
-
return { steps: seed, current };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// --- generated steps -----------------------------------------------------------------------------
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Recomputed (never stored) generated-ness: a checkpoint state is "generated" iff it is non-inert
|
|
254
|
-
* AND the current plan body parses to NO explicit `## Steps` — explicit steps could only have been
|
|
255
|
-
* seeded from the body, so a non-inert state over a prose body must be LLM-derived.
|
|
256
|
-
*/
|
|
257
|
-
export function isGeneratedState(cwd: string, state: CheckpointState): boolean {
|
|
258
|
-
return !isInert(state) && extractSteps(readPlanBody(cwd)).length === 0;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Promote generated step texts to checkpoint steps (1-based, all incomplete). */
|
|
262
|
-
function toCheckpointSteps(texts: string[]): CheckpointStep[] {
|
|
263
|
-
return texts.map((text, i) => ({ step: i + 1, text, completed: false }));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Read the generated-steps artifact through the accessor seam (provenance-pointer validated)
|
|
268
|
-
* and return its steps — but only when its stored `plan_body_digest` matches the CURRENT plan
|
|
269
|
-
* body (a replan/rematerialized body invalidates the cache). `null` on any refusal: no pointer,
|
|
270
|
-
* digest mismatch (file or plan body), unparseable JSON, or an unusable steps shape.
|
|
271
|
-
*/
|
|
272
|
-
function readGeneratedStepsArtifact(
|
|
273
|
-
ctx: Parameters<typeof readSessionArtifact>[0],
|
|
274
|
-
planBody: string,
|
|
275
|
-
): string[] | null {
|
|
276
|
-
const artifact = readSessionArtifact(ctx, STEPS_ARTIFACT_NAME);
|
|
277
|
-
if (artifact === null) return null;
|
|
278
|
-
try {
|
|
279
|
-
const parsed = JSON.parse(artifact.content) as { plan_body_digest?: unknown; steps?: unknown };
|
|
280
|
-
if (parsed.plan_body_digest !== digestSessionData(planBody)) return null;
|
|
281
|
-
const steps = parsed.steps;
|
|
282
|
-
if (!Array.isArray(steps) || steps.length < 2) return null;
|
|
283
|
-
if (!steps.every((s) => typeof s === "string" && s.length > 0)) return null;
|
|
284
|
-
return steps as string[];
|
|
285
|
-
} catch {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** The hidden context message teaching the model the generated checklist's exact numbering. */
|
|
291
|
-
function stepsContextContent(steps: readonly CheckpointStep[]): string {
|
|
292
|
-
const lines = steps.map((s) => `${s.step}. ${s.text}`);
|
|
293
|
-
return (
|
|
294
|
-
"perk generated the following implementation checklist for this prose plan. Emit `[WIP:n]` " +
|
|
295
|
-
"when you start step n and `[DONE:n]` when it completes, using EXACTLY these numbers:\n\n" +
|
|
296
|
-
lines.join("\n")
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Whether `e` is pi-core's stale-`ctx` compaction proxy error. During `session_compact` pi may
|
|
302
|
-
* replace the running session out from under an in-flight event handler, invalidating the extension
|
|
303
|
-
* runner's `ctx` proxy; the next read off it throws `/stale after session replacement/`. That is a
|
|
304
|
-
* benign race (the dying session is discarded and the replacement session's `session_start`
|
|
305
|
-
* re-renders), so the `session_compact` handler swallows it. Adapted from `@juicesharp/rpiv-todo`'s
|
|
306
|
-
* `index.ts` `isStaleCtxError`.
|
|
307
|
-
*/
|
|
308
|
-
export function isStaleCtxError(e: unknown): boolean {
|
|
309
|
-
return /stale after session replacement/.test(String(e));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// --- the controller -----------------------------------------------------------------------------
|
|
313
|
-
|
|
314
|
-
/** A coarse descriptor of the active plan when there is no `## Steps` checklist (the prose path). */
|
|
315
|
-
interface CoarseDescriptor {
|
|
316
|
-
stage: string;
|
|
317
|
-
planId: string;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function coarseDescriptor(
|
|
321
|
-
ctx: ExtensionContext,
|
|
322
|
-
branch: readonly BranchEntry[],
|
|
323
|
-
): CoarseDescriptor | null {
|
|
324
|
-
const wf = rebuildWorkflowState(branch);
|
|
325
|
-
if (wf.active_plan_ref == null) return null;
|
|
326
|
-
const stageRaw = wf.run_id != null ? readHandoff(ctx.cwd, wf.run_id)?.stage : undefined;
|
|
327
|
-
const stage = typeof stageRaw === "string" && stageRaw ? stageRaw : "active";
|
|
328
|
-
return { stage, planId: wf.active_plan_ref.pr_id };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Surface progress in the UI (headless-safe — the handle + widget setter no-op without UI). */
|
|
332
|
-
function renderStatus(
|
|
333
|
-
ctx: ExtensionContext,
|
|
334
|
-
status: PerkStatusHandle,
|
|
335
|
-
state: CheckpointState,
|
|
336
|
-
branch: readonly BranchEntry[],
|
|
337
|
-
): void {
|
|
338
|
-
if (isInert(state)) {
|
|
339
|
-
// Coarse fallback: an active prose plan (no `## Steps`) still surfaces SOMETHING — the same
|
|
340
|
-
// themed factory path as the steps widget (one dim line, stateless render, belowEditor).
|
|
341
|
-
const coarse = coarseDescriptor(ctx, branch);
|
|
342
|
-
if (coarse) {
|
|
343
|
-
status.set(ctx, "checkpoints", `${MARK_CHECKPOINTS} ${coarse.stage}`);
|
|
344
|
-
setStandingWidget(
|
|
345
|
-
ctx,
|
|
346
|
-
WIDGET_SLOT_CHECKPOINTS,
|
|
347
|
-
(_tui: unknown, theme: ThemeLike) => ({
|
|
348
|
-
render: (width: number) => renderCoarsePlanLines(coarse.planId, theme, width),
|
|
349
|
-
invalidate: () => {},
|
|
350
|
-
}),
|
|
351
|
-
{ placement: "belowEditor" },
|
|
352
|
-
);
|
|
353
|
-
} else {
|
|
354
|
-
status.set(ctx, "checkpoints", undefined);
|
|
355
|
-
setStandingWidget(ctx, WIDGET_SLOT_CHECKPOINTS, undefined);
|
|
356
|
-
}
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
// Themed component factory: lines are computed inside render() per call — never cached — over
|
|
360
|
-
// the freshly-rebuilt state snapshot; windowed + width-truncated in surfaces.ts.
|
|
361
|
-
const snapshot = state;
|
|
362
|
-
status.set(ctx, "checkpoints", `${MARK_CHECKPOINTS} ${progressLine(state)}`);
|
|
363
|
-
setStandingWidget(
|
|
364
|
-
ctx,
|
|
365
|
-
WIDGET_SLOT_CHECKPOINTS,
|
|
366
|
-
(_tui: unknown, theme: ThemeLike) => ({
|
|
367
|
-
render: (width: number) => renderProgressLines(snapshot, theme, width),
|
|
368
|
-
invalidate: () => {},
|
|
369
|
-
}),
|
|
370
|
-
{ placement: "belowEditor" },
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Register perk-owned checkpoints. Seeds on `session_start` from the plan body's `## Steps` (only in
|
|
376
|
-
* an active workflow, and only once — a later session keeps the existing entry); rebuilds on
|
|
377
|
-
* `session_start` AND `session_tree`; advances on `turn_end`; lists via `/checkpoints`.
|
|
378
|
-
*/
|
|
379
|
-
export function registerCheckpoints(pi: ExtensionAPI, status: PerkStatusHandle): void {
|
|
380
|
-
// Transcript marker for `perk:checkpoint` snapshots (audit §2.3): renderer body in surfaces.ts,
|
|
381
|
-
// registration = wiring, feature-detect inside the seam (pre-0.80.4 hosts stay inert). No
|
|
382
|
-
// todo-provider deferral here: entries exist only when perk's checkpoints appended them, so
|
|
383
|
-
// rendering history stays correct under any later provider selection.
|
|
384
|
-
registerTranscriptRenderer(pi, CHECKPOINT_TYPE, checkpointEntryRenderer);
|
|
385
|
-
|
|
386
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
387
|
-
try {
|
|
388
|
-
// Todo-provider deferral: when a foreign `[providers] todo` is selected, step the progress
|
|
389
|
-
// surface aside silently (no seed, no render) — the foreign provider owns it.
|
|
390
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
|
|
391
|
-
const branch = branchOf(ctx);
|
|
392
|
-
const existing = rebuildCheckpoint(branch);
|
|
393
|
-
// Seed once: only when there is no checkpoint yet, a workflow is active, and the plan body
|
|
394
|
-
// carries a `## Steps` list. Otherwise stay inert (no entry appended).
|
|
395
|
-
if (isInert(existing)) {
|
|
396
|
-
const wf = rebuildWorkflowState(branch);
|
|
397
|
-
const active = wf.active_plan_ref != null;
|
|
398
|
-
const planBody = active ? readPlanBody(ctx.cwd) : null;
|
|
399
|
-
let steps = active ? extractSteps(planBody) : [];
|
|
400
|
-
// Generation branch: an active IMPLEMENT session over a prose plan body (no usable
|
|
401
|
-
// `## Steps` — the extractor already maps malformed sections to []) asks the session model
|
|
402
|
-
// for a bounded checklist. Artifact reuse first (the pointer-validated cache, keyed on the
|
|
403
|
-
// plan-body digest); every failure falls through to the coarse fallback unchanged.
|
|
404
|
-
if (steps.length === 0 && planBody !== null && wf.active_plan_ref != null) {
|
|
405
|
-
const stageRaw = wf.run_id != null ? readHandoff(ctx.cwd, wf.run_id)?.stage : undefined;
|
|
406
|
-
if (stageRaw === "implement") {
|
|
407
|
-
let texts = readGeneratedStepsArtifact(ctx, planBody);
|
|
408
|
-
if (texts === null) {
|
|
409
|
-
texts = await generatePlanSteps(ctx, planBody);
|
|
410
|
-
if (texts !== null) {
|
|
411
|
-
// Best-effort persistence for reuse across reload/compaction: a failed artifact
|
|
412
|
-
// write (already warned by the seam) never drops a successful generation.
|
|
413
|
-
writeSessionArtifact(
|
|
414
|
-
pi,
|
|
415
|
-
ctx,
|
|
416
|
-
STEPS_ARTIFACT_NAME,
|
|
417
|
-
`${JSON.stringify(
|
|
418
|
-
{
|
|
419
|
-
plan_id: wf.active_plan_ref.pr_id,
|
|
420
|
-
plan_body_digest: digestSessionData(planBody),
|
|
421
|
-
steps: texts,
|
|
422
|
-
},
|
|
423
|
-
null,
|
|
424
|
-
2,
|
|
425
|
-
)}\n`,
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (texts !== null) steps = toCheckpointSteps(texts);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (steps.length > 0) {
|
|
433
|
-
pi.appendEntry(CHECKPOINT_TYPE, { steps });
|
|
434
|
-
renderStatus(ctx, status, { steps, current: computeCurrent(steps, null) }, branch);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
renderStatus(ctx, status, existing, branch);
|
|
439
|
-
} catch (error) {
|
|
440
|
-
console.error(`perk: checkpoint seed/rebuild failed on session_start — ${error}`);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
445
|
-
try {
|
|
446
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
|
|
447
|
-
const branch = branchOf(ctx);
|
|
448
|
-
renderStatus(ctx, status, rebuildCheckpoint(branch), branch);
|
|
449
|
-
} catch (error) {
|
|
450
|
-
console.error(`perk: checkpoint rebuild failed on session_tree — ${error}`);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// session_compact re-render (adapted from `@juicesharp/rpiv-todo`): a compaction replaces the
|
|
455
|
-
// session entries, so rebuild + re-render the progress surface (mirror `session_tree`: NO
|
|
456
|
-
// re-seed). The catch arm diverges from the other handlers — a stale-`ctx` compaction race is
|
|
457
|
-
// benign and swallowed silently (the replacement session's `session_start` re-renders); only
|
|
458
|
-
// genuine replay bugs are logged per the log-not-throw convention.
|
|
459
|
-
pi.on("session_compact", async (_event, ctx) => {
|
|
460
|
-
try {
|
|
461
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
|
|
462
|
-
const branch = branchOf(ctx);
|
|
463
|
-
renderStatus(ctx, status, rebuildCheckpoint(branch), branch);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
if (isStaleCtxError(error)) return;
|
|
466
|
-
console.error(`perk: checkpoint rebuild failed on session_compact — ${error}`);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
pi.on("turn_end", async (event, ctx) => {
|
|
471
|
-
try {
|
|
472
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
|
|
473
|
-
const branch = branchOf(ctx);
|
|
474
|
-
const state = rebuildCheckpoint(branch);
|
|
475
|
-
if (isInert(state)) {
|
|
476
|
-
// Coarse path: an active prose plan still surfaces a status (no entry to advance).
|
|
477
|
-
renderStatus(ctx, status, state, branch);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const text = isAssistantText(event.message as never);
|
|
481
|
-
if (text === null) {
|
|
482
|
-
renderStatus(ctx, status, state, branch);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const advanced = markCompletedSteps(text, state.steps) > 0;
|
|
486
|
-
// A WIP declared THIS turn wins; otherwise preserve the prior `current` (unless it completed).
|
|
487
|
-
state.current = computeCurrent(
|
|
488
|
-
state.steps,
|
|
489
|
-
latestWipStep(text, state.steps) ?? state.current,
|
|
490
|
-
);
|
|
491
|
-
if (advanced) {
|
|
492
|
-
// Persist the advanced completion as a new marker entry (carries completion forward).
|
|
493
|
-
pi.appendEntry(CHECKPOINT_TYPE, { steps: state.steps });
|
|
494
|
-
}
|
|
495
|
-
// Always re-render: `current` can change without completion advancing.
|
|
496
|
-
renderStatus(ctx, status, state, branch);
|
|
497
|
-
} catch (error) {
|
|
498
|
-
console.error(`perk: checkpoint advance failed on turn_end — ${error}`);
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// The once-only generated-checklist injection: when the seeded checkpoints are
|
|
503
|
-
// LLM-generated (recomputed: non-inert over a prose plan body), inject the numbered list as a
|
|
504
|
-
// hidden context message so `[WIP:n]`/`[DONE:n]` markers use exactly these numbers. Injected
|
|
505
|
-
// custom messages persist to the branch, so the branch-carries-it guard makes this once-only
|
|
506
|
-
// (and a rewind past the injection naturally re-injects). No strip handler: the steps stay
|
|
507
|
-
// relevant for the whole implement session (there is no off state).
|
|
508
|
-
pi.on("before_agent_start", async (_event, ctx) => {
|
|
509
|
-
try {
|
|
510
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
|
|
511
|
-
const branch = branchOf(ctx);
|
|
512
|
-
if (branchCarries(branch, STEPS_CONTEXT_TYPE)) return;
|
|
513
|
-
const state = rebuildCheckpoint(branch);
|
|
514
|
-
if (isInert(state) || !isGeneratedState(ctx.cwd, state)) return;
|
|
515
|
-
return {
|
|
516
|
-
message: {
|
|
517
|
-
customType: STEPS_CONTEXT_TYPE,
|
|
518
|
-
content: stepsContextContent(state.steps),
|
|
519
|
-
display: false,
|
|
520
|
-
},
|
|
521
|
-
};
|
|
522
|
-
} catch (error) {
|
|
523
|
-
console.error(`perk: steps-context injection failed on before_agent_start — ${error}`);
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
registerPerkCommand(pi, "checkpoints", {
|
|
528
|
-
description: "Show perk implementation checkpoints (read-only).",
|
|
529
|
-
handler: async (_args, ctx) => {
|
|
530
|
-
// Todo-provider deferral: announce the deferral headless-safe and step aside when a
|
|
531
|
-
// foreign `[providers] todo` is selected (the surface-facing mirror of the silent handlers).
|
|
532
|
-
if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) {
|
|
533
|
-
const deferral = `checkpoints deferred — a foreign todo provider (\`${resolvedTodoProviderId(
|
|
534
|
-
ctx.cwd,
|
|
535
|
-
)}\`) is selected via [providers] todo.`;
|
|
536
|
-
report(ctx, "checkpoints", "info", deferral);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const state = rebuildCheckpoint(branchOf(ctx));
|
|
540
|
-
// One line: `done/total · ▸n <current step text>`; the tail drops when no
|
|
541
|
-
// step is current (all complete).
|
|
542
|
-
const current = state.steps.find((s) => s.step === state.current);
|
|
543
|
-
const generated = !isInert(state) && isGeneratedState(ctx.cwd, state) ? " (generated)" : "";
|
|
544
|
-
const message = isInert(state)
|
|
545
|
-
? "no checkpoints — this plan has no `## Steps` list (checkpoints are inert)."
|
|
546
|
-
: `${progressLine(state)}${current ? ` ${current.text}` : ""}${generated}`;
|
|
547
|
-
report(ctx, "checkpoints", "info", message);
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Generated checkpoint TODOs for prose plans: the second consumer of the structured-output
|
|
2
|
-
// substrate (the `planTitle.ts` idiom). When an implement session starts on a plan body that lacks
|
|
3
|
-
// a usable `## Steps` numbered list, checkpoints ask the session model for a bounded, ordered
|
|
4
|
-
// implementation checklist via a single structured tool call. Every failure mode (the offline
|
|
5
|
-
// gate, no model, unresolved auth, a model error, no tool call, schema-invalid args, an unusable
|
|
6
|
-
// sanitized list) returns `null`, so the caller's deterministic fallback (today's coarse prose
|
|
7
|
-
// behavior) takes over — a session start is never failed by this path.
|
|
8
|
-
|
|
9
|
-
import { Type } from "@earendil-works/pi-ai";
|
|
10
|
-
import {
|
|
11
|
-
completeStructured,
|
|
12
|
-
type ModelAuthContext,
|
|
13
|
-
resolveModelAuth,
|
|
14
|
-
} from "../substrate/structuredOutput.ts";
|
|
15
|
-
|
|
16
|
-
/** Bounds shared by the schema and the sanitizer. */
|
|
17
|
-
const STEPS_MIN = 2;
|
|
18
|
-
const STEPS_MAX = 12;
|
|
19
|
-
const STEP_TEXT_MAX_CHARS = 200;
|
|
20
|
-
|
|
21
|
-
/** The structured result schema: a bounded, ordered list of concise imperative steps. */
|
|
22
|
-
const PlanStepsSchema = Type.Object({
|
|
23
|
-
steps: Type.Array(Type.String({ minLength: 1, maxLength: STEP_TEXT_MAX_CHARS }), {
|
|
24
|
-
minItems: STEPS_MIN,
|
|
25
|
-
maxItems: STEPS_MAX,
|
|
26
|
-
description:
|
|
27
|
-
"Concise, ordered, imperative implementation steps covering the whole plan " +
|
|
28
|
-
"(one short phrase per step, no numbering, no markdown).",
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
/** Cap the plan markdown handed to the model (steps need the plan, not an unbounded payload). */
|
|
33
|
-
export const STEPS_INPUT_CHAR_CAP = 24_000;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Normalize raw model steps into a clean checkpoint list, or `null` when unusable. Pure: per item,
|
|
37
|
-
* trims, strips a leading list marker the model may echo (`1.` / `1)` / `-` / `*`), collapses
|
|
38
|
-
* internal whitespace/newlines to single spaces, truncates to ≤200 chars; drops empties; caps at
|
|
39
|
-
* 12 items; returns `null` when fewer than 2 survive.
|
|
40
|
-
*/
|
|
41
|
-
export function sanitizeSteps(raw: string[]): string[] | null {
|
|
42
|
-
const out: string[] = [];
|
|
43
|
-
for (const item of raw) {
|
|
44
|
-
if (typeof item !== "string") continue;
|
|
45
|
-
let s = item.trim();
|
|
46
|
-
// Strip a leading echoed list marker: `1.`, `1)`, `-`, `*`.
|
|
47
|
-
s = s.replace(/^(?:\d+[.)]|[-*])\s+/, "");
|
|
48
|
-
s = s.replace(/\s+/g, " ").trim();
|
|
49
|
-
if (!s) continue;
|
|
50
|
-
if (s.length > STEP_TEXT_MAX_CHARS) s = s.slice(0, STEP_TEXT_MAX_CHARS).trim();
|
|
51
|
-
out.push(s);
|
|
52
|
-
if (out.length >= STEPS_MAX) break;
|
|
53
|
-
}
|
|
54
|
-
return out.length >= STEPS_MIN ? out : null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The deterministic offline gate. `PERK_NO_LLM` (set by the test harness, never by the production
|
|
59
|
-
* `perk` CLI) disables step generation so tests stay fully offline regardless of ambient API keys —
|
|
60
|
-
* the same gate as `llmTitlesEnabled`. Pure and unit-testable.
|
|
61
|
-
*/
|
|
62
|
-
export function llmStepsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
63
|
-
return !env.PERK_NO_LLM;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Best-effort: generate an ordered implementation checklist for `planMarkdown` via the session
|
|
68
|
-
* model, or `null` on any failure / when the gate is on. Never throws. Reports a genuine model
|
|
69
|
-
* error (`console.error`) but stays non-fatal — the caller falls back to the coarse prose path.
|
|
70
|
-
*/
|
|
71
|
-
export async function generatePlanSteps(
|
|
72
|
-
ctx: ModelAuthContext,
|
|
73
|
-
planMarkdown: string,
|
|
74
|
-
signal?: AbortSignal,
|
|
75
|
-
): Promise<string[] | null> {
|
|
76
|
-
if (!llmStepsEnabled()) return null;
|
|
77
|
-
|
|
78
|
-
let auth: Awaited<ReturnType<typeof resolveModelAuth>>;
|
|
79
|
-
try {
|
|
80
|
-
auth = await resolveModelAuth(ctx);
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
if (!auth.ok) return null; // silent: no model / no auth configured (fail-safe).
|
|
85
|
-
|
|
86
|
-
const outcome = await completeStructured({
|
|
87
|
-
model: auth.model,
|
|
88
|
-
schema: PlanStepsSchema,
|
|
89
|
-
toolName: "set_plan_steps",
|
|
90
|
-
toolDescription: "Provide the ordered implementation checklist for the plan.",
|
|
91
|
-
system:
|
|
92
|
-
"You decompose software engineering plans into concise, ordered implementation checklists.",
|
|
93
|
-
instruction:
|
|
94
|
-
"Read this implementation plan and produce an ordered implementation checklist covering the " +
|
|
95
|
-
"whole plan: 2–12 steps, each a single concise imperative phrase (no numbering, no markdown).",
|
|
96
|
-
input: planMarkdown.slice(0, STEPS_INPUT_CHAR_CAP),
|
|
97
|
-
apiKey: auth.apiKey,
|
|
98
|
-
headers: auth.headers,
|
|
99
|
-
signal,
|
|
100
|
-
timeoutMs: 30_000,
|
|
101
|
-
});
|
|
102
|
-
if (!outcome.ok || !outcome.value) {
|
|
103
|
-
// Report (don't swallow) a genuine model error; the session start still proceeds.
|
|
104
|
-
if (!outcome.ok) console.error(`perk: plan-steps — ${outcome.error}`);
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
return sanitizeSteps(outcome.value.steps);
|
|
108
|
-
}
|