@mgiles/perk 2.1.0 → 2.2.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.
@@ -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:
@@ -386,7 +386,7 @@ 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
391
  providers' research tools, the read-only Linear tools, and the pi-subagents delegation family
392
392
  (`subagent`/`wait` + the parent supervisor pair — the gated objective-plan explorer spawn must be
@@ -1169,11 +1169,12 @@ carries `base` from the `plan-header` so resume paths recover the pinned value.
1169
1169
 
1170
1170
  **Label taxonomy (minimal):** `perk:plan` (green `1f883d`), `perk:learn` (purple `8250df`),
1171
1171
  `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.)
1172
+ (indigo `5319e7`, on Linear project-backed roadmap node-issues), `perk:gist` (yellow `fbca04`,
1173
+ description "perk gist issue (a rough statement of intent)" — §8.41), and `perk:consolidated`
1174
+ (gray `6e7781`, description "perk learn issue consolidated into docs/learned"), each **lazily
1175
+ created** by its gateway create-op on first use. Query by a **single** label GitHub label
1176
+ filters are AND-semantics. (On Linear, `perk init` / `doctor --fix` proactively ensure the six
1177
+ `perk:*` labels at **workspace** scope — §8.21.)
1177
1178
 
1178
1179
  **The `pending-learn` semaphore.** An existence-only `cache.markers` file
1179
1180
  (`.perk/workflow/markers/pending-learn`, name shared as `PENDING_LEARN` in both planes): **`land`
@@ -1317,7 +1318,7 @@ second `--fix` at `fixed == []`).
1317
1318
  **Groups.** `environment` (tools; required tools missing = `fail`; optional tools (e.g. ast-grep)
1318
1319
  missing = `warn`) · `github` (auth/access; non-fatal `warn`) ·
1319
1320
  `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
1321
+ `[issues] backend` is `"linear"`; warn-level, the github D3 mirror; `--fix` ensures the six perk
1321
1322
  labels — §8.21) · `runner` (remote-runner prereqs; report-only, non-fatal — §8.16) ·
1322
1323
  `package` (settings wiring + perk-package ref reconcile + the `extension-install` install-ownership
1323
1324
  check + the `required-perk-version` managed check over the committed `.perk/required-perk-version`
@@ -1528,6 +1529,7 @@ perk's workflow skills are prompt-hidden; `transclude` exists for the user-bindi
1528
1529
  | trigger | skill | mode |
1529
1530
  |---|---|---|
1530
1531
  | `stage:plan` | `perk-plan` | `nudge` |
1532
+ | `stage:gist-author` | `perk-gist-author` | `nudge` |
1531
1533
  | `stage:objective-author` | `perk-objective-author` | `nudge` |
1532
1534
  | `stage:objective-plan` | `perk-objective-plan` | `nudge` |
1533
1535
  | `stage:implement` | `perk-implement` | `nudge` |
@@ -3009,8 +3011,9 @@ init/doctor probe — report-shaped, never raises; phases short-circuit auth →
3009
3011
  remediation "export LINEAR_API_KEY (create a personal API key at linear.app Settings →
3010
3012
  Security & access), or set [linear] api_key in .perk/local.toml".
3011
3013
  - `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,
3014
+ - `linear-labels` — all six perk labels present (`perk:plan`, `perk:learn`, `perk:consolidated`,
3015
+ `perk:objective`, `perk:objective-node`, `perk:gist`): ok; otherwise warn listing the missing
3016
+ names,
3014
3017
  remediation "run `perk init` or `perk doctor --fix`". perk's labels are created
3015
3018
  **workspace-scoped** (no `teamId` on create — Linear's cross-team-label guidance; the lookup is
3016
3019
  unscoped, so a pre-existing team-scoped label still counts).
@@ -3031,14 +3034,14 @@ safely auto-create them).
3031
3034
 
3032
3035
  **The `--fix` label repair gesture** (`_fix_linear_labels`, verify-gated like the skills sync —
3033
3036
  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;
3037
+ key + team are available, `check_readiness(..., ensure_labels=True)` ensures the six labels;
3035
3038
  created names land on `fixed` (`Linear: created label perk:plan`), failures on `fix_errors`.
3036
3039
  Lookup-first idempotency: a converged workspace reports nothing (the doctor idempotency rule).
3037
3040
 
3038
3041
  **The init readiness step** (`perk/convergence/init/__init__.py::_linear_readiness`, verify-gated, non-fatal — the
3039
3042
  GitHub D3 mirror: file convergence already succeeded). Only when `verify` AND the committed
3040
3043
  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
3044
+ runs with `ensure_labels=True` (init converges the six perk labels upfront; the lazy write-time
3042
3045
  `ensure_label` calls remain the safety net). Created labels are reported through the
3043
3046
  `LinearReport` (the `--json` `linear` key, §8.5; the human `✓ Linear: <user>, team <key>` line) —
3044
3047
  **never** appended to `InitReport.changes`, which stays a pure filesystem-delta list.
@@ -3213,6 +3216,34 @@ one-stop current shape.
3213
3216
  | `plannotator-plan` | `PLAN_ADAPTER_PLANNOTATOR_CONTEXT` | browser bridge | present + `/plan-save` |
3214
3217
  | `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
3218
 
3219
+ - **Plannotator "Direct Edits" (browser edits of the reviewed document).** Plannotator's
3220
+ plan-review browser lets the reviewer edit the reviewed document directly; the edits arrive as
3221
+ PROSE inside the existing `feedback` string, never a new envelope field — a `# Direct Edits`
3222
+ section (heading + preamble + a ```` ```diff ```` fence containing a jsdiff
3223
+ `createTwoFilesPatch(…, { context: 3 })` patch, `trimEnd()`'d, against the exact bytes perk
3224
+ submitted), composed FIRST, with non-sentinel annotation feedback following after
3225
+ `\n\n---\n\n` (format pin: plannotator `packages/editor/directEdits.ts`
3226
+ `buildDirectEditsSection`/`composeFeedbackWithDirectEdits` @ v0.26.1 — prose, not a pinned
3227
+ API). perk handles it asymmetrically per arm:
3228
+ - **Plan arm, APPROVE:** mechanical apply — strict extraction (`extractDirectEdits`,
3229
+ `extension/adapters/planAdapterPlannotator.ts`) → strict clean-apply (`applyUnifiedDiff`,
3230
+ `extension/substrate/unifiedDiff.ts`, a vendored zero-runtime-dep applier; null on any
3231
+ anomaly) → `writePlanDraft` write-back (reviewed bytes == artifact bytes == saved bytes) →
3232
+ save the EDITED bytes with `details.edited: true` and the annotation remainder as the only
3233
+ surviving feedback. The **fail-open ladder**: no section → today's path byte-stable; a
3234
+ heading that cannot be parsed / applied / written back → the verbatim save plus a loud
3235
+ warning in the approved text and `details.direct_edits_applied: false` (the diff stays in
3236
+ the surfaced feedback for a manual follow-up). Worst-case upstream format drift degrades to
3237
+ exactly the pre-Direct-Edits behavior.
3238
+ - **Objective arm, APPROVE with a Direct Edits section:** NO save — the save seam re-reads the
3239
+ STRUCTURED draft, so rendered-markdown edits (roadmap-table rows included) cannot be folded
3240
+ back mechanically. The arm returns a NON-terminating revise round (`details.status:
3241
+ "revise"`, `reason: "direct_edits"`, gate untouched): the model folds the diff into
3242
+ `objective_draft`, then calls `plan_review` again to confirm. perk never saves an objective
3243
+ the reviewer explicitly edited away from.
3244
+ - **DENY (both arms):** model-mediated — the feedback (diff included) passes through verbatim
3245
+ for the `plan_draft`/`objective_draft` rewrite.
3246
+
3216
3247
  - **Link/`consumed_learn` recovery carriers.** Approval-triggered saves carry **no model params**;
3217
3248
  the **cold** `handoff_extra` carrier (→ §8.2) and the **warm** `objective_node_claim` carrier
3218
3249
  (→ §8.3) recover `objective_id`/`node_id` with identical semantics — fill both-or-neither,
@@ -4815,7 +4846,9 @@ stage (a forked implement session is an implement session); **adopt never impers
4815
4846
  subagent children stay unscoped — their fresh branch carries no stage). Stage-borrowing cold
4816
4847
  doors land on real stage ids (`plan from`/`plan replan`/`learn docs`/`learn code` borrow `plan`;
4817
4848
  `objective replan`/`objective author --from` borrow `objective-author`; `skills create/refine`
4818
- borrow `save`), so the per-stage sets cover every borrower. **Scoped universe:
4849
+ borrow `save`), so the per-stage sets cover every borrower. The gist stages (`gist-author`,
4850
+ `gist-save` — §8.41) each carry `ask_user_question` + `gist_draft` + `gist_save` + the research
4851
+ families (the objective-author shape; `plan_review` governs via the gate-ON set). **Scoped universe:
4819
4852
  `PERK_TOOLS ∪ BORROWED_TOOLS`** — perk's own name-keyed census plus the enumerated
4820
4853
  borrowed-package census (the web-provider union, pi-mono-linear's 25 tools, pi-subagents'
4821
4854
  delegation four, `todo`, `plannotator_submit_plan`); builtins and un-enumerated foreign names
@@ -4870,3 +4903,76 @@ lever the gate's allowlist uses; `edit`/`write`/`bash` blocking remains the gate
4870
4903
  config surface for the map (the §8.39 non-interference posture; fail-open on unknown ids covers
4871
4904
  version skew). **Bare-session zero-change guarantee:** a session that never engages either
4872
4905
  concern gets **zero `setActiveTools` calls** — bare warm sessions stay byte-identical.
4906
+
4907
+ ## §8.41 · The gist tier (lightweight statements of intent)
4908
+
4909
+ A **gist** is a backend-tracked **statement of intent** — a rough, thematically
4910
+ problem-space-focused note of "something we would likely want to do", upstream of both plans and
4911
+ objectives. It is **code-informed but carries no implementation strategy** (no steps, no roadmap,
4912
+ no estimates); the lightness lives in the ARTIFACT and the skill guidance, not the machinery. A
4913
+ gist's **scope** (`plan` | `objective`) records its intended consumption tier: a storage
4914
+ discriminator on Linear (issue vs project), a header hint on GitHub.
4915
+
4916
+ **Registry topology (settled decision).** The two gist stages (`gist-author` → `gist-save`) form
4917
+ a **separate, disconnected component** — no edges into the main loop. Gists are optional; nothing
4918
+ routes off "which stage is initial" except the validator, which requires **at least one** initial
4919
+ stage (zero initials — a pure cycle — stays an error). Consumption happens via the unchanged
4920
+ §8.29/§8.30 in-place adoption doors (`perk plan from <gist>` /
4921
+ `perk objective author --from <gist>`), never via stage edges: a gist is consumable because it is
4922
+ an ordinary OPEN backend object without plan/objective metadata.
4923
+
4924
+ **Metadata.** The `perk:gist` label (yellow `fbca04`, description "perk gist issue (a rough
4925
+ statement of intent)"), lazily created by its create-op on first use — the sixth `perk:*` label.
4926
+ The `gist-header` metadata block carries `run_id`, `created`, and `scope` (`plan` | `objective`;
4927
+ lenient read — an unknown stored scope parses to `None`). Per-backend storage:
4928
+
4929
+ - **GitHub** — an issue: the html-style `gist-header` block rendered into the body above the
4930
+ prose.
4931
+ - **Linear, scope `plan`** — an issue: clean transcoded prose body + a `gist-header` **native
4932
+ attachment** (kind `gist-header`, URL `https://perk.invalid/gist/<run_id>`, title
4933
+ "Perk gist", subtitle = scope; the §8.24 attachment-metadata posture).
4934
+ - **Linear, scope `objective`** — a **project**: the overview carries an inline-code
4935
+ `gist-header` block above the transcoded prose. No milestones, no node-issues, no metadata
4936
+ sentinel — deliberately light; the overview block IS the identity (projects have no
4937
+ attachments).
4938
+
4939
+ **The `IssueBackend` gist trio** (docstring contracts mirroring the learn trio):
4940
+ `find_gist_issue{run_id}` (label + header-key scoped — cannot return a plan/learn issue),
4941
+ `create_gist_issue{title, body, run_id, scope, dry_run}` (idempotent via the finder; stamps
4942
+ `scope` into the header), and `list_gist_issues{} -> GistSummary[]{id, title, url, body, scope,
4943
+ adopted}` — every OPEN `perk:gist` issue; raises on infra failure, never masks as empty. The
4944
+ `ObjectiveStore` grows the project-tier pair in the no-op-return family:
4945
+ `create_gist_source{title, prose, run_id, dry_run} -> ObjectiveRef | None` (`None` = "no project
4946
+ surface" — the CLI falls back to the issue tier; the Linear project store creates/finds the gist
4947
+ project) and `list_gist_sources{} -> GistSummary[]` (`()` outside the project store).
4948
+
4949
+ **Adopted detection.** A gist whose stored metadata ALSO carries the adopting tier's metadata
4950
+ (distinct keys — adoption stamps additively beside the `gist-header`, no collision) is
4951
+ **adopted**: on GitHub the `plan-header`/`objective-header` block joins the body; on a Linear
4952
+ issue the signal is a `plan-header` attachment; on a Linear project it is the **Reconcilable
4953
+ region** the adoption composer writes into the overview (the objective headers ride the
4954
+ sentinel's attachments, never an overview block; the original gist overview — with its
4955
+ `gist-header` — survives verbatim in the Immutable archive note, keeping the project scannable
4956
+ as a gist). `perk gist list` default **hides** adopted gists (the "what's still unconsumed" backlog
4957
+ view); `--all` shows everything with an adopted marker. No gist-specific consumption
4958
+ bookkeeping: in-place adoption means the gist *becomes* the plan/objective and inherits its
4959
+ lifecycle.
4960
+
4961
+ **The save worker (`perk gist create --json`).** Options `--body <path>` (required), `--title`,
4962
+ `--scope [plan|objective]`, `--run-id`, `--dry-run`. Scope resolution: explicit `--scope` > the
4963
+ `gist_scope` **handoff** key (a declared `Handoff` field, stashed by `perk gist author --scope`
4964
+ — the `adopt_from` handoff pattern; best-effort recovery, never blocks a save) > `"plan"`. Scope
4965
+ `objective` routes to `ObjectiveStore.create_gist_source` first, falling back to the issue tier
4966
+ on a `None` return; scope `plan` goes to the issue backend directly. Envelope:
4967
+ `{"success": true, "error_type": null, "gist": {"id", "url", "existed"}, "scope", "dry_run"}` —
4968
+ opaque string ids (§8.21). Human output prints the created/found line plus a consumption hint
4969
+ (`perk plan from <id>` / `perk objective author --from <id>`).
4970
+
4971
+ **The warm flow** is the full review-first mirror of plan/objective authoring: the `gist_draft`
4972
+ tool (the third draft-file tool — §8.1's carve-out family; artifact `gist-draft.json`, shape
4973
+ `{schema_version: 1, title?, scope?, prose}`) keeps the draft current; `plan_review` in a
4974
+ `gist-author` session reviews the **rendered markdown** (title + scope line + prose — never raw
4975
+ JSON), VIEW-ONLY first-party (the objective-arm shape; deny+feedback is the change channel);
4976
+ APPROVED auto-saves via `gistApprovalSave` → the `gist_save` tool / `perk gist create`;
4977
+ `/gist-save` is the manual failsafe. No draft → soft-skip `reason: "no_gist_draft"`. No session
4978
+ 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