@mgiles/perk 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +68 -44
  2. package/extension/adapters/planAdapterPlannotator.ts +27 -41
  3. package/extension/adapters/planAdapterTombell.ts +15 -28
  4. package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
  5. package/extension/checkpoints/checkpoints.ts +19 -12
  6. package/extension/doors/address.ts +4 -4
  7. package/extension/doors/askUser.ts +12 -8
  8. package/extension/doors/ciExecutor.ts +21 -14
  9. package/extension/doors/hunkHandoff.ts +202 -0
  10. package/extension/doors/land.ts +31 -9
  11. package/extension/doors/learn.ts +2 -2
  12. package/extension/doors/learnFactory.ts +144 -0
  13. package/extension/doors/plannotatorHandoff.ts +509 -0
  14. package/extension/doors/prReview.ts +4 -4
  15. package/extension/doors/prReviewBrowser.ts +341 -0
  16. package/extension/doors/prReviewTerminal.ts +267 -0
  17. package/extension/doors/selfcheck.ts +238 -5
  18. package/extension/doors/submit.ts +20 -0
  19. package/extension/doors/submitPrReview.ts +408 -0
  20. package/extension/factories/objective.ts +15 -5
  21. package/extension/factories/objectiveAuthor.ts +15 -32
  22. package/extension/factories/objectiveDraft.ts +1 -1
  23. package/extension/factories/objectivePlan.ts +12 -10
  24. package/extension/factories/objectiveSave.ts +2 -2
  25. package/extension/factories/planMode.ts +22 -40
  26. package/extension/factories/planReview.ts +213 -191
  27. package/extension/factories/planSave.ts +7 -7
  28. package/extension/index.ts +83 -25
  29. package/extension/substrate/bindingDelivery.ts +32 -10
  30. package/extension/substrate/bindings.ts +4 -2
  31. package/extension/substrate/cache.ts +34 -7
  32. package/extension/substrate/clipboard.ts +81 -0
  33. package/extension/substrate/config.ts +88 -65
  34. package/extension/substrate/git.ts +43 -0
  35. package/extension/substrate/paths.ts +1 -1
  36. package/extension/substrate/prompts.ts +2 -2
  37. package/extension/substrate/providers.ts +62 -8
  38. package/extension/substrate/sessionPointers.ts +35 -6
  39. package/extension/substrate/structuredOutput.ts +3 -1
  40. package/extension/substrate/terminalLaunch.ts +178 -0
  41. package/extension/substrate/toolGating.ts +330 -79
  42. package/extension/substrate/toolParams.ts +7 -0
  43. package/extension/substrate/workflowState.ts +54 -2
  44. package/extension/surfaces/footerProvider.ts +8 -4
  45. package/extension/surfaces/surfaces.ts +330 -12
  46. package/extension/vendor/btw/btw.ts +10 -0
  47. package/extension/worker/readOnlySession.ts +19 -6
  48. package/extension/worker/worker.ts +77 -7
  49. package/extension/workerMain.ts +12 -13
  50. package/package.json +3 -3
  51. package/prompts/_fixtures/live.yaml +117 -2
  52. package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
  53. package/prompts/contexts/adapters/plannotator-objective.md +7 -0
  54. package/prompts/contexts/adapters/plannotator-plan.md +6 -0
  55. package/prompts/contexts/adapters/tombell-plan.md +17 -0
  56. package/prompts/contexts/objective-authoring.md +20 -0
  57. package/prompts/contexts/plan-authoring.md +24 -0
  58. package/prompts/contexts/read-only.md +10 -0
  59. package/prompts/stages/conflict-resolution.md +1 -1
  60. package/prompts/stages/learn-code.md +1 -1
  61. package/prompts/stages/learn-docs.md +2 -2
  62. package/prompts/stages/learn-orchestrate.md +1 -1
  63. package/prompts/stages/objective-author/adopt.md +1 -1
  64. package/prompts/stages/objective-author/file.md +1 -1
  65. package/prompts/stages/objective-plan/guidance.md +1 -1
  66. package/prompts/stages/objective-plan/seed.md +1 -1
  67. package/prompts/stages/objective-reconcile.md +1 -1
  68. package/prompts/stages/objective-replan.md +1 -1
  69. package/prompts/stages/plan-from/adopt.md +2 -2
  70. package/prompts/stages/plan-from/file.md +2 -2
  71. package/prompts/stages/pr-review-browser/active.md +11 -0
  72. package/prompts/stages/pr-review-browser/foreign.md +11 -0
  73. package/prompts/stages/pr-review-terminal/active.md +12 -0
  74. package/prompts/stages/pr-review-terminal/foreign.md +13 -0
  75. package/prompts/stages/pr-review-terminal/local.md +4 -0
  76. package/prompts/stages/pr-review.md +1 -1
  77. package/prompts/stages/replan.md +2 -2
  78. package/prompts/stages/skills/create-from.md +1 -1
  79. package/prompts/stages/skills/create.md +1 -1
  80. package/prompts/stages/skills/refine.md +1 -1
  81. package/shared/README.md +22 -18
  82. package/shared/bindings.yaml +10 -2
  83. package/shared/contracts-history.md +24 -0
  84. package/shared/contracts.md +1442 -1787
  85. package/shared/providers.yaml +8 -1
  86. package/shared/registry.yaml +7 -8
  87. package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
  88. package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
  89. package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
  90. package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
  91. package/extension/doors/learnCode.ts +0 -100
  92. package/extension/doors/learnDocs.ts +0 -100
  93. package/extension/doors/prReviewLocal.ts +0 -229
@@ -16,7 +16,8 @@
16
16
  # id — the stable provider id. NOT the cache.plan-ref `provider` string:
17
17
  # that field is the ISSUE BACKEND (the stamped `backend_id`, e.g.
18
18
  # "github"), not the seam id — see contracts.md §8.10 / §8.21.
19
- # seam — `plan` | `todo` | `askuser` | `footer` | `web` (the five seams pluggability-taxonomy.md scope-fenced).
19
+ # seam — `plan` | `todo` | `askuser` | `footer` | `web` (the five seams; see
20
+ # pluggability-taxonomy.md for the scope fence).
20
21
  # package — the foreign Pi package spec added to .pi/settings.json `packages`
21
22
  # (e.g. "npm:@tombell/pi-plan"); `null` for perk's own bundled reference
22
23
  # provider (nothing to add — perk produces the contract natively). NOT
@@ -95,6 +96,12 @@
95
96
  # is pi-web-access-specific (it depends on `fetch_content`'s GitHub-clone path) and is accepted as
96
97
  # lost under a foreign web selection. Fail-safe: any config-read error resolves to `pi-web-access`,
97
98
  # keeping today's behavior exactly.
99
+ #
100
+ # There is NO review seam: the two surface-named review doors (`/pr-review-terminal` — the hunk
101
+ # terminal TUI; `/pr-review-browser` — the plannotator browser UI) are the selection (the command
102
+ # IS the surface pick). The hunk CLI converges unconditionally (init/doctor's best-effort
103
+ # `review-cli` install/verify); the plannotator package rides the plan seam's `plannotator-plan`
104
+ # entry.
98
105
 
99
106
  schema_version: 1
100
107
 
@@ -52,12 +52,11 @@ state_keys:
52
52
  # actual dedicated cold-door routing lives in code via `perk/cli/stages.py` `DEDICATED_STAGES`
53
53
  # (the generic launcher generator skips those ids). The field documents intent; it does not route.
54
54
  # `doors.cold_remote` records which stages a remote runner may run (cli-vs-pi §4.5) —
55
- # a real registry-recorded constraint, not merely "not built yet". P2.T8c flips it
56
- # `true` on `implement` + `address` (the agentic, headless-runnable stages a Phase-3 CI
57
- # worker drives); `plan`/`save` need interactive local exploration and
58
- # `submit`/`land`/`learn` are quick deterministic ops, so they stay `false`. Phase 2
59
- # *builds + resolves* the target (the resolver + this door + the --json descriptor);
60
- # Phase 3 *drives* it. Revisable in Phase 3.
55
+ # a settled design decision, not merely "not built yet". It is `true` exactly on the
56
+ # bounded agentic stages (`implement`, `address`), where the goal is already pinned by
57
+ # a plan or by reviewer feedback. The interactive/exploratory stages (planning above
58
+ # all) and the quick deterministic human-gate ops (`submit`/`land`/`learn`) are
59
+ # local-only by design review, merge, and judgment capture stay where a human is.
61
60
  # ---------------------------------------------------------------------------
62
61
  stages:
63
62
  - id: objective-author
@@ -129,7 +128,7 @@ stages:
129
128
  summary: Do the work on a branch (requires fresh context; cold-only).
130
129
  mode: read-write
131
130
  worktree: create # first stage that needs a branch/worktree (folds in erk's `prepare`)
132
- doors: { warm: false, cold_local: true, cold_remote: true } # P2.T8c: a Phase-3 CI worker can run it
131
+ doors: { warm: false, cold_local: true, cold_remote: true } # bounded agentic stage remotely runnable
133
132
  run_id: { warm: keep, cold_local: mint, cold_remote: mint }
134
133
  command: implement
135
134
  requires: [cache.plan-ref] # T4: cold door derives plan-<pr_id> from the active ref
@@ -155,7 +154,7 @@ stages:
155
154
  summary: Classify PR review feedback (isolated child) and resolve the threads.
156
155
  mode: read-write
157
156
  worktree: reuse
158
- doors: { warm: true, cold_local: true, cold_remote: true } # P2.T8c: a Phase-3 CI worker can run it
157
+ doors: { warm: true, cold_local: true, cold_remote: true } # bounded agentic stage remotely runnable
159
158
  run_id: { warm: keep, cold_local: mint, cold_remote: mint }
160
159
  command: pr address
161
160
  # P2.T7: a spawned read-only child (borrowed pi-subagents) fetches + classifies feedback;
@@ -0,0 +1,66 @@
1
+ {
2
+ "$defs": {
3
+ "ReviewSubmitCommentInput": {
4
+ "additionalProperties": false,
5
+ "description": "One inline comment in the machine-authored submission batch (strict: a typo fails loudly).\n\n`line` is **non-nullable** by design: unanchorable (`line: null`) findings are folded into\nthe review body upstream, during the triage curation \u2014 never submitted as inline comments.",
6
+ "properties": {
7
+ "path": {
8
+ "minLength": 1,
9
+ "title": "Path",
10
+ "type": "string"
11
+ },
12
+ "line": {
13
+ "title": "Line",
14
+ "type": "integer"
15
+ },
16
+ "side": {
17
+ "default": "RIGHT",
18
+ "enum": [
19
+ "LEFT",
20
+ "RIGHT"
21
+ ],
22
+ "title": "Side",
23
+ "type": "string"
24
+ },
25
+ "body": {
26
+ "minLength": 1,
27
+ "title": "Body",
28
+ "type": "string"
29
+ }
30
+ },
31
+ "required": [
32
+ "path",
33
+ "line",
34
+ "body"
35
+ ],
36
+ "title": "ReviewSubmitCommentInput",
37
+ "type": "object"
38
+ }
39
+ },
40
+ "additionalProperties": false,
41
+ "description": "The strict `review-submit` batch shape (`{body, comments?}` \u2014 the event rides the flag).\n\nNo `fyi` field: in-session triage color is structurally unpostable through this door\n(strict extra-forbid rejects it). `comments` stays nullable so an explicit `null` is\ntolerated, normalized to `[]` in conversion.",
42
+ "properties": {
43
+ "body": {
44
+ "default": "",
45
+ "title": "Body",
46
+ "type": "string"
47
+ },
48
+ "comments": {
49
+ "anyOf": [
50
+ {
51
+ "items": {
52
+ "$ref": "#/$defs/ReviewSubmitCommentInput"
53
+ },
54
+ "type": "array"
55
+ },
56
+ {
57
+ "type": "null"
58
+ }
59
+ ],
60
+ "default": null,
61
+ "title": "Comments"
62
+ }
63
+ },
64
+ "title": "ReviewSubmitBatchInput",
65
+ "type": "object"
66
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "additionalProperties": false,
3
+ "description": "The ``--json`` serialization boundary of :class:`ReviewCheckoutResult` (flat; envelope\nkeys first). ``path`` is absolute; the SHAs are full 40-char.",
4
+ "properties": {
5
+ "success": {
6
+ "title": "Success",
7
+ "type": "boolean"
8
+ },
9
+ "error_type": {
10
+ "anyOf": [
11
+ {
12
+ "type": "string"
13
+ },
14
+ {
15
+ "type": "null"
16
+ }
17
+ ],
18
+ "title": "Error Type"
19
+ },
20
+ "message": {
21
+ "anyOf": [
22
+ {
23
+ "type": "string"
24
+ },
25
+ {
26
+ "type": "null"
27
+ }
28
+ ],
29
+ "title": "Message"
30
+ },
31
+ "path": {
32
+ "title": "Path",
33
+ "type": "string"
34
+ },
35
+ "pr": {
36
+ "title": "Pr",
37
+ "type": "integer"
38
+ },
39
+ "url": {
40
+ "title": "Url",
41
+ "type": "string"
42
+ },
43
+ "head_sha": {
44
+ "title": "Head Sha",
45
+ "type": "string"
46
+ },
47
+ "base_sha": {
48
+ "title": "Base Sha",
49
+ "type": "string"
50
+ },
51
+ "base_ref": {
52
+ "title": "Base Ref",
53
+ "type": "string"
54
+ }
55
+ },
56
+ "required": [
57
+ "success",
58
+ "error_type",
59
+ "message",
60
+ "path",
61
+ "pr",
62
+ "url",
63
+ "head_sha",
64
+ "base_sha",
65
+ "base_ref"
66
+ ],
67
+ "title": "PrReviewCheckoutOut",
68
+ "type": "object"
69
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "additionalProperties": false,
3
+ "description": "The ``--json`` serialization boundary of :class:`ReviewCleanupResult` (flat; envelope\nkeys first). ``path`` is absolute.",
4
+ "properties": {
5
+ "success": {
6
+ "title": "Success",
7
+ "type": "boolean"
8
+ },
9
+ "error_type": {
10
+ "anyOf": [
11
+ {
12
+ "type": "string"
13
+ },
14
+ {
15
+ "type": "null"
16
+ }
17
+ ],
18
+ "title": "Error Type"
19
+ },
20
+ "message": {
21
+ "anyOf": [
22
+ {
23
+ "type": "string"
24
+ },
25
+ {
26
+ "type": "null"
27
+ }
28
+ ],
29
+ "title": "Message"
30
+ },
31
+ "pr": {
32
+ "title": "Pr",
33
+ "type": "integer"
34
+ },
35
+ "path": {
36
+ "title": "Path",
37
+ "type": "string"
38
+ },
39
+ "removed": {
40
+ "title": "Removed",
41
+ "type": "boolean"
42
+ }
43
+ },
44
+ "required": [
45
+ "success",
46
+ "error_type",
47
+ "message",
48
+ "pr",
49
+ "path",
50
+ "removed"
51
+ ],
52
+ "title": "PrReviewCleanupOut",
53
+ "type": "object"
54
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "additionalProperties": false,
3
+ "description": "The ``--json`` serialization boundary of the submission result (flat; envelope keys\nfirst). ``event`` is the flag spelling; ``mode`` \u2208\n``validated | review | review_folded | comment_fallback``.",
4
+ "properties": {
5
+ "success": {
6
+ "title": "Success",
7
+ "type": "boolean"
8
+ },
9
+ "error_type": {
10
+ "anyOf": [
11
+ {
12
+ "type": "string"
13
+ },
14
+ {
15
+ "type": "null"
16
+ }
17
+ ],
18
+ "title": "Error Type"
19
+ },
20
+ "message": {
21
+ "anyOf": [
22
+ {
23
+ "type": "string"
24
+ },
25
+ {
26
+ "type": "null"
27
+ }
28
+ ],
29
+ "title": "Message"
30
+ },
31
+ "dry_run": {
32
+ "title": "Dry Run",
33
+ "type": "boolean"
34
+ },
35
+ "pr": {
36
+ "title": "Pr",
37
+ "type": "integer"
38
+ },
39
+ "event": {
40
+ "title": "Event",
41
+ "type": "string"
42
+ },
43
+ "mode": {
44
+ "title": "Mode",
45
+ "type": "string"
46
+ },
47
+ "comment_count": {
48
+ "title": "Comment Count",
49
+ "type": "integer"
50
+ }
51
+ },
52
+ "required": [
53
+ "success",
54
+ "error_type",
55
+ "message",
56
+ "dry_run",
57
+ "pr",
58
+ "event",
59
+ "mode",
60
+ "comment_count"
61
+ ],
62
+ "title": "PrReviewSubmitOut",
63
+ "type": "object"
64
+ }
@@ -1,100 +0,0 @@
1
- // hop-2 — the learn-code plan factory's warm transition surface: the `/learn-code` command.
2
- //
3
- // The warm twin of the `perk learn code` cold door (sibling of `/learn-docs`). It DELEGATES the
4
- // gather to the Python plane (`perk learn code --gather --json` via the shared cold-door client
5
- // `runColdDoor` — gate-safe, not subject to the read-only bash allowlist), decodes
6
- // `{ inbox_path, learn_numbers }`, then injects the factory guidance via `pi.sendUserMessage` so
7
- // the model reads the inbox, authors a code plan, and calls `plan_save` with `consumed_learn`. No
8
- // model tool — the model uses the existing `plan_save` tool.
9
- //
10
- // Headless-safe: rich UI is guarded by `ctx.hasUI`; without a UI it logs to stderr and returns
11
- // (the gather still runs so the inbox is materialized, but no turn is driven).
12
-
13
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
15
- import { type ColdJson, runColdDoor, stringField } from "../substrate/coldDoor.ts";
16
- import { registerPerkCommand } from "../substrate/command.ts";
17
- import { render } from "../substrate/prompts.ts";
18
- import { report } from "../surfaces/report.ts";
19
-
20
- /** The decoded `perk learn code --gather --json` payload slice the warm door consumes. */
21
- interface LearnCodeGatherPayload {
22
- inbox_path: string;
23
- /** Opaque string learn-issue ids (GitHub "45", Linear "ENG-45") — §8.21. */
24
- learn_numbers: string[];
25
- }
26
-
27
- /** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. Exported for offline reject-branch tests. */
28
- export function decodeGather(payload: ColdJson): LearnCodeGatherPayload | null {
29
- const inboxPath = stringField(payload, "inbox_path");
30
- const numbers = payload.learn_numbers;
31
- if (inboxPath === undefined) return null;
32
- // String ids are canonical (§8.21); numbers are tolerated + coerced (older envelopes).
33
- if (
34
- !Array.isArray(numbers) ||
35
- !numbers.every((n) => typeof n === "string" || typeof n === "number")
36
- ) {
37
- return null;
38
- }
39
- return { inbox_path: inboxPath, learn_numbers: numbers.map((n) => String(n)) };
40
- }
41
-
42
- /**
43
- * The seed guidance the warm `/learn-code` injects to start the factory loop (the perk-learn-code
44
- * skill pointer rides the skill-binding suffix — not hardcoded here). Pure + exported
45
- * for offline tests.
46
- */
47
- export function learnCodeGuidance(inboxPath: string, learnNumbers: string[]): string {
48
- return render("stages/learn-code.md", {
49
- inbox_path: inboxPath,
50
- num_list: learnNumbers.join(", "),
51
- });
52
- }
53
-
54
- /** Register the warm learn-code door: the `/learn-code` command (no model tool). */
55
- export function registerLearnCode(pi: ExtensionAPI): void {
56
- registerPerkCommand(pi, "learn-code", {
57
- description:
58
- "Start the learn-code plan factory: gather pre-stamped SHOULD_BE_CODE perk:learn issues into " +
59
- "an inbox and author a plan routing each into its real code home.",
60
- handler: async (_args, ctx: ExtensionContext) => {
61
- // Report-only door (no Result type): branch on `errorType` directly (the coldDoor header
62
- // convention). A clean "nothing to route" exits non-zero with error_type=no_learn_issues —
63
- // the client's envelope-aware arm surfaces it gently.
64
- const r = await runColdDoor<LearnCodeGatherPayload>(
65
- pi,
66
- ctx,
67
- ["learn", "code", "--gather", "--json"],
68
- { label: "perk learn code", decode: decodeGather },
69
- );
70
- if (!r.ok) {
71
- if (r.errorType === "no_learn_issues") {
72
- report(
73
- ctx,
74
- "learn-code",
75
- "warning",
76
- "nothing to route into code (no SHOULD_BE_CODE perk:learn issues).",
77
- );
78
- } else {
79
- report(ctx, "learn-code", "error", `gather failed: ${r.message}`);
80
- }
81
- return;
82
- }
83
-
84
- if (!ctx.hasUI) {
85
- // Headless can't drive a turn — the inbox is materialized; log and return (fail-safe).
86
- console.error(
87
- `perk: /learn-code invoked (headless) — gathered ${r.data.learn_numbers.length} ` +
88
- `learn issue(s) into ${r.data.inbox_path}; run interactively to author the code plan.`,
89
- );
90
- return;
91
- }
92
-
93
- report(ctx, "learn-code", "info", `gathered ${r.data.learn_numbers.length} learn issue(s)`);
94
- pi.sendUserMessage(
95
- learnCodeGuidance(r.data.inbox_path, r.data.learn_numbers) +
96
- bindingSuffix(ctx.cwd, "command:learn-code"),
97
- );
98
- },
99
- });
100
- }
@@ -1,100 +0,0 @@
1
- // hop-2 — the learned-docs plan factory's warm transition surface: the `/learn-docs` command.
2
- //
3
- // The warm twin of the `perk learn docs` cold door. It DELEGATES the gather to the Python plane
4
- // (`perk learn docs --gather --json` via the shared cold-door client `runColdDoor` — gate-safe,
5
- // not subject to the read-only bash allowlist), decodes `{ inbox_path, learn_numbers }`, then
6
- // injects the factory guidance via
7
- // `pi.sendUserMessage` so the model reads the inbox, authors a docs plan, and calls `plan_save`
8
- // with `consumed_learn`. No model tool — the model uses the existing `plan_save` tool.
9
- //
10
- // Headless-safe: rich UI is guarded by `ctx.hasUI`; without a UI it logs to stderr and returns
11
- // (the gather still runs so the inbox is materialized, but no turn is driven).
12
-
13
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
15
- import { type ColdJson, runColdDoor, stringField } from "../substrate/coldDoor.ts";
16
- import { registerPerkCommand } from "../substrate/command.ts";
17
- import { render } from "../substrate/prompts.ts";
18
- import { report } from "../surfaces/report.ts";
19
-
20
- /** The decoded `perk learn docs --gather --json` payload slice the warm door consumes. */
21
- interface LearnDocsGatherPayload {
22
- inbox_path: string;
23
- /** Opaque string learn-issue ids (GitHub "45", Linear "ENG-45") — §8.21. */
24
- learn_numbers: string[];
25
- }
26
-
27
- /** Strict decode — the guidance dereferences both fields; `launched` is unconsumed. Exported for offline reject-branch tests. */
28
- export function decodeGather(payload: ColdJson): LearnDocsGatherPayload | null {
29
- const inboxPath = stringField(payload, "inbox_path");
30
- const numbers = payload.learn_numbers;
31
- if (inboxPath === undefined) return null;
32
- // String ids are canonical (§8.21); numbers are tolerated + coerced (older envelopes).
33
- if (
34
- !Array.isArray(numbers) ||
35
- !numbers.every((n) => typeof n === "string" || typeof n === "number")
36
- ) {
37
- return null;
38
- }
39
- return { inbox_path: inboxPath, learn_numbers: numbers.map((n) => String(n)) };
40
- }
41
-
42
- /**
43
- * The seed guidance the warm `/learn-docs` injects to start the factory loop (the perk-learn-docs
44
- * skill pointer rides the skill-binding suffix — not hardcoded here). Pure + exported
45
- * for offline tests.
46
- */
47
- export function learnDocsGuidance(inboxPath: string, learnNumbers: string[]): string {
48
- return render("stages/learn-docs.md", {
49
- inbox_path: inboxPath,
50
- num_list: learnNumbers.join(", "),
51
- });
52
- }
53
-
54
- /** Register the warm learned-docs door: the `/learn-docs` command (no model tool). */
55
- export function registerLearnDocs(pi: ExtensionAPI): void {
56
- registerPerkCommand(pi, "learn-docs", {
57
- description:
58
- "Start the learned-docs plan factory: gather open perk:learn issues into an inbox and author " +
59
- "a docs/learned consolidation plan.",
60
- handler: async (_args, ctx: ExtensionContext) => {
61
- // Report-only door (no Result type): branch on `errorType` directly (the coldDoor header
62
- // convention). A clean "nothing to consolidate" exits non-zero with
63
- // error_type=no_learn_issues — the client's envelope-aware arm surfaces it gently.
64
- const r = await runColdDoor<LearnDocsGatherPayload>(
65
- pi,
66
- ctx,
67
- ["learn", "docs", "--gather", "--json"],
68
- { label: "perk learn docs", decode: decodeGather },
69
- );
70
- if (!r.ok) {
71
- if (r.errorType === "no_learn_issues") {
72
- report(
73
- ctx,
74
- "learn-docs",
75
- "warning",
76
- "nothing to consolidate (no open perk:learn issues).",
77
- );
78
- } else {
79
- report(ctx, "learn-docs", "error", `gather failed: ${r.message}`);
80
- }
81
- return;
82
- }
83
-
84
- if (!ctx.hasUI) {
85
- // Headless can't drive a turn — the inbox is materialized; log and return (fail-safe).
86
- console.error(
87
- `perk: /learn-docs invoked (headless) — gathered ${r.data.learn_numbers.length} ` +
88
- `learn issue(s) into ${r.data.inbox_path}; run interactively to author the docs plan.`,
89
- );
90
- return;
91
- }
92
-
93
- report(ctx, "learn-docs", "info", `gathered ${r.data.learn_numbers.length} learn issue(s)`);
94
- pi.sendUserMessage(
95
- learnDocsGuidance(r.data.inbox_path, r.data.learn_numbers) +
96
- bindingSuffix(ctx.cwd, "command:learn-docs"),
97
- );
98
- },
99
- });
100
- }