codedeck 0.5.2 → 0.5.3

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