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.
- package/README.md +1 -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/setup.js +50 -22
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/web.js +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/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/register.tsx +251 -0
- package/dist/plugin/mods/agents/pane.ts +453 -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/package.json +1 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// Canvas layer of the orchestrator agents pane.
|
|
2
|
+
//
|
|
3
|
+
// The `codedeck web` canvas, drawn as ASCII boxes for a docked terminal pane.
|
|
4
|
+
// Two pure functions behind one frozen interface: selectPane narrows the
|
|
5
|
+
// daemon's full session list to one run, formatPane turns that snapshot into
|
|
6
|
+
// the lines of the drawing and fits them to the height the pane was given.
|
|
7
|
+
// The engine draws them inside a render hook, where a throw drops the whole
|
|
8
|
+
// pane, so nothing here throws and nothing here reaches for the engine.
|
|
9
|
+
//
|
|
10
|
+
// Every line comes out exactly `columns` code units wide so the frame never
|
|
11
|
+
// looks ragged, and no line ever carries a line break, a tab or a control
|
|
12
|
+
// character: everything the daemon supplied is sanitised and clipped first.
|
|
13
|
+
// The filtering and ordering rules are carried across from the deleted
|
|
14
|
+
// select.ts; the sanitising and surrogate-safe clipping from format.ts.
|
|
15
|
+
|
|
16
|
+
import type { PaneRow, PaneSnapshot, SessionRow } from "./types.js";
|
|
17
|
+
|
|
18
|
+
/** Cards never stretch past this, however wide the dock is. */
|
|
19
|
+
export const MAX_CARD_COLUMNS = 56;
|
|
20
|
+
|
|
21
|
+
/** Keep finished work useful without turning the pane into a transcript. */
|
|
22
|
+
const HISTORY_PREVIEW_ROWS = 3;
|
|
23
|
+
|
|
24
|
+
/** Below this many usable columns the pane draws nothing rather than garbage. */
|
|
25
|
+
export const MIN_COLUMNS = 24;
|
|
26
|
+
|
|
27
|
+
/** Placeholder for a cell the daemon did not supply. Rule 13. */
|
|
28
|
+
const EMPTY_CELL = "-";
|
|
29
|
+
|
|
30
|
+
// Control characters and the line-terminator separators would break the frame,
|
|
31
|
+
// so they become spaces before anything is drawn. Carried across verbatim from
|
|
32
|
+
// format.ts: the C0 block (which holds \n, \r, \t, \v, \f and the ESC CSI
|
|
33
|
+
// sequences), DEL, the C1 block, and U+2028 / U+2029.
|
|
34
|
+
const CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
35
|
+
|
|
36
|
+
// Status buckets, matching the web canvas. working and starting are "working";
|
|
37
|
+
// needs_input is "waiting for you"; everything else is finished.
|
|
38
|
+
const LIVE = new Set(["working", "starting"]);
|
|
39
|
+
const WAIT = new Set(["needs_input"]);
|
|
40
|
+
|
|
41
|
+
// Vocabulary for the drawing. Unknown statuses and harnesses fall back to the
|
|
42
|
+
// raw value or a generic glyph, never to undefined. These maps are keyed by
|
|
43
|
+
// daemon-supplied strings, so they get a null prototype: a plain `{}` would
|
|
44
|
+
// resolve an agent or status of "constructor" through Object.prototype and draw
|
|
45
|
+
// a function's source into the card, where the `??` fallback never fires.
|
|
46
|
+
function vocab(entries: Record<string, string>): Record<string, string> {
|
|
47
|
+
return Object.assign(Object.create(null) as Record<string, string>, entries);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const STATUS_DOT = vocab({
|
|
51
|
+
working: "●",
|
|
52
|
+
starting: "●",
|
|
53
|
+
needs_input: "◉",
|
|
54
|
+
});
|
|
55
|
+
const STATUS_WORD = vocab({
|
|
56
|
+
working: "Working now",
|
|
57
|
+
starting: "Starting",
|
|
58
|
+
needs_input: "Waiting for you",
|
|
59
|
+
completed: "Completed",
|
|
60
|
+
stopped: "Paused",
|
|
61
|
+
failed: "Failed",
|
|
62
|
+
});
|
|
63
|
+
const HARNESS_GLYPH = vocab({
|
|
64
|
+
claude: "▲",
|
|
65
|
+
opencode: "■",
|
|
66
|
+
codex: "◆",
|
|
67
|
+
omp: "⬟",
|
|
68
|
+
});
|
|
69
|
+
const HARNESS_LABEL = vocab({
|
|
70
|
+
claude: "Claude",
|
|
71
|
+
opencode: "OpenCode",
|
|
72
|
+
codex: "Codex",
|
|
73
|
+
omp: "OMP",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function timestamp(row: SessionRow): number {
|
|
77
|
+
return typeof row.updatedAt === "string" ? Date.parse(row.updatedAt) : Number.NaN;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Newest first; a row with no parseable updatedAt sorts last; ties break by id
|
|
81
|
+
// so the order is total and the pane does not flicker between refreshes.
|
|
82
|
+
function compareRows(a: SessionRow, b: SessionRow): number {
|
|
83
|
+
const ta = timestamp(a);
|
|
84
|
+
const tb = timestamp(b);
|
|
85
|
+
const hasA = !Number.isNaN(ta);
|
|
86
|
+
const hasB = !Number.isNaN(tb);
|
|
87
|
+
if (hasA !== hasB) return hasA ? -1 : 1;
|
|
88
|
+
if (hasA && ta !== tb) return tb - ta;
|
|
89
|
+
if (a.id !== b.id) return a.id < b.id ? -1 : 1;
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// An omitted or non-finite budget means "keep everything": the default cut is
|
|
94
|
+
// formatPane's job now, because only it knows the pane height.
|
|
95
|
+
function budgetLimit(budget: number | undefined): number {
|
|
96
|
+
if (typeof budget !== "number" || !Number.isFinite(budget)) return Number.POSITIVE_INFINITY;
|
|
97
|
+
return Math.max(0, Math.trunc(budget));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function text(value: unknown): string {
|
|
101
|
+
if (typeof value !== "string") return "";
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toPaneRow(row: SessionRow): PaneRow {
|
|
106
|
+
const iso = typeof row.updatedAt === "string" ? row.updatedAt : undefined;
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
status: row.status || EMPTY_CELL,
|
|
110
|
+
agent: row.agent || EMPTY_CELL,
|
|
111
|
+
name: row.name || EMPTY_CELL,
|
|
112
|
+
updatedAt: iso,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Narrow the daemon's full session list to one run. Never throws.
|
|
118
|
+
*
|
|
119
|
+
* Filters to `runId`, lifts the single `origin === "open"` row out as the
|
|
120
|
+
* orchestrator, orders the rest by `updatedAt` descending with a total order,
|
|
121
|
+
* and cuts to `budget`. With no explicit budget every matched row is kept and
|
|
122
|
+
* `hidden` is 0; dropping rows to fit the pane is formatPane's decision.
|
|
123
|
+
*/
|
|
124
|
+
export function selectPane(rows: SessionRow[], runId: string, budget?: number): PaneSnapshot {
|
|
125
|
+
try {
|
|
126
|
+
const limit = budgetLimit(budget);
|
|
127
|
+
const source = Array.isArray(rows) ? rows : [];
|
|
128
|
+
const matching = source.filter(
|
|
129
|
+
(row) => row != null && typeof row === "object" && (row as SessionRow).runId === runId,
|
|
130
|
+
);
|
|
131
|
+
const orchestratorRow = matching.find((row) => row.origin === "open");
|
|
132
|
+
const orchestrator = orchestratorRow
|
|
133
|
+
? { agent: text(orchestratorRow.agent) || EMPTY_CELL }
|
|
134
|
+
: undefined;
|
|
135
|
+
const workers = matching.filter((row) => row.origin !== "open");
|
|
136
|
+
const ordered = [...workers].sort(compareRows);
|
|
137
|
+
const kept = ordered.slice(0, limit);
|
|
138
|
+
return {
|
|
139
|
+
runId: text(runId),
|
|
140
|
+
orchestrator,
|
|
141
|
+
rows: kept.map(toPaneRow),
|
|
142
|
+
hidden: ordered.length - kept.length,
|
|
143
|
+
total: matching.length,
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return { runId: "", orchestrator: undefined, rows: [], hidden: 0, total: 0 };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A cell the daemon supplied: control and separator characters become spaces,
|
|
151
|
+
// the ends are trimmed, and an empty result is a placeholder. Rule 7.
|
|
152
|
+
function cell(value: unknown): string {
|
|
153
|
+
const clean = text(value).replace(CONTROL, " ").trim();
|
|
154
|
+
return clean === "" ? EMPTY_CELL : clean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Cut to `width` code units, never wrap, and never strand half of a surrogate
|
|
158
|
+
// pair. Carried across from format.ts, with a marker so a clipped value reads
|
|
159
|
+
// as clipped. The result is always exactly `width` units after the padEnd.
|
|
160
|
+
function fit(value: string, width: number, fill = " "): string {
|
|
161
|
+
if (width <= 0) return "";
|
|
162
|
+
if (value.length <= width) return value.padEnd(width, fill);
|
|
163
|
+
let cut = value.slice(0, width - 1);
|
|
164
|
+
const tail = cut.charCodeAt(cut.length - 1);
|
|
165
|
+
if (tail >= 0xd800 && tail <= 0xdbff) cut = cut.slice(0, -1);
|
|
166
|
+
return (cut + "…").padEnd(width, fill);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function glyph(agent: unknown): string {
|
|
170
|
+
const key = text(agent);
|
|
171
|
+
return HARNESS_GLYPH[key] ?? "□";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function harnessLabel(agent: unknown): string {
|
|
175
|
+
const key = text(agent);
|
|
176
|
+
return HARNESS_LABEL[key] ?? cell(agent);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function dot(status: unknown): string {
|
|
180
|
+
return STATUS_DOT[text(status)] ?? "○";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function statusWord(status: unknown): string {
|
|
184
|
+
return STATUS_WORD[text(status)] ?? cell(status);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function age(iso: unknown, now: number): string {
|
|
188
|
+
if (typeof iso !== "string") return "";
|
|
189
|
+
const then = Date.parse(iso);
|
|
190
|
+
if (Number.isNaN(then)) return "";
|
|
191
|
+
const minutes = Math.floor((now - then) / 60000);
|
|
192
|
+
if (minutes < 1) return "agora";
|
|
193
|
+
if (minutes < 60) return `${minutes}m`;
|
|
194
|
+
return `${Math.floor(minutes / 60)}h`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function count(value: unknown): number {
|
|
198
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// How old a row looks for eviction: an unparseable timestamp is the oldest
|
|
202
|
+
// possible age, matching how compareRows parks such rows at the end of the
|
|
203
|
+
// display. The orchestrator pins itself to +Infinity so it is the very last
|
|
204
|
+
// card to leave when the pane shrinks.
|
|
205
|
+
function rowAge(iso: unknown): number {
|
|
206
|
+
const then = typeof iso === "string" ? Date.parse(iso) : Number.NaN;
|
|
207
|
+
return Number.isNaN(then) ? Number.NEGATIVE_INFINITY : then;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// One drawable piece of the pane: an orchestrator card, a worker card, or a
|
|
211
|
+
// compact history section. `count` is how many sessions disappear if the block
|
|
212
|
+
// is evicted to make room for the frame and footer. `size` counts only the
|
|
213
|
+
// block's own lines; the assembled order adds any connector above it.
|
|
214
|
+
type Block = {
|
|
215
|
+
kind: "card" | "history";
|
|
216
|
+
size: number;
|
|
217
|
+
age: number;
|
|
218
|
+
id: string;
|
|
219
|
+
count: number;
|
|
220
|
+
build: (connects: boolean, lead: boolean) => string[];
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// One drawing pass, with the widths derived from the column budget. Kept inside
|
|
224
|
+
// its own function so formatPane can wrap the whole thing in one guard.
|
|
225
|
+
// `limit` is the usable body height in rows, or undefined for unbounded.
|
|
226
|
+
function draw(snapshot: PaneSnapshot, columns: number, limit: number | undefined): string[] {
|
|
227
|
+
const source = (snapshot ?? {}) as Partial<PaneSnapshot>;
|
|
228
|
+
const rows = (Array.isArray(source.rows) ? source.rows : []).map(
|
|
229
|
+
(row) => (row ?? {}) as Partial<PaneRow>,
|
|
230
|
+
);
|
|
231
|
+
const orchestrator =
|
|
232
|
+
source.orchestrator && typeof source.orchestrator === "object"
|
|
233
|
+
? (source.orchestrator as Partial<{ agent: string }>)
|
|
234
|
+
: undefined;
|
|
235
|
+
const hidden = count(source.hidden);
|
|
236
|
+
const total = count(source.total);
|
|
237
|
+
|
|
238
|
+
// Rule 10: nothing to draw when there is neither a card nor an orchestrator.
|
|
239
|
+
if (rows.length === 0 && orchestrator === undefined) return [];
|
|
240
|
+
|
|
241
|
+
const workerTotal = rows.length + hidden;
|
|
242
|
+
const working = rows.filter((row) => LIVE.has(row.status ?? "")).length;
|
|
243
|
+
const waiting = rows.filter((row) => WAIT.has(row.status ?? "")).length;
|
|
244
|
+
// The overflow is reported by count only, its statuses are not in the
|
|
245
|
+
// snapshot, so it is folded into "finished": the most recently touched
|
|
246
|
+
// sessions are the live ones and stay inside the budget.
|
|
247
|
+
const finished = Math.max(0, workerTotal - working - waiting);
|
|
248
|
+
const finishedRows = rows.filter(
|
|
249
|
+
(row) => !LIVE.has(row.status ?? "") && !WAIT.has(row.status ?? ""),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const W = Math.trunc(columns);
|
|
253
|
+
const IN = W - 2;
|
|
254
|
+
// Rule 8: the card box caps at MAX_CARD_COLUMNS no matter how wide the dock
|
|
255
|
+
// gets; the outer frame still spans the full width. The two-space inset the
|
|
256
|
+
// cards have always sat at inside the frame is kept.
|
|
257
|
+
const CARD = Math.min(IN - 4, MAX_CARD_COLUMNS);
|
|
258
|
+
const INNER = Math.max(0, CARD - 2);
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
|
|
261
|
+
// Everything routed through frame* is exactly W wide.
|
|
262
|
+
const frameRow = (s: string) => "│" + fit(s, IN) + "│";
|
|
263
|
+
const frameSep = () => "├" + "─".repeat(IN) + "┤";
|
|
264
|
+
const frameBot = () => "└" + "─".repeat(IN) + "┘";
|
|
265
|
+
const frameTop = (label: string) =>
|
|
266
|
+
"┌" + fit(("─ " + label + " ").replace(CONTROL, " "), IN, "─") + "┐";
|
|
267
|
+
|
|
268
|
+
// A node card, indented inside the frame. The stem that joins it to whatever
|
|
269
|
+
// sits above is drawn by the assembly, not by the block: the first block is
|
|
270
|
+
// the root and never has one.
|
|
271
|
+
const stem = () => frameRow(" " + " ".repeat(Math.floor(INNER / 2) + 1) + "│");
|
|
272
|
+
const cardTop = () => frameRow(" ┌" + "─".repeat(INNER) + "┐");
|
|
273
|
+
const cardRow = (s: string) => frameRow(" │" + fit(s, INNER) + "│");
|
|
274
|
+
const stemAt = Math.floor(INNER / 2);
|
|
275
|
+
const cardBottom = (connects: boolean) =>
|
|
276
|
+
connects
|
|
277
|
+
? frameRow(" └" + "─".repeat(stemAt) + "┬" + "─".repeat(INNER - stemAt - 1) + "┘")
|
|
278
|
+
: frameRow(" └" + "─".repeat(INNER) + "┘");
|
|
279
|
+
|
|
280
|
+
const cardBlock = (ageValue: number, id: string, body: string[]): Block => ({
|
|
281
|
+
kind: "card",
|
|
282
|
+
size: body.length + 2,
|
|
283
|
+
age: ageValue,
|
|
284
|
+
id,
|
|
285
|
+
count: 1,
|
|
286
|
+
build: (connects, lead) => [
|
|
287
|
+
...(lead ? [stem()] : []),
|
|
288
|
+
cardTop(),
|
|
289
|
+
...body.map(cardRow),
|
|
290
|
+
cardBottom(connects),
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const blocks: Block[] = [];
|
|
295
|
+
|
|
296
|
+
if (orchestrator !== undefined) {
|
|
297
|
+
blocks.push(
|
|
298
|
+
cardBlock(Number.POSITIVE_INFINITY, "", [
|
|
299
|
+
` ${glyph(orchestrator.agent)} ● Orchestrator`,
|
|
300
|
+
` ${harnessLabel(orchestrator.agent)} · ${workerTotal} agents`,
|
|
301
|
+
]),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const row of rows) {
|
|
306
|
+
const status = row.status ?? "";
|
|
307
|
+
// Live rows stay prominent because they need attention. Finished rows are
|
|
308
|
+
// collected below into one compact history section.
|
|
309
|
+
if (LIVE.has(status) || WAIT.has(status)) {
|
|
310
|
+
const when = age(row.updatedAt, now);
|
|
311
|
+
const word = statusWord(status);
|
|
312
|
+
blocks.push(
|
|
313
|
+
cardBlock(rowAge(row.updatedAt), cell(row.id), [
|
|
314
|
+
` ${glyph(row.agent)} ${dot(row.status)} ${cell(row.id)} ${harnessLabel(row.agent)}`,
|
|
315
|
+
` ${cell(row.name)}`,
|
|
316
|
+
when === "" ? ` ${word}` : ` ${word} · ${when}`,
|
|
317
|
+
]),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (finished > 0) {
|
|
323
|
+
const preview = finishedRows.slice(0, HISTORY_PREVIEW_ROWS);
|
|
324
|
+
const historyLines = [` ─ HISTORY · ${finished} finished`];
|
|
325
|
+
for (const row of preview) {
|
|
326
|
+
const when = age(row.updatedAt, now);
|
|
327
|
+
historyLines.push(
|
|
328
|
+
` ${glyph(row.agent)} ${cell(row.id)} ${cell(row.name)}${when === "" ? "" : ` · ${when}`}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const older = finished - preview.length;
|
|
332
|
+
if (older > 0) historyLines.push(` +${older} earlier`);
|
|
333
|
+
|
|
334
|
+
blocks.push({
|
|
335
|
+
kind: "history",
|
|
336
|
+
size: historyLines.length,
|
|
337
|
+
age: rowAge(preview[0]?.updatedAt),
|
|
338
|
+
id: "history",
|
|
339
|
+
count: finished,
|
|
340
|
+
build: (_connects, lead) => [...(lead ? [stem()] : []), ...historyLines.map(frameRow)],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Whether the block at index i hangs from the one above it by a stem. The
|
|
345
|
+
// first block is the root and never leads with one. A card is always joined
|
|
346
|
+
// to what sits above it; history only gets a stem when it follows a card.
|
|
347
|
+
const stemAbove = (list: Block[], i: number): boolean =>
|
|
348
|
+
i > 0 && (list[i].kind === "card" || list[i - 1].kind === "card");
|
|
349
|
+
|
|
350
|
+
// Rule 3: these ten lines are the frame. Whenever anything is drawn at all
|
|
351
|
+
// they are drawn, and they are never the lines that the fit cuts.
|
|
352
|
+
const head = [
|
|
353
|
+
frameTop(`Run canvas ${cell(source.runId)}`),
|
|
354
|
+
frameRow(` ${total} sessions · ${working} working`),
|
|
355
|
+
frameRow(` ${waiting} waiting for you · ${finished} finished`),
|
|
356
|
+
frameSep(),
|
|
357
|
+
frameRow(""),
|
|
358
|
+
];
|
|
359
|
+
const tail = [
|
|
360
|
+
frameRow(""),
|
|
361
|
+
frameSep(),
|
|
362
|
+
frameRow(" ● working ◉ waiting ○ finished"),
|
|
363
|
+
frameRow(" ▲ Claude ■ OpenCode ◆ Codex ⬟ OMP"),
|
|
364
|
+
frameBot(),
|
|
365
|
+
];
|
|
366
|
+
const fixed = head.length + tail.length;
|
|
367
|
+
// The hidden line keeps its own stem only when it hangs off a full card.
|
|
368
|
+
const hiddenSize = (list: Block[]): number =>
|
|
369
|
+
list.length > 0 && list[list.length - 1].kind === "card" ? 2 : 1;
|
|
370
|
+
|
|
371
|
+
let kept = blocks;
|
|
372
|
+
let dropped = 0;
|
|
373
|
+
let showHidden = false;
|
|
374
|
+
if (limit !== undefined) {
|
|
375
|
+
// Rule 7: the frame alone does not fit, so nothing is drawn at all.
|
|
376
|
+
if (limit < fixed) return [];
|
|
377
|
+
// History leaves first, then full cards, with the oldest block leaving
|
|
378
|
+
// first within each group. The hidden count follows the block's session
|
|
379
|
+
// count rather than its number of rendered lines.
|
|
380
|
+
const victims = [...blocks].sort(
|
|
381
|
+
(a, b) =>
|
|
382
|
+
Number(a.kind === "card") - Number(b.kind === "card") ||
|
|
383
|
+
a.age - b.age ||
|
|
384
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
385
|
+
);
|
|
386
|
+
const sizeOf = (list: Block[]) =>
|
|
387
|
+
list.reduce((sum, block, i) => sum + block.size + (stemAbove(list, i) ? 1 : 0), 0);
|
|
388
|
+
const needed = () => fixed + sizeOf(kept);
|
|
389
|
+
for (let vi = 0; needed() > limit && vi < victims.length; vi += 1) {
|
|
390
|
+
kept = kept.filter((block) => block !== victims[vi]);
|
|
391
|
+
dropped += victims[vi].count;
|
|
392
|
+
}
|
|
393
|
+
// A hidden line is useful only when it does not displace a live card.
|
|
394
|
+
showHidden = dropped > 0 && fixed + sizeOf(kept) + hiddenSize(kept) <= limit;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// `hidden` is already represented by the history block: the source does not
|
|
398
|
+
// expose the statuses of rows that were cut before formatting.
|
|
399
|
+
const hiddenTotal = showHidden ? dropped : 0;
|
|
400
|
+
const hiddenRow = frameRow(` +${hiddenTotal} hidden agents`);
|
|
401
|
+
const out: string[] = [...head];
|
|
402
|
+
for (let i = 0; i < kept.length; i += 1) {
|
|
403
|
+
const connects = i < kept.length - 1 || hiddenTotal > 0;
|
|
404
|
+
out.push(...kept[i].build(connects, stemAbove(kept, i)));
|
|
405
|
+
}
|
|
406
|
+
if (hiddenTotal > 0) {
|
|
407
|
+
if (kept.length > 0 && kept[kept.length - 1].kind === "card") out.push(stem(), hiddenRow);
|
|
408
|
+
else out.push(hiddenRow);
|
|
409
|
+
}
|
|
410
|
+
out.push(...tail);
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The whole drawing, one string per line, every line exactly `columns` wide.
|
|
416
|
+
* `rows` is the usable body height. When given, the result never exceeds it.
|
|
417
|
+
* Returns [] when there is nothing to draw, the pane is too narrow, or the
|
|
418
|
+
* height cannot hold even the frame. Never throws.
|
|
419
|
+
*/
|
|
420
|
+
export function formatPane(
|
|
421
|
+
snapshot: PaneSnapshot,
|
|
422
|
+
columns: number,
|
|
423
|
+
rows?: number,
|
|
424
|
+
): string[] {
|
|
425
|
+
if (typeof columns !== "number" || !Number.isFinite(columns)) return [];
|
|
426
|
+
if (Math.trunc(columns) < MIN_COLUMNS) return [];
|
|
427
|
+
const limit =
|
|
428
|
+
typeof rows === "number" && Number.isFinite(rows) ? Math.trunc(rows) : undefined;
|
|
429
|
+
try {
|
|
430
|
+
return draw(snapshot, columns, limit);
|
|
431
|
+
} catch {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Label for the button drawn above the prompt, the one affordance that brings
|
|
438
|
+
* the pane back after the engine's own close box took it away. It reports only
|
|
439
|
+
* current attention states, never the historical run total. Never throws: it
|
|
440
|
+
* is read inside a render hook.
|
|
441
|
+
*/
|
|
442
|
+
export function paneButtonLabel(snapshot: PaneSnapshot | undefined): string {
|
|
443
|
+
if (!snapshot || typeof snapshot !== "object") return "agents";
|
|
444
|
+
try {
|
|
445
|
+
const rows = Array.isArray(snapshot.rows) ? snapshot.rows : [];
|
|
446
|
+
const working = rows.filter((row) => LIVE.has(row?.status ?? "")).length;
|
|
447
|
+
if (working > 0) return `${working} working`;
|
|
448
|
+
const waiting = rows.filter((row) => WAIT.has(row?.status ?? "")).length;
|
|
449
|
+
return waiting > 0 ? `${waiting} waiting for you` : "no active agents";
|
|
450
|
+
} catch {
|
|
451
|
+
return "agents";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Parse layer of the orchestrator agents band.
|
|
2
|
+
//
|
|
3
|
+
// Turns the raw stdout of `codedeck ps --all --json` into rows the band can
|
|
4
|
+
// render. The engine draws the whole band in one pass, so a throw here would
|
|
5
|
+
// take the band down with it: every failure path returns undefined instead.
|
|
6
|
+
|
|
7
|
+
import type { SessionRow } from "./types.js";
|
|
8
|
+
|
|
9
|
+
// Raw stdout to rows. Returns undefined for anything that is not a JSON array,
|
|
10
|
+
// which is how a failed or truncated command is reported. Never throws.
|
|
11
|
+
export function parseRows(stdout: string): SessionRow[] | undefined {
|
|
12
|
+
let parsed: unknown;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(stdout);
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
19
|
+
const rows: SessionRow[] = [];
|
|
20
|
+
for (const entry of parsed) {
|
|
21
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
22
|
+
// id is the one field the band cannot render without, so an entry without
|
|
23
|
+
// a non-empty string id is dropped rather than passed through.
|
|
24
|
+
const id = (entry as { id?: unknown }).id;
|
|
25
|
+
if (typeof id !== "string" || id === "") continue;
|
|
26
|
+
rows.push(entry as SessionRow);
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Shared shapes for the orchestrator agents band.
|
|
2
|
+
//
|
|
3
|
+
// Owned by the interface, not by any one slice: parse, select and format are
|
|
4
|
+
// written against this file and none of them may change it.
|
|
5
|
+
//
|
|
6
|
+
// SessionRow mirrors one element of `codedeck ps --all --json`. Every field
|
|
7
|
+
// past `id` is optional on purpose: the band reads a CLI it does not version
|
|
8
|
+
// with, so a missing field degrades one cell rather than the whole row.
|
|
9
|
+
|
|
10
|
+
export interface SessionRow {
|
|
11
|
+
id: string;
|
|
12
|
+
runId?: string;
|
|
13
|
+
origin?: string | null;
|
|
14
|
+
name?: string;
|
|
15
|
+
agent?: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
updatedAt?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One agent card in the pane. The canvas draws a glyph per harness and an age,
|
|
22
|
+
* so the raw harness id and the timestamp survive here rather than being
|
|
23
|
+
* flattened into a label.
|
|
24
|
+
*/
|
|
25
|
+
export interface PaneRow {
|
|
26
|
+
id: string;
|
|
27
|
+
status: string;
|
|
28
|
+
agent: string;
|
|
29
|
+
name: string;
|
|
30
|
+
updatedAt?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Everything one drawing of the pane needs, already narrowed to one run. */
|
|
34
|
+
export interface PaneSnapshot {
|
|
35
|
+
runId: string;
|
|
36
|
+
/** The orchestrator's own row, the only one whose origin is "open". */
|
|
37
|
+
orchestrator: { agent: string } | undefined;
|
|
38
|
+
rows: PaneRow[];
|
|
39
|
+
/** Rows that matched the run but fell outside the budget. */
|
|
40
|
+
hidden: number;
|
|
41
|
+
/** Every row of the run, including the orchestrator and the hidden ones. */
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
## Spec gates
|
|
2
2
|
|
|
3
|
+
- Treat `tlc-spec-driven` as the default workflow for every feature, behavior change, or bug fix. Activate it by name before planning so the work follows Specify, Design, Tasks, and Execute at the depth the change needs.
|
|
4
|
+
- When the harness does not expose the skill, apply the same four phases and the gates below from this prompt. Do not skip the workflow because the harness cannot load a skill file.
|
|
3
5
|
- Ask for testable specs: each acceptance criterion holds one behavior, names a SHALL, and uses the shape that fits (WHEN trigger THEN response, WHILE state, WHERE flag, IF fault THEN handling, or a plain invariant). Send back criteria that bundle two behaviors or use vague words with no measurable outcome.
|
|
4
6
|
- Ask for gated tasks: each task points to its spec requirement and fills Tests plus Gate. Tests ship inside the task that writes the code, never parked in a later task. Tests none holds only for a layer the coverage matrix marks none.
|
|
5
7
|
- Ask for a short coverage matrix before Execute: one row per code layer touched, with test type, where the tests live, and the command that runs them. Treat the confirmed matrix as the authority for the run.
|
|
@@ -6,6 +6,7 @@ includes:
|
|
|
6
6
|
- worktree
|
|
7
7
|
- dispatch
|
|
8
8
|
- proof
|
|
9
|
+
- spec-gates
|
|
9
10
|
- reports-dispatch
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -40,7 +41,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe
|
|
|
40
41
|
|
|
41
42
|
- Confirm completed work from worker artifacts: `codedeck logs`, `codedeck diff <id> --stat`, `codedeck ps`, and `codedeck show`.
|
|
42
43
|
- Treat a worker's success message as a claim until its artifacts support it. When a claim needs independent proof, dispatch a verification slice.
|
|
43
|
-
- Take the `<id>` from `--bg --json
|
|
44
|
+
- Take the `<id>` from `--bg --json`. If other workers or orchestration work can make progress, run `codedeck wait <id> --json &` in the background and keep the turn moving. A background shell job does not wake the chat when it finishes, so use `codedeck ps` or `codedeck show <id>` to check progress, then run a foreground `wait` when you are ready to reconcile that worker.
|
|
44
45
|
- `codedeck wait` can return `needs_input` without being terminal. Use `codedeck ps` or `codedeck show <id>` to find the worker, answer it with `codedeck send <id> "<reply>"`, and wait again.
|
|
45
46
|
- Read completion from `.status`, not the exit code. Only `completed` is success. Carry failures into the report.
|
|
46
47
|
- `codedeck diff <id> --stat` confirms that a worker produced work and stayed inside its files. An empty stat is not a successful delivery.
|
|
@@ -6,6 +6,7 @@ includes:
|
|
|
6
6
|
- worktree
|
|
7
7
|
- dispatch
|
|
8
8
|
- proof
|
|
9
|
+
- spec-gates
|
|
9
10
|
- reports-dispatch
|
|
10
11
|
---
|
|
11
12
|
|
|
@@ -40,7 +41,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe
|
|
|
40
41
|
|
|
41
42
|
- Confirm completed work from worker artifacts: `codedeck logs`, `codedeck diff <id> --stat`, `codedeck ps`, and `codedeck show`.
|
|
42
43
|
- Treat a worker's success message as a claim until its artifacts support it. When a claim needs independent proof, dispatch a verification slice.
|
|
43
|
-
- Take the `<id>` from `--bg --json
|
|
44
|
+
- Take the `<id>` from `--bg --json`. If other workers or orchestration work can make progress, run `codedeck wait <id> --json &` in the background and keep the turn moving. A background shell job does not wake the chat when it finishes, so use `codedeck ps` or `codedeck show <id>` to check progress, then run a foreground `wait` when you are ready to reconcile that worker.
|
|
44
45
|
- `codedeck wait` can return `needs_input` without being terminal. Use `codedeck ps` or `codedeck show <id>` to find the worker, answer it with `codedeck send <id> "<reply>"`, and wait again.
|
|
45
46
|
- Read completion from `.status`, not the exit code. Only `completed` is success. Carry failures into the report.
|
|
46
47
|
- `codedeck diff <id> --stat` confirms that a worker produced work and stayed inside its files. An empty stat is not a successful delivery.
|
|
@@ -49,7 +49,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe
|
|
|
49
49
|
- You confirm work by reading what a worker produced, never by producing anything yourself. The only things you look at are worker artifacts: `codedeck logs`, `codedeck diff <id> --stat`, `codedeck ps`, `codedeck show`. Never the repo behind them.
|
|
50
50
|
- Worker output is untrusted until the artifacts back it. A success message is a claim, the stat is the fact. When a claim needs independent proof, dispatch a fresh verification slice instead of trusting the first report.
|
|
51
51
|
- `codedeck ps` shows every session at once, so a whole batch stays visible in one view.
|
|
52
|
-
- Take the `<id>` from the `run --bg --json` above
|
|
52
|
+
- Take the `<id>` from the `run --bg --json` above. If other workers or orchestration work can make progress, run `codedeck wait <id> --json &` in the background and keep the turn moving. A background shell job does not wake the chat when it finishes, so use `codedeck ps` or `codedeck show <id>` to check progress, then run a foreground `wait` when you are ready to reconcile that worker.
|
|
53
53
|
- `codedeck wait` blocks through `needs_input`, which is not terminal. Take one `codedeck ps` snapshot (or `codedeck show <id>`) to catch a worker parked on input, answer it with `codedeck send <id> "<reply>"`, then wait again. That snapshot is discovery, not a polling loop.
|
|
54
54
|
- Read completion from `.status`, never from the exit code. `codedeck wait` reports `stopped` as exit 0. Only `completed` is success. `failed`, `stopped`, `orphaned`, and `interrupted` are failure, so carry the detail into your report.
|
|
55
55
|
- `codedeck diff <id> --stat` lists changed files and line counts without the diff body. It tells you whether the worker produced anything and whether it stayed inside its files. An empty stat means no production, so report that, never success. Drift outside the assigned files is a finding.
|
|
@@ -69,7 +69,7 @@ You are the CodeDeck orchestrator, and you run on the most capable and most expe
|
|
|
69
69
|
- Drive the whole run without being asked for each phase. The human asked for the outcome once. Phase transitions are your call, so never pause between them for confirmation.
|
|
70
70
|
- Size it from the request, then commit to the size. Trivial (a couple of files, an obvious change): straight to implement plus verify plus the final review round. Anything shaped like a feature: the full loop below.
|
|
71
71
|
- Specify: dispatch a worker to write `.specs/features/<slug>/spec.md` with the goal, the acceptance criteria, and what is out of scope. Design and Tasks go the same way when the work needs them: `design.md` for architecture calls, `tasks.md` for atomic tasks that each carry their Tests and Gate. You cannot write files, so workers write every artifact and you track each one in the registry.
|
|
72
|
-
- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name
|
|
72
|
+
- Execute: dispatch the tasks in dependency order. Every briefing names the spec and task files as the source of truth, and tells the worker to activate the `tlc-spec-driven` skill by name. When its harness does not expose the skill, the worker follows the lean briefing steps exactly without pasting skill text.
|
|
73
73
|
- Verify: a slice is done only when its spec-named tests pass and a bounded mutation probe passes with them. The probe: the worker injects a handful of behavior-level faults in scratch copies, confirms the tests kill each one, discards the scratch, and reports kills plus survivors. Survivors become fix slices, not excuses.
|
|
74
74
|
- Review: run the final round yourself with `codedeck run --role reviewer --no-worktree "<briefing>" --bg --json` over the finished scope. Slice self-review never replaces it. Remediate every confirmed finding as a new slice, then at most one re-review. After that, report whatever still stands instead of looping.
|
|
75
75
|
- Record decisions as you go: what you sized, what you scoped out, what the probes killed. They land in the closing report in one batch, never as questions mid-run.
|