@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
package/extension/doors/learn.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `
|
|
1
|
+
// The warm `/learn` door — a multi-angle knowledge-capture orchestrator (mirrors `/pr-review`).
|
|
2
|
+
//
|
|
3
|
+
// Bare interactive `/learn` gathers a reproducible evidence bundle ONCE via the cold door
|
|
4
|
+
// (`perk learn evidence --render --json`; the parent owns the gather per §8.35), then branches:
|
|
5
|
+
// a learn-docs plan short-circuits to a deterministic marker-clear no-op; a gather failure (or a
|
|
6
|
+
// bundle-less success) degrades to the simple `learnGuidance` injection (/learn is never a dead
|
|
7
|
+
// end); otherwise it injects the orchestration seed (`learnOrchestrateGuidance`) so the model spawns
|
|
8
|
+
// 2–4 fresh-context `perk.learn-analyst` children, reconciles their reports into ONE classified
|
|
9
|
+
// decision, and captures (via the `learn` tool, with the routable `decision`/`target` persisted on
|
|
10
|
+
// the issue header — both backends) or skips.
|
|
11
|
+
//
|
|
12
|
+
// The `learn` tool is the capture half: with a `summary`, DELEGATE to `perk learn capture --json`
|
|
13
|
+
// via the shared cold-door client (`runColdDoor` — the body rides the run-scratch stdin channel,
|
|
14
|
+
// the `decision`/`target` classification rides flags; canonical write in Python), creating a
|
|
15
|
+
// `perk:learn` issue + clearing `pending-learn`, then mirror the marker-clear in-session
|
|
16
|
+
// (idempotent). With no `summary`, DELEGATE to `perk learn skip --json` (contracts.md §8.36) —
|
|
17
|
+
// the deliberate skip is recorded canonically on the plan-header (`learn_state: skipped`, unless
|
|
18
|
+
// already `captured`), never a TS-only marker-clear.
|
|
19
|
+
// Never throws (soft `details.ok`); both decodes are fully LENIENT — a `success: true`
|
|
20
|
+
// envelope always yields the terminating ok result even when the payload is undecodable
|
|
21
|
+
// (render-only fields; see `decodeLearnCapture` / `decodeLearnSkip`).
|
|
22
|
+
//
|
|
23
|
+
// Headless bare `/learn` stays the safe no-summary path (cannot drive a turn / spawn children).
|
|
24
|
+
// `/learn <text>` / `/learn skip` stay the existing verbatim-capture / skip-recording paths
|
|
25
|
+
// (decision-less escape hatches). Cold `perk learn` launch stays the simple investigate+capture.
|
|
26
|
+
//
|
|
27
|
+
// The analyst model is configurable via `[models.subagents] learn-analyst` in `.perk/config.toml`; because
|
|
28
|
+
// `subagents.agentOverrides` does NOT reach project agents, the orchestration seed injects that
|
|
29
|
+
// model as a per-call inline `model` override on every analyst spawn.
|
|
10
30
|
|
|
31
|
+
import { join } from "node:path";
|
|
11
32
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
33
|
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
13
34
|
import {
|
|
@@ -24,6 +45,8 @@ import {
|
|
|
24
45
|
runColdDoor,
|
|
25
46
|
stringField,
|
|
26
47
|
} from "../substrate/coldDoor.ts";
|
|
48
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
49
|
+
import { loadPerkConfig } from "../substrate/config.ts";
|
|
27
50
|
import { render } from "../substrate/prompts.ts";
|
|
28
51
|
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
29
52
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
@@ -46,6 +69,61 @@ interface LearnCapturePayload {
|
|
|
46
69
|
learn_issue?: { id: string; url: string; existed: boolean };
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
/** The decoded `perk learn skip --json` payload slice (render-only fields). */
|
|
73
|
+
interface LearnSkipPayload {
|
|
74
|
+
learn_state: string | null;
|
|
75
|
+
pending_cleared: boolean | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decode the `perk learn skip --json` success payload — fully LENIENT (mirrors `decodeEvidence`):
|
|
80
|
+
* it **never returns null**, so any success envelope yields a usable object and the `bad_output`
|
|
81
|
+
* arm is deliberately unreachable for this door. Both fields are render-only (they flavor the
|
|
82
|
+
* report text); the `success: true` envelope is the cold door's authoritative statement that the
|
|
83
|
+
* skip was recorded and the on-disk marker cleared.
|
|
84
|
+
*/
|
|
85
|
+
function decodeLearnSkip(payload: ColdJson): LearnSkipPayload {
|
|
86
|
+
return {
|
|
87
|
+
learn_state: stringField(payload, "learn_state") ?? null,
|
|
88
|
+
pending_cleared: booleanField(payload, "pending_cleared") ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The closed CAPTURED-classification set persisted on a `perk:learn` header (contracts.md §8.35) —
|
|
94
|
+
* the reconciliation DECISION set minus `SKIP` (a skip creates no issue). Mirrors
|
|
95
|
+
* `plan.CapturedDecision` (the Python SSOT) and the `learn` tool's JSON-schema enum.
|
|
96
|
+
*/
|
|
97
|
+
const CAPTURED_DECISIONS = [
|
|
98
|
+
"CAPTURE_LEARN",
|
|
99
|
+
"SHOULD_BE_CODE",
|
|
100
|
+
"UPDATE_EXISTING_DOC",
|
|
101
|
+
"NEW_DOC",
|
|
102
|
+
"STALE_DOC",
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
/** The decoded `perk learn evidence --json` slice the orchestrator branches on. */
|
|
106
|
+
interface EvidenceDecode {
|
|
107
|
+
skipped: boolean;
|
|
108
|
+
skip_reason: string | null;
|
|
109
|
+
bundle_dir: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Decode the `perk learn evidence --render --json` success payload — fully LENIENT (mirrors
|
|
114
|
+
* `decodeLearnCapture`): it **never returns null**, so any success envelope yields a usable object
|
|
115
|
+
* and the `runColdDoor` `bad_output` arm is deliberately unreachable for this door. A missing/
|
|
116
|
+
* mistyped `skipped` defaults false; `bundle_dir`/`skip_reason` default null. `!r.ok` (exec /
|
|
117
|
+
* transport / `success:false`) routes to the gather-failure fallback, not here.
|
|
118
|
+
*/
|
|
119
|
+
function decodeEvidence(payload: ColdJson): EvidenceDecode {
|
|
120
|
+
return {
|
|
121
|
+
skipped: booleanField(payload, "skipped") ?? false,
|
|
122
|
+
skip_reason: stringField(payload, "skip_reason") ?? null,
|
|
123
|
+
bundle_dir: stringField(payload, "bundle_dir") ?? null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
49
127
|
/**
|
|
50
128
|
* Narrow the `perk learn capture --json` success payload — fully LENIENT, per the decode-policy
|
|
51
129
|
* criterion (strict iff the field is appended to workflow-state; see
|
|
@@ -78,29 +156,48 @@ function clearPending(ctx: ExtensionContext): { wasPending: boolean } {
|
|
|
78
156
|
}
|
|
79
157
|
|
|
80
158
|
/**
|
|
81
|
-
* The single learn implementation both surfaces call. With a `summary`, delegate the capture to
|
|
82
|
-
* Python cold door
|
|
83
|
-
*
|
|
159
|
+
* The single learn implementation both surfaces call. With a `summary`, delegate the capture to
|
|
160
|
+
* the Python cold door; without one, delegate the skip-recording to `perk learn skip` (§8.36 —
|
|
161
|
+
* the canonical `learn_state: skipped` stamp, no empty issue). Both arms mirror the marker-clear
|
|
162
|
+
* in-session on success. Returns a soft result (never throws).
|
|
84
163
|
*/
|
|
85
164
|
export async function learnDone(
|
|
86
165
|
pi: ExtensionAPI,
|
|
87
166
|
ctx: ExtensionContext,
|
|
88
167
|
summary?: string,
|
|
168
|
+
decision?: string,
|
|
169
|
+
target?: string,
|
|
89
170
|
): Promise<LearnResult> {
|
|
90
171
|
const trimmed = (summary ?? "").trim();
|
|
172
|
+
const fail = failFor(ctx, "learn");
|
|
91
173
|
|
|
92
|
-
// No summary: the
|
|
174
|
+
// No summary: record the deliberate skip canonically (the cold door stamps the plan-header and
|
|
175
|
+
// clears the marker; the skip carries no classification, so `decision`/`target` are
|
|
176
|
+
// intentionally ignored on this arm). On failure the marker is NOT cleared — never silently
|
|
177
|
+
// close the learn cycle on uncertainty (the marker is the retry signal).
|
|
93
178
|
if (trimmed.length === 0) {
|
|
179
|
+
const r = await runColdDoor<LearnSkipPayload>(pi, ctx, ["learn", "skip", "--json"], {
|
|
180
|
+
label: "perk learn skip",
|
|
181
|
+
decode: decodeLearnSkip,
|
|
182
|
+
});
|
|
183
|
+
if (!r.ok) return fail(r.message, r.errorType);
|
|
184
|
+
// Mirror the marker-clear in-session (idempotent; the worker already cleared it on disk).
|
|
94
185
|
const { wasPending } = clearPending(ctx);
|
|
95
|
-
const text =
|
|
96
|
-
|
|
97
|
-
|
|
186
|
+
const text =
|
|
187
|
+
r.data.learn_state === "captured"
|
|
188
|
+
? "Learnings were already captured — kept; pending-learn cleared."
|
|
189
|
+
: "Skip recorded on the plan; pending-learn cleared — the worktree is releasable. " +
|
|
190
|
+
"(No summary given; no learn issue created.)";
|
|
98
191
|
return ok(text, { was_pending: wasPending, captured: false }, { terminate: true });
|
|
99
192
|
}
|
|
100
193
|
|
|
101
|
-
|
|
194
|
+
// The captured classification (contracts.md §8.35) rides flags on the capture argv; Click parses
|
|
195
|
+
// them regardless of order, and the `--body` stdin channel is unchanged.
|
|
196
|
+
const argv = ["learn", "capture", "--json"];
|
|
197
|
+
if (decision !== undefined) argv.push("--decision", decision);
|
|
198
|
+
if (target !== undefined) argv.push("--target", target);
|
|
102
199
|
|
|
103
|
-
const r = await runColdDoor<LearnCapturePayload>(pi, ctx,
|
|
200
|
+
const r = await runColdDoor<LearnCapturePayload>(pi, ctx, argv, {
|
|
104
201
|
label: "perk learn capture",
|
|
105
202
|
decode: decodeLearnCapture,
|
|
106
203
|
stdin: { flag: "--body", content: `${trimmed}\n`, filename: `learn-${Date.now()}.md` },
|
|
@@ -128,8 +225,8 @@ export async function learnDone(
|
|
|
128
225
|
}
|
|
129
226
|
|
|
130
227
|
const TOOL_GUIDELINES = [
|
|
131
|
-
"Call learn after a plan has landed; pass a `summary` of the durable learnings to capture them in a perk:learn issue (and clear pending-learn). Omit `summary` to
|
|
132
|
-
"
|
|
228
|
+
"Call learn after a plan has landed; pass a `summary` of the durable learnings to capture them in a perk:learn issue (and clear pending-learn). Omit `summary` to record the skip on the plan and clear the marker.",
|
|
229
|
+
"learn captures the summary verbatim — write the learnings as markdown (what changed vs. the plan, deviations, residual risks).",
|
|
133
230
|
];
|
|
134
231
|
|
|
135
232
|
/** Resolve the active plan-ref (worktree first, then the rebuilt workflow-state). */
|
|
@@ -167,6 +264,26 @@ export function learnGuidance(planRef: PlanRef | null): string {
|
|
|
167
264
|
});
|
|
168
265
|
}
|
|
169
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The orchestration seed the warm bare `/learn` injects to spawn the angle-specialized analysts and
|
|
269
|
+
* reconcile their reports into one classified capture/skip (the perk-learn skill pointer rides the
|
|
270
|
+
* skill-binding suffix — stage:learn — not hardcoded here). Pure + exported for offline tests
|
|
271
|
+
* (mirrors `prReviewGuidance`). When `model` is set, EVERY analyst spawn carries an inline `model`
|
|
272
|
+
* override; otherwise the agent's default is used. `manifestPath` is absolute; `bundleDir` is the
|
|
273
|
+
* absolute bundle directory.
|
|
274
|
+
*/
|
|
275
|
+
export function learnOrchestrateGuidance(opts: {
|
|
276
|
+
model?: string;
|
|
277
|
+
manifestPath: string;
|
|
278
|
+
bundleDir: string;
|
|
279
|
+
}): string {
|
|
280
|
+
return render("stages/learn-orchestrate.md", {
|
|
281
|
+
model: opts.model ?? "",
|
|
282
|
+
manifest_path: opts.manifestPath,
|
|
283
|
+
bundle_dir: opts.bundleDir,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
170
287
|
/** Register the warm door: the `learn` terminating tool + the `/learn` command twin. */
|
|
171
288
|
export function registerLearn(pi: ExtensionAPI): void {
|
|
172
289
|
pi.registerTool({
|
|
@@ -174,8 +291,8 @@ export function registerLearn(pi: ExtensionAPI): void {
|
|
|
174
291
|
label: "Finish learn",
|
|
175
292
|
description:
|
|
176
293
|
"Capture learnings from a landed plan into a perk:learn issue (pass `summary`), then clear " +
|
|
177
|
-
"the pending-learn semaphore and release the worktree. Omit `summary` to
|
|
178
|
-
"Terminating: ends the turn.",
|
|
294
|
+
"the pending-learn semaphore and release the worktree. Omit `summary` to record the skip " +
|
|
295
|
+
"on the plan and clear pending-learn. Terminating: ends the turn.",
|
|
179
296
|
promptSnippet:
|
|
180
297
|
"Capture learnings (optional summary) and clear pending-learn (terminates the turn)",
|
|
181
298
|
promptGuidelines: TOOL_GUIDELINES,
|
|
@@ -186,30 +303,60 @@ export function registerLearn(pi: ExtensionAPI): void {
|
|
|
186
303
|
properties: {
|
|
187
304
|
summary: {
|
|
188
305
|
type: "string",
|
|
189
|
-
description:
|
|
306
|
+
description:
|
|
307
|
+
"Markdown learnings to capture in a perk:learn issue. Omit to record the skip.",
|
|
308
|
+
},
|
|
309
|
+
decision: {
|
|
310
|
+
type: "string",
|
|
311
|
+
enum: [...CAPTURED_DECISIONS],
|
|
312
|
+
description:
|
|
313
|
+
"The reconciled captured-classification token, persisted on the perk:learn header. " +
|
|
314
|
+
"Omit on a verbatim /learn <text> capture (the decision-less escape hatch).",
|
|
315
|
+
},
|
|
316
|
+
target: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description:
|
|
319
|
+
"An optional routable pointer (e.g. an existing doc path) for the classification.",
|
|
190
320
|
},
|
|
191
321
|
},
|
|
192
322
|
},
|
|
193
323
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
194
|
-
// Tool-boundary decode: absent → undefined (the
|
|
324
|
+
// Tool-boundary decode (mirrors the `summary` strictness): absent → undefined (the
|
|
325
|
+
// marker-clear / decision-less path); a present-but-mistyped/out-of-enum value →
|
|
195
326
|
// strict-fail — never silently clear the pending-learn marker on uncertainty.
|
|
196
327
|
const p = paramsOf(params);
|
|
328
|
+
const fail = failFor(ctx, "learn");
|
|
197
329
|
const summary = p === null ? undefined : stringParam(p, "summary");
|
|
198
330
|
if (summary === null) {
|
|
199
|
-
return
|
|
331
|
+
return fail("learn `summary` must be a string", "bad_input");
|
|
200
332
|
}
|
|
201
|
-
|
|
333
|
+
const decision = p === null ? undefined : stringParam(p, "decision");
|
|
334
|
+
if (decision === null) {
|
|
335
|
+
return fail("learn `decision` must be a string", "bad_input");
|
|
336
|
+
}
|
|
337
|
+
if (decision !== undefined && !(CAPTURED_DECISIONS as readonly string[]).includes(decision)) {
|
|
338
|
+
return fail(
|
|
339
|
+
`learn \`decision\` must be one of ${CAPTURED_DECISIONS.join(", ")}`,
|
|
340
|
+
"bad_input",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const target = p === null ? undefined : stringParam(p, "target");
|
|
344
|
+
if (target === null) {
|
|
345
|
+
return fail("learn `target` must be a string", "bad_input");
|
|
346
|
+
}
|
|
347
|
+
return learnDone(pi, ctx, summary, decision, target);
|
|
202
348
|
},
|
|
203
349
|
});
|
|
204
350
|
|
|
205
|
-
pi
|
|
351
|
+
registerPerkCommand(pi, "learn", {
|
|
206
352
|
description:
|
|
207
353
|
"Investigate the landed change and capture learnings (bare /learn drives the workflow); " +
|
|
208
|
-
"/learn skip
|
|
354
|
+
"/learn skip records the skip on the plan and clears pending-learn; " +
|
|
355
|
+
"/learn <text> captures the text verbatim.",
|
|
209
356
|
handler: async (args, ctx) => {
|
|
210
357
|
const trimmed = (args ?? "").trim();
|
|
211
358
|
|
|
212
|
-
// Explicit text (or `skip`): the existing learnDone path — capture verbatim /
|
|
359
|
+
// Explicit text (or `skip`): the existing learnDone path — capture verbatim / record skip.
|
|
213
360
|
if (trimmed.length > 0) {
|
|
214
361
|
const summary = trimmed === "skip" ? "" : args;
|
|
215
362
|
const result = await learnDone(pi, ctx, summary);
|
|
@@ -220,16 +367,75 @@ export function registerLearn(pi: ExtensionAPI): void {
|
|
|
220
367
|
return;
|
|
221
368
|
}
|
|
222
369
|
|
|
223
|
-
// Bare `/learn`: headless can't drive a turn —
|
|
224
|
-
//
|
|
225
|
-
// (it clears the marker itself by calling the `learn` tool — do NOT clear it here).
|
|
370
|
+
// Bare `/learn`: headless can't drive a turn or spawn children — take the safe no-summary
|
|
371
|
+
// path (the canonical skip recording; fail-safe).
|
|
226
372
|
if (!ctx.hasUI) {
|
|
227
373
|
const result = await learnDone(pi, ctx, "");
|
|
228
374
|
console.error(`perk: /learn invoked (headless) — ${result.content[0]?.text ?? "cleared"}`);
|
|
229
375
|
return;
|
|
230
376
|
}
|
|
231
|
-
|
|
232
|
-
|
|
377
|
+
|
|
378
|
+
// Interactive bare `/learn`: the multi-angle orchestrator (mirrors /pr-review). Gather the
|
|
379
|
+
// evidence bundle ONCE (the parent owns the gather — §8.35), then branch.
|
|
380
|
+
const fallback = () => {
|
|
381
|
+
// Graceful degrade — /learn is never a dead end. Fall back to the simple learn pass (the
|
|
382
|
+
// prior behavior); the agent clears the marker itself via the `learn` tool.
|
|
383
|
+
pi.sendUserMessage(
|
|
384
|
+
learnGuidance(activePlanRef(ctx)) + bindingSuffix(ctx.cwd, "stage:learn"),
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const r = await runColdDoor<EvidenceDecode>(
|
|
389
|
+
pi,
|
|
390
|
+
ctx,
|
|
391
|
+
["learn", "evidence", "--render", "--json"],
|
|
392
|
+
{ label: "perk learn evidence", decode: decodeEvidence },
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Gather failure (exec / transport / success:false): degrade to the simple learn pass.
|
|
396
|
+
if (!r.ok) {
|
|
397
|
+
report(
|
|
398
|
+
ctx,
|
|
399
|
+
"learn",
|
|
400
|
+
"info",
|
|
401
|
+
"evidence gather unavailable — falling back to the simple learn pass",
|
|
402
|
+
);
|
|
403
|
+
fallback();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Short-circuit: a learn-docs consolidation plan — clear the local marker only, inject
|
|
408
|
+
// nothing (land already stamped `learn_state: skipped` for a `consumed_learn` plan, §8.36 —
|
|
409
|
+
// no cold skip delegation needed here).
|
|
410
|
+
if (r.data.skipped) {
|
|
411
|
+
clearPending(ctx);
|
|
412
|
+
report(ctx, "learn", "info", "learn-docs plan; learn capture skipped");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Defensive: a success envelope with no bundle dir — same graceful fallback.
|
|
417
|
+
if (r.data.bundle_dir === null) {
|
|
418
|
+
report(
|
|
419
|
+
ctx,
|
|
420
|
+
"learn",
|
|
421
|
+
"info",
|
|
422
|
+
"evidence bundle unavailable — falling back to the simple learn pass",
|
|
423
|
+
);
|
|
424
|
+
fallback();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Orchestrate: spawn analysts over the shared bundle, reconcile, capture-or-skip. `bundle_dir`
|
|
429
|
+
// is repo_root-relative; the door's cwd is the worktree root the command resolved against.
|
|
430
|
+
const bundleDir = join(ctx.cwd, r.data.bundle_dir);
|
|
431
|
+
const manifestPath = join(bundleDir, "manifest.json");
|
|
432
|
+
const model = loadPerkConfig(ctx.cwd).subagents["learn-analyst"];
|
|
433
|
+
report(ctx, "learn", "info", "multi-angle learn: spawn analysts → reconcile → capture");
|
|
434
|
+
// The agent captures via the `learn` tool (clearing the marker itself) — do NOT clear here.
|
|
435
|
+
pi.sendUserMessage(
|
|
436
|
+
learnOrchestrateGuidance({ model, manifestPath, bundleDir }) +
|
|
437
|
+
bindingSuffix(ctx.cwd, "stage:learn"),
|
|
438
|
+
);
|
|
233
439
|
},
|
|
234
440
|
});
|
|
235
441
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// hop-2 — the two learn plan factories' warm transition surfaces: the `/learn-docs` and
|
|
2
|
+
// `/learn-code` commands (the warm twins of the `perk learn docs` / `perk learn code` cold
|
|
3
|
+
// doors). One shared register parameterized by a kind config — mirroring the Python plane's
|
|
4
|
+
// `factory_common.py` (`LearnFactoryKind` + `DOCS_FACTORY`/`CODE_FACTORY` + `run_factory`).
|
|
5
|
+
//
|
|
6
|
+
// Each door DELEGATES the gather to the Python plane (`perk learn <kind> --gather --json` via the
|
|
7
|
+
// shared cold-door client `runColdDoor` — gate-safe, not subject to the read-only bash allowlist),
|
|
8
|
+
// decodes `{ inbox_path, learn_numbers }`, then injects the factory guidance via
|
|
9
|
+
// `pi.sendUserMessage` so the model reads the inbox, authors the plan, and saves it. The save is
|
|
10
|
+
// surface-dependent (the seed spells this out): where `plan_save` is active (a read-write session,
|
|
11
|
+
// the warm doors' usual host) the model passes `consumed_learn` explicitly — load-bearing here,
|
|
12
|
+
// because the warm gather is side-effect-free and writes NO handoff carrier; in a gated read-only
|
|
13
|
+
// session `plan_save` is hidden (toolGating.ts), so the save lands review-first via `plan_review`
|
|
14
|
+
// and the COLD doors' handoff carrier supplies `consumed_learn`. No model tool is registered here.
|
|
15
|
+
//
|
|
16
|
+
// Headless-safe: rich UI is guarded by `ctx.hasUI`; without a UI it logs to stderr and returns
|
|
17
|
+
// (the gather still runs so the inbox is materialized, but no turn is driven).
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
21
|
+
import { type ColdJson, runColdDoor, stringField } from "../substrate/coldDoor.ts";
|
|
22
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
23
|
+
import { render } from "../substrate/prompts.ts";
|
|
24
|
+
import { report } from "../surfaces/report.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The per-door parameter bundle shared by the two warm learn-factory doors (the TS twin of the
|
|
28
|
+
* frozen `LearnFactoryKind` dataclass). `subcommand` derives the cold argv, the `runColdDoor`
|
|
29
|
+
* label, and the headless log tail; `seedTemplate` and `bindingTrigger` stay explicit so the
|
|
30
|
+
* strings remain greppable against `prompts/stages/` and `shared/bindings.yaml`.
|
|
31
|
+
*/
|
|
32
|
+
export interface LearnFactoryDoorKind {
|
|
33
|
+
/** The command id and `report()` scope. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/** The cold-door verb under `perk learn`. */
|
|
36
|
+
readonly subcommand: string;
|
|
37
|
+
readonly seedTemplate: string;
|
|
38
|
+
readonly bindingTrigger: string;
|
|
39
|
+
/** The `registerPerkCommand` description. */
|
|
40
|
+
readonly description: string;
|
|
41
|
+
/** The gentle `no_learn_issues` warning. */
|
|
42
|
+
readonly emptyMessage: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DOCS_DOOR: LearnFactoryDoorKind = {
|
|
46
|
+
name: "learn-docs",
|
|
47
|
+
subcommand: "docs",
|
|
48
|
+
seedTemplate: "stages/learn-docs.md",
|
|
49
|
+
bindingTrigger: "command:learn-docs",
|
|
50
|
+
description:
|
|
51
|
+
"Start the learned-docs plan factory: gather open perk:learn issues into an inbox and author " +
|
|
52
|
+
"a docs/learned consolidation plan.",
|
|
53
|
+
emptyMessage: "nothing to consolidate (no open perk:learn issues).",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const CODE_DOOR: LearnFactoryDoorKind = {
|
|
57
|
+
name: "learn-code",
|
|
58
|
+
subcommand: "code",
|
|
59
|
+
seedTemplate: "stages/learn-code.md",
|
|
60
|
+
bindingTrigger: "command:learn-code",
|
|
61
|
+
description:
|
|
62
|
+
"Start the learn-code plan factory: gather pre-stamped SHOULD_BE_CODE perk:learn issues into " +
|
|
63
|
+
"an inbox and author a plan routing each into its real code home.",
|
|
64
|
+
emptyMessage: "nothing to route into code (no SHOULD_BE_CODE perk:learn issues).",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The decoded `perk learn <kind> --gather --json` payload slice the warm door consumes. */
|
|
68
|
+
export interface LearnGatherPayload {
|
|
69
|
+
inbox_path: string;
|
|
70
|
+
/** Opaque string learn-issue ids (GitHub "45", Linear "ENG-45") — §8.21. */
|
|
71
|
+
learn_numbers: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. Exported for offline reject-branch tests. */
|
|
75
|
+
export function decodeGather(payload: ColdJson): LearnGatherPayload | null {
|
|
76
|
+
const inboxPath = stringField(payload, "inbox_path");
|
|
77
|
+
const numbers = payload.learn_numbers;
|
|
78
|
+
if (inboxPath === undefined) return null;
|
|
79
|
+
// String ids are canonical (§8.21); numbers are tolerated + coerced (older envelopes).
|
|
80
|
+
if (
|
|
81
|
+
!Array.isArray(numbers) ||
|
|
82
|
+
!numbers.every((n) => typeof n === "string" || typeof n === "number")
|
|
83
|
+
) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { inbox_path: inboxPath, learn_numbers: numbers.map((n) => String(n)) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The seed guidance the warm door injects to start the factory loop (the per-kind skill pointer
|
|
91
|
+
* rides the skill-binding suffix — not hardcoded here). Pure + exported for offline tests.
|
|
92
|
+
*/
|
|
93
|
+
export function learnFactoryGuidance(
|
|
94
|
+
kind: LearnFactoryDoorKind,
|
|
95
|
+
inboxPath: string,
|
|
96
|
+
learnNumbers: string[],
|
|
97
|
+
): string {
|
|
98
|
+
return render(kind.seedTemplate, {
|
|
99
|
+
inbox_path: inboxPath,
|
|
100
|
+
num_list: learnNumbers.join(", "),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Register one warm learn-factory door: the `/<kind.name>` command (no model tool). */
|
|
105
|
+
export function registerLearnFactoryDoor(pi: ExtensionAPI, kind: LearnFactoryDoorKind): void {
|
|
106
|
+
registerPerkCommand(pi, kind.name, {
|
|
107
|
+
description: kind.description,
|
|
108
|
+
handler: async (_args, ctx: ExtensionContext) => {
|
|
109
|
+
// Report-only door (no Result type): branch on `errorType` directly (the coldDoor header
|
|
110
|
+
// convention). A clean empty inbox exits non-zero with error_type=no_learn_issues — the
|
|
111
|
+
// client's envelope-aware arm surfaces it gently.
|
|
112
|
+
const r = await runColdDoor<LearnGatherPayload>(
|
|
113
|
+
pi,
|
|
114
|
+
ctx,
|
|
115
|
+
["learn", kind.subcommand, "--gather", "--json"],
|
|
116
|
+
{ label: `perk learn ${kind.subcommand}`, decode: decodeGather },
|
|
117
|
+
);
|
|
118
|
+
if (!r.ok) {
|
|
119
|
+
if (r.errorType === "no_learn_issues") {
|
|
120
|
+
report(ctx, kind.name, "warning", kind.emptyMessage);
|
|
121
|
+
} else {
|
|
122
|
+
report(ctx, kind.name, "error", `gather failed: ${r.message}`);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!ctx.hasUI) {
|
|
128
|
+
// Headless can't drive a turn — the inbox is materialized; log and return (fail-safe).
|
|
129
|
+
console.error(
|
|
130
|
+
`perk: /${kind.name} invoked (headless) — gathered ${r.data.learn_numbers.length} ` +
|
|
131
|
+
`learn issue(s) into ${r.data.inbox_path}; run interactively to author the ` +
|
|
132
|
+
`${kind.subcommand} plan.`,
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
report(ctx, kind.name, "info", `gathered ${r.data.learn_numbers.length} learn issue(s)`);
|
|
138
|
+
pi.sendUserMessage(
|
|
139
|
+
learnFactoryGuidance(kind, r.data.inbox_path, r.data.learn_numbers) +
|
|
140
|
+
bindingSuffix(ctx.cwd, kind.bindingTrigger),
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ExtensionContext,
|
|
15
15
|
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { PlanRef } from "../substrate/cache.ts";
|
|
17
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
17
18
|
import { render } from "../substrate/prompts.ts";
|
|
18
19
|
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
19
20
|
import { report } from "../surfaces/report.ts";
|
|
@@ -104,7 +105,7 @@ export function implementHandoffPrompt(ref: PlanRef): string {
|
|
|
104
105
|
* either way), fail-safe-headless.
|
|
105
106
|
*/
|
|
106
107
|
function registerImplementGuard(pi: ExtensionAPI): void {
|
|
107
|
-
pi
|
|
108
|
+
registerPerkCommand(pi, "implement", {
|
|
108
109
|
description:
|
|
109
110
|
"Refresh implement context (in-worktree handoff); cross-worktree is `perk implement`.",
|
|
110
111
|
handler: async (_args, ctx) => {
|