@mgiles/perk 1.0.1
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 +105 -0
- package/extension/adapters/planAdapterPlannotator.ts +269 -0
- package/extension/adapters/planAdapterTombell.ts +147 -0
- package/extension/adapters/todoAdapterJuicesharp.ts +105 -0
- package/extension/checkpoints/checkpoints.ts +542 -0
- package/extension/checkpoints/planSteps.ts +108 -0
- package/extension/doors/address.ts +360 -0
- package/extension/doors/askUser.ts +194 -0
- package/extension/doors/ciExecutor.ts +583 -0
- package/extension/doors/land.ts +222 -0
- package/extension/doors/learn.ts +235 -0
- package/extension/doors/learnDocs.ts +99 -0
- package/extension/doors/lifecycleGates.ts +171 -0
- package/extension/doors/prReview.ts +339 -0
- package/extension/doors/ready.ts +86 -0
- package/extension/doors/selfcheck.ts +155 -0
- package/extension/doors/submit.ts +253 -0
- package/extension/factories/objective.ts +240 -0
- package/extension/factories/objectiveAuthor.ts +114 -0
- package/extension/factories/objectiveDraft.ts +343 -0
- package/extension/factories/objectivePlan.ts +838 -0
- package/extension/factories/objectiveSave.ts +285 -0
- package/extension/factories/planDraft.ts +140 -0
- package/extension/factories/planMode.ts +214 -0
- package/extension/factories/planReview.ts +644 -0
- package/extension/factories/planSave.ts +589 -0
- package/extension/factories/planTitle.ts +123 -0
- package/extension/index.ts +459 -0
- package/extension/substrate/bindingDelivery.ts +199 -0
- package/extension/substrate/bindings.ts +180 -0
- package/extension/substrate/cache.ts +163 -0
- package/extension/substrate/coldDoor.ts +226 -0
- package/extension/substrate/config.ts +339 -0
- package/extension/substrate/miniYaml.ts +262 -0
- package/extension/substrate/prompts.ts +35 -0
- package/extension/substrate/providers.ts +177 -0
- package/extension/substrate/registry.ts +62 -0
- package/extension/substrate/resources.ts +41 -0
- package/extension/substrate/result.ts +72 -0
- package/extension/substrate/runId.ts +49 -0
- package/extension/substrate/sessionData.ts +229 -0
- package/extension/substrate/structuredOutput.ts +141 -0
- package/extension/substrate/toolGating.ts +400 -0
- package/extension/substrate/toolParams.ts +106 -0
- package/extension/substrate/workflowState.ts +233 -0
- package/extension/surfaces/footerProvider.ts +43 -0
- package/extension/surfaces/report.ts +34 -0
- package/extension/surfaces/surfaces.ts +460 -0
- package/extension/vendor/btw/btw.ts +964 -0
- package/extension/vendor/btw/core.ts +153 -0
- package/extension/vendor/whimsical/whimsical.ts +485 -0
- package/extension/worker/readOnlySession.ts +282 -0
- package/extension/worker/worker.ts +765 -0
- package/extension/workerMain.ts +150 -0
- package/package.json +55 -0
- package/prompts/README.md +15 -0
- package/prompts/_fixtures/cases.yaml +140 -0
- package/prompts/_fixtures/golden/address-action-model.txt +10 -0
- package/prompts/_fixtures/golden/address-action.txt +10 -0
- package/prompts/_fixtures/golden/address-preview-model.txt +6 -0
- package/prompts/_fixtures/golden/address-preview.txt +6 -0
- package/prompts/_fixtures/golden/hello.txt +1 -0
- package/prompts/_fixtures/golden/implement-github.txt +8 -0
- package/prompts/_fixtures/golden/learn-docs.txt +8 -0
- package/prompts/_fixtures/golden/learn-github.txt +11 -0
- package/prompts/_fixtures/golden/learn-linear.txt +11 -0
- package/prompts/_fixtures/golden/learn-no-ref.txt +8 -0
- package/prompts/_fixtures/golden/learn-other.txt +8 -0
- package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +8 -0
- package/prompts/_fixtures/golden/objective-plan-guidance.txt +8 -0
- package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +20 -0
- package/prompts/_fixtures/golden/objective-plan-seed.txt +15 -0
- package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +1 -0
- package/prompts/_fixtures/golden/objective-read-linear.txt +1 -0
- package/prompts/_fixtures/golden/plan-read-github.txt +1 -0
- package/prompts/_fixtures/golden/plan-read-linear.txt +1 -0
- package/prompts/_fixtures/golden/plan-read-other.txt +1 -0
- package/prompts/_fixtures/golden/with_include.txt +4 -0
- package/prompts/_fixtures/templates/_greeting.md +1 -0
- package/prompts/_fixtures/templates/hello.md +1 -0
- package/prompts/_fixtures/templates/with_include.md +4 -0
- package/prompts/common/objective-read/linear.md +1 -0
- package/prompts/common/plan-read/github.md +1 -0
- package/prompts/common/plan-read/linear.md +1 -0
- package/prompts/common/plan-read/other.md +1 -0
- package/prompts/stages/address/action.md +10 -0
- package/prompts/stages/address/preview.md +6 -0
- package/prompts/stages/implement.md +8 -0
- package/prompts/stages/learn-docs.md +8 -0
- package/prompts/stages/learn.md +21 -0
- package/prompts/stages/objective-plan/guidance.md +12 -0
- package/prompts/stages/objective-plan/seed.md +20 -0
- package/shared/README.md +29 -0
- package/shared/bindings.yaml +64 -0
- package/shared/contracts-history.md +403 -0
- package/shared/contracts.md +4172 -0
- package/shared/providers.yaml +221 -0
- package/shared/registry.yaml +199 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# perk
|
|
2
|
+
|
|
3
|
+
A Pi-native, plan-oriented engineering workflow — a Python `perk` CLI (the session
|
|
4
|
+
*exterior*) plus a TypeScript Pi extension (the session *interior*).
|
|
5
|
+
|
|
6
|
+
> Start at [`docs/user-docs/`](docs/user-docs/index.md).
|
|
7
|
+
|
|
8
|
+
> Originally based on prior art `erk`, by the team at [dagster](https://github.com/dagster-io/dagster)
|
|
9
|
+
|
|
10
|
+
## What perk is
|
|
11
|
+
|
|
12
|
+
perk implements a plan-oriented engineering workflow (explore read-only → save a plan →
|
|
13
|
+
implement on a branch → submit → land → learn) to [Pi](https://github.com/earendil-works),
|
|
14
|
+
split across **two planes**:
|
|
15
|
+
|
|
16
|
+
- the **exterior** — a Python `perk` CLI that scaffolds repos, positions worktrees, mints
|
|
17
|
+
run ids, and launches primed `pi` sessions (everything that happens *outside* a session);
|
|
18
|
+
- the **interior** — a TypeScript Pi extension that drives stage transitions and state
|
|
19
|
+
*inside* a running session.
|
|
20
|
+
|
|
21
|
+
A language-neutral [`shared/`](shared/) contract (the stage registry + cross-plane specs)
|
|
22
|
+
is the single source both planes read, so the two stay in lockstep without a codegen step.
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
perk targets any git repo. From the repo you want to wire:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install perk # (or run from source — see Develop)
|
|
33
|
+
perk init # scaffold/converge Pi wiring (idempotent; safe to re-run)
|
|
34
|
+
perk doctor # report health; perk doctor --fix repairs drift
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`perk init` requires a git repo + `git`, `gh`, `node ≥ 22`, `ast-grep`, and `pi` on PATH. GitHub auth
|
|
38
|
+
is verified but never required (it is reported, never fatal).
|
|
39
|
+
|
|
40
|
+
For the guided first run, follow
|
|
41
|
+
[Get started with perk](docs/user-docs/tutorials/get-started.md).
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
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:
|
|
47
|
+
|
|
48
|
+
- **[Tutorials](docs/user-docs/tutorials/index.md)** — learning-oriented lessons; start with
|
|
49
|
+
[Get started with perk](docs/user-docs/tutorials/get-started.md).
|
|
50
|
+
- **[How-to guides](docs/user-docs/how-to/index.md)** — goal-oriented recipes (resume a plan,
|
|
51
|
+
address review feedback, switch to Linear, attach a skill, …).
|
|
52
|
+
- **[Reference](docs/user-docs/reference/index.md)** — the CLI surface, in-session commands &
|
|
53
|
+
tools, the objective roadmap model, configuration, and providers & backends.
|
|
54
|
+
- **[Explanation](docs/user-docs/explanation/index.md)** — how perk thinks; headless/remote
|
|
55
|
+
maturity.
|
|
56
|
+
|
|
57
|
+
perk's internal research and planning record lives under [`docs/`](docs/index.md) and is for
|
|
58
|
+
perk's own developers.
|
|
59
|
+
|
|
60
|
+
## Layout
|
|
61
|
+
|
|
62
|
+
- `perk/` — the Python CLI (the session exterior).
|
|
63
|
+
- `extension/` — the TypeScript Pi extension (the session interior).
|
|
64
|
+
- `shared/` — cross-plane contracts (the stage registry + specs), bundled into both build
|
|
65
|
+
artifacts.
|
|
66
|
+
- `docs/` — research inputs, design notes, and durable learnings.
|
|
67
|
+
|
|
68
|
+
## Develop
|
|
69
|
+
|
|
70
|
+
Two pinned toolchains:
|
|
71
|
+
|
|
72
|
+
- **Python** — [uv](https://docs.astral.sh/uv/) (3.13, pinned in `.python-version`),
|
|
73
|
+
[ruff](https://docs.astral.sh/ruff/) (lint/format), [ty](https://docs.astral.sh/ty/) (types).
|
|
74
|
+
- **TypeScript** — npm (Node ≥ 22, `.npmrc`), [Biome](https://biomejs.dev/) (lint/format),
|
|
75
|
+
`tsc` (types).
|
|
76
|
+
|
|
77
|
+
With [`just`](https://github.com/casey/just):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
just setup # uv sync + npm install + git hooks + install-cli (the `perk` CLI on PATH)
|
|
81
|
+
just install-cli # just the `perk` CLI on PATH (editable: tracks this clone)
|
|
82
|
+
just fmt # ruff format + biome format
|
|
83
|
+
just lint # ruff check + biome check
|
|
84
|
+
just typecheck # ty + tsc
|
|
85
|
+
just test # pytest + node:test (the regression gate)
|
|
86
|
+
just ci # setup + lint + typecheck + test
|
|
87
|
+
```
|
|
88
|
+
|
|
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
|
+
`just setup` also runs `just hooks` (`prek install`), wiring a [prek](https://prek.j178.dev)
|
|
99
|
+
pre-commit hook that runs `ruff check` on staged Python (config in `prek.toml`; the ruff
|
|
100
|
+
env is built by prek from the remote ruff-pre-commit repo, so it never depends on a
|
|
101
|
+
system/`.venv` ruff). Re-run `just hooks` after a fresh clone.
|
|
102
|
+
|
|
103
|
+
Without `just`: `uv run …` for Python (`uv run perk init`, `uv run pytest`,
|
|
104
|
+
`uv run ruff check perk tests`, `uv run ty check`) and `npm run …` for TypeScript
|
|
105
|
+
(`npm run lint`, `npm run typecheck`).
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// The SECOND 3rd-party plan adapter — and the first with the AUGMENT posture. A perk-owned shim
|
|
2
|
+
// that enables `@plannotator/pi-extension` as a REAL, selectable plan provider: unlike the tombell
|
|
3
|
+
// adapter (REPLACE posture — perk's plan surface fully vacates), plannotator AUGMENTS perk's plan
|
|
4
|
+
// flow. perk's `/plan` mode, authoring injection, and read-only gate STAY (planMode skips only the
|
|
5
|
+
// `--plan` flag + `Ctrl+Alt+P` shortcut — the two real registration collisions).
|
|
6
|
+
//
|
|
7
|
+
// INJECTION + BRIDGE ONLY: the `plan_review` TOOL lives in `extension/factories/planReview.ts`
|
|
8
|
+
// (perk's backend-neutral review door); this module is the injection-only adapter shape. It owns
|
|
9
|
+
// (1) the plannotator review-step authoring context (injected while the gate is active AND
|
|
10
|
+
// plannotator is selected — TWO content flavors, one customType: the plan bridge context, or the
|
|
11
|
+
// objective flavor when the stage is `objective-author`) and (2) the pure event-bus bridge
|
|
12
|
+
// (`createPlannotatorBridge`) that planReview.ts dispatches to when plannotator is the
|
|
13
|
+
// selected plan provider. The bridge speaks plannotator's published `plannotator:request` event
|
|
14
|
+
// API (in-process `pi.events` bus).
|
|
15
|
+
//
|
|
16
|
+
// INERT BY DEFAULT. The shim is ALWAYS registered in index.ts but the injection fires only when
|
|
17
|
+
// the resolved `[providers] plan` selection is `plannotator-plan` (read fresh per-event, same
|
|
18
|
+
// shape as planMode/planAdapterTombell). On any other selection the context handler only strips
|
|
19
|
+
// its own stale marker — zero behavior change on the default path.
|
|
20
|
+
//
|
|
21
|
+
// INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
|
|
22
|
+
// restamps `cache.plan-ref.provider` (stays `"github"`). The adapter is INJECTION-ONLY again
|
|
23
|
+
// (Invariant 1: composes, never owns) — the review tool, the `approvalSave` composition, and the
|
|
24
|
+
// gate exit all live behind planReview.ts's seams; the injection's gate-active check reads the
|
|
25
|
+
// persisted `perk:workflow-state.mode`, the gate's own state twin.
|
|
26
|
+
//
|
|
27
|
+
// EVENT ENVELOPE (pinned against `@plannotator/pi-extension@0.20.0`, `plannotator-events.ts`):
|
|
28
|
+
// request — pi.events.emit("plannotator:request", { requestId, action: "plan-review",
|
|
29
|
+
// payload: { planContent, origin? }, respond }) // respond = in-payload callback
|
|
30
|
+
// handshake — respond({ status: "handled", result: { status: "pending", reviewId } })
|
|
31
|
+
// | respond({ status: "unavailable", error? }) | respond({ status: "error", error })
|
|
32
|
+
// decision — pi.events.on("plannotator:review-result", { reviewId, approved, feedback?, ... })
|
|
33
|
+
|
|
34
|
+
import { randomUUID } from "node:crypto";
|
|
35
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
36
|
+
import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
|
|
37
|
+
import { resolvedPlanProviderId } from "../factories/planMode.ts";
|
|
38
|
+
// Type-only (erased at runtime — no cycle): the outcome vocabulary lives with the review door.
|
|
39
|
+
import type { ReviewOutcome } from "../factories/planReview.ts";
|
|
40
|
+
import { PLANNOTATOR_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
|
|
41
|
+
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
42
|
+
|
|
43
|
+
/** The plannotator plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
|
|
44
|
+
export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE = "perk:plan-adapter-plannotator";
|
|
45
|
+
const PLAN_ADAPTER_PLANNOTATOR_MARKER = "[PLAN ADAPTER: PLANNOTATOR]";
|
|
46
|
+
const OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER = "[OBJECTIVE ADAPTER: PLANNOTATOR]";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The handshake timeout for plannotator's immediate `respond` callback (mirrors plannotator's own
|
|
50
|
+
* `PLANNOTATOR_TIMEOUT_MS = 5_000`). Overridable for tests via PERK_PLANNOTATOR_HANDSHAKE_MS.
|
|
51
|
+
*/
|
|
52
|
+
export const PLANNOTATOR_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
53
|
+
|
|
54
|
+
function handshakeTimeoutMs(): number {
|
|
55
|
+
const raw = Number(process.env.PERK_PLANNOTATOR_HANDSHAKE_MS ?? "");
|
|
56
|
+
return Number.isFinite(raw) && raw > 0 ? raw : PLANNOTATOR_HANDSHAKE_TIMEOUT_MS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The augment-posture bridge prompt: perk's plan-authoring discipline plus the plannotator review
|
|
61
|
+
* step. Prompting, NOT enforcement (perk's own gate is the read-only authority). Durable anchors
|
|
62
|
+
* only — mirrors PLAN_AUTHORING_CONTEXT, which is also injected (perk's plan mode stays).
|
|
63
|
+
*/
|
|
64
|
+
export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT = `${PLAN_ADAPTER_PLANNOTATOR_MARKER}
|
|
65
|
+
A Plannotator browser review surface is configured for plan authoring in this repo. Author the plan
|
|
66
|
+
read-only exactly as the plan-authoring contract describes; then add one review step:
|
|
67
|
+
|
|
68
|
+
- Keep the working draft current with plan_draft — the validated plan-draft artifact is what gets
|
|
69
|
+
reviewed AND auto-saved; the plan param is only a fallback for sessions that never wrote a draft.
|
|
70
|
+
- When the plan is decision-complete, call the plan_review tool. The Plannotator browser UI opens
|
|
71
|
+
for the human reviewer.
|
|
72
|
+
- If the review is DENIED: revise per the returned annotations/feedback, rewrite the working draft
|
|
73
|
+
with plan_draft, then call plan_review again.
|
|
74
|
+
- If the review is APPROVED: the plan is auto-saved to GitHub and the session leaves read-only.
|
|
75
|
+
Do NOT re-dump the plan as a final message and do NOT tell the user to run /plan-save — relay
|
|
76
|
+
the save outcome (and any reviewer feedback) instead.
|
|
77
|
+
- If plan_review reports it was skipped or no review surface is available: fall back to presenting
|
|
78
|
+
the complete plan to the user; the human runs /plan-save (the manual failsafe).`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The objective flavor of the bridge prompt, injected in an
|
|
82
|
+
* `objective-author` session instead of the plan flavor. An APPROVED review auto-saves the
|
|
83
|
+
* objective via the `objectiveApprovalSave` seam; `/objective-save` is the manual failsafe on
|
|
84
|
+
* the skipped/unavailable arms.
|
|
85
|
+
*/
|
|
86
|
+
export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = `${OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER}
|
|
87
|
+
A Plannotator browser review surface is configured for objective authoring in this repo. Author
|
|
88
|
+
the objective read-only exactly as the objective-authoring contract describes; then add one
|
|
89
|
+
review step:
|
|
90
|
+
|
|
91
|
+
- Keep the working objective current with objective_draft — pass the FULL prose and the FULL
|
|
92
|
+
structured roadmap each call (it rewrites the whole draft); never hand-write roadmap YAML.
|
|
93
|
+
- When the objective + roadmap are decision-complete, call the plan_review tool. The Plannotator
|
|
94
|
+
browser UI shows the RENDERED objective (the prose + a roadmap table) derived from the draft
|
|
95
|
+
artifact — never raw JSON.
|
|
96
|
+
- If the review is DENIED: revise per the returned annotations/feedback, rewrite the working
|
|
97
|
+
draft with objective_draft, then call plan_review again.
|
|
98
|
+
- If the review is APPROVED: the objective is auto-saved to GitHub and the session leaves
|
|
99
|
+
read-only — do NOT re-dump the objective as a final message and do NOT tell the user to run
|
|
100
|
+
/objective-save; relay the save outcome (and any reviewer feedback) instead.
|
|
101
|
+
- If plan_review reports it was skipped or no review surface is available: present the complete
|
|
102
|
+
objective + structured roadmap to the user; the human runs /objective-save (the manual
|
|
103
|
+
failsafe).`;
|
|
104
|
+
|
|
105
|
+
/** Whether the foreign `plannotator-plan` provider is the selected plan provider for `cwd`. */
|
|
106
|
+
export function isPlannotatorPlanSelected(cwd: string): boolean {
|
|
107
|
+
return resolvedPlanProviderId(cwd) === PLANNOTATOR_PLAN_PROVIDER_ID;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ------------------------------------------------------------------ the event-bus bridge core
|
|
111
|
+
|
|
112
|
+
/** The minimal `pi.events` surface the bridge needs (mirrors pi's EventBus). */
|
|
113
|
+
export interface PlannotatorBus {
|
|
114
|
+
emit(channel: string, data: unknown): void;
|
|
115
|
+
on(channel: string, handler: (data: unknown) => void): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Plannotator's immediate `respond` handshake payload (pinned envelope, see header). */
|
|
119
|
+
interface HandshakeResponse {
|
|
120
|
+
status?: string;
|
|
121
|
+
error?: string;
|
|
122
|
+
result?: { status?: string; reviewId?: string };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The human decision arriving on `plannotator:review-result`. */
|
|
126
|
+
interface ReviewDecision {
|
|
127
|
+
approved: boolean;
|
|
128
|
+
feedback?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create the plannotator bridge over an event bus: ONE persistent `plannotator:review-result`
|
|
133
|
+
* listener registered up front, resolving pending reviews from a Map keyed by reviewId (no
|
|
134
|
+
* dependence on an undocumented `pi.events.off`). Pure over the bus → unit-testable offline with a
|
|
135
|
+
* fake plannotator listener.
|
|
136
|
+
*/
|
|
137
|
+
export function createPlannotatorBridge(bus: PlannotatorBus): {
|
|
138
|
+
review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome>;
|
|
139
|
+
} {
|
|
140
|
+
const pending = new Map<string, (decision: ReviewDecision) => void>();
|
|
141
|
+
|
|
142
|
+
bus.on("plannotator:review-result", (data) => {
|
|
143
|
+
const d = data as { reviewId?: unknown; approved?: unknown; feedback?: unknown };
|
|
144
|
+
if (typeof d?.reviewId !== "string") return;
|
|
145
|
+
const resolve = pending.get(d.reviewId);
|
|
146
|
+
if (resolve === undefined) return;
|
|
147
|
+
pending.delete(d.reviewId);
|
|
148
|
+
resolve({
|
|
149
|
+
approved: d.approved === true,
|
|
150
|
+
feedback: typeof d.feedback === "string" && d.feedback.trim() ? d.feedback : undefined,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
async function review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> {
|
|
155
|
+
if (signal?.aborted) return { status: "aborted" };
|
|
156
|
+
|
|
157
|
+
// 1. Emit the request and await the immediate `respond` handshake (bounded — fail-open).
|
|
158
|
+
const requestId = randomUUID();
|
|
159
|
+
let respondResolve: (response: HandshakeResponse) => void = () => {};
|
|
160
|
+
const handshake = new Promise<HandshakeResponse | "timeout">((resolve) => {
|
|
161
|
+
respondResolve = resolve;
|
|
162
|
+
});
|
|
163
|
+
const timer = setTimeout(() => respondResolve("timeout" as never), handshakeTimeoutMs());
|
|
164
|
+
bus.emit("plannotator:request", {
|
|
165
|
+
requestId,
|
|
166
|
+
action: "plan-review",
|
|
167
|
+
payload: { planContent: plan, origin: "perk" },
|
|
168
|
+
respond: (response: unknown) => respondResolve(response as HandshakeResponse),
|
|
169
|
+
});
|
|
170
|
+
const response = await handshake;
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
|
|
173
|
+
if (response === "timeout") {
|
|
174
|
+
return {
|
|
175
|
+
status: "unavailable",
|
|
176
|
+
warning: "plannotator did not respond to the review request (handshake timeout)",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (response?.status !== "handled") {
|
|
180
|
+
const detail = response?.error ? `: ${response.error}` : "";
|
|
181
|
+
return {
|
|
182
|
+
status: "unavailable",
|
|
183
|
+
warning: `plannotator reported ${response?.status ?? "an invalid response"}${detail}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const reviewId = response.result?.reviewId;
|
|
187
|
+
if (response.result?.status !== "pending" || typeof reviewId !== "string") {
|
|
188
|
+
return {
|
|
189
|
+
status: "unavailable",
|
|
190
|
+
warning: "plannotator handshake returned no pending reviewId",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. Await the human decision (no timeout — the reviewer takes as long as they take), but
|
|
195
|
+
// honor a turn abort so an interrupted session never leaks a wedged promise.
|
|
196
|
+
return await new Promise<ReviewOutcome>((resolve) => {
|
|
197
|
+
const onAbort = (): void => {
|
|
198
|
+
pending.delete(reviewId);
|
|
199
|
+
resolve({ status: "aborted" });
|
|
200
|
+
};
|
|
201
|
+
pending.set(reviewId, (decision) => {
|
|
202
|
+
signal?.removeEventListener("abort", onAbort);
|
|
203
|
+
resolve({ status: "completed", reviewId, ...decision });
|
|
204
|
+
});
|
|
205
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { review };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ----------------------------------------------------------------------------- registration
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Register the plannotator plan adapter: the augment-posture authoring-context injection, inert
|
|
216
|
+
* unless `[providers] plan = "plannotator-plan"`. INJECTION-ONLY (Invariant 1: composes, never
|
|
217
|
+
* owns) — the `plan_review` tool lives in planReview.ts (the backend-neutral review door), which
|
|
218
|
+
* dispatches to this module's bridge when plannotator is selected; the adapter itself never
|
|
219
|
+
* arbitrates tools and needs no gating.
|
|
220
|
+
*/
|
|
221
|
+
export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
|
|
222
|
+
// Inject the bridge context while the read-only gate is active AND plannotator is selected.
|
|
223
|
+
// Two content flavors, one customType: an objective-author session (also read-only) gets the
|
|
224
|
+
// objective flavor (the review surface renders the objective draft); any other
|
|
225
|
+
// gated stage gets the plan flavor. The gate-active check reads the persisted
|
|
226
|
+
// `perk:workflow-state.mode` (the gate's state twin) — never the gate itself.
|
|
227
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
228
|
+
if (!isPlannotatorPlanSelected(ctx.cwd)) return;
|
|
229
|
+
const state = rebuildWorkflowState(branchOf(ctx));
|
|
230
|
+
if (state.mode !== "read-only") return;
|
|
231
|
+
const content =
|
|
232
|
+
state.stage === OBJECTIVE_AUTHOR_STAGE
|
|
233
|
+
? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
|
|
234
|
+
: PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
|
|
235
|
+
return {
|
|
236
|
+
message: {
|
|
237
|
+
customType: PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE,
|
|
238
|
+
content,
|
|
239
|
+
display: false,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Strip the stale bridge markers (BOTH flavors) from context when plannotator-plan is no
|
|
245
|
+
// longer selected (same hygiene as the tombell shim), so they never linger across a deselect.
|
|
246
|
+
const hasMarker = (text: string): boolean =>
|
|
247
|
+
text.includes(PLAN_ADAPTER_PLANNOTATOR_MARKER) ||
|
|
248
|
+
text.includes(OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER);
|
|
249
|
+
pi.on("context", async (event, ctx) => {
|
|
250
|
+
if (isPlannotatorPlanSelected(ctx.cwd)) return;
|
|
251
|
+
return {
|
|
252
|
+
messages: event.messages.filter((m) => {
|
|
253
|
+
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
254
|
+
if (msg.customType === PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE) return false;
|
|
255
|
+
if (msg.role !== "user") return true;
|
|
256
|
+
const content = msg.content;
|
|
257
|
+
if (typeof content === "string") return !hasMarker(content);
|
|
258
|
+
if (Array.isArray(content)) {
|
|
259
|
+
return !content.some(
|
|
260
|
+
(c) =>
|
|
261
|
+
(c as { type?: string; text?: string }).type === "text" &&
|
|
262
|
+
hasMarker((c as { text?: string }).text ?? ""),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The FIRST 3rd-party plan adapter. A perk-owned, injection-only bridge that re-enables
|
|
2
|
+
// `@tombell/pi-plan` as a REAL, selectable plan provider: it bridges that package's free-form prose
|
|
3
|
+
// `/plan` surface to perk's canonical produced contract (`plan_save` → `cache.plan-ref`).
|
|
4
|
+
//
|
|
5
|
+
// INERT BY DEFAULT. This shim is ALWAYS registered in index.ts but does nothing unless the resolved
|
|
6
|
+
// `[providers] plan` selection is `tombell-plan` (read fresh per-event, same shape as planMode). On
|
|
7
|
+
// any non-tombell selection it injects nothing and only strips its own stale marker — zero behavior
|
|
8
|
+
// change on the default path.
|
|
9
|
+
//
|
|
10
|
+
// WHAT IT DOES (and does NOT do):
|
|
11
|
+
// - It injects a hidden (`display:false`) `perk:plan-adapter-tombell` context that tells the model
|
|
12
|
+
// the foreign `/plan` surface authors a FREE-FORM PROSE plan, and directs it through perk's
|
|
13
|
+
// review-first discipline: keep the draft current with `plan_draft`, then call
|
|
14
|
+
// `plan_review` — which (for any non-plannotator selection, tombell included) runs the
|
|
15
|
+
// first-party in-TUI editor review, and whose APPROVED outcome auto-saves via the
|
|
16
|
+
// `approvalSave` seam. The injection is CONDITIONED: it fires only when perk's read-only gate
|
|
17
|
+
// is active (per the persisted `perk:workflow-state.mode`) OR tombell's own persisted
|
|
18
|
+
// `plan-mode-state` entry says plan mode is enabled — never in an objective-author session
|
|
19
|
+
// (objectiveAuthor.ts owns that authoring context).
|
|
20
|
+
// - The present + `/plan-save` flow is the explicit FAIL-OPEN fallback, not the primary path:
|
|
21
|
+
// it applies when the review reports skipped/unavailable, or when `@tombell/pi-plan`'s own
|
|
22
|
+
// interactive `/plan` `setActiveTools` restriction hides `plan_draft`/`plan_review` from the
|
|
23
|
+
// tool set. `/plan-save` prefers the validated draft artifact and falls back to the
|
|
24
|
+
// `extractPlanMarkdown` transcript scrape (planSave.ts) — no new save machinery; the shim
|
|
25
|
+
// only directs flow.
|
|
26
|
+
// - It does NOT own, replace, or duplicate the read-only gate (Invariant 1) and NEVER calls
|
|
27
|
+
// `setActiveTools` / registers a `tool_call` handler. The read-only tier during foreign planning
|
|
28
|
+
// comes from (a) perk's gate, already engaged by the cold-door launch (session_start →
|
|
29
|
+
// syncFromState(handoff.mode=read-only)), and (b) the foreign package's own self-enforcement for
|
|
30
|
+
// ad-hoc interactive `pi --plan`. The shim is purely a prompting bridge.
|
|
31
|
+
// - It does NOT restamp `cache.plan-ref.provider` — a tombell-authored prose plan lands with
|
|
32
|
+
// `provider="github"` exactly like a perk-authored plan (the authoring-provider id lives only in
|
|
33
|
+
// the `[providers] plan` selection; `provider` is the issue storage backend). All downstream
|
|
34
|
+
// stages bind only to the provider-agnostic plan-ref and are unchanged.
|
|
35
|
+
|
|
36
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
37
|
+
import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
|
|
38
|
+
import { resolvedPlanProviderId } from "../factories/planMode.ts";
|
|
39
|
+
import { TOMBELL_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
|
|
40
|
+
import { type BranchEntry, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
41
|
+
|
|
42
|
+
/** The tombell plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
|
|
43
|
+
export const PLAN_ADAPTER_TOMBELL_CONTEXT_TYPE = "perk:plan-adapter-tombell";
|
|
44
|
+
const PLAN_ADAPTER_TOMBELL_MARKER = "[PLAN ADAPTER: TOMBELL]";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The bridge prompt: directs the foreign free-form prose `/plan` surface into perk's review-first
|
|
48
|
+
* discipline (mirrors PLAN_AUTHORING_CONTEXT's review-first ending), with the present +
|
|
49
|
+
* `/plan-save` flow as the explicit fail-open fallback. Prompting, NOT enforcement (perk's gate,
|
|
50
|
+
* engaged by the cold-door launch, is the read-only authority). Durable anchors only, no line
|
|
51
|
+
* numbers.
|
|
52
|
+
*/
|
|
53
|
+
export const PLAN_ADAPTER_TOMBELL_CONTEXT = `${PLAN_ADAPTER_TOMBELL_MARKER}
|
|
54
|
+
You are authoring a plan through the @tombell/pi-plan \`/plan\` surface — a read-only exploration mode
|
|
55
|
+
that produces a FREE-FORM PROSE plan (it emits no structured plan and no save tool of its own).
|
|
56
|
+
|
|
57
|
+
Gather before you plan, then write the plan so an executor with zero prior context can implement it
|
|
58
|
+
without guessing: anchor every change durably — function/class names, behavioral descriptions,
|
|
59
|
+
structural locations — never line numbers, and resolve every open choice before you save.
|
|
60
|
+
|
|
61
|
+
perk persists the plan to the provider-agnostic plan reference (cache.plan-ref); the objective/node
|
|
62
|
+
linkage and any consumed-learn numbers are recovered automatically from the launch handoff — never
|
|
63
|
+
try to write the plan reference yourself.
|
|
64
|
+
|
|
65
|
+
- Keep the working draft current with the plan_draft tool — the validated plan-draft artifact is
|
|
66
|
+
what gets reviewed AND auto-saved.
|
|
67
|
+
- When the plan is decision-complete, call the plan_review tool — the human reviews the draft in
|
|
68
|
+
perk's in-TUI editor review.
|
|
69
|
+
- If the review is DENIED: revise per the feedback, rewrite the draft with plan_draft, then call
|
|
70
|
+
plan_review again.
|
|
71
|
+
- If the review is APPROVED: the plan is auto-saved and the session leaves read-only. Relay the
|
|
72
|
+
save outcome — do NOT re-dump the plan as a final message and do NOT tell the user to run
|
|
73
|
+
/plan-save.
|
|
74
|
+
- If plan_review reports it was skipped or unavailable, OR the plan_draft/plan_review tools are not
|
|
75
|
+
in your tool set (this plan surface restricts tools): write the COMPLETE final plan as your last
|
|
76
|
+
message and present it to the user — the human runs the /plan-save command when satisfied (it
|
|
77
|
+
prefers the validated draft artifact and falls back to scraping your latest message, so that
|
|
78
|
+
final message must be the clean, complete plan and nothing else).`;
|
|
79
|
+
|
|
80
|
+
/** Whether the foreign `tombell-plan` provider is the selected plan provider for `cwd`. */
|
|
81
|
+
export function isTombellPlanSelected(cwd: string): boolean {
|
|
82
|
+
return resolvedPlanProviderId(cwd) === TOMBELL_PLAN_PROVIDER_ID;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether @tombell/pi-plan's own plan mode is enabled, per the latest `plan-mode-state` custom
|
|
87
|
+
* entry on the branch (the package's persisted state twin — it writes one per toggle via
|
|
88
|
+
* `pi.appendEntry`, latest wins, mirroring its own `session_start` rebuild). Defensive: any
|
|
89
|
+
* missing / malformed entry ⇒ false.
|
|
90
|
+
*/
|
|
91
|
+
export function isTombellPlanModeEnabled(branch: readonly BranchEntry[]): boolean {
|
|
92
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
93
|
+
const entry = branch[i];
|
|
94
|
+
if (entry?.type !== "custom" || entry.customType !== "plan-mode-state") continue;
|
|
95
|
+
return entry.data?.enabled === true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Register the tombell plan adapter: an injection-only bridge, inert unless `[providers] plan =
|
|
102
|
+
* "tombell-plan"`. It NEVER touches tool gating / setActiveTools (Invariant 1) and never throws.
|
|
103
|
+
*/
|
|
104
|
+
export function registerPlanAdapterTombell(pi: ExtensionAPI): void {
|
|
105
|
+
// Inject the bridge context while the foreign tombell-plan provider is selected AND a plan
|
|
106
|
+
// authoring mode is on — perk's read-only gate (per the persisted `perk:workflow-state.mode`,
|
|
107
|
+
// the gate's state twin — never the gate object) OR tombell's own persisted `plan-mode-state`
|
|
108
|
+
// entry (the ad-hoc interactive `/plan` arm). Objective-author sessions are excepted
|
|
109
|
+
// (objectiveAuthor owns that session; mirrors the plannotator adapter's recipe).
|
|
110
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
111
|
+
if (!isTombellPlanSelected(ctx.cwd)) return;
|
|
112
|
+
const branch = branchOf(ctx);
|
|
113
|
+
const state = rebuildWorkflowState(branch);
|
|
114
|
+
if (state.stage === OBJECTIVE_AUTHOR_STAGE) return;
|
|
115
|
+
if (state.mode !== "read-only" && !isTombellPlanModeEnabled(branch)) return;
|
|
116
|
+
return {
|
|
117
|
+
message: {
|
|
118
|
+
customType: PLAN_ADAPTER_TOMBELL_CONTEXT_TYPE,
|
|
119
|
+
content: PLAN_ADAPTER_TOMBELL_CONTEXT,
|
|
120
|
+
display: false,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Strip the stale bridge marker from context when tombell-plan is no longer selected (same
|
|
126
|
+
// hygiene planMode/objectiveAuthor/toolGating apply), so it never lingers across a deselect.
|
|
127
|
+
pi.on("context", async (event, ctx) => {
|
|
128
|
+
if (isTombellPlanSelected(ctx.cwd)) return;
|
|
129
|
+
return {
|
|
130
|
+
messages: event.messages.filter((m) => {
|
|
131
|
+
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
132
|
+
if (msg.customType === PLAN_ADAPTER_TOMBELL_CONTEXT_TYPE) return false;
|
|
133
|
+
if (msg.role !== "user") return true;
|
|
134
|
+
const content = msg.content;
|
|
135
|
+
if (typeof content === "string") return !content.includes(PLAN_ADAPTER_TOMBELL_MARKER);
|
|
136
|
+
if (Array.isArray(content)) {
|
|
137
|
+
return !content.some(
|
|
138
|
+
(c) =>
|
|
139
|
+
(c as { type?: string; text?: string }).type === "text" &&
|
|
140
|
+
((c as { text?: string }).text ?? "").includes(PLAN_ADAPTER_TOMBELL_MARKER),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The FIRST 3rd-party todo adapter. A perk-owned, injection-only bridge that re-enables
|
|
2
|
+
// `@juicesharp/rpiv-todo` as a REAL, selectable todo provider: it carries perk's implement-progress
|
|
3
|
+
// discipline onto that package's checklist-overlay surface (the todo-seam mirror of
|
|
4
|
+
// planAdapterTombell, which bridges the plan seam).
|
|
5
|
+
//
|
|
6
|
+
// INERT BY DEFAULT. This shim is ALWAYS registered in index.ts but does nothing unless the resolved
|
|
7
|
+
// `[providers] todo` selection is `juicesharp-todo` (read fresh per-event via the same
|
|
8
|
+
// `resolvedTodoProviderId` the reference checkpoints provider uses) AND the session is an active
|
|
9
|
+
// workflow (`active_plan_ref != null`, the same gate the reference checkpoints provider seeds on).
|
|
10
|
+
// On any non-juicesharp selection it injects nothing and only strips its own stale marker — zero
|
|
11
|
+
// behavior change on the default path.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IT DOES (and does NOT do):
|
|
14
|
+
// - It injects a hidden (`display:false`) `perk:todo-adapter-juicesharp` context that tells the
|
|
15
|
+
// model the foreign `@juicesharp/rpiv-todo` checklist overlay is the sole progress surface here
|
|
16
|
+
// (perk's own checkpoints stepped aside) and directs it to carry perk's
|
|
17
|
+
// implement-progress DISCIPLINE onto that overlay: seed it from the plan body's `## Steps` and
|
|
18
|
+
// mark each item complete in order — the same gather-then-advance flow perk's checkpoints embody.
|
|
19
|
+
// - It does NOT own, replace, or duplicate the read-only gate (Invariant 1) and NEVER calls
|
|
20
|
+
// `setActiveTools` / registers a `tool_call` handler (the todo seam composes no shared primitive).
|
|
21
|
+
// - It does NOT write perk's `perk:checkpoint` entry and does NOT revive perk's deferred marker
|
|
22
|
+
// scanner (Correction 2): that entry is a transient TS-only progress overlay nothing downstream
|
|
23
|
+
// consumes, and perk's render + scanner are already deferred. Re-populating it would
|
|
24
|
+
// be dead duplication — the foreign overlay is the sole, uncontested progress surface.
|
|
25
|
+
// - It adds NO registration-time vacating (Correction 1): unlike the plan seam (where perk and the
|
|
26
|
+
// foreign package both register `/plan`, forcing Pi to suffix the duplicate), the todo seam has
|
|
27
|
+
// no command-name collision — perk registers `/checkpoints`; the foreign overlay registers its
|
|
28
|
+
// own differently-named command(s) — so the runtime deferral is already sufficient.
|
|
29
|
+
|
|
30
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
31
|
+
import { resolvedTodoProviderId } from "../checkpoints/checkpoints.ts";
|
|
32
|
+
import { JUICESHARP_TODO_PROVIDER_ID } from "../substrate/providers.ts";
|
|
33
|
+
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
34
|
+
|
|
35
|
+
/** The juicesharp todo-adapter bridge customType (distinct from checkpoints' `perk:checkpoint`). */
|
|
36
|
+
export const TODO_ADAPTER_JUICESHARP_CONTEXT_TYPE = "perk:todo-adapter-juicesharp";
|
|
37
|
+
const TODO_ADAPTER_JUICESHARP_MARKER = "[TODO ADAPTER: JUICESHARP]";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The bridge prompt: carries perk's implement-progress discipline onto the foreign checklist
|
|
41
|
+
* overlay. Prompting, NOT enforcement (perk's checkpoint scanner is deferred under this selection).
|
|
42
|
+
* Durable anchors only, no line numbers.
|
|
43
|
+
*/
|
|
44
|
+
export const TODO_ADAPTER_JUICESHARP_CONTEXT = `${TODO_ADAPTER_JUICESHARP_MARKER}
|
|
45
|
+
This implement session tracks progress through the \`@juicesharp/rpiv-todo\` checklist overlay — the
|
|
46
|
+
selected todo provider (\`[providers] todo = "juicesharp-todo"\`). perk's own checkpoint surface has
|
|
47
|
+
stepped aside (Node 3.1), so the foreign overlay is the sole progress surface here.
|
|
48
|
+
|
|
49
|
+
Carry perk's implement-progress discipline onto that overlay: seed it from the plan body's
|
|
50
|
+
\`## Steps\` numbered list — one checklist item per step, in order — then mark each item complete as
|
|
51
|
+
you finish the corresponding step, the same gather-then-advance flow perk's checkpoints embody. Use
|
|
52
|
+
the overlay's own controls to add and complete items; you do not need perk's \`[WIP:n]\`/\`[DONE:n]\`
|
|
53
|
+
markers here (perk's checkpoint scanner is deferred under this selection). If the plan has no
|
|
54
|
+
\`## Steps\` list, there is nothing to seed — let the overlay behave as its defaults suggest.`;
|
|
55
|
+
|
|
56
|
+
/** Whether the foreign `juicesharp-todo` provider is the selected todo provider for `cwd`. */
|
|
57
|
+
export function isJuicesharpTodoSelected(cwd: string): boolean {
|
|
58
|
+
return resolvedTodoProviderId(cwd) === JUICESHARP_TODO_PROVIDER_ID;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register the juicesharp todo adapter: an injection-only bridge, inert unless `[providers] todo =
|
|
63
|
+
* "juicesharp-todo"` AND the session is an active workflow. It NEVER touches tool gating /
|
|
64
|
+
* setActiveTools (Invariant 1) and never throws.
|
|
65
|
+
*/
|
|
66
|
+
export function registerTodoAdapterJuicesharp(pi: ExtensionAPI): void {
|
|
67
|
+
// Inject the bridge context while the foreign juicesharp-todo provider is selected AND a workflow
|
|
68
|
+
// is active (active_plan_ref != null) — the same active-workflow scope the reference checkpoints
|
|
69
|
+
// provider seeds on, so the bridge never reaches planning/objective sessions (display:false).
|
|
70
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
71
|
+
if (!isJuicesharpTodoSelected(ctx.cwd)) return;
|
|
72
|
+
const branch = branchOf(ctx);
|
|
73
|
+
if (rebuildWorkflowState(branch).active_plan_ref == null) return;
|
|
74
|
+
return {
|
|
75
|
+
message: {
|
|
76
|
+
customType: TODO_ADAPTER_JUICESHARP_CONTEXT_TYPE,
|
|
77
|
+
content: TODO_ADAPTER_JUICESHARP_CONTEXT,
|
|
78
|
+
display: false,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Strip the stale bridge marker from context when juicesharp-todo is no longer selected (same
|
|
84
|
+
// hygiene the sibling adapters apply), so it never lingers across a deselect.
|
|
85
|
+
pi.on("context", async (event, ctx) => {
|
|
86
|
+
if (isJuicesharpTodoSelected(ctx.cwd)) return;
|
|
87
|
+
return {
|
|
88
|
+
messages: event.messages.filter((m) => {
|
|
89
|
+
const msg = m as { customType?: string; role?: string; content?: unknown };
|
|
90
|
+
if (msg.customType === TODO_ADAPTER_JUICESHARP_CONTEXT_TYPE) return false;
|
|
91
|
+
if (msg.role !== "user") return true;
|
|
92
|
+
const content = msg.content;
|
|
93
|
+
if (typeof content === "string") return !content.includes(TODO_ADAPTER_JUICESHARP_MARKER);
|
|
94
|
+
if (Array.isArray(content)) {
|
|
95
|
+
return !content.some(
|
|
96
|
+
(c) =>
|
|
97
|
+
(c as { type?: string; text?: string }).type === "text" &&
|
|
98
|
+
((c as { text?: string }).text ?? "").includes(TODO_ADAPTER_JUICESHARP_MARKER),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|