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.
- package/README.md +2 -1
- package/dist/cli/commands/doctor.js +24 -6
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/profile.js +7 -2
- package/dist/cli/commands/profile.js.map +1 -1
- package/dist/cli/commands/ps.js +41 -6
- package/dist/cli/commands/ps.js.map +1 -1
- package/dist/cli/commands/review.js +122 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/commands/setup.js +50 -22
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/web.js +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/events.js.map +1 -1
- package/dist/core/pricing.js +51 -6
- package/dist/core/pricing.js.map +1 -1
- package/dist/daemon/daemon.js +30 -1
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/drivers/antigravity/driver.js +105 -2
- package/dist/drivers/antigravity/driver.js.map +1 -1
- package/dist/drivers/antigravity/parser.js +108 -2
- package/dist/drivers/antigravity/parser.js.map +1 -1
- package/dist/drivers/claude/parser.js +3 -1
- package/dist/drivers/claude/parser.js.map +1 -1
- package/dist/drivers/opencode/driver.js +24 -2
- package/dist/drivers/opencode/driver.js.map +1 -1
- package/dist/drivers/opencode/parser.js +26 -1
- package/dist/drivers/opencode/parser.js.map +1 -1
- package/dist/open/launchers/opencode.js +1 -1
- package/dist/open/launchers/opencode.js.map +1 -1
- package/dist/open/runtime.js +8 -0
- package/dist/open/runtime.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/orchestrator-edit.md +11 -2
- package/dist/plugin/agents/orchestrator-read.md +11 -2
- package/dist/plugin/agents/orchestrator.md +4 -2
- package/dist/plugin/hooks/hooks.json +2 -0
- package/dist/plugin/hooks/pane-toggle.ts +13 -0
- package/dist/plugin/hooks/register.tsx +246 -0
- package/dist/plugin/mods/agents/pane.ts +484 -0
- package/dist/plugin/mods/agents/parse.ts +29 -0
- package/dist/plugin/mods/agents/types.ts +43 -0
- package/dist/plugin/prompts/_partials/spec-gates.md +2 -0
- package/dist/plugin/prompts/roles/orchestrator-edit.md +2 -1
- package/dist/plugin/prompts/roles/orchestrator-read.md +2 -1
- package/dist/plugin/prompts/roles/orchestrator.md +2 -2
- package/dist/plugin/skills/tlc-spec-driven/SKILL.md +186 -0
- package/dist/plugin/skills/tlc-spec-driven/references/code-analysis.md +98 -0
- package/dist/plugin/skills/tlc-spec-driven/references/coding-principles.md +72 -0
- package/dist/plugin/skills/tlc-spec-driven/references/context-limits.md +31 -0
- package/dist/plugin/skills/tlc-spec-driven/references/design.md +199 -0
- package/dist/plugin/skills/tlc-spec-driven/references/discuss.md +159 -0
- package/dist/plugin/skills/tlc-spec-driven/references/implement.md +436 -0
- package/dist/plugin/skills/tlc-spec-driven/references/lessons.md +115 -0
- package/dist/plugin/skills/tlc-spec-driven/references/memory.md +144 -0
- package/dist/plugin/skills/tlc-spec-driven/references/specify.md +228 -0
- package/dist/plugin/skills/tlc-spec-driven/references/sub-agents.md +147 -0
- package/dist/plugin/skills/tlc-spec-driven/references/tasks.md +451 -0
- package/dist/plugin/skills/tlc-spec-driven/references/validate.md +355 -0
- package/dist/plugin/skills/tlc-spec-driven/scripts/check_commit.py +133 -0
- package/dist/plugin/skills/tlc-spec-driven/scripts/lessons.py +412 -0
- package/dist/plugin/skills/tlc-spec-driven/scripts/validate_spec.py +284 -0
- package/dist/plugin/skills/tlc-spec-driven/scripts/validate_state.py +196 -0
- package/dist/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py +287 -0
- package/dist/plugin/statusline.sh +0 -1
- package/dist/web/canvas-page.js +82 -82
- package/dist/web/review-page.js +1 -2
- package/dist/web/review-page.js.map +1 -1
- 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
|
+
};
|