@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.
- package/extension/adapters/planAdapterPlannotator.ts +64 -1
- package/extension/doors/commitCompact.ts +163 -0
- package/extension/factories/gistAuthor.ts +94 -0
- package/extension/factories/gistDraft.ts +265 -0
- package/extension/factories/gistSave.ts +251 -0
- package/extension/factories/planMode.ts +8 -5
- package/extension/factories/planReview.ts +233 -12
- package/extension/index.ts +19 -0
- package/extension/substrate/config.ts +9 -5
- package/extension/substrate/git.ts +38 -0
- package/extension/substrate/toolGating.ts +8 -0
- package/extension/substrate/unifiedDiff.ts +224 -0
- package/package.json +2 -1
- package/prompts/_fixtures/live.yaml +13 -0
- package/prompts/commit-and-compact.md +7 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -1
- package/prompts/contexts/adapters/plannotator-plan.md +6 -1
- package/prompts/contexts/gist-authoring.md +22 -0
- package/prompts/stages/gist-author/seed.md +10 -0
- package/prompts/stages/gist-save.md +9 -0
- package/shared/bindings.yaml +3 -0
- package/shared/contracts.md +215 -29
- package/shared/registry.yaml +31 -1
package/shared/contracts.md
CHANGED
|
@@ -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.
|
|
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),
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
|
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.
|
|
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
|
|
3004
|
-
`perk:objective`, `perk:objective-node`): ok; otherwise warn listing the missing
|
|
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
|
|
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
|
|
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
|
|
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
|
-
(`
|
|
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
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
`
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
phase
|
|
3923
|
-
|
|
3924
|
-
|
|
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.
|
|
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.
|
package/shared/registry.yaml
CHANGED
|
@@ -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
|