@mgiles/perk 1.0.1 → 1.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 +7 -21
- package/extension/checkpoints/checkpoints.ts +2 -1
- package/extension/doors/address.ts +2 -1
- package/extension/doors/ciExecutor.ts +3 -2
- package/extension/doors/land.ts +2 -1
- package/extension/doors/learn.ts +239 -33
- package/extension/doors/learnCode.ts +100 -0
- package/extension/doors/learnDocs.ts +4 -3
- package/extension/doors/lifecycleGates.ts +2 -1
- package/extension/doors/prReview.ts +20 -35
- package/extension/doors/prReviewLocal.ts +229 -0
- package/extension/doors/ready.ts +2 -1
- package/extension/doors/selfcheck.ts +2 -1
- package/extension/doors/submit.ts +17 -19
- package/extension/factories/implementHere.ts +116 -0
- package/extension/factories/objective.ts +2 -1
- package/extension/factories/objectivePlan.ts +4 -24
- package/extension/factories/objectiveSave.ts +5 -15
- package/extension/factories/planMode.ts +5 -1
- package/extension/factories/planReview.ts +103 -11
- package/extension/factories/planSave.ts +16 -1
- package/extension/index.ts +42 -3
- package/extension/substrate/bindings.ts +1 -1
- package/extension/substrate/cache.ts +2 -2
- package/extension/substrate/command.ts +25 -0
- package/extension/substrate/config.ts +12 -13
- package/extension/substrate/consoleCapture.ts +90 -0
- package/extension/substrate/git.ts +34 -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 +1 -1
- package/extension/substrate/sessionData.ts +1 -1
- package/extension/substrate/sessionPointers.ts +155 -0
- package/extension/substrate/toolGating.ts +8 -0
- package/extension/surfaces/surfaces.ts +10 -3
- package/extension/worker/worker.ts +111 -25
- package/extension/workerMain.ts +5 -3
- package/package.json +1 -5
- 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 +281 -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/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/seed.md +2 -1
- 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.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 +7 -1
- package/shared/bindings.yaml +12 -0
- package/shared/contracts-history.md +167 -0
- package/shared/contracts.md +1307 -342
- package/shared/registry.yaml +3 -3
- 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/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-context.schema.json +86 -0
- package/shared/schemas/outputs/pr-submit.schema.json +147 -0
- 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/README.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# perk
|
|
2
2
|
|
|
3
|
-
A Pi-native, plan-oriented engineering workflow
|
|
3
|
+
A Pi-native, plan-oriented engineering workflow: a Python `perk` CLI (the session
|
|
4
4
|
*exterior*) plus a TypeScript Pi extension (the session *interior*).
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> Inspired by `erk`, formely maintained by the team at [dagster](https://github.com/dagster-io/dagster)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
perk was built with perk.
|
|
9
9
|
|
|
10
10
|
## What perk is
|
|
11
11
|
|
|
12
|
-
perk implements a plan-oriented engineering workflow (
|
|
13
|
-
|
|
14
|
-
split across **two planes**:
|
|
12
|
+
perk implements a plan-oriented engineering workflow (plan → implement → submit → review →
|
|
13
|
+
address → land → learn) for [Pi](https://github.com/earendil-works), split across **two planes**:
|
|
15
14
|
|
|
16
15
|
- the **exterior** — a Python `perk` CLI that scaffolds repos, positions worktrees, mints
|
|
17
16
|
run ids, and launches primed `pi` sessions (everything that happens *outside* a session);
|
|
@@ -19,10 +18,7 @@ split across **two planes**:
|
|
|
19
18
|
*inside* a running session.
|
|
20
19
|
|
|
21
20
|
A language-neutral [`shared/`](shared/) contract (the stage registry + cross-plane specs)
|
|
22
|
-
is the single source both planes read,
|
|
23
|
-
|
|
24
|
-
perk is built to **bootstrap its own development**: each phase leaves perk capable of
|
|
25
|
-
driving the next, and perk's own repo is the first thing it scaffolds.
|
|
21
|
+
is the single source both planes read, hopefully staying in lockstep without codegen.
|
|
26
22
|
|
|
27
23
|
## Quickstart
|
|
28
24
|
|
|
@@ -42,8 +38,7 @@ For the guided first run, follow
|
|
|
42
38
|
|
|
43
39
|
## Documentation
|
|
44
40
|
|
|
45
|
-
The operator-facing docs live under [`docs/user-docs/`](docs/user-docs/index.md), organized as
|
|
46
|
-
the four [Divio](https://docs.divio.com/documentation-system/) quadrants:
|
|
41
|
+
The operator-facing docs live under [`docs/user-docs/`](docs/user-docs/index.md), organized as:
|
|
47
42
|
|
|
48
43
|
- **[Tutorials](docs/user-docs/tutorials/index.md)** — learning-oriented lessons; start with
|
|
49
44
|
[Get started with perk](docs/user-docs/tutorials/get-started.md).
|
|
@@ -86,15 +81,6 @@ just test # pytest + node:test (the regression gate)
|
|
|
86
81
|
just ci # setup + lint + typecheck + test
|
|
87
82
|
```
|
|
88
83
|
|
|
89
|
-
After `just setup` (or `just install-cli`), call `perk` directly — no `uv run`. The install is
|
|
90
|
-
**editable**, so a `git pull` reflects Python changes live; re-run `just install-cli` after a
|
|
91
|
-
dependency change. It lands in uv's tool bin (`~/.local/bin`) — if `perk` isn't found, that dir
|
|
92
|
-
is not on your `PATH`; run `uv tool update-shell` (then restart your shell). Remove it with
|
|
93
|
-
`uv tool uninstall perk`.
|
|
94
|
-
|
|
95
|
-
Releasing perk → see [docs/releasing.md](docs/releasing.md) (version SSOT, dual-plane runbook,
|
|
96
|
-
the `validate-release-versions` tag gate).
|
|
97
|
-
|
|
98
84
|
`just setup` also runs `just hooks` (`prek install`), wiring a [prek](https://prek.j178.dev)
|
|
99
85
|
pre-commit hook that runs `ruff check` on staged Python (config in `prek.toml`; the ruff
|
|
100
86
|
env is built by prek from the remote ruff-pre-commit repo, so it never depends on a
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
31
31
|
import { readHandoff, readPlanBody } from "../substrate/cache.ts";
|
|
32
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
32
33
|
import { loadPerkConfig } from "../substrate/config.ts";
|
|
33
34
|
import {
|
|
34
35
|
loadProviders,
|
|
@@ -516,7 +517,7 @@ export function registerCheckpoints(pi: ExtensionAPI, status: PerkStatusHandle):
|
|
|
516
517
|
}
|
|
517
518
|
});
|
|
518
519
|
|
|
519
|
-
pi
|
|
520
|
+
registerPerkCommand(pi, "checkpoints", {
|
|
520
521
|
description: "Show perk implementation checkpoints (read-only).",
|
|
521
522
|
handler: async (_args, ctx) => {
|
|
522
523
|
// Todo-provider deferral: announce the deferral headless-safe and step aside when a
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
runColdDoor,
|
|
21
21
|
stringField,
|
|
22
22
|
} from "../substrate/coldDoor.ts";
|
|
23
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
23
24
|
import { loadPerkConfig } from "../substrate/config.ts";
|
|
24
25
|
import { render } from "../substrate/prompts.ts";
|
|
25
26
|
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
@@ -324,7 +325,7 @@ export function registerAddress(pi: ExtensionAPI): void {
|
|
|
324
325
|
},
|
|
325
326
|
});
|
|
326
327
|
|
|
327
|
-
pi
|
|
328
|
+
registerPerkCommand(pi, "address", {
|
|
328
329
|
description:
|
|
329
330
|
"Classify PR review feedback (isolated child) and resolve threads (submit → address). " +
|
|
330
331
|
"Pass --preview to classify only (take no action).",
|
|
@@ -29,6 +29,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { ensureRunScratch, scratchDir } from "../substrate/cache.ts";
|
|
32
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
32
33
|
import { type CiCheck, loadPerkConfig } from "../substrate/config.ts";
|
|
33
34
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
34
35
|
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
@@ -367,7 +368,7 @@ export function renderCiProse(report: CiReport): string {
|
|
|
367
368
|
);
|
|
368
369
|
}
|
|
369
370
|
if (report.error_type === "no_checks_configured") {
|
|
370
|
-
return "No CI checks configured ([[ci]] in .
|
|
371
|
+
return "No CI checks configured ([[ci]] in .perk/config.toml is empty). Nothing to run.";
|
|
371
372
|
}
|
|
372
373
|
if (report.error_type === "unknown_check") {
|
|
373
374
|
return `perk CI: ${report.error}`;
|
|
@@ -565,7 +566,7 @@ export function registerCiExecutor(pi: ExtensionAPI): void {
|
|
|
565
566
|
},
|
|
566
567
|
});
|
|
567
568
|
|
|
568
|
-
pi
|
|
569
|
+
registerPerkCommand(pi, "ci", {
|
|
569
570
|
description: "Run the project's configured CI checks and report results (never auto-fixes).",
|
|
570
571
|
handler: async (args, ctx) => {
|
|
571
572
|
const check = args.trim() === "" ? undefined : args.trim();
|
package/extension/doors/land.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
runColdDoor,
|
|
16
16
|
stringField,
|
|
17
17
|
} from "../substrate/coldDoor.ts";
|
|
18
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
18
19
|
import { failFor, ok, type Result } from "../substrate/result.ts";
|
|
19
20
|
import { report } from "../surfaces/report.ts";
|
|
20
21
|
|
|
@@ -208,7 +209,7 @@ export function registerLand(pi: ExtensionAPI): void {
|
|
|
208
209
|
},
|
|
209
210
|
});
|
|
210
211
|
|
|
211
|
-
pi
|
|
212
|
+
registerPerkCommand(pi, "land", {
|
|
212
213
|
description: "Merge the active plan's PR and set pending-learn (submit → land).",
|
|
213
214
|
handler: async (_args, ctx) => {
|
|
214
215
|
const result = await landPr(pi, ctx);
|
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 `[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,7 +225,7 @@ 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
|
|
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.",
|
|
132
229
|
"The summary is captured verbatim — write the learnings as markdown (what changed vs. the plan, deviations, residual risks).",
|
|
133
230
|
];
|
|
134
231
|
|
|
@@ -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,100 @@
|
|
|
1
|
+
// hop-2 — the learn-code plan factory's warm transition surface: the `/learn-code` command.
|
|
2
|
+
//
|
|
3
|
+
// The warm twin of the `perk learn code` cold door (sibling of `/learn-docs`). It DELEGATES the
|
|
4
|
+
// gather to the Python plane (`perk learn code --gather --json` via the shared cold-door client
|
|
5
|
+
// `runColdDoor` — gate-safe, not subject to the read-only bash allowlist), decodes
|
|
6
|
+
// `{ inbox_path, learn_numbers }`, then injects the factory guidance via `pi.sendUserMessage` so
|
|
7
|
+
// the model reads the inbox, authors a code plan, and calls `plan_save` with `consumed_learn`. No
|
|
8
|
+
// model tool — the model uses the existing `plan_save` tool.
|
|
9
|
+
//
|
|
10
|
+
// Headless-safe: rich UI is guarded by `ctx.hasUI`; without a UI it logs to stderr and returns
|
|
11
|
+
// (the gather still runs so the inbox is materialized, but no turn is driven).
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
15
|
+
import { type ColdJson, runColdDoor, stringField } from "../substrate/coldDoor.ts";
|
|
16
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
17
|
+
import { render } from "../substrate/prompts.ts";
|
|
18
|
+
import { report } from "../surfaces/report.ts";
|
|
19
|
+
|
|
20
|
+
/** The decoded `perk learn code --gather --json` payload slice the warm door consumes. */
|
|
21
|
+
interface LearnCodeGatherPayload {
|
|
22
|
+
inbox_path: string;
|
|
23
|
+
/** Opaque string learn-issue ids (GitHub "45", Linear "ENG-45") — §8.21. */
|
|
24
|
+
learn_numbers: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. Exported for offline reject-branch tests. */
|
|
28
|
+
export function decodeGather(payload: ColdJson): LearnCodeGatherPayload | null {
|
|
29
|
+
const inboxPath = stringField(payload, "inbox_path");
|
|
30
|
+
const numbers = payload.learn_numbers;
|
|
31
|
+
if (inboxPath === undefined) return null;
|
|
32
|
+
// String ids are canonical (§8.21); numbers are tolerated + coerced (older envelopes).
|
|
33
|
+
if (
|
|
34
|
+
!Array.isArray(numbers) ||
|
|
35
|
+
!numbers.every((n) => typeof n === "string" || typeof n === "number")
|
|
36
|
+
) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { inbox_path: inboxPath, learn_numbers: numbers.map((n) => String(n)) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The seed guidance the warm `/learn-code` injects to start the factory loop (the perk-learn-code
|
|
44
|
+
* skill pointer rides the skill-binding suffix — not hardcoded here). Pure + exported
|
|
45
|
+
* for offline tests.
|
|
46
|
+
*/
|
|
47
|
+
export function learnCodeGuidance(inboxPath: string, learnNumbers: string[]): string {
|
|
48
|
+
return render("stages/learn-code.md", {
|
|
49
|
+
inbox_path: inboxPath,
|
|
50
|
+
num_list: learnNumbers.join(", "),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Register the warm learn-code door: the `/learn-code` command (no model tool). */
|
|
55
|
+
export function registerLearnCode(pi: ExtensionAPI): void {
|
|
56
|
+
registerPerkCommand(pi, "learn-code", {
|
|
57
|
+
description:
|
|
58
|
+
"Start the learn-code plan factory: gather pre-stamped SHOULD_BE_CODE perk:learn issues into " +
|
|
59
|
+
"an inbox and author a plan routing each into its real code home.",
|
|
60
|
+
handler: async (_args, ctx: ExtensionContext) => {
|
|
61
|
+
// Report-only door (no Result type): branch on `errorType` directly (the coldDoor header
|
|
62
|
+
// convention). A clean "nothing to route" exits non-zero with error_type=no_learn_issues —
|
|
63
|
+
// the client's envelope-aware arm surfaces it gently.
|
|
64
|
+
const r = await runColdDoor<LearnCodeGatherPayload>(
|
|
65
|
+
pi,
|
|
66
|
+
ctx,
|
|
67
|
+
["learn", "code", "--gather", "--json"],
|
|
68
|
+
{ label: "perk learn code", decode: decodeGather },
|
|
69
|
+
);
|
|
70
|
+
if (!r.ok) {
|
|
71
|
+
if (r.errorType === "no_learn_issues") {
|
|
72
|
+
report(
|
|
73
|
+
ctx,
|
|
74
|
+
"learn-code",
|
|
75
|
+
"warning",
|
|
76
|
+
"nothing to route into code (no SHOULD_BE_CODE perk:learn issues).",
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
report(ctx, "learn-code", "error", `gather failed: ${r.message}`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!ctx.hasUI) {
|
|
85
|
+
// Headless can't drive a turn — the inbox is materialized; log and return (fail-safe).
|
|
86
|
+
console.error(
|
|
87
|
+
`perk: /learn-code invoked (headless) — gathered ${r.data.learn_numbers.length} ` +
|
|
88
|
+
`learn issue(s) into ${r.data.inbox_path}; run interactively to author the code plan.`,
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
report(ctx, "learn-code", "info", `gathered ${r.data.learn_numbers.length} learn issue(s)`);
|
|
94
|
+
pi.sendUserMessage(
|
|
95
|
+
learnCodeGuidance(r.data.inbox_path, r.data.learn_numbers) +
|
|
96
|
+
bindingSuffix(ctx.cwd, "command:learn-code"),
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
15
15
|
import { type ColdJson, runColdDoor, stringField } from "../substrate/coldDoor.ts";
|
|
16
|
+
import { registerPerkCommand } from "../substrate/command.ts";
|
|
16
17
|
import { render } from "../substrate/prompts.ts";
|
|
17
18
|
import { report } from "../surfaces/report.ts";
|
|
18
19
|
|
|
@@ -23,8 +24,8 @@ interface LearnDocsGatherPayload {
|
|
|
23
24
|
learn_numbers: string[];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. */
|
|
27
|
-
function decodeGather(payload: ColdJson): LearnDocsGatherPayload | null {
|
|
27
|
+
/** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. Exported for offline reject-branch tests. */
|
|
28
|
+
export function decodeGather(payload: ColdJson): LearnDocsGatherPayload | null {
|
|
28
29
|
const inboxPath = stringField(payload, "inbox_path");
|
|
29
30
|
const numbers = payload.learn_numbers;
|
|
30
31
|
if (inboxPath === undefined) return null;
|
|
@@ -52,7 +53,7 @@ export function learnDocsGuidance(inboxPath: string, learnNumbers: string[]): st
|
|
|
52
53
|
|
|
53
54
|
/** Register the warm learned-docs door: the `/learn-docs` command (no model tool). */
|
|
54
55
|
export function registerLearnDocs(pi: ExtensionAPI): void {
|
|
55
|
-
pi
|
|
56
|
+
registerPerkCommand(pi, "learn-docs", {
|
|
56
57
|
description:
|
|
57
58
|
"Start the learned-docs plan factory: gather open perk:learn issues into an inbox and author " +
|
|
58
59
|
"a docs/learned consolidation plan.",
|
|
@@ -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) => {
|