@mgiles/perk 2.0.0 → 2.1.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/substrate/config.ts +9 -5
- package/package.json +1 -1
- package/shared/contracts.md +96 -16
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
14
|
import { parseUserBindings, type SkillBinding } from "./bindings.ts";
|
|
15
|
+
import { mainCheckoutRoot } from "./git.ts";
|
|
15
16
|
import { configFile, localConfigFile } from "./paths.ts";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -344,14 +345,17 @@ export const GITHUB_ISSUE_BACKEND_ID: IssueBackendId = "github";
|
|
|
344
345
|
*
|
|
345
346
|
* Reads ONLY committed `.perk/config.toml` — deliberately not `loadPerkConfig`'s overlay, mirroring
|
|
346
347
|
* the Python committed-only read (the backend decides where canonical durable state is written;
|
|
347
|
-
* a per-user `.perk/local.toml` override would fragment the canonical store).
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
348
|
+
* a per-user `.perk/local.toml` override would fragment the canonical store). The read is
|
|
349
|
+
* anchored to the MAIN checkout via `mainCheckoutRoot` (fail-open: `cwd` outside a git repo),
|
|
350
|
+
* mirroring Python's main-worktree anchoring — a linked worktree's checkout state (detached /
|
|
351
|
+
* stale branch / missing `.perk/`) must never flip a Linear repo's prompt clauses to GitHub.
|
|
352
|
+
* Python (`perk/backends/resolve.py::resolve_issue_backend_id`) is the AUTHORITATIVE validator and
|
|
353
|
+
* **raises** on unknown values; this mirror is fail-safe (absence/unknown/any error → `"github"`)
|
|
354
|
+
* because the TS plane only renders prompts — it never writes canonical issues.
|
|
351
355
|
*/
|
|
352
356
|
export function resolveIssueBackendId(cwd: string): IssueBackendId {
|
|
353
357
|
try {
|
|
354
|
-
const committed = readTomlFile(configFile(cwd));
|
|
358
|
+
const committed = readTomlFile(configFile(mainCheckoutRoot(cwd)));
|
|
355
359
|
const backend = committed.tables.issues?.backend;
|
|
356
360
|
if (backend === "github" || backend === "linear") return backend;
|
|
357
361
|
return GITHUB_ISSUE_BACKEND_ID;
|
package/package.json
CHANGED
package/shared/contracts.md
CHANGED
|
@@ -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)
|
|
@@ -2928,7 +2931,13 @@ team = "ENG" # the Linear team key — required when backend = "linear"
|
|
|
2928
2931
|
`load_committed_issues_backend` / `load_committed_issues_team`; TS: `resolveIssueBackendId` reads
|
|
2929
2932
|
only the committed file). Rationale: the backend decides where canonical durable state
|
|
2930
2933
|
(plan/learn/objective issues) is *written*; a per-user override would fragment the canonical
|
|
2931
|
-
store.
|
|
2934
|
+
store. **Main-checkout anchored, both planes.** Both planes resolve the read root to the **main
|
|
2935
|
+
checkout** (git-common-dir resolution — Python `git.main_worktree_root(repo_root) or repo_root`,
|
|
2936
|
+
TS `mainCheckoutRoot(cwd)`; both fall back to the invocation root outside a git repo), so a
|
|
2937
|
+
linked worktree's checkout state (detached HEAD, a stale branch, a checkout without `.perk/`)
|
|
2938
|
+
can never change where canonical durable state is written. Deliberate consequence: a plan branch
|
|
2939
|
+
that *edits* `[issues]` does not take effect from inside its own worktree — the canonical-store
|
|
2940
|
+
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
2941
|
`[linear] api_key`** (an exported env var wins over the config) — **never** in a committed file.
|
|
2933
2942
|
The config read is local-file-only (`config.load_local_linear_api_key`, the inverse of the
|
|
2934
2943
|
`load_committed_*` readers; fail-soft on malformed TOML — returns `None`, never raised). Two seams
|
|
@@ -3543,6 +3552,71 @@ actor is the human user; these changes make perk's footprint read as native:
|
|
|
3543
3552
|
round-trip is proven lossless (else dropped). Becoming a true Linear **Agent** (`actor=app`) is a
|
|
3544
3553
|
separate, out-of-scope follow-up.
|
|
3545
3554
|
|
|
3555
|
+
**Native-attachment metadata amendment (#1355) — Linear perk metadata rides issue attachments.**
|
|
3556
|
+
Linear-only (**GitHub renders are byte-identical**; the issue-tier protocol reshape below is the
|
|
3557
|
+
one cross-backend change). The five machine metadata blocks — `plan-header`, `learn-header`,
|
|
3558
|
+
`objective-node` (issue-scoped) and `objective-header`, `objective-manifest` (project-scoped) —
|
|
3559
|
+
**no longer render into Linear bodies at all**: each rides a native issue **attachment** with a
|
|
3560
|
+
machine-readable `metadata` envelope. Bodies/overviews are clean human prose. This supersedes the
|
|
3561
|
+
prose-first-composition bullet above (there are no machine blocks left to position), the
|
|
3562
|
+
collapsed-toggle deferral (moot), `_insert_or_replace_manifest` (deleted), and every
|
|
3563
|
+
list-and-parse find scan. A **clean break**: no legacy read fallback — pre-existing Linear
|
|
3564
|
+
artifacts with body-block metadata are simply not found (re-save/re-create them). Still inline in
|
|
3565
|
+
bodies (structural sentinels, not metadata): the `plan-body`/marked-comment markers, the
|
|
3566
|
+
Reconcilable region markers, the `Adopted-from` archive note, and the copyable command callouts.
|
|
3567
|
+
|
|
3568
|
+
- **The envelope** (`perk/backends/linear/attachments.py::encode`): `attachmentCreate` with
|
|
3569
|
+
`metadata: { source: "perk", schema_version: 1, kind: <block key>, payload_json: <JSON fields>,
|
|
3570
|
+
created, title, attributes: {…} }` — `payload_json` is the authoritative field payload (the
|
|
3571
|
+
same fields the body blocks carried); `attributes` duplicates scalars for Linear-side
|
|
3572
|
+
filterability. Cards render human-readable (title = the block kind, subtitle = a salient
|
|
3573
|
+
field). Decode is `find_perk_attachment(nodes, kind=)` — absent → `None` (tolerant),
|
|
3574
|
+
present-but-malformed → raises (fail-loud); `has_perk_attachment` is the presence-only check.
|
|
3575
|
+
- **The URL scheme is the identity** (live-verified: Linear accepts non-resolving URLs, and
|
|
3576
|
+
`attachmentCreate` **upserts by `(url, issueId)` with REPLACE metadata semantics** — every
|
|
3577
|
+
write must carry the complete envelope): `https://perk.invalid/plan/<run_id-or-identifier>`,
|
|
3578
|
+
`/learn/<run_id-or-identifier>`, `/node/<issue identifier>` (carry-path stable),
|
|
3579
|
+
`/objective/<run_id>`, `/manifest/<run_id>`. Writers always **reuse a found attachment's URL**
|
|
3580
|
+
(never re-derive — re-deriving would orphan the existing card).
|
|
3581
|
+
- **O(1) finds via `attachmentsForURL`** (`find_issue_by_attachment_url`): `find_plan_issue` /
|
|
3582
|
+
`find_learn_issue` / `find_objective` are each ONE workspace-wide exact-URL query (no
|
|
3583
|
+
team-scoped label scans). **Open-only parity rule:** the issue-tier finds treat a hit in a
|
|
3584
|
+
terminal state (`completed`/`canceled`) as not-found — parity with the legacy open-only scan,
|
|
3585
|
+
so a landed plan's run_id never resurrects the closed issue. On **multiple** hits (a landed
|
|
3586
|
+
plan's closed issue + an open re-save sharing the URL) the find prefers the first
|
|
3587
|
+
**non-terminal** hit, so the parity filter is deterministic — never at the mercy of the
|
|
3588
|
+
server's node order. The objective find is
|
|
3589
|
+
state-independent by design (its sentinel is born canceled) and takes the project ref from the
|
|
3590
|
+
hit issue's `project` (a header hit with no project raises — a broken sentinel).
|
|
3591
|
+
- **The project metadata sentinel.** Linear exposes no project-attachment mutation, so each
|
|
3592
|
+
perk project carries one **sentinel issue** (`Perk: objective metadata`, empty body, born in
|
|
3593
|
+
the team's canceled state — cosmetic; created **fail-loud** immediately after `projectCreate`,
|
|
3594
|
+
before milestones/node-issues) holding the `objective-header` + `objective-manifest`
|
|
3595
|
+
attachments. Discovery keys on the header **attachment**, never the title. One best-effort
|
|
3596
|
+
`entityExternalLinkCreate` adds it to the project's Resources (fail-open). Readers find it in
|
|
3597
|
+
the same `project_issues` scan they already run (zero extra queries); it is excluded from
|
|
3598
|
+
roadmap reads, engagement, and adoption candidate maps. `update_objective_header` /
|
|
3599
|
+
manifest syncs (`_sync_manifest_*`, `_refresh_manifest_phase_pins`, the drift backfill) are
|
|
3600
|
+
merge-and-upsert against the sentinel's attachments; a sentinel-less project is **not a perk
|
|
3601
|
+
objective** (`get_objective → None`).
|
|
3602
|
+
- **Node-issues + unified plans.** The `objective-node` payload rides a `/node/<identifier>`
|
|
3603
|
+
attachment (descriptions are clean prose); a unified node-issue carries TWO envelopes — node +
|
|
3604
|
+
plan — disambiguated by `kind`. The node→plan backlink derivation is unchanged but now keys on
|
|
3605
|
+
the **plan-header attachment's presence**. Attachments cascade-delete with their issue.
|
|
3606
|
+
- **Accepted create window (issue tier).** Every Linear create is now two writes — `issueCreate`
|
|
3607
|
+
then the identity-carrying attachment upsert — so a crash between them orphans a header-less
|
|
3608
|
+
issue invisible to the URL finds (a retry mints a fresh one; the orphan is human-visible
|
|
3609
|
+
garbage to close). The same accepted one-round-trip window as the metadata sentinel's,
|
|
3610
|
+
now explicit for plan/learn/node creates too.
|
|
3611
|
+
- **The issue-tier protocol reshape (all backends).** `create_plan_issue(title, header_fields,
|
|
3612
|
+
run_id, dry_run)` replaces the pre-rendered `body` param — the backend owns the header carrier
|
|
3613
|
+
(GitHub renders the body block itself, byte-identical; Linear creates a clean empty body + the
|
|
3614
|
+
attachment). Two additive fields: `LearnIssueSummary.header: LearnHeader | None` (decoded
|
|
3615
|
+
backend-side; GitHub parses the body, Linear the attachment — degrade-to-None either way) and
|
|
3616
|
+
`AdoptableIssue.already_plan: bool` (backend-decided — GitHub `has_metadata_block(body,
|
|
3617
|
+
plan-header)`, Linear the plan-header attachment), consumed by `plan from`'s `already_a_plan`
|
|
3618
|
+
refusal.
|
|
3619
|
+
|
|
3546
3620
|
## §8.25 · The human-engagement read contract (Objective #682, Node 1.2)
|
|
3547
3621
|
|
|
3548
3622
|
A backend-neutral **READ** surface for human engagement — comments, description edits, and
|
|
@@ -3806,14 +3880,17 @@ are verbatim human content". A normally-authored plan leaves it `None`.
|
|
|
3806
3880
|
`plan-header` block additively into the issue **body** (human prose preserved verbatim, **title
|
|
3807
3881
|
untouched**); (c) idempotently prepend the `perk impl <id>` callout above the body; (d) upsert
|
|
3808
3882
|
the `plan-body` comment carrying the authored markdown. Returns `IssueRef(existed=True)`.
|
|
3809
|
-
Idempotent on re-save; GitHub stamps HTML-encoded
|
|
3883
|
+
Idempotent on re-save; GitHub stamps the body block HTML-encoded; Linear upserts the
|
|
3884
|
+
plan-header **attachment** instead — the human body stays verbatim apart from the callout
|
|
3885
|
+
(the §8.24 native-attachment metadata amendment).
|
|
3810
3886
|
|
|
3811
3887
|
**The cold door (`perk plan from <issue>`).** A dedicated launcher verb in the `plan` hybrid group
|
|
3812
3888
|
(mirrors `replan`/`resume`; `from` is a valid Click command string). It performs every Linear/GitHub
|
|
3813
3889
|
read up front (the read-only plan-mode session has no `gh`/Linear access), then re-launches the
|
|
3814
3890
|
`plan` stage seeded to author a plan over the materialized source. It **refuses** when: the issue is
|
|
3815
3891
|
not found (`adopt_not_found`), not OPEN (`adopt_not_open`), or already a perk plan
|
|
3816
|
-
(`
|
|
3892
|
+
(`AdoptableIssue.already_plan` — backend-decided: GitHub the body block, Linear the plan-header
|
|
3893
|
+
attachment → `already_a_plan`, hinting `perk plan replan <id>`).
|
|
3817
3894
|
Engagement is read fail-soft (`render_adopted_engagement` → `<untrusted_adopted_issue_engagement>`;
|
|
3818
3895
|
`IssueBackendError` → omitted). The source is materialized to `scratch/adopt-<issue_id>.md` (title +
|
|
3819
3896
|
body wrapped in `<untrusted_adopted_issue>` + the optional engagement block). A **fresh** `run_id`
|
|
@@ -3910,18 +3987,21 @@ reconcile. Mapped issues' titles/bodies are independently preserved verbatim by
|
|
|
3910
3987
|
`perk objective plan <n>` callout prepended), backfill `objective_comment_id`. `adopt_map` is
|
|
3911
3988
|
ignored (no child issues).
|
|
3912
3989
|
- **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
|
-
|
|
3990
|
+
`project_issues` selecting `title` too; both selections now also carry the attachment nodes —
|
|
3991
|
+
the §8.24 native-attachment metadata amendment).
|
|
3992
|
+
`read_objective_source` → the project overview `content` + its issues (the metadata sentinel
|
|
3993
|
+
excluded). `adopt_source_as_objective` composes the new overview preserving the original
|
|
3994
|
+
verbatim (`to_linear_markdown(` Reconcilable(`<model prose>`) +
|
|
3995
|
+
`render_adopted_overview_note(<original overview>)` below the markers `)`), `update_project
|
|
3996
|
+
_content` (in place, NOT `create_project`), prepends the callout; the
|
|
3997
|
+
`objective-header`(`adopted_from=source_id`) + `objective-manifest` ride a fresh metadata
|
|
3998
|
+
sentinel's attachments (the §8.24 native-attachment metadata amendment); one milestone per
|
|
3999
|
+
phase via `ensure_phase_milestone` seeded from `project_milestones` (de-dupe against existing);
|
|
4000
|
+
for each node in `node_sort_key` order a **mapped** node upserts the `objective-node`
|
|
4001
|
+
attachment onto the existing issue (title/body verbatim — no description write — +
|
|
4002
|
+
`perk:objective-node` label added + phase-milestone attach), an **unmapped** node mints a fresh
|
|
4003
|
+
node-issue; blocking relations per explicit `depends_on`. Raises on an `adopt_issue` id not in
|
|
4004
|
+
the project (fail-loud). Idempotent on `run_id`.
|
|
3925
4005
|
- **Issue-backed Linear (dormant):** both `read_objective_source` and `adopt_source_as_objective`
|
|
3926
4006
|
return `None` (honest no-op; keeps `ty` green).
|
|
3927
4007
|
|