@mgiles/perk 2.1.0 → 2.3.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.
Files changed (50) hide show
  1. package/extension/adapters/planAdapterPlannotator.ts +64 -1
  2. package/extension/doors/address.ts +3 -3
  3. package/extension/doors/commitCompact.ts +163 -0
  4. package/extension/doors/learn.ts +219 -23
  5. package/extension/doors/prReview.ts +189 -18
  6. package/extension/doors/prReviewDynamic.ts +249 -0
  7. package/extension/doors/submit.ts +4 -3
  8. package/extension/factories/gistAuthor.ts +94 -0
  9. package/extension/factories/gistDraft.ts +265 -0
  10. package/extension/factories/gistSave.ts +251 -0
  11. package/extension/factories/objectivePlan.ts +3 -2
  12. package/extension/factories/planMode.ts +8 -5
  13. package/extension/factories/planReview.ts +233 -12
  14. package/extension/index.ts +26 -0
  15. package/extension/substrate/config.ts +8 -4
  16. package/extension/substrate/git.ts +38 -0
  17. package/extension/substrate/terminalLaunch.ts +1 -1
  18. package/extension/substrate/toolGating.ts +44 -3
  19. package/extension/substrate/unifiedDiff.ts +224 -0
  20. package/extension/waves/learnWave.ts +155 -0
  21. package/extension/waves/memoryAdapter.ts +126 -0
  22. package/extension/waves/prReviewDynamicWave.ts +466 -0
  23. package/extension/waves/prReviewWave.ts +229 -0
  24. package/extension/waves/reportWave.ts +449 -0
  25. package/extension/waves/rpcAdapter.ts +201 -0
  26. package/package.json +7 -1
  27. package/prompts/_fixtures/live.yaml +22 -11
  28. package/prompts/commit-and-compact.md +7 -0
  29. package/prompts/common/output-schemas/objective-explorer.md +36 -0
  30. package/prompts/common/output-schemas/review-classifier.md +47 -0
  31. package/prompts/contexts/adapters/plannotator-objective.md +8 -1
  32. package/prompts/contexts/adapters/plannotator-plan.md +6 -1
  33. package/prompts/contexts/gist-authoring.md +22 -0
  34. package/prompts/stages/address/action.md +15 -4
  35. package/prompts/stages/address/preview.md +14 -3
  36. package/prompts/stages/conflict-resolution.md +1 -1
  37. package/prompts/stages/gist-author/seed.md +10 -0
  38. package/prompts/stages/gist-save.md +9 -0
  39. package/prompts/stages/learn-orchestrate.md +7 -5
  40. package/prompts/stages/objective-plan/guidance.md +12 -1
  41. package/prompts/stages/objective-plan/seed.md +12 -1
  42. package/prompts/stages/pr-review-browser/active.md +11 -3
  43. package/prompts/stages/pr-review-browser/foreign.md +11 -3
  44. package/prompts/stages/pr-review-dynamic.md +7 -0
  45. package/prompts/stages/pr-review-terminal/active.md +11 -3
  46. package/prompts/stages/pr-review-terminal/foreign.md +11 -3
  47. package/prompts/stages/pr-review.md +7 -6
  48. package/shared/bindings.yaml +6 -0
  49. package/shared/contracts.md +221 -45
  50. package/shared/registry.yaml +31 -1
@@ -1,7 +1,7 @@
1
1
  # perk cross-plane contracts
2
2
 
3
3
  The language-neutral contracts both planes obey, authored once here and bundled into each
4
- build artifact. This document holds the numbered **prose contract sections** (`§8.1`–`§8.40`,
4
+ build artifact. This document holds the numbered **prose contract sections** (`§8.1`–`§8.41`,
5
5
  non-contiguous: `§8.8` is skipped and `§8.6a` exists; no parser): the Python CLI (`perk`)
6
6
  and the TS extension (`@mgiles/perk`) each implement one side, against the exact names/paths/
7
7
  fields pinned in each section. `perk doctor` verifies conformance. The numbering convention:
@@ -336,7 +336,7 @@ end of the section).
336
336
  | `active_plan_ref` | object \| null | the provider-agnostic plan ref (§8.4); null during early `plan` |
337
337
  | `active_objective` | string \| null | the active objective id (`/objective <id>` sets it, `/objective clear` nulls it) |
338
338
  | `last_review_batch` | object \| null | the last processed review batch: `{ pr, counts:{actionable,informational,praise,question}, resolved_thread_ids:[…], at:ISO }` |
339
- | `last_pr_review` | object \| null | the last `/pr-review` outcome posted via the warm `post_pr_review` tool: `{ pr, verdict, angles, comment_count, mode, at:ISO }`; best-effort tier (the PR review is the canonical record) |
339
+ | `last_pr_review` | object \| null | the last `/pr-review` (or the experimental `/pr-review-dynamic`) outcome posted via the shared warm `post_pr_review` tool: `{ pr, verdict, angles, comment_count, mode, at:ISO }`; best-effort tier (the PR review is the canonical record) |
340
340
  | `last_review` | object \| null | the last review-door outcome posted via the warm `submit_pr_review` tool: `{ pr, event, comment_count, mode, at:ISO }`; best-effort tier (the submitted PR review is the canonical record) |
341
341
  | `session_artifacts` | object \| null | per-name session-artifact provenance pointers `{run_id, name, path, digest, at}` (§8.1); appends carry the **whole merged map** (per-field LWW); strict-append tier |
342
342
  | `objective_node_claim` | object \| null | the objective node this session has claimed `planning` (`{ objective, node }`); written by the warm `objective_node` tool on a successful `planning` transition, cleared on a successful non-planning transition for the same node and after a successful node-linked plan save; best-effort tier (cheaply reconstructable; loud-but-non-fatal) |
@@ -386,9 +386,12 @@ swallowed**: a failed advance shows a visible `⚠ … NOT advanced — re-run /
386
386
  **Tool gating.** The `mode` field **structurally gates tools** — enforcement, not prompting. When
387
387
  `mode == "read-only"` the interior (`extension/substrate/toolGating.ts`): (1) restricts the
388
388
  active tool set to `READ_ONLY_TOOLS` (`read`/`grep`/`find`/`ls`/`bash` + `ask_user_question` +
389
- `plan_review` + the `plan_draft`/`objective_draft` session-data carve-outs + `objective_node`
389
+ `plan_review` + the `plan_draft`/`objective_draft`/`gist_draft` session-data carve-outs + `objective_node`
390
390
  (delegates a bounded node transition to the canonical Python plane) + the **`web` seam**
391
- providers' research tools, the read-only Linear tools, and the pi-subagents delegation family
391
+ providers' research tools, the read-only Linear tools, the pi-fff search family (both mode
392
+ name-sets — `fffind`/`ffgrep`/`fff-multi-grep` + override's `multi_grep`; the override names
393
+ `find`/`grep` are already present — local search belongs in read-only exploration, and FFF's
394
+ frecency state lives under `~/.pi/agent/fff/`, outside the worktree), and the pi-subagents delegation family
392
395
  (`subagent`/`wait` + the parent supervisor pair — the gated objective-plan explorer spawn must be
393
396
  reachable; **accepted no-backstop posture**: spawned children are unscoped by design (§8.40
394
397
  adopt-never-impersonates) — the explorer's agent def is write-blocked by its `tools` frontmatter,
@@ -470,10 +473,10 @@ session-lifecycle gates + the warm `/implement` handoff (`extension/doors/lifecy
470
473
  adapter (`extension/factories/planMode.ts`, `extension/adapters/todoAdapterJuicesharp.ts`; §8.10
471
474
  owns the provider seams); in-process read-only child sessions
472
475
  (`extension/worker/readOnlySession.ts`); the read-only CI executor
473
- (`extension/doors/ciExecutor.ts`); the spawned delegation seam + `/address` + `/pr-review` + `/pr-review-terminal` + `/pr-review-browser`
474
- (`extension/doors/address.ts` / `prReview.ts` / `prReviewTerminal.ts` /
476
+ (`extension/doors/ciExecutor.ts`); the spawned delegation seam + `/address` + `/pr-review` + `/pr-review-dynamic` + `/pr-review-terminal` + `/pr-review-browser`
477
+ (`extension/doors/address.ts` / `prReview.ts` / `prReviewDynamic.ts` / `prReviewTerminal.ts` /
475
478
  `prReviewBrowser.ts` / `submitPrReview.ts` / `hunkHandoff.ts` / `plannotatorHandoff.ts`, `agents/*.md`, `skills/perk-address/` /
476
- `perk-pr-review/` / `perk-pr-review-terminal/` / `perk-pr-review-browser/`; the gateway op shapes stay in §8.4); the conflict-resolution drive
479
+ `perk-pr-review/` / `perk-pr-review-dynamic/` / `perk-pr-review-terminal/` / `perk-pr-review-browser/`; the gateway op shapes stay in §8.4); the conflict-resolution drive
477
480
  (`extension/doors/submit.ts`; the probe contract stays in §8.4).
478
481
 
479
482
 
@@ -687,6 +690,11 @@ add_pr_reaction{ pr_number } -> void
687
690
  # failure (mutations raise; nothing review-shaped is lost).
688
691
  ```
689
692
 
693
+ The experimental `/pr-review-dynamic` door shares `post_pr_review`/`review-post` and the clean
694
+ guard unchanged — angle selection is delegated to a fresh `perk.review-angle-selector` lane and
695
+ normalized in module-rendered code (`extension/waves/prReviewDynamicWave.ts`); the baseline
696
+ `/pr-review` stays canonical.
697
+
690
698
  ### PR-review toolbox ops (checkout / cleanup / review-submit)
691
699
 
692
700
  The two human-in-the-loop review doors (`/pr-review-terminal`, `/pr-review-browser`) review a
@@ -797,7 +805,7 @@ waves to `POST <url>/api/external-annotations`
797
805
  side: LEFT→"old" / RIGHT-or-omitted→"new", text: "[severity/confidence] …"}]}`; batches are
798
806
  atomic; 201 returns `{ids}` — captured for cleanup). The wave cadence: on
799
807
  `/pr-review-browser` a wave is pushed per ARRIVING fenced-JSON batch inside the streaming
800
- `wait({timeoutMs})` loop (a `path`+`line` ledger dedupes — a pushed anchor is never re-pushed —
808
+ `subagent_wait({timeoutMs})` loop (a `path`+`line` ledger dedupes — a pushed anchor is never re-pushed —
801
809
  and the discipline is hold-and-accumulate: a refused POST before any door failure notice means
802
810
  "not up yet", retried on the next wait-loop return, never a degrade).
803
811
  `line: null` findings ARE pushed on this surface (path → `scope: "file"`, none →
@@ -882,19 +890,28 @@ pieces; a neutral re-home is a deferred residual).
882
890
  `prompts/stages/pr-review-terminal/foreign.md` (the untrusted-foreign-code posture, the triage
883
891
  loop, the posting contract, and the `perk pr review cleanup` step).
884
892
  - **The streaming fan-out (foreign + active; guidance-driven — no door plumbing):** the guidance
885
- spawns the 2–3 reviewers as ONE async `subagent` call (a `tasks` array, `context: "fresh"`,
886
- `async: true`; each child's task names its angle, the PR number, and the worktree path ONLY —
887
- never the surface handle), then loops `wait({ timeoutMs })` while the run is active. The why:
888
- progress updates neither wake `wait()` nor enter pi-subagents' `pending` map delivery is an
889
- injected steer message when a tool call returns so the timed wait loop IS the streaming
890
- cadence and the parent must hold its turn open (an ended turn stops streaming). Each arriving
891
- fenced-JSON batch is pushed into hunk incrementally with **`path`+`line` dedupe** (an
893
+ spawns the 2–3 reviewers as ONE async `subagent` call in `workflowScript` mode (top-level
894
+ `async: true` + `context: "fresh"` workflow-level defaults flowing to every lane; the script
895
+ is a single all-settled `runs.all` with one item per angle a stable angle-slug `key`,
896
+ `phase`/`label` trace metadata, and a task naming the angle, the PR number, and the worktree
897
+ path ONLY never the surface handle; a failed lane resolves `{key, ok: false, error}` without
898
+ sinking its siblings; the script returns the mapped per-lane outputs so the full reports
899
+ persist in the run's `status.json`), then loops `subagent_wait({ timeoutMs })` while the run
900
+ is active. The why: progress updates never wake `subagent_wait` and never enter pi-subagents'
901
+ `pending` map — delivery is an injected (now `triggerTurn`-bearing) message when a tool call
902
+ returns — so the timed wait loop IS the streaming cadence and the parent holds its turn open
903
+ (an ended turn degrades streaming to churny per-batch wake-ups instead of a held relay). Each
904
+ arriving fenced-JSON batch is pushed into hunk incrementally with **`path`+`line` dedupe** (an
892
905
  in-conversation ledger; a pushed anchor is never re-pushed; hold-and-accumulate until the
893
- handshake connects). On the grouped completion notification the parent reconciles from the
894
- fenced-JSON **completion reports** (union + dedupe — the source of truth for triage and
895
- posting; streamed batches were provisional), pushes any not-yet-pushed remainder, and when
896
- the handshake never connected applies the unchanged check-in posture (ask, wait, degrade
897
- only on the human's explicit choice).
906
+ handshake connects). On completion — the workflow notification carries only a truncated
907
+ return preview, never the reports — the parent retrieves the full reports via
908
+ `subagent({action: "status", id})` (the per-lane step lines + the `Dir:` line) → `read`
909
+ `<Dir>/status.json` `workflow.value`, reconciles from the fenced-JSON **completion
910
+ reports** (union + dedupe — the source of truth for triage and posting; streamed batches were
911
+ provisional; an `ok: false` lane is reported honestly to the human — angle + error, never
912
+ papered over), pushes any not-yet-pushed remainder, and — when the handshake never connected
913
+ — applies the unchanged check-in posture (ask, wait, degrade only on the human's explicit
914
+ choice).
898
915
  - **Active mode (no PR arg):** the shared active-PR resolution ladder — `perk pr url --json` →
899
916
  `resolveReviewTarget` with the plan-ref's pinned base. A resolved PR → the same flow re-homed
900
917
  to the human's own worktree (`active.md`: no checkout and **no cleanup step**; the children
@@ -1023,8 +1040,9 @@ core), imported by this door and `/pr-review-terminal`'s active mode.
1023
1040
  `prompts/stages/pr-review-browser/foreign.md` (the untrusted-foreign-code posture, the
1024
1041
  `perk pr review cleanup` step).
1025
1042
  - **The streaming fan-out (foreign + active; guidance-driven — no door plumbing):** the 2–3
1026
- adversarial reviewers spawn as ONE async `subagent` call and the parent holds the
1027
- `wait({timeoutMs})` streaming loop, exactly as on `/pr-review-terminal` — but each arriving
1043
+ adversarial reviewers spawn as ONE async `subagent` call in `workflowScript` mode and the
1044
+ parent holds the
1045
+ `subagent_wait({timeoutMs})` streaming loop, exactly as on `/pr-review-terminal` — but each arriving
1028
1046
  fenced-JSON batch is pushed as ONE atomic wave to `POST <url>/api/external-annotations` (the
1029
1047
  ledger dedupe + hold-and-accumulate discipline in the findings-stream block above). Children
1030
1048
  never receive the surface handle — not the URL, not the port. Once the fan-out turn ends the
@@ -1169,11 +1187,12 @@ carries `base` from the `plan-header` so resume paths recover the pinned value.
1169
1187
 
1170
1188
  **Label taxonomy (minimal):** `perk:plan` (green `1f883d`), `perk:learn` (purple `8250df`),
1171
1189
  `perk:objective` (indigo `5319e7`, description "perk objective issue"), `perk:objective-node`
1172
- (indigo `5319e7`, on Linear project-backed roadmap node-issues), and `perk:consolidated` (gray
1173
- `6e7781`, description "perk learn issue consolidated into docs/learned"), each **lazily created**
1174
- by its gateway create-op on first use. Query by a **single** label — GitHub label filters are
1175
- AND-semantics. (On Linear, `perk init` / `doctor --fix` proactively ensure the five `perk:*`
1176
- labels at **workspace** scope §8.21.)
1190
+ (indigo `5319e7`, on Linear project-backed roadmap node-issues), `perk:gist` (yellow `fbca04`,
1191
+ description "perk gist issue (a rough statement of intent)" — §8.41), and `perk:consolidated`
1192
+ (gray `6e7781`, description "perk learn issue consolidated into docs/learned"), each **lazily
1193
+ created** by its gateway create-op on first use. Query by a **single** label GitHub label
1194
+ filters are AND-semantics. (On Linear, `perk init` / `doctor --fix` proactively ensure the six
1195
+ `perk:*` labels at **workspace** scope — §8.21.)
1177
1196
 
1178
1197
  **The `pending-learn` semaphore.** An existence-only `cache.markers` file
1179
1198
  (`.perk/workflow/markers/pending-learn`, name shared as `PENDING_LEARN` in both planes): **`land`
@@ -1317,13 +1336,16 @@ second `--fix` at `fixed == []`).
1317
1336
  **Groups.** `environment` (tools; required tools missing = `fail`; optional tools (e.g. ast-grep)
1318
1337
  missing = `warn`) · `github` (auth/access; non-fatal `warn`) ·
1319
1338
  `linear` (verify-gated Linear readiness — auth/team/labels; present only when the committed
1320
- `[issues] backend` is `"linear"`; warn-level, the github D3 mirror; `--fix` ensures the five perk
1339
+ `[issues] backend` is `"linear"`; warn-level, the github D3 mirror; `--fix` ensures the six perk
1321
1340
  labels — §8.21) · `runner` (remote-runner prereqs; report-only, non-fatal — §8.16) ·
1322
1341
  `package` (settings wiring + perk-package ref reconcile + the `extension-install` install-ownership
1323
1342
  check + the `required-perk-version` managed check over the committed `.perk/required-perk-version`
1324
1343
  pin + the report-only `cli-version` CLI-vs-repo-pin warning (warn, never fail) + the report-only
1325
1344
  `resource-overrides` probe over pi resource overrides that touch perk's own resources (warn, never
1326
- fail, no `--fix` arm — §8.6a);
1345
+ fail, no `--fix` arm — §8.6a) + the report-only `subagent-compat` pi-subagents surface probe
1346
+ (installed version + source-file markers for the orchestration surfaces perk's guidance assumes;
1347
+ `info` when not installed; warn on divergence, never fail; no `--fix` arm — the package stays
1348
+ unpinned);
1327
1349
  `--fix` also migrates a former git-clone consumer forward by removing the orphaned clone — §8.6a) ·
1328
1350
  `repository` (gitignore/agents blocks + config present/valid) ·
1329
1351
  `registry` (the registry self-check) · `skills` (the skills-CLI manifest fragment + the
@@ -1528,6 +1550,7 @@ perk's workflow skills are prompt-hidden; `transclude` exists for the user-bindi
1528
1550
  | trigger | skill | mode |
1529
1551
  |---|---|---|
1530
1552
  | `stage:plan` | `perk-plan` | `nudge` |
1553
+ | `stage:gist-author` | `perk-gist-author` | `nudge` |
1531
1554
  | `stage:objective-author` | `perk-objective-author` | `nudge` |
1532
1555
  | `stage:objective-plan` | `perk-objective-plan` | `nudge` |
1533
1556
  | `stage:implement` | `perk-implement` | `nudge` |
@@ -1538,6 +1561,7 @@ perk's workflow skills are prompt-hidden; `transclude` exists for the user-bindi
1538
1561
  | `command:learn-docs` | `perk-learn-docs` | `nudge` |
1539
1562
  | `command:learn-code` | `perk-learn-code` | `nudge` |
1540
1563
  | `command:pr-review` | `perk-pr-review` | `nudge` |
1564
+ | `command:pr-review-dynamic` | `perk-pr-review-dynamic` | `nudge` |
1541
1565
  | `command:pr-review-terminal` | `perk-pr-review-terminal` | `nudge` |
1542
1566
  | `command:pr-review-browser` | `perk-pr-review-browser` | `nudge` |
1543
1567
  | `command:skills-create` | `perk-skill-author` | `nudge` |
@@ -1847,7 +1871,11 @@ test on both planes).
1847
1871
 
1848
1872
  **`perk init` two-directional settings wiring:** provider wiring composes on top of the static
1849
1873
  `_desired_packages` (perk + `BORROWED_PACKAGES`: `npm:@tombell/pi-diff`,
1850
- `npm:pi-subagents`) layer within the same `_converge_settings` body — `npm:pi-web-access` is **no
1874
+ `npm:pi-subagents`, `npm:@ff-labs/pi-fff`) layer within the same `_converge_settings` body —
1875
+ perk launches inject the env default `PI_FFF_MODE=override` at **both spawn sites** (local
1876
+ `_exec_pi`, remote `_spawn_worker`) with operator env winning by merge order, so stage sessions
1877
+ get FFF as `find`/`grep` while warm/bare sessions keep pi-fff's additive default mode
1878
+ (`fffind`/`ffgrep`) — `npm:pi-web-access` is **no
1851
1879
  longer borrowed** (#529): it is the `web` seam's `default: true` provider, converged via the
1852
1880
  provider path (see the web status note in contracts-history.md §8.10), so a default repo still installs it but deselecting `web`
1853
1881
  removes it like any provider package —
@@ -1934,7 +1962,8 @@ compaction drift; `doctor --fix` reconverges). **Committed-only read** (a `local
1934
1962
  table touches nothing; removing it leaves the written keys to clean up by hand (perk cannot prove
1935
1963
  ownership of a bare settings key). Relatedly, `[models.subagents]` values are **blessed** to carry
1936
1964
  the same `:thinking` suffix (and pi-subagents' `inherit` sentinel — child inherits the parent
1937
- session's model), resolved by pi-subagents on the per-call inline `model` override; doctor's
1965
+ session's model), resolved by pi-subagents from the workflow-level `model` the guidance passes
1966
+ on the `subagent` workflowScript call; doctor's
1938
1967
  warn-level `models` check flags suspicious suffixes (alphabetic-only last-colon segment outside
1939
1968
  the vocabulary) across `[models].default`, `[models.subagents]` values, and
1940
1969
  `[models.stages.<id>].model`. Resulting precedence — cold launch: explicit `perk <stage>
@@ -1965,6 +1994,19 @@ pi-subagents' `applyBuiltinOverrides` consults **before** the bulk flag and whic
1965
1994
  never touches; a **user-global** `~/.pi/agent/settings.json` re-enable does **not** work (the
1966
1995
  project bulk-disable is checked before user-scope overrides).
1967
1996
 
1997
+ **`tuiMode` seed (init-owned):** perk seeds `"tuiMode": "fullscreen"` into `.pi/settings.json`
1998
+ **write-when-absent** — the third convergence shape beside write-when-present
1999
+ (`[compaction]`/`[models]`) and constant-enforced (`subagents.disableBuiltins`): the key is
2000
+ written once, only when absent, and never overwrites a present key (any value — presence, not
2001
+ value, is the guard). Python-plane-only (pi consumes `settings.json` itself; the extension never
2002
+ reads it), composed inside `_converge_settings`
2003
+ (`perk/convergence/init/settings.py::_converge_tui_mode`) so it rides the `settings-wiring`
2004
+ `ManagedConvergence`. **Excluded** from the desired/observed managed-state settings portions: a
2005
+ seeded default is user-ownable after the seed — including it would misclassify an opt-out as
2006
+ `locally-modified`. The opt-out is committing any `tuiMode` value (e.g. `"regular"`), which
2007
+ survives init/doctor; a user-global `/settings` change does **not** durably override the project
2008
+ key (pi merges project settings over global).
2009
+
1968
2010
  > **Interactive save discipline (as of Node 2.5 the present + `/plan-save` flow is
1969
2011
  > FALLBACK-ONLY on every interactive path — perk-plan included):** the prior
1970
2012
  > `PLAN_AUTHORING_CONTEXT` ending ("disable plan mode (/plan off), then call the plan_save
@@ -2114,13 +2156,18 @@ stream's terminal `run_finished` event (§8.12) — the same frozen object, carr
2114
2156
  channel.
2115
2157
 
2116
2158
  > **Open dependency (carried risk).** The `address` drive's seeded prompt instructs the model to
2117
- > spawn `perk.review-classifier` via the borrowed `pi-subagents` `subagent` tool. `pi-subagents`
2118
- > now loads in the worker from the managed settings `packages` list (Gap 4 above). The worker's
2119
- > address prompt now also injects the configured classifier model when `[models.subagents]
2120
- > review-classifier` is set in the worktree's `.perk/config.toml` (#196), as a per-call inline `model`
2121
- > override byte-identical to `_address_prompt`'s parity twin. The **subagent-under-worker live
2122
- > smoke** stays an open dependency **deferred to the Phase-3 `doctor workflow`**;
2123
- > Node 1.2 does not prove it.
2159
+ > run `perk.review-classifier` via ONE foreground `workflowScript` call on the borrowed
2160
+ > `pi-subagents` `subagent` tool (direct `{agent, task}` execution was removed upstream at 0.43).
2161
+ > `pi-subagents` now loads in the worker from the managed settings `packages` list (Gap 4 above).
2162
+ > The worker's address prompt now also injects the configured classifier model when
2163
+ > `[models.subagents] review-classifier` is set in the worktree's `.perk/config.toml` (#196), as a
2164
+ > workflow-level `model` default on that one call, byte-identical to `_address_prompt`'s parity
2165
+ > twin. The seeded classify call also passes a top-level `outputSchema` (rendered from the shared
2166
+ > template's `prompts/common/output-schemas/review-classifier.md` include) — the classifier's
2167
+ > report is engine-validated structured output read from the projection's `report`
2168
+ > (`structuredOutput`), so `ok: true` ⟺ a schema-valid report is present. The
2169
+ > **subagent-under-worker live smoke** stays an open dependency **deferred to the Phase-3
2170
+ > `doctor workflow`**; Node 1.2 does not prove it.
2124
2171
 
2125
2172
  ## §8.12 · The structured run-event stream (Node 1.3)
2126
2173
 
@@ -3009,8 +3056,9 @@ init/doctor probe — report-shaped, never raises; phases short-circuit auth →
3009
3056
  remediation "export LINEAR_API_KEY (create a personal API key at linear.app Settings →
3010
3057
  Security & access), or set [linear] api_key in .perk/local.toml".
3011
3058
  - `linear-team` — ok: `team <key> found`; failure: warn with the error detail.
3012
- - `linear-labels` — all five perk labels present (`perk:plan`, `perk:learn`, `perk:consolidated`,
3013
- `perk:objective`, `perk:objective-node`): ok; otherwise warn listing the missing names,
3059
+ - `linear-labels` — all six perk labels present (`perk:plan`, `perk:learn`, `perk:consolidated`,
3060
+ `perk:objective`, `perk:objective-node`, `perk:gist`): ok; otherwise warn listing the missing
3061
+ names,
3014
3062
  remediation "run `perk init` or `perk doctor --fix`". perk's labels are created
3015
3063
  **workspace-scoped** (no `teamId` on create — Linear's cross-team-label guidance; the lookup is
3016
3064
  unscoped, so a pre-existing team-scoped label still counts).
@@ -3031,14 +3079,14 @@ safely auto-create them).
3031
3079
 
3032
3080
  **The `--fix` label repair gesture** (`_fix_linear_labels`, verify-gated like the skills sync —
3033
3081
  network I/O, so never a `ManagedConvergence`): when `fix` AND `verify` AND linear is selected AND
3034
- key + team are available, `check_readiness(..., ensure_labels=True)` ensures the five labels;
3082
+ key + team are available, `check_readiness(..., ensure_labels=True)` ensures the six labels;
3035
3083
  created names land on `fixed` (`Linear: created label perk:plan`), failures on `fix_errors`.
3036
3084
  Lookup-first idempotency: a converged workspace reports nothing (the doctor idempotency rule).
3037
3085
 
3038
3086
  **The init readiness step** (`perk/convergence/init/__init__.py::_linear_readiness`, verify-gated, non-fatal — the
3039
3087
  GitHub D3 mirror: file convergence already succeeded). Only when `verify` AND the committed
3040
3088
  backend is `"linear"`: missing key/team degrade to an errored `LinearReport`; otherwise the probe
3041
- runs with `ensure_labels=True` (init converges the five perk labels upfront; the lazy write-time
3089
+ runs with `ensure_labels=True` (init converges the six perk labels upfront; the lazy write-time
3042
3090
  `ensure_label` calls remain the safety net). Created labels are reported through the
3043
3091
  `LinearReport` (the `--json` `linear` key, §8.5; the human `✓ Linear: <user>, team <key>` line) —
3044
3092
  **never** appended to `InitReport.changes`, which stays a pure filesystem-delta list.
@@ -3213,6 +3261,34 @@ one-stop current shape.
3213
3261
  | `plannotator-plan` | `PLAN_ADAPTER_PLANNOTATOR_CONTEXT` | browser bridge | present + `/plan-save` |
3214
3262
  | `tombell-plan` | `PLAN_ADAPTER_TOMBELL_CONTEXT` (conditioned injection, Node 2.6) | first-party in-TUI review | present + `/plan-save` (incl. tombell's own interactive `/plan` `setActiveTools` restriction arm) |
3215
3263
 
3264
+ - **Plannotator "Direct Edits" (browser edits of the reviewed document).** Plannotator's
3265
+ plan-review browser lets the reviewer edit the reviewed document directly; the edits arrive as
3266
+ PROSE inside the existing `feedback` string, never a new envelope field — a `# Direct Edits`
3267
+ section (heading + preamble + a ```` ```diff ```` fence containing a jsdiff
3268
+ `createTwoFilesPatch(…, { context: 3 })` patch, `trimEnd()`'d, against the exact bytes perk
3269
+ submitted), composed FIRST, with non-sentinel annotation feedback following after
3270
+ `\n\n---\n\n` (format pin: plannotator `packages/editor/directEdits.ts`
3271
+ `buildDirectEditsSection`/`composeFeedbackWithDirectEdits` @ v0.26.1 — prose, not a pinned
3272
+ API). perk handles it asymmetrically per arm:
3273
+ - **Plan arm, APPROVE:** mechanical apply — strict extraction (`extractDirectEdits`,
3274
+ `extension/adapters/planAdapterPlannotator.ts`) → strict clean-apply (`applyUnifiedDiff`,
3275
+ `extension/substrate/unifiedDiff.ts`, a vendored zero-runtime-dep applier; null on any
3276
+ anomaly) → `writePlanDraft` write-back (reviewed bytes == artifact bytes == saved bytes) →
3277
+ save the EDITED bytes with `details.edited: true` and the annotation remainder as the only
3278
+ surviving feedback. The **fail-open ladder**: no section → today's path byte-stable; a
3279
+ heading that cannot be parsed / applied / written back → the verbatim save plus a loud
3280
+ warning in the approved text and `details.direct_edits_applied: false` (the diff stays in
3281
+ the surfaced feedback for a manual follow-up). Worst-case upstream format drift degrades to
3282
+ exactly the pre-Direct-Edits behavior.
3283
+ - **Objective arm, APPROVE with a Direct Edits section:** NO save — the save seam re-reads the
3284
+ STRUCTURED draft, so rendered-markdown edits (roadmap-table rows included) cannot be folded
3285
+ back mechanically. The arm returns a NON-terminating revise round (`details.status:
3286
+ "revise"`, `reason: "direct_edits"`, gate untouched): the model folds the diff into
3287
+ `objective_draft`, then calls `plan_review` again to confirm. perk never saves an objective
3288
+ the reviewer explicitly edited away from.
3289
+ - **DENY (both arms):** model-mediated — the feedback (diff included) passes through verbatim
3290
+ for the `plan_draft`/`objective_draft` rewrite.
3291
+
3216
3292
  - **Link/`consumed_learn` recovery carriers.** Approval-triggered saves carry **no model params**;
3217
3293
  the **cold** `handoff_extra` carrier (→ §8.2) and the **warm** `objective_node_claim` carrier
3218
3294
  (→ §8.3) recover `objective_id`/`node_id` with identical semantics — fill both-or-neither,
@@ -4510,6 +4586,30 @@ evidence --render --json`) and **also writes `<bundle_dir>/manifest.json`** —
4510
4586
  children can `read` the manifest (they cannot read the door's stdout). Written unconditionally on
4511
4587
  a materialized bundle, deterministic (no wall-clock); no write on a skip.
4512
4588
 
4589
+ **The analyst wave (the report-wave module).** The multi-angle analyst fan-out runs through the
4590
+ Perk-owned report-wave module (`extension/waves/reportWave.ts`) via the flow-scoped
4591
+ **`run_learn_wave`** tool (`extension/doors/learn.ts` — non-terminating; the parent continues to
4592
+ reconcile): the module renders the tested `workflowScript`, spawns it async over the pi-subagents
4593
+ v1 extension RPC (`mission: false`, `context: "fresh"`), blocks under the module-owned timeout,
4594
+ and reads the durable `status.json` aggregate — the wave mechanics are CODE, never model-authored
4595
+ prompt mechanics. Analyst reports are **engine-validated structured output** against the TS-owned
4596
+ `LEARN_ANALYST_REPORT_SCHEMA` (`extension/waves/learnWave.ts` — closed shape, all-required,
4597
+ `target` required-nullable, deliberately NO verdict↔candidates conditional: the parent derives
4598
+ the real verdict from `candidates[]`, so salvaging an inconsistent report beats failing its
4599
+ lane), replacing fenced-JSON scraping — covered angle ⟺ ok lane ⟺ schema-valid report.
4600
+ Completeness is the module's **`best-effort`** policy as tested implementation: a lane-level
4601
+ failure is an explicitly-reported **skipped angle** (never a failed pass, no retry); a
4602
+ **wave-level** failure is a loud tool soft-failure (`error_type` = the wave failure reason) —
4603
+ never a silent fallback to model-authored scripts — and the guidance routes the parent to a
4604
+ single-context analysis of the bundle instead. The **angle policy is tool-enforced**
4605
+ (`angleSelectionError`): 2–4 angles, no duplicates, only the four known slugs
4606
+ (`session-deviations` / `plan-vs-implementation` / `existing-docs` / `validation-risk`), and
4607
+ `session-deviations` always included; violations are `bad_input`. The tool takes the
4608
+ guidance-rendered `bundle_dir` (the model relays it verbatim — the same trust plane as the task
4609
+ text), derives `manifest.json` itself (`bad_input` when absent), and resolves the analyst model
4610
+ from `[models.subagents] learn-analyst` at execute time (the wave's workflow-level `model`
4611
+ default). The manifest write rule above and the DECISION vocabulary are unchanged.
4612
+
4513
4613
  **The `learn` tool's classification params.** The warm `learn` tool carries `decision` (a
4514
4614
  JSON-schema enum of the five captured tokens) + `target` (string), threaded to `perk learn
4515
4615
  capture --decision/--target`. The tool-boundary decode mirrors the `summary` strictness: a
@@ -4815,10 +4915,13 @@ stage (a forked implement session is an implement session); **adopt never impers
4815
4915
  subagent children stay unscoped — their fresh branch carries no stage). Stage-borrowing cold
4816
4916
  doors land on real stage ids (`plan from`/`plan replan`/`learn docs`/`learn code` borrow `plan`;
4817
4917
  `objective replan`/`objective author --from` borrow `objective-author`; `skills create/refine`
4818
- borrow `save`), so the per-stage sets cover every borrower. **Scoped universe:
4918
+ borrow `save`), so the per-stage sets cover every borrower. The gist stages (`gist-author`,
4919
+ `gist-save` — §8.41) each carry `ask_user_question` + `gist_draft` + `gist_save` + the research
4920
+ families (the objective-author shape; `plan_review` governs via the gate-ON set). **Scoped universe:
4819
4921
  `PERK_TOOLS ∪ BORROWED_TOOLS`** — perk's own name-keyed census plus the enumerated
4820
4922
  borrowed-package census (the web-provider union, pi-mono-linear's 25 tools, pi-subagents'
4821
- delegation four, `todo`, `plannotator_submit_plan`); builtins and un-enumerated foreign names
4923
+ delegation four, pi-fff's search names both mode name-sets, `fffind`/`ffgrep`/`fff-multi-grep`
4924
+ plus override's `multi_grep` — `todo`, `plannotator_submit_plan`); builtins and un-enumerated foreign names
4822
4925
  pass through untouched (fail-open — enumeration is diet-completeness, not correctness).
4823
4926
 
4824
4927
  **The borrowed census posture.** Static names, inert when absent (the `READ_ONLY_TOOLS`
@@ -4833,7 +4936,7 @@ appear in `BORROWED_TOOLS`; hygiene-tested). Foreign packages that run their own
4833
4936
  `setActiveTools` (plannotator's phase machinery, @tombell/pi-plan's plan mode) win between
4834
4937
  perk's rebuild points (the fail-open direction), and a mid-session rebuild re-installs perk's
4835
4938
  stage set over a foreign restriction — recorded interplay, not re-engineered. Stage placement:
4836
- the research families (web union + Linear reads) ride EVERY stage list; delegation
4939
+ the research families (web union + Linear reads + FFF local search) ride EVERY stage list; delegation
4837
4940
  (`subagent`/`wait`/the supervisor pair) and `todo` are worktree-family only among the gate-OFF
4838
4941
  stage lists (delegation additionally rides the read-only gate — §8.3);
4839
4942
  `LINEAR_MUTATING_TOOLS` (incl. `linear_configure_auth`, which writes `~/.pi/agent/auth.json`)
@@ -4870,3 +4973,76 @@ lever the gate's allowlist uses; `edit`/`write`/`bash` blocking remains the gate
4870
4973
  config surface for the map (the §8.39 non-interference posture; fail-open on unknown ids covers
4871
4974
  version skew). **Bare-session zero-change guarantee:** a session that never engages either
4872
4975
  concern gets **zero `setActiveTools` calls** — bare warm sessions stay byte-identical.
4976
+
4977
+ ## §8.41 · The gist tier (lightweight statements of intent)
4978
+
4979
+ A **gist** is a backend-tracked **statement of intent** — a rough, thematically
4980
+ problem-space-focused note of "something we would likely want to do", upstream of both plans and
4981
+ objectives. It is **code-informed but carries no implementation strategy** (no steps, no roadmap,
4982
+ no estimates); the lightness lives in the ARTIFACT and the skill guidance, not the machinery. A
4983
+ gist's **scope** (`plan` | `objective`) records its intended consumption tier: a storage
4984
+ discriminator on Linear (issue vs project), a header hint on GitHub.
4985
+
4986
+ **Registry topology (settled decision).** The two gist stages (`gist-author` → `gist-save`) form
4987
+ a **separate, disconnected component** — no edges into the main loop. Gists are optional; nothing
4988
+ routes off "which stage is initial" except the validator, which requires **at least one** initial
4989
+ stage (zero initials — a pure cycle — stays an error). Consumption happens via the unchanged
4990
+ §8.29/§8.30 in-place adoption doors (`perk plan from <gist>` /
4991
+ `perk objective author --from <gist>`), never via stage edges: a gist is consumable because it is
4992
+ an ordinary OPEN backend object without plan/objective metadata.
4993
+
4994
+ **Metadata.** The `perk:gist` label (yellow `fbca04`, description "perk gist issue (a rough
4995
+ statement of intent)"), lazily created by its create-op on first use — the sixth `perk:*` label.
4996
+ The `gist-header` metadata block carries `run_id`, `created`, and `scope` (`plan` | `objective`;
4997
+ lenient read — an unknown stored scope parses to `None`). Per-backend storage:
4998
+
4999
+ - **GitHub** — an issue: the html-style `gist-header` block rendered into the body above the
5000
+ prose.
5001
+ - **Linear, scope `plan`** — an issue: clean transcoded prose body + a `gist-header` **native
5002
+ attachment** (kind `gist-header`, URL `https://perk.invalid/gist/<run_id>`, title
5003
+ "Perk gist", subtitle = scope; the §8.24 attachment-metadata posture).
5004
+ - **Linear, scope `objective`** — a **project**: the overview carries an inline-code
5005
+ `gist-header` block above the transcoded prose. No milestones, no node-issues, no metadata
5006
+ sentinel — deliberately light; the overview block IS the identity (projects have no
5007
+ attachments).
5008
+
5009
+ **The `IssueBackend` gist trio** (docstring contracts mirroring the learn trio):
5010
+ `find_gist_issue{run_id}` (label + header-key scoped — cannot return a plan/learn issue),
5011
+ `create_gist_issue{title, body, run_id, scope, dry_run}` (idempotent via the finder; stamps
5012
+ `scope` into the header), and `list_gist_issues{} -> GistSummary[]{id, title, url, body, scope,
5013
+ adopted}` — every OPEN `perk:gist` issue; raises on infra failure, never masks as empty. The
5014
+ `ObjectiveStore` grows the project-tier pair in the no-op-return family:
5015
+ `create_gist_source{title, prose, run_id, dry_run} -> ObjectiveRef | None` (`None` = "no project
5016
+ surface" — the CLI falls back to the issue tier; the Linear project store creates/finds the gist
5017
+ project) and `list_gist_sources{} -> GistSummary[]` (`()` outside the project store).
5018
+
5019
+ **Adopted detection.** A gist whose stored metadata ALSO carries the adopting tier's metadata
5020
+ (distinct keys — adoption stamps additively beside the `gist-header`, no collision) is
5021
+ **adopted**: on GitHub the `plan-header`/`objective-header` block joins the body; on a Linear
5022
+ issue the signal is a `plan-header` attachment; on a Linear project it is the **Reconcilable
5023
+ region** the adoption composer writes into the overview (the objective headers ride the
5024
+ sentinel's attachments, never an overview block; the original gist overview — with its
5025
+ `gist-header` — survives verbatim in the Immutable archive note, keeping the project scannable
5026
+ as a gist). `perk gist list` default **hides** adopted gists (the "what's still unconsumed" backlog
5027
+ view); `--all` shows everything with an adopted marker. No gist-specific consumption
5028
+ bookkeeping: in-place adoption means the gist *becomes* the plan/objective and inherits its
5029
+ lifecycle.
5030
+
5031
+ **The save worker (`perk gist create --json`).** Options `--body <path>` (required), `--title`,
5032
+ `--scope [plan|objective]`, `--run-id`, `--dry-run`. Scope resolution: explicit `--scope` > the
5033
+ `gist_scope` **handoff** key (a declared `Handoff` field, stashed by `perk gist author --scope`
5034
+ — the `adopt_from` handoff pattern; best-effort recovery, never blocks a save) > `"plan"`. Scope
5035
+ `objective` routes to `ObjectiveStore.create_gist_source` first, falling back to the issue tier
5036
+ on a `None` return; scope `plan` goes to the issue backend directly. Envelope:
5037
+ `{"success": true, "error_type": null, "gist": {"id", "url", "existed"}, "scope", "dry_run"}` —
5038
+ opaque string ids (§8.21). Human output prints the created/found line plus a consumption hint
5039
+ (`perk plan from <id>` / `perk objective author --from <id>`).
5040
+
5041
+ **The warm flow** is the full review-first mirror of plan/objective authoring: the `gist_draft`
5042
+ tool (the third draft-file tool — §8.1's carve-out family; artifact `gist-draft.json`, shape
5043
+ `{schema_version: 1, title?, scope?, prose}`) keeps the draft current; `plan_review` in a
5044
+ `gist-author` session reviews the **rendered markdown** (title + scope line + prose — never raw
5045
+ JSON), VIEW-ONLY first-party (the objective-arm shape; deny+feedback is the change channel);
5046
+ APPROVED auto-saves via `gistApprovalSave` → the `gist_save` tool / `perk gist create`;
5047
+ `/gist-save` is the manual failsafe. No draft → soft-skip `reason: "no_gist_draft"`. No session
5048
+ linkage after save — nothing consumes a gist in-session.
@@ -27,6 +27,7 @@ state_keys:
27
27
  - comments # discussion comments
28
28
  - review-threads # review threads (distinct API from comments)
29
29
  - learn # the perk:learn knowledge-capture issue (P2.T8b)
30
+ - gist # the perk:gist statement-of-intent issue (contracts.md §8.41)
30
31
  # Tier 2 · .perk/workflow/ — the local cache (see contracts.md §8.1).
31
32
  cache:
32
33
  - plan # materialized plan body cache
@@ -41,7 +42,10 @@ state_keys:
41
42
 
42
43
  # ---------------------------------------------------------------------------
43
44
  # Stages — the loop (Q5): objective-author -> objective-save -> objective-plan -> plan ->
44
- # save -> implement -> submit -> address -> land -> learn.
45
+ # save -> implement -> submit -> address -> land -> learn — plus the OPTIONAL gist
46
+ # component (gist-author -> gist-save), deliberately disconnected from the loop:
47
+ # a saved gist is consumed by the in-place adoption doors (`perk plan from`,
48
+ # `perk objective author --from`), not by stage edges (contracts.md §8.41).
45
49
  #
46
50
  # Every field is locked EXCEPT `requires`/`reads`/`writes` (empty until handlers
47
51
  # land). `run_id` is uniform AND shape-locked by the validator: `perk/substrate/registry.py`
@@ -59,6 +63,32 @@ state_keys:
59
63
  # local-only by design — review, merge, and judgment capture stay where a human is.
60
64
  # ---------------------------------------------------------------------------
61
65
  stages:
66
+ - id: gist-author
67
+ summary: Draft a gist — a rough statement of intent (read-only authoring).
68
+ mode: read-only
69
+ worktree: none # interactive authoring, like plan — no branch, no gist yet
70
+ doors: { warm: true, cold_local: true, cold_remote: false } # interactive local authoring
71
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
72
+ command: gist author # dedicated seeded cold door (perk gist author)
73
+ requires: [] # nothing exists yet — this stage CREATES the gist
74
+ reads: []
75
+ writes: [session.workflow-state, cache.session-data] # the read-only `mode` append; gist_draft writes the gist-draft artifact + its session_artifacts pointer
76
+ predecessors: [] # an initial of the disconnected gist component
77
+ successors: [gist-save]
78
+
79
+ - id: gist-save
80
+ summary: Persist the drafted gist to the issue backend; the read-only -> read-write boundary.
81
+ mode: read-write
82
+ worktree: none # still no branch; save writes the gist issue/project
83
+ doors: { warm: true, cold_local: true, cold_remote: false }
84
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
85
+ command: gist save # the gist_save terminating tool + /gist-save command
86
+ requires: []
87
+ reads: []
88
+ writes: [github.gist, session.workflow-state] # create the gist issue/project
89
+ predecessors: [gist-author]
90
+ successors: [] # a terminal — consumption happens via the adoption doors, not edges
91
+
62
92
  - id: objective-author
63
93
  summary: Draft a new objective + roadmap (read-only authoring).
64
94
  mode: read-only