@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
@@ -0,0 +1,178 @@
1
+ // The terminal review door's R7 terminal auto-launch ladder — open hunk in a terminal the human can
2
+ // see, since hunk's TUI is constitutively a human surface (a `pi.exec` child gets pipes, no TTY).
3
+ //
4
+ // A pure `resolveTerminalLaunch` (platform + env + request → an argv + the rung tag, or `null` for
5
+ // "no launcher matched") and a thin fail-soft `launchInTerminal` runner. Never throws, never
6
+ // notifies — the caller owns messaging, and the loud print + clipboard copy are the universal
7
+ // fallback when no rung matches or a rung fails. The `hunk session get` handshake — not a spawn
8
+ // success — remains the ONLY verification the surface is actually up.
9
+ //
10
+ // The `PERK_TERMINAL_LAUNCH` env seam: unset → the platform default ladder; empty → disabled (the
11
+ // test seam); non-empty → a custom launcher receiving the worktree as `$1` and the command as
12
+ // `$2`.
13
+
14
+ import type { ExecHost } from "./coldDoor.ts";
15
+
16
+ /** Which rung of the ladder launched (or would launch) the surface. */
17
+ export type LaunchVia = "custom" | "tmux" | "ghostty" | "iterm2" | "terminal-app";
18
+
19
+ /** A human-readable name for each rung, for the door's launched-in-a-<surface> message. */
20
+ export const LAUNCH_SURFACE: Record<LaunchVia, string> = {
21
+ custom: "terminal window",
22
+ tmux: "tmux pane",
23
+ ghostty: "Ghostty window",
24
+ iterm2: "iTerm2 window",
25
+ "terminal-app": "terminal window",
26
+ };
27
+
28
+ // The macOS AppleScript rungs are `on run argv` bodies: paths + commands ride argv (never
29
+ // interpolated into the script text), so no AppleScript string-quoting hazard exists.
30
+
31
+ /** Single-quote `s` for a POSIX shell line (the standard `'\''` embedded-quote escape). */
32
+ function shQuote(s: string): string {
33
+ return `'${s.replaceAll("'", `'\\''`)}'`;
34
+ }
35
+
36
+ /**
37
+ * Wrap `command` in the human's interactive **login shell** (`$SHELL -i -l -c '<command>'`) for
38
+ * the rungs whose execution context is rc-less — Ghostty's surface `command` is argv-exec'd
39
+ * (quote-aware word split, a relative arg0 joined onto the working directory, never a shell
40
+ * line), and tmux commands run under the server environment. `-i -l` sources the human's rc
41
+ * files, so the launched window resolves binaries exactly like the human's own terminal — which
42
+ * is what the bare command needs: `hunk` AND the `node` its `#!/usr/bin/env node` shebang
43
+ * re-resolves are often on PATH only via rc activation (mise/nvm; both misses were hit live).
44
+ * `$SHELL` when absolute, else `/bin/zsh` on darwin (the macOS default) / `/bin/sh` elsewhere.
45
+ * The shell-line rungs (iTerm2/Terminal.app) type into an interactive login shell already and
46
+ * take the bare command; so does the custom launcher (it owns its own environment).
47
+ */
48
+ function interactiveShellWrap(
49
+ platform: string,
50
+ env: Record<string, string | undefined>,
51
+ command: string,
52
+ ): string {
53
+ const shell =
54
+ env.SHELL !== undefined && env.SHELL.startsWith("/")
55
+ ? env.SHELL
56
+ : platform === "darwin"
57
+ ? "/bin/zsh"
58
+ : "/bin/sh";
59
+ return `${shell} -i -l -c ${shQuote(command)}`;
60
+ }
61
+
62
+ /** Ghostty ≥ 1.3: a native surface configuration (cwd + command as argv items 1/2). */
63
+ const GHOSTTY_SCRIPT = `on run argv
64
+ tell application "Ghostty"
65
+ set cfg to (new surface configuration)
66
+ set (initial working directory of cfg) to (item 1 of argv)
67
+ set (command of cfg) to (item 2 of argv)
68
+ set (wait after command of cfg) to true
69
+ new window with configuration cfg
70
+ activate
71
+ end tell
72
+ end run`;
73
+
74
+ /** iTerm2: a new window whose current session runs the composed shell line (argv item 1). */
75
+ const ITERM_SCRIPT = `on run argv
76
+ tell application "iTerm"
77
+ set w to (create window with default profile)
78
+ tell current session of w to write text (item 1 of argv)
79
+ activate
80
+ end tell
81
+ end run`;
82
+
83
+ /** Terminal.app: the universal macOS fallback — `do script` the composed shell line (argv item 1). */
84
+ const TERMINAL_SCRIPT = `on run argv
85
+ tell application "Terminal"
86
+ do script (item 1 of argv)
87
+ activate
88
+ end tell
89
+ end run`;
90
+
91
+ export interface LaunchRequest {
92
+ /** The review worktree — the terminal's working directory. */
93
+ cwd: string;
94
+ /**
95
+ * The bare surface command to run there, e.g. `hunk diff <sha12>`. The rc-less rungs wrap it
96
+ * in the human's interactive login shell (see `interactiveShellWrap`).
97
+ */
98
+ command: string;
99
+ }
100
+
101
+ /**
102
+ * Resolve the terminal-launch argv + rung tag for `platform` + `env` + `req`, first match wins:
103
+ *
104
+ * 1. `PERK_TERMINAL_LAUNCH === ""` → `null` (the test seam / opt-out);
105
+ * 2. `PERK_TERMINAL_LAUNCH` non-empty → a custom `sh -c` launcher (worktree `$1`, command `$2`);
106
+ * 3. `TMUX` set → a `tmux split-window` pane beside pi (no consent prompts);
107
+ * 4. `darwin`, keyed off `TERM_PROGRAM` → the matching osascript rung (ghostty / iterm2 /
108
+ * terminal-app; anything unrecognized — vscode, WezTerm, absent — falls to terminal-app, the
109
+ * universal macOS fallback);
110
+ * 5. otherwise → `null` (no Linux emulator sniffing — tmux + the custom seam cover it).
111
+ *
112
+ * Rungs that set the working directory natively (ghostty's `initial working directory`, tmux's
113
+ * `-c`) receive the command wrapped in the human's interactive login shell (see
114
+ * `interactiveShellWrap` — their execution contexts are rc-less); the shell-line rungs
115
+ * (iterm2/terminal-app) receive `cd '<cwd>' && <command>` as one arg (the cwd single-quoted —
116
+ * worktree paths can carry spaces; the command is perk-composed, never quoted) typed into an
117
+ * interactive login shell the terminal itself opens.
118
+ */
119
+ export function resolveTerminalLaunch(
120
+ platform: string,
121
+ env: Record<string, string | undefined>,
122
+ req: LaunchRequest,
123
+ ): { argv: string[]; via: LaunchVia } | null {
124
+ const custom = env.PERK_TERMINAL_LAUNCH;
125
+ if (custom !== undefined) {
126
+ if (custom === "") return null;
127
+ return { argv: ["sh", "-c", custom, "sh", req.cwd, req.command], via: "custom" };
128
+ }
129
+
130
+ if (env.TMUX !== undefined && env.TMUX !== "") {
131
+ const wrapped = interactiveShellWrap(platform, env, req.command);
132
+ return { argv: ["tmux", "split-window", "-h", "-c", req.cwd, wrapped], via: "tmux" };
133
+ }
134
+
135
+ if (platform === "darwin") {
136
+ // The shell-line rungs run this through the terminal's shell — quote the cwd (paths can
137
+ // carry spaces/quotes); the command is perk-composed (`hunk diff <sha12>`), never quoted.
138
+ const shellLine = `cd ${shQuote(req.cwd)} && ${req.command}`;
139
+ const term = env.TERM_PROGRAM;
140
+ if (term === "ghostty") {
141
+ const wrapped = interactiveShellWrap(platform, env, req.command);
142
+ return { argv: ["osascript", "-e", GHOSTTY_SCRIPT, req.cwd, wrapped], via: "ghostty" };
143
+ }
144
+ if (term === "iTerm.app") {
145
+ return { argv: ["osascript", "-e", ITERM_SCRIPT, shellLine], via: "iterm2" };
146
+ }
147
+ // Apple_Terminal, vscode, WezTerm, or absent → Terminal.app (the universal macOS fallback).
148
+ return { argv: ["osascript", "-e", TERMINAL_SCRIPT, shellLine], via: "terminal-app" };
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ /**
155
+ * Launch the surface command in a terminal the human can see, best-effort. Resolves the rung from
156
+ * `process.platform` + `process.env`; a `null` resolution (disabled seam, unmatched platform)
157
+ * short-circuits to `{launched: false}`. Otherwise shells the argv with a 15s cap (guards an
158
+ * unanswered macOS Automation/TCC consent dialog blocking `osascript`). Fail-soft on
159
+ * throw/nonzero/killed — a denied or unanswered consent is just another failed rung, and the
160
+ * print+clipboard fallback already covers the human. Never rejects, never notifies.
161
+ */
162
+ export async function launchInTerminal(
163
+ pi: ExecHost,
164
+ ctx: { cwd: string; signal?: AbortSignal },
165
+ req: LaunchRequest,
166
+ ): Promise<{ launched: boolean; via?: LaunchVia }> {
167
+ const resolved = resolveTerminalLaunch(process.platform, process.env, req);
168
+ if (resolved === null) return { launched: false };
169
+ const [cmd, ...rest] = resolved.argv;
170
+ if (cmd === undefined) return { launched: false };
171
+ try {
172
+ const res = await pi.exec(cmd, rest, { cwd: ctx.cwd, signal: ctx.signal, timeout: 15_000 });
173
+ if (res.killed || res.code !== 0) return { launched: false };
174
+ return { launched: true, via: resolved.via };
175
+ } catch {
176
+ return { launched: false };
177
+ }
178
+ }