@mgiles/perk 2.0.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
@@ -1130,7 +1130,10 @@ validate_pr_body(body, *, pr_number) -> string[] (empty == vali
1130
1130
 
1131
1131
  **Plan-header block (the queryable metadata in the issue *body*).** The minimal
1132
1132
  observably-distinct set; rendered as a `perk:metadata-block:plan-header` collapsible YAML
1133
- block; the full plan markdown lives in the `plan-body` first comment:
1133
+ block; the full plan markdown lives in the `plan-body` first comment. **Carrier is
1134
+ backend-owned:** GitHub renders it in the issue body; **Linear stores the same fields as a
1135
+ native issue-attachment envelope** (the §8.24 native-attachment metadata amendment) — the field
1136
+ set below is the cross-backend contract either way:
1134
1137
 
1135
1138
  ```
1136
1139
  { run_id: string, # the §8.2 run that created the plan (idempotency key)
@@ -1166,11 +1169,12 @@ carries `base` from the `plan-header` so resume paths recover the pinned value.
1166
1169
 
1167
1170
  **Label taxonomy (minimal):** `perk:plan` (green `1f883d`), `perk:learn` (purple `8250df`),
1168
1171
  `perk:objective` (indigo `5319e7`, description "perk objective issue"), `perk:objective-node`
1169
- (indigo `5319e7`, on Linear project-backed roadmap node-issues), and `perk:consolidated` (gray
1170
- `6e7781`, description "perk learn issue consolidated into docs/learned"), each **lazily created**
1171
- by its gateway create-op on first use. Query by a **single** label — GitHub label filters are
1172
- AND-semantics. (On Linear, `perk init` / `doctor --fix` proactively ensure the five `perk:*`
1173
- 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.)
1174
1178
 
1175
1179
  **The `pending-learn` semaphore.** An existence-only `cache.markers` file
1176
1180
  (`.perk/workflow/markers/pending-learn`, name shared as `PENDING_LEARN` in both planes): **`land`
@@ -1314,7 +1318,7 @@ second `--fix` at `fixed == []`).
1314
1318
  **Groups.** `environment` (tools; required tools missing = `fail`; optional tools (e.g. ast-grep)
1315
1319
  missing = `warn`) · `github` (auth/access; non-fatal `warn`) ·
1316
1320
  `linear` (verify-gated Linear readiness — auth/team/labels; present only when the committed
1317
- `[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
1318
1322
  labels — §8.21) · `runner` (remote-runner prereqs; report-only, non-fatal — §8.16) ·
1319
1323
  `package` (settings wiring + perk-package ref reconcile + the `extension-install` install-ownership
1320
1324
  check + the `required-perk-version` managed check over the committed `.perk/required-perk-version`
@@ -1525,6 +1529,7 @@ perk's workflow skills are prompt-hidden; `transclude` exists for the user-bindi
1525
1529
  | trigger | skill | mode |
1526
1530
  |---|---|---|
1527
1531
  | `stage:plan` | `perk-plan` | `nudge` |
1532
+ | `stage:gist-author` | `perk-gist-author` | `nudge` |
1528
1533
  | `stage:objective-author` | `perk-objective-author` | `nudge` |
1529
1534
  | `stage:objective-plan` | `perk-objective-plan` | `nudge` |
1530
1535
  | `stage:implement` | `perk-implement` | `nudge` |
@@ -2928,7 +2933,13 @@ team = "ENG" # the Linear team key — required when backend = "linear"
2928
2933
  `load_committed_issues_backend` / `load_committed_issues_team`; TS: `resolveIssueBackendId` reads
2929
2934
  only the committed file). Rationale: the backend decides where canonical durable state
2930
2935
  (plan/learn/objective issues) is *written*; a per-user override would fragment the canonical
2931
- store. **`LINEAR_API_KEY` lives in the environment or the gitignored `.perk/local.toml`
2936
+ store. **Main-checkout anchored, both planes.** Both planes resolve the read root to the **main
2937
+ checkout** (git-common-dir resolution — Python `git.main_worktree_root(repo_root) or repo_root`,
2938
+ TS `mainCheckoutRoot(cwd)`; both fall back to the invocation root outside a git repo), so a
2939
+ linked worktree's checkout state (detached HEAD, a stale branch, a checkout without `.perk/`)
2940
+ can never change where canonical durable state is written. Deliberate consequence: a plan branch
2941
+ that *edits* `[issues]` does not take effect from inside its own worktree — the canonical-store
2942
+ selection must not fork mid-plan; it switches when the edit reaches the main checkout. **`LINEAR_API_KEY` lives in the environment or the gitignored `.perk/local.toml`
2932
2943
  `[linear] api_key`** (an exported env var wins over the config) — **never** in a committed file.
2933
2944
  The config read is local-file-only (`config.load_local_linear_api_key`, the inverse of the
2934
2945
  `load_committed_*` readers; fail-soft on malformed TOML — returns `None`, never raised). Two seams
@@ -3000,8 +3011,9 @@ init/doctor probe — report-shaped, never raises; phases short-circuit auth →
3000
3011
  remediation "export LINEAR_API_KEY (create a personal API key at linear.app Settings →
3001
3012
  Security & access), or set [linear] api_key in .perk/local.toml".
3002
3013
  - `linear-team` — ok: `team <key> found`; failure: warn with the error detail.
3003
- - `linear-labels` — all five perk labels present (`perk:plan`, `perk:learn`, `perk:consolidated`,
3004
- `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,
3005
3017
  remediation "run `perk init` or `perk doctor --fix`". perk's labels are created
3006
3018
  **workspace-scoped** (no `teamId` on create — Linear's cross-team-label guidance; the lookup is
3007
3019
  unscoped, so a pre-existing team-scoped label still counts).
@@ -3022,14 +3034,14 @@ safely auto-create them).
3022
3034
 
3023
3035
  **The `--fix` label repair gesture** (`_fix_linear_labels`, verify-gated like the skills sync —
3024
3036
  network I/O, so never a `ManagedConvergence`): when `fix` AND `verify` AND linear is selected AND
3025
- 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;
3026
3038
  created names land on `fixed` (`Linear: created label perk:plan`), failures on `fix_errors`.
3027
3039
  Lookup-first idempotency: a converged workspace reports nothing (the doctor idempotency rule).
3028
3040
 
3029
3041
  **The init readiness step** (`perk/convergence/init/__init__.py::_linear_readiness`, verify-gated, non-fatal — the
3030
3042
  GitHub D3 mirror: file convergence already succeeded). Only when `verify` AND the committed
3031
3043
  backend is `"linear"`: missing key/team degrade to an errored `LinearReport`; otherwise the probe
3032
- 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
3033
3045
  `ensure_label` calls remain the safety net). Created labels are reported through the
3034
3046
  `LinearReport` (the `--json` `linear` key, §8.5; the human `✓ Linear: <user>, team <key>` line) —
3035
3047
  **never** appended to `InitReport.changes`, which stays a pure filesystem-delta list.
@@ -3204,6 +3216,34 @@ one-stop current shape.
3204
3216
  | `plannotator-plan` | `PLAN_ADAPTER_PLANNOTATOR_CONTEXT` | browser bridge | present + `/plan-save` |
3205
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) |
3206
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
+
3207
3247
  - **Link/`consumed_learn` recovery carriers.** Approval-triggered saves carry **no model params**;
3208
3248
  the **cold** `handoff_extra` carrier (→ §8.2) and the **warm** `objective_node_claim` carrier
3209
3249
  (→ §8.3) recover `objective_id`/`node_id` with identical semantics — fill both-or-neither,
@@ -3543,6 +3583,71 @@ actor is the human user; these changes make perk's footprint read as native:
3543
3583
  round-trip is proven lossless (else dropped). Becoming a true Linear **Agent** (`actor=app`) is a
3544
3584
  separate, out-of-scope follow-up.
3545
3585
 
3586
+ **Native-attachment metadata amendment (#1355) — Linear perk metadata rides issue attachments.**
3587
+ Linear-only (**GitHub renders are byte-identical**; the issue-tier protocol reshape below is the
3588
+ one cross-backend change). The five machine metadata blocks — `plan-header`, `learn-header`,
3589
+ `objective-node` (issue-scoped) and `objective-header`, `objective-manifest` (project-scoped) —
3590
+ **no longer render into Linear bodies at all**: each rides a native issue **attachment** with a
3591
+ machine-readable `metadata` envelope. Bodies/overviews are clean human prose. This supersedes the
3592
+ prose-first-composition bullet above (there are no machine blocks left to position), the
3593
+ collapsed-toggle deferral (moot), `_insert_or_replace_manifest` (deleted), and every
3594
+ list-and-parse find scan. A **clean break**: no legacy read fallback — pre-existing Linear
3595
+ artifacts with body-block metadata are simply not found (re-save/re-create them). Still inline in
3596
+ bodies (structural sentinels, not metadata): the `plan-body`/marked-comment markers, the
3597
+ Reconcilable region markers, the `Adopted-from` archive note, and the copyable command callouts.
3598
+
3599
+ - **The envelope** (`perk/backends/linear/attachments.py::encode`): `attachmentCreate` with
3600
+ `metadata: { source: "perk", schema_version: 1, kind: <block key>, payload_json: <JSON fields>,
3601
+ created, title, attributes: {…} }` — `payload_json` is the authoritative field payload (the
3602
+ same fields the body blocks carried); `attributes` duplicates scalars for Linear-side
3603
+ filterability. Cards render human-readable (title = the block kind, subtitle = a salient
3604
+ field). Decode is `find_perk_attachment(nodes, kind=)` — absent → `None` (tolerant),
3605
+ present-but-malformed → raises (fail-loud); `has_perk_attachment` is the presence-only check.
3606
+ - **The URL scheme is the identity** (live-verified: Linear accepts non-resolving URLs, and
3607
+ `attachmentCreate` **upserts by `(url, issueId)` with REPLACE metadata semantics** — every
3608
+ write must carry the complete envelope): `https://perk.invalid/plan/<run_id-or-identifier>`,
3609
+ `/learn/<run_id-or-identifier>`, `/node/<issue identifier>` (carry-path stable),
3610
+ `/objective/<run_id>`, `/manifest/<run_id>`. Writers always **reuse a found attachment's URL**
3611
+ (never re-derive — re-deriving would orphan the existing card).
3612
+ - **O(1) finds via `attachmentsForURL`** (`find_issue_by_attachment_url`): `find_plan_issue` /
3613
+ `find_learn_issue` / `find_objective` are each ONE workspace-wide exact-URL query (no
3614
+ team-scoped label scans). **Open-only parity rule:** the issue-tier finds treat a hit in a
3615
+ terminal state (`completed`/`canceled`) as not-found — parity with the legacy open-only scan,
3616
+ so a landed plan's run_id never resurrects the closed issue. On **multiple** hits (a landed
3617
+ plan's closed issue + an open re-save sharing the URL) the find prefers the first
3618
+ **non-terminal** hit, so the parity filter is deterministic — never at the mercy of the
3619
+ server's node order. The objective find is
3620
+ state-independent by design (its sentinel is born canceled) and takes the project ref from the
3621
+ hit issue's `project` (a header hit with no project raises — a broken sentinel).
3622
+ - **The project metadata sentinel.** Linear exposes no project-attachment mutation, so each
3623
+ perk project carries one **sentinel issue** (`Perk: objective metadata`, empty body, born in
3624
+ the team's canceled state — cosmetic; created **fail-loud** immediately after `projectCreate`,
3625
+ before milestones/node-issues) holding the `objective-header` + `objective-manifest`
3626
+ attachments. Discovery keys on the header **attachment**, never the title. One best-effort
3627
+ `entityExternalLinkCreate` adds it to the project's Resources (fail-open). Readers find it in
3628
+ the same `project_issues` scan they already run (zero extra queries); it is excluded from
3629
+ roadmap reads, engagement, and adoption candidate maps. `update_objective_header` /
3630
+ manifest syncs (`_sync_manifest_*`, `_refresh_manifest_phase_pins`, the drift backfill) are
3631
+ merge-and-upsert against the sentinel's attachments; a sentinel-less project is **not a perk
3632
+ objective** (`get_objective → None`).
3633
+ - **Node-issues + unified plans.** The `objective-node` payload rides a `/node/<identifier>`
3634
+ attachment (descriptions are clean prose); a unified node-issue carries TWO envelopes — node +
3635
+ plan — disambiguated by `kind`. The node→plan backlink derivation is unchanged but now keys on
3636
+ the **plan-header attachment's presence**. Attachments cascade-delete with their issue.
3637
+ - **Accepted create window (issue tier).** Every Linear create is now two writes — `issueCreate`
3638
+ then the identity-carrying attachment upsert — so a crash between them orphans a header-less
3639
+ issue invisible to the URL finds (a retry mints a fresh one; the orphan is human-visible
3640
+ garbage to close). The same accepted one-round-trip window as the metadata sentinel's,
3641
+ now explicit for plan/learn/node creates too.
3642
+ - **The issue-tier protocol reshape (all backends).** `create_plan_issue(title, header_fields,
3643
+ run_id, dry_run)` replaces the pre-rendered `body` param — the backend owns the header carrier
3644
+ (GitHub renders the body block itself, byte-identical; Linear creates a clean empty body + the
3645
+ attachment). Two additive fields: `LearnIssueSummary.header: LearnHeader | None` (decoded
3646
+ backend-side; GitHub parses the body, Linear the attachment — degrade-to-None either way) and
3647
+ `AdoptableIssue.already_plan: bool` (backend-decided — GitHub `has_metadata_block(body,
3648
+ plan-header)`, Linear the plan-header attachment), consumed by `plan from`'s `already_a_plan`
3649
+ refusal.
3650
+
3546
3651
  ## §8.25 · The human-engagement read contract (Objective #682, Node 1.2)
3547
3652
 
3548
3653
  A backend-neutral **READ** surface for human engagement — comments, description edits, and
@@ -3806,14 +3911,17 @@ are verbatim human content". A normally-authored plan leaves it `None`.
3806
3911
  `plan-header` block additively into the issue **body** (human prose preserved verbatim, **title
3807
3912
  untouched**); (c) idempotently prepend the `perk impl <id>` callout above the body; (d) upsert
3808
3913
  the `plan-body` comment carrying the authored markdown. Returns `IssueRef(existed=True)`.
3809
- Idempotent on re-save; GitHub stamps HTML-encoded, Linear inline-code (Linear-safe).
3914
+ Idempotent on re-save; GitHub stamps the body block HTML-encoded; Linear upserts the
3915
+ plan-header **attachment** instead — the human body stays verbatim apart from the callout
3916
+ (the §8.24 native-attachment metadata amendment).
3810
3917
 
3811
3918
  **The cold door (`perk plan from <issue>`).** A dedicated launcher verb in the `plan` hybrid group
3812
3919
  (mirrors `replan`/`resume`; `from` is a valid Click command string). It performs every Linear/GitHub
3813
3920
  read up front (the read-only plan-mode session has no `gh`/Linear access), then re-launches the
3814
3921
  `plan` stage seeded to author a plan over the materialized source. It **refuses** when: the issue is
3815
3922
  not found (`adopt_not_found`), not OPEN (`adopt_not_open`), or already a perk plan
3816
- (`has_metadata_block(body, plan-header)` `already_a_plan`, hinting `perk plan replan <id>`).
3923
+ (`AdoptableIssue.already_plan` — backend-decided: GitHub the body block, Linear the plan-header
3924
+ attachment → `already_a_plan`, hinting `perk plan replan <id>`).
3817
3925
  Engagement is read fail-soft (`render_adopted_engagement` → `<untrusted_adopted_issue_engagement>`;
3818
3926
  `IssueBackendError` → omitted). The source is materialized to `scratch/adopt-<issue_id>.md` (title +
3819
3927
  body wrapped in `<untrusted_adopted_issue>` + the optional engagement block). A **fresh** `run_id`
@@ -3910,18 +4018,21 @@ reconcile. Mapped issues' titles/bodies are independently preserved verbatim by
3910
4018
  `perk objective plan <n>` callout prepended), backfill `objective_comment_id`. `adopt_map` is
3911
4019
  ignored (no child issues).
3912
4020
  - **Linear project-backed (full):** `_LinearProjectOps.project_issues_for_adoption` (a sibling of
3913
- `project_issues` selecting `title` too; the byte-stable `project_issues` left untouched).
3914
- `read_objective_source` → the project overview `content` + its issues. `adopt_source_as_objective`
3915
- composes the new overview preserving the original verbatim (`to_linear_markdown(`
3916
- Reconcilable(`<model prose>`) + `objective-header`(`adopted_from=source_id`) + `objective-manifest`
3917
- + `render_adopted_overview_note(<original overview>)` below the markers `)`), `update_project
3918
- _content` (in place, NOT `create_project`), prepends the callout; one milestone per phase via
3919
- `ensure_phase_milestone` seeded from `project_milestones` (de-dupe against existing); for each
3920
- node in `node_sort_key` order a **mapped** node stamps the `objective-node` block additively into
3921
- the existing issue (title/body verbatim, description PATCH + `perk:objective-node` label added +
3922
- phase-milestone attach), an **unmapped** node mints a fresh node-issue; blocking relations per
3923
- explicit `depends_on`. Raises on an `adopt_issue` id not in the project (fail-loud). Idempotent on
3924
- `run_id`.
4021
+ `project_issues` selecting `title` too; both selections now also carry the attachment nodes
4022
+ the §8.24 native-attachment metadata amendment).
4023
+ `read_objective_source` the project overview `content` + its issues (the metadata sentinel
4024
+ excluded). `adopt_source_as_objective` composes the new overview preserving the original
4025
+ verbatim (`to_linear_markdown(` Reconcilable(`<model prose>`) +
4026
+ `render_adopted_overview_note(<original overview>)` below the markers `)`), `update_project
4027
+ _content` (in place, NOT `create_project`), prepends the callout; the
4028
+ `objective-header`(`adopted_from=source_id`) + `objective-manifest` ride a fresh metadata
4029
+ sentinel's attachments (the §8.24 native-attachment metadata amendment); one milestone per
4030
+ phase via `ensure_phase_milestone` seeded from `project_milestones` (de-dupe against existing);
4031
+ for each node in `node_sort_key` order a **mapped** node upserts the `objective-node`
4032
+ attachment onto the existing issue (title/body verbatim — no description write — +
4033
+ `perk:objective-node` label added + phase-milestone attach), an **unmapped** node mints a fresh
4034
+ node-issue; blocking relations per explicit `depends_on`. Raises on an `adopt_issue` id not in
4035
+ the project (fail-loud). Idempotent on `run_id`.
3925
4036
  - **Issue-backed Linear (dormant):** both `read_objective_source` and `adopt_source_as_objective`
3926
4037
  return `None` (honest no-op; keeps `ty` green).
3927
4038
 
@@ -4735,7 +4846,9 @@ stage (a forked implement session is an implement session); **adopt never impers
4735
4846
  subagent children stay unscoped — their fresh branch carries no stage). Stage-borrowing cold
4736
4847
  doors land on real stage ids (`plan from`/`plan replan`/`learn docs`/`learn code` borrow `plan`;
4737
4848
  `objective replan`/`objective author --from` borrow `objective-author`; `skills create/refine`
4738
- 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:
4739
4852
  `PERK_TOOLS ∪ BORROWED_TOOLS`** — perk's own name-keyed census plus the enumerated
4740
4853
  borrowed-package census (the web-provider union, pi-mono-linear's 25 tools, pi-subagents'
4741
4854
  delegation four, `todo`, `plannotator_submit_plan`); builtins and un-enumerated foreign names
@@ -4790,3 +4903,76 @@ lever the gate's allowlist uses; `edit`/`write`/`bash` blocking remains the gate
4790
4903
  config surface for the map (the §8.39 non-interference posture; fail-open on unknown ids covers
4791
4904
  version skew). **Bare-session zero-change guarantee:** a session that never engages either
4792
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