codedeck 0.5.2 → 0.6.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 (70) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/commands/doctor.js +24 -6
  3. package/dist/cli/commands/doctor.js.map +1 -1
  4. package/dist/cli/commands/profile.js +7 -2
  5. package/dist/cli/commands/profile.js.map +1 -1
  6. package/dist/cli/commands/ps.js +41 -6
  7. package/dist/cli/commands/ps.js.map +1 -1
  8. package/dist/cli/commands/review.js +122 -0
  9. package/dist/cli/commands/review.js.map +1 -0
  10. package/dist/cli/commands/setup.js +50 -22
  11. package/dist/cli/commands/setup.js.map +1 -1
  12. package/dist/cli/commands/web.js +1 -1
  13. package/dist/cli/index.js +3 -3
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/core/events.js.map +1 -1
  16. package/dist/core/pricing.js +51 -6
  17. package/dist/core/pricing.js.map +1 -1
  18. package/dist/daemon/daemon.js +30 -1
  19. package/dist/daemon/daemon.js.map +1 -1
  20. package/dist/drivers/antigravity/driver.js +105 -2
  21. package/dist/drivers/antigravity/driver.js.map +1 -1
  22. package/dist/drivers/antigravity/parser.js +108 -2
  23. package/dist/drivers/antigravity/parser.js.map +1 -1
  24. package/dist/drivers/claude/parser.js +3 -1
  25. package/dist/drivers/claude/parser.js.map +1 -1
  26. package/dist/drivers/opencode/driver.js +24 -2
  27. package/dist/drivers/opencode/driver.js.map +1 -1
  28. package/dist/drivers/opencode/parser.js +26 -1
  29. package/dist/drivers/opencode/parser.js.map +1 -1
  30. package/dist/open/launchers/opencode.js +1 -1
  31. package/dist/open/launchers/opencode.js.map +1 -1
  32. package/dist/open/runtime.js +8 -0
  33. package/dist/open/runtime.js.map +1 -1
  34. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  35. package/dist/plugin/agents/orchestrator-edit.md +11 -2
  36. package/dist/plugin/agents/orchestrator-read.md +11 -2
  37. package/dist/plugin/agents/orchestrator.md +4 -2
  38. package/dist/plugin/hooks/hooks.json +2 -0
  39. package/dist/plugin/hooks/pane-toggle.ts +13 -0
  40. package/dist/plugin/hooks/register.tsx +246 -0
  41. package/dist/plugin/mods/agents/pane.ts +484 -0
  42. package/dist/plugin/mods/agents/parse.ts +29 -0
  43. package/dist/plugin/mods/agents/types.ts +43 -0
  44. package/dist/plugin/prompts/_partials/spec-gates.md +2 -0
  45. package/dist/plugin/prompts/roles/orchestrator-edit.md +2 -1
  46. package/dist/plugin/prompts/roles/orchestrator-read.md +2 -1
  47. package/dist/plugin/prompts/roles/orchestrator.md +2 -2
  48. package/dist/plugin/skills/tlc-spec-driven/SKILL.md +186 -0
  49. package/dist/plugin/skills/tlc-spec-driven/references/code-analysis.md +98 -0
  50. package/dist/plugin/skills/tlc-spec-driven/references/coding-principles.md +72 -0
  51. package/dist/plugin/skills/tlc-spec-driven/references/context-limits.md +31 -0
  52. package/dist/plugin/skills/tlc-spec-driven/references/design.md +199 -0
  53. package/dist/plugin/skills/tlc-spec-driven/references/discuss.md +159 -0
  54. package/dist/plugin/skills/tlc-spec-driven/references/implement.md +436 -0
  55. package/dist/plugin/skills/tlc-spec-driven/references/lessons.md +115 -0
  56. package/dist/plugin/skills/tlc-spec-driven/references/memory.md +144 -0
  57. package/dist/plugin/skills/tlc-spec-driven/references/specify.md +228 -0
  58. package/dist/plugin/skills/tlc-spec-driven/references/sub-agents.md +147 -0
  59. package/dist/plugin/skills/tlc-spec-driven/references/tasks.md +451 -0
  60. package/dist/plugin/skills/tlc-spec-driven/references/validate.md +355 -0
  61. package/dist/plugin/skills/tlc-spec-driven/scripts/check_commit.py +133 -0
  62. package/dist/plugin/skills/tlc-spec-driven/scripts/lessons.py +412 -0
  63. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_spec.py +284 -0
  64. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_state.py +196 -0
  65. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py +287 -0
  66. package/dist/plugin/statusline.sh +0 -1
  67. package/dist/web/canvas-page.js +82 -82
  68. package/dist/web/review-page.js +1 -2
  69. package/dist/web/review-page.js.map +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ /** @jsx h */
2
+ import type { Register } from "claude-code";
3
+
4
+ // The .js extension on a .ts source is the proven import form here:
5
+ // `npm run build:plugin` copies plugin/ verbatim and the engine loads the .ts
6
+ // file directly, so what resolves is what the engine's loader accepts.
7
+ import { formatPane, paneButtonLabel, selectPane } from "../mods/agents/pane.js";
8
+ import { parseRows } from "../mods/agents/parse.js";
9
+ import type { PaneSnapshot } from "../mods/agents/types.js";
10
+ import { togglePane } from "./pane-toggle.js";
11
+
12
+ // Stable pane id: 1 to 64 letters, digits, "_" or "-". open carries it into
13
+ // e.requestId on the render event, which is how this module tells its own pane
14
+ // from any other: e.id is undefined on that event. Verified live.
15
+ const PANE_ID = "codedeck-agents";
16
+
17
+ // Key of the button drawn above the prompt. It comes back as e.element on the
18
+ // press event, which is how this module tells its own button from every other
19
+ // plugin's. Verified live.
20
+ const BUTTON_KEY = "codedeck-agents-open";
21
+
22
+ // The engine ships no type package for $, so this mirrors only the
23
+ // capabilities the refresh helper calls. Both arguments of process.run are
24
+ // positional: the host implementation destructures { argv, init }, but the
25
+ // module-side proxy packs the positional arguments into that object first, and
26
+ // calling it with the object shape is refused at runtime with "process.run:
27
+ // takes argv, a non-empty list of strings naming the command first". Verified
28
+ // in a live PTY session, see docs/mods.md.
29
+ type Engine$ = {
30
+ env: {
31
+ get(name: string): Promise<string | undefined>;
32
+ };
33
+ process: {
34
+ run(
35
+ argv: string[],
36
+ init?: { cwd?: string; env?: Record<string, string>; stdin?: string; timeoutMs?: number },
37
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }>;
38
+ };
39
+ ui: {
40
+ open(pane: { id: string; width?: number; side?: "right" }): Promise<unknown>;
41
+ close(pane: { id: string }): Promise<unknown>;
42
+ invalidate(target: string): Promise<unknown>;
43
+ };
44
+ };
45
+
46
+ const toggleAgentsPane = async ($: Engine$, isOpen: boolean): Promise<boolean> =>
47
+ togglePane(isOpen, {
48
+ open: () => $.ui.open({ id: PANE_ID, side: "right" }),
49
+ close: () => $.ui.close({ id: PANE_ID }),
50
+ });
51
+
52
+ // Passing $ into a helper is allowed, verified. What the engine refuses is
53
+ // pulling a namespace off it: `const P = $.process` fails to load the module.
54
+ // So refresh takes $ as a parameter and the state register owns travels beside
55
+ // it in this object. paneOpen never leaves register, so it stays a bare let.
56
+ type PaneState = {
57
+ // Last good snapshot; undefined until one refresh has fully succeeded, which
58
+ // is how the pane draws nothing rather than a guess.
59
+ snapshot: PaneSnapshot | undefined;
60
+ inFlight: boolean;
61
+ lastRefreshEndedAt: number;
62
+ // True once session.start saw a CODEDECK_RUN_ID. Cached because ui.render
63
+ // fires on every drawing pass and must not await an env read to decide
64
+ // whether to draw one button.
65
+ hasRun: boolean;
66
+ };
67
+
68
+ const TOOL_REFRESH_GAP_MS = 1500;
69
+
70
+ const refresh = async ($: Engine$, state: PaneState): Promise<void> => {
71
+ // Claim the slot before the first await. Guard and set must not straddle a
72
+ // yield point: with the `$.env.get` read in between, two tool.call firings
73
+ // that interleave in that window both pass the guard and both spawn
74
+ // `codedeck ps`. Nothing between these two lines can yield.
75
+ if (state.inFlight) return;
76
+ state.inFlight = true;
77
+ try {
78
+ // Not process.env: a hooks module runs in a realm with no `process`
79
+ // global at all, and reading it throws
80
+ // `ReferenceError: process is not defined`. The engine's own $.env.get is
81
+ // the door, and it takes a literal name. Verified live: this returns the
82
+ // run id that `codedeck open` exports. The await sits inside the try
83
+ // because the call sites use `void refresh(...)`: a rejection here must
84
+ // land in the catch, not surface as an unhandled rejection.
85
+ const runId = await $.env.get("CODEDECK_RUN_ID");
86
+ if (!runId) return;
87
+ const { exitCode, stdout } = await $.process.run(["codedeck", "ps", "--all", "--json"]);
88
+ if (exitCode !== 0) return;
89
+ const rows = parseRows(stdout);
90
+ if (!rows) return;
91
+ const nextSnapshot = selectPane(rows, runId);
92
+ // Cheap compare: an unchanged list must not invalidate, or the tool.call
93
+ // cadence turns into a redraw storm.
94
+ const drawn = JSON.stringify(state.snapshot);
95
+ state.snapshot = nextSnapshot;
96
+ if (JSON.stringify(nextSnapshot) !== drawn) {
97
+ await $.ui.invalidate("ui.render");
98
+ }
99
+ } catch {
100
+ // $.process.run throws on timeout and on start failure. A thrown hook
101
+ // makes the engine drop the whole drawing, so nothing escapes here: the
102
+ // pane keeps the last good snapshot.
103
+ } finally {
104
+ state.inFlight = false;
105
+ state.lastRefreshEndedAt = Date.now();
106
+ }
107
+ };
108
+
109
+ export const register: Register = (on) => {
110
+ const state: PaneState = {
111
+ snapshot: undefined,
112
+ inFlight: false,
113
+ lastRefreshEndedAt: 0,
114
+ hasRun: false,
115
+ };
116
+ let paneOpen = false;
117
+
118
+ on("session.start", async ($, e, next) => {
119
+ // Not "agents": the engine refuses it with `$.command.register: "/agents"
120
+ // refused: it is the built-in /agents`. "band", "deck" and
121
+ // "codedeck-agents" were each verified free. The description is load
122
+ // bearing: without it the whole session.start hook dies.
123
+ await $.command.register({
124
+ name: "band",
125
+ description: "toggle the codedeck agents pane",
126
+ immediate: true,
127
+ });
128
+ // Only open when there is actually a run behind the pane. A session that
129
+ // loads this plugin by manual --plugin-dir or a global install has no
130
+ // CODEDECK_RUN_ID, and the dock there is permanently empty while eating
131
+ // most of the screen for the whole session. /band stays the escape hatch
132
+ // for anyone who wants the pane anyway.
133
+ const runId = await $.env.get("CODEDECK_RUN_ID");
134
+ state.hasRun = Boolean(runId);
135
+ if (runId) {
136
+ // No width is requested: the engine sizes the dock itself and reports
137
+ // the usable content back as e.props.bodyColumns on every render.
138
+ await $.ui.open({ id: PANE_ID, side: "right" });
139
+ paneOpen = true;
140
+ // Populate before the first turn, or the pane stays blank through the
141
+ // whole opening exchange, which is exactly when the run is dispatched.
142
+ void refresh($, state);
143
+ }
144
+ return next(e);
145
+ });
146
+
147
+ on("command.run", { command: "band" }, async ($) => {
148
+ // The action derives from the current state and the flag flips only after
149
+ // the engine call resolves: if $.ui.close ever rejects, its argument shape
150
+ // is the one call here not yet verified in a PTY session, paneOpen stays
151
+ // true and the next /band retries the close, instead of the flag and the
152
+ // pane desyncing for the rest of the session.
153
+ paneOpen = await toggleAgentsPane($, paneOpen);
154
+ await $.ui.invalidate("ui.render");
155
+ return { text: paneOpen ? "agents pane open" : "agents pane closed" };
156
+ });
157
+
158
+ on("turn.complete", async ($, e, next) => {
159
+ void refresh($, state);
160
+ return next(e);
161
+ });
162
+
163
+ on("tool.call", async ($, e, next) => {
164
+ if (Date.now() - state.lastRefreshEndedAt >= TOOL_REFRESH_GAP_MS) {
165
+ void refresh($, state);
166
+ }
167
+ // The downstream result goes back unchanged: the pane must never alter a
168
+ // tool result.
169
+ return await next(e);
170
+ });
171
+
172
+ on("ui.press", async ($, e, next) => {
173
+ // e.element is the key of the pressed button, so everything else on screen
174
+ // belongs to somebody else. The work sits here rather than in the button's
175
+ // own onPress because this $ is the one the engine hands the press event.
176
+ // A $ captured from a past render does work, verified, but it outlives the
177
+ // event it came from and nothing promises how long.
178
+ if ((e as { element?: string }).element === BUTTON_KEY) {
179
+ paneOpen = await toggleAgentsPane($, paneOpen);
180
+ await $.ui.invalidate("ui.render");
181
+ if (paneOpen) void refresh($, state);
182
+ }
183
+ return await next(e);
184
+ });
185
+
186
+ // Both drawings. The pane carries the canvas; above the prompt there is one
187
+ // button and nothing else, which is all that is left of the rejected design's
188
+ // flat table. ui.render fires once per component, so every gate here is load
189
+ // bearing, and each branch returns exactly one tree element, never an array,
190
+ // or the engine drops the drawing with "ui.render hook skipped: returned the
191
+ // wrong shape".
192
+ on("ui.render", async ($, e, next) => {
193
+ if (e.surface !== "terminal") return await next(e);
194
+ // The pane can be closed from its own close box, which fires no hook.
195
+ // Keep one button above the prompt as the visible toggle for the state
196
+ // transitions this module owns.
197
+ if (e.component === "AbovePrompt") {
198
+ if (!state.hasRun) return await next(e);
199
+ const { Box, Button } = await $.ui.resolve(e, "Box", "Button");
200
+ return (
201
+ <Box flexDirection="column">
202
+ <Button
203
+ key={BUTTON_KEY}
204
+ label={paneButtonLabel(state.snapshot)}
205
+ // Mandatory, and empty on purpose: a Button without onPress makes
206
+ // the engine drop the whole drawing with "returned a Button without
207
+ // an onPress function". Its own argument is the press event, not $,
208
+ // so `async ($) => $.ui.open(...)` here throws. The ui.press hook
209
+ // above does the work.
210
+ onPress={() => {}}
211
+ />
212
+ {await next(e)}
213
+ </Box>
214
+ );
215
+ }
216
+ if (e.component !== "Pane") return await next(e);
217
+ if (e.requestId !== PANE_ID) return await next(e);
218
+ if (state.snapshot === undefined) return await next(e);
219
+ // The usable content size is what the pane reports, never an assumed
220
+ // number. Width comes as props.bodyColumns and height as
221
+ // props.scroll.bodyRows: measured live as 89 and 44 in a 200 column,
222
+ // 50 row terminal. A requested width on $.ui.open is ignored, the engine
223
+ // sizes the dock itself, so reading both off the event is the only way to
224
+ // fit. Without the width there is no safe number to hand formatPane; the
225
+ // height is optional and only caps the drawing.
226
+ const props = (e as { props?: { bodyColumns?: number; scroll?: { bodyRows?: number } } }).props;
227
+ const columns = props?.bodyColumns;
228
+ if (typeof columns !== "number") return await next(e);
229
+ const lines = formatPane(state.snapshot, columns, props?.scroll?.bodyRows);
230
+ if (lines.length === 0) return await next(e);
231
+ // Capitalized tags from the resolve table; a lowercase <text> is refused
232
+ // as "JSX element <text> is not an element".
233
+ const { Box, Text } = await $.ui.resolve(e, "Box", "Text");
234
+ // flexDirection is load bearing: Box lays children out in a row by default,
235
+ // so without it the 51 lines are drawn side by side as narrow columns and
236
+ // the pane comes out as shredded box characters. Verified on screen.
237
+ return (
238
+ <Box flexDirection="column">
239
+ {lines.map((line) => (
240
+ <Text key={line}>{line}</Text>
241
+ ))}
242
+ {await next(e)}
243
+ </Box>
244
+ );
245
+ });
246
+ };