jules-orchestrator-kit 0.52.7 → 0.53.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.
@@ -1,146 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { createHash } from "node:crypto";
4
-
5
- /**
6
- * @typedef {"idle" | "reserved" | "preparing" | "running" | "verifying" | "draining" | "failed" | "stale"} SlotState
7
- *
8
- * @typedef {Object} SwarmSlot
9
- * @property {string} id
10
- * @property {SlotState} state
11
- * @property {string} [taskId]
12
- * @property {number} [attempt]
13
- * @property {string} [worktree]
14
- * @property {string} [branch]
15
- * @property {number} [pid]
16
- * @property {string} [processStartTime]
17
- * @property {string} [startedAt]
18
- * @property {number} [durationMs]
19
- * @property {string[]} lockIds
20
- * @property {string[]} writeScope
21
- * @property {number} [cpuPercent]
22
- * @property {number} [rssBytes]
23
- * @property {"full" | "partial" | "unavailable"} resourceSupport
24
- * @property {string} [lastHeartbeat]
25
- * @property {string} [warning]
26
- */
27
-
28
- /**
29
- * Check if PID is currently alive on current system.
30
- * @param {number} pid
31
- * @returns {boolean}
32
- */
33
- function isPidAlive(pid) {
34
- if (!pid || pid <= 0) return false;
35
- try {
36
- process.kill(pid, 0);
37
- return true;
38
- } catch (err) {
39
- return err.code === "EPERM"; // EPERM means process exists but we lack permission to signal
40
- }
41
- }
42
-
43
- /**
44
- * Build immutable SwarmSnapshot probing active worker slots.
45
- * @param {string} root
46
- * @param {Object} [options]
47
- * @param {boolean} [options.sampleResources=false]
48
- * @returns {Promise<{ slots: SwarmSlot[], warnings: string[], revision: string }>}
49
- */
50
- export async function buildSwarmSnapshot(root, _options = {}) {
51
- const repoRoot = resolve(root || process.cwd());
52
- const swarmDir = join(repoRoot, ".agent", "state", "swarm");
53
- const warnings = [];
54
- /** @type {SwarmSlot[]} */
55
- const slots = [];
56
-
57
- // Default 4 concurrency slots if no swarm config files exist
58
- const defaultSlotIds = ["slot-01", "slot-02", "slot-03", "slot-04"];
59
-
60
- if (existsSync(swarmDir)) {
61
- try {
62
- const slotFiles = readdirSync(swarmDir).filter((f) => f.startsWith("slot-") && f.endsWith(".json"));
63
- for (const file of slotFiles) {
64
- const slotPath = join(swarmDir, file);
65
- try {
66
- const content = readFileSync(slotPath, "utf-8");
67
- /** @type {SwarmSlot} */
68
- const slotData = JSON.parse(content);
69
-
70
- let state = slotData.state || "idle";
71
- let warning = slotData.warning;
72
-
73
- if (slotData.pid && (state === "running" || state === "verifying")) {
74
- if (!isPidAlive(slotData.pid)) {
75
- state = "stale";
76
- warning = `Process PID ${slotData.pid} is no longer alive`;
77
- warnings.push(`Slot ${slotData.id} has dead PID ${slotData.pid}`);
78
- }
79
- }
80
-
81
- slots.push({
82
- id: slotData.id || file.replace(/\.json$/, ""),
83
- state,
84
- taskId: slotData.taskId,
85
- attempt: slotData.attempt,
86
- worktree: slotData.worktree,
87
- branch: slotData.branch,
88
- pid: slotData.pid,
89
- processStartTime: slotData.processStartTime,
90
- startedAt: slotData.startedAt,
91
- durationMs: slotData.durationMs,
92
- lockIds: slotData.lockIds || [],
93
- writeScope: slotData.writeScope || [],
94
- resourceSupport: "partial",
95
- warning,
96
- });
97
- } catch (err) {
98
- warnings.push(`Failed to parse slot file ${file}: ${err.message}`);
99
- }
100
- }
101
- } catch (err) {
102
- warnings.push(`Failed to read swarm state directory: ${err.message}`);
103
- }
104
- }
105
-
106
- // Populate default idle slots if fewer than 4 slots exist
107
- if (slots.length < 4) {
108
- for (const defaultId of defaultSlotIds) {
109
- if (!slots.some((s) => s.id === defaultId)) {
110
- slots.push({
111
- id: defaultId,
112
- state: "idle",
113
- lockIds: [],
114
- writeScope: [],
115
- resourceSupport: "partial",
116
- });
117
- }
118
- }
119
- }
120
-
121
- slots.sort((a, b) => a.id.localeCompare(b.id));
122
-
123
- const revisionPayload = JSON.stringify({ slots: slots.map((s) => ({ id: s.id, state: s.state, pid: s.pid })) });
124
- const revision = "sha256:" + createHash("sha256").update(revisionPayload).digest("hex");
125
-
126
- return { slots, warnings, revision };
127
- }
128
-
129
- /**
130
- * Swarm state reducer for updating view navigation state.
131
- * @param {Record<string, any>} state
132
- * @param {import("../key-decoder.mjs").KeyEvent | { type: string, payload?: any }} event
133
- * @returns {Record<string, any>}
134
- */
135
- export function reduceSwarmState(state, event) {
136
- const nextState = { ...state };
137
- if (!event) return nextState;
138
-
139
- if (typeof event === "object" && "type" in event) {
140
- if (event.type === "SELECT_SLOT") {
141
- nextState.selectedSlotId = String(event.payload || "");
142
- }
143
- }
144
-
145
- return nextState;
146
- }
@@ -1,215 +0,0 @@
1
- import { detectTerminalCapabilities } from "./capabilities.mjs";
2
- import { createKeyDecoder } from "./key-decoder.mjs";
3
-
4
- export class WizardCancelledError extends Error {
5
- /**
6
- * @param {string} [message]
7
- * @param {"escape" | "ctrl-c" | "quit" | "stream-closed"} [reason="ctrl-c"]
8
- */
9
- constructor(message = "Wizard operation cancelled by user", reason = "ctrl-c") {
10
- super(message);
11
- this.name = "WizardCancelledError";
12
- this.code = 130;
13
- this.reason = reason;
14
- }
15
- }
16
-
17
- /**
18
- * @typedef {import("./capabilities.mjs").TerminalCapabilities} TerminalCapabilities
19
- * @typedef {import("./capabilities.mjs").TerminalSessionOptions} TerminalSessionOptions
20
- * @typedef {import("./key-decoder.mjs").KeyEvent} KeyEvent
21
- * @typedef {import("./renderer.mjs").RenderFrame} RenderFrame
22
- */
23
-
24
- /**
25
- * @typedef {Object} TerminalSession
26
- * @property {TerminalCapabilities} capabilities
27
- * @property {() => Promise<void>} enter
28
- * @property {(frame: RenderFrame) => Promise<void>} render
29
- * @property {<T>(operation: () => Promise<T>) => Promise<T>} suspend
30
- * @property {() => Promise<void>} restore
31
- * @property {() => Promise<void>} close
32
- * @property {(listener: (key: KeyEvent) => void) => () => void} onKey
33
- * @property {(listener: (size: { columns: number, rows: number }) => void) => () => void} onResize
34
- */
35
-
36
- /**
37
- * Create terminal raw-mode session manager.
38
- * @param {TerminalSessionOptions} [options]
39
- * @returns {TerminalSession}
40
- */
41
- export function createTerminalSession(options = {}) {
42
- const input = options.input || process.stdin;
43
- const output = options.output || process.stdout;
44
-
45
- const capabilities = detectTerminalCapabilities(options);
46
- const keyDecoder = createKeyDecoder();
47
-
48
- let isEntered = false;
49
- let isClosed = false;
50
- let wasRaw = Boolean(input && input.isRaw);
51
-
52
- const keyListeners = new Set();
53
- const resizeListeners = new Set();
54
-
55
- let winchScheduled = false;
56
-
57
- const handleData = (chunk) => {
58
- if (isClosed) return;
59
- const events = keyDecoder.push(chunk);
60
- for (const event of events) {
61
- if (event.name === "ctrl-c") {
62
- session.close().catch(() => {});
63
- throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).", "ctrl-c");
64
- }
65
- for (const listener of keyListeners) {
66
- try {
67
- listener(event);
68
- } catch (err) {
69
- if (err instanceof WizardCancelledError) {
70
- session.close().catch(() => {});
71
- throw err;
72
- }
73
- }
74
- }
75
- }
76
- };
77
-
78
- const handleSigwinch = () => {
79
- if (winchScheduled || isClosed) return;
80
- winchScheduled = true;
81
- queueMicrotask(() => {
82
- winchScheduled = false;
83
- if (isClosed) return;
84
- capabilities.columns = Math.max(10, output.columns || input.columns || process.stdout.columns || 80);
85
- capabilities.rows = Math.max(5, output.rows || input.rows || process.stdout.rows || 24);
86
- for (const listener of resizeListeners) {
87
- listener({ columns: capabilities.columns, rows: capabilities.rows });
88
- }
89
- });
90
- };
91
-
92
- const handleSigint = () => {
93
- session.close().catch(() => {});
94
- throw new WizardCancelledError("Wizard operation cancelled by user (SIGINT).", "ctrl-c");
95
- };
96
-
97
- const session = {
98
- capabilities,
99
-
100
- async enter() {
101
- if (isEntered || isClosed) return;
102
- isEntered = true;
103
-
104
- if (capabilities.inputIsTTY && typeof input.setRawMode === "function") {
105
- wasRaw = Boolean(input.isRaw);
106
- input.setRawMode(true);
107
- input.resume();
108
- if (typeof input.setEncoding === "function") {
109
- input.setEncoding("utf-8");
110
- }
111
- }
112
-
113
- if (capabilities.ansi && capabilities.outputIsTTY) {
114
- if (capabilities.alternateScreen) {
115
- output.write("\u001b[?1049h"); // Enter alternate screen
116
- }
117
- output.write("\u001b[?25l"); // Hide cursor
118
- }
119
-
120
- input.on("data", handleData);
121
- process.on("SIGWINCH", handleSigwinch);
122
- process.on("SIGINT", handleSigint);
123
- },
124
-
125
- async render(frame) {
126
- if (isClosed || !capabilities.ansi) return;
127
- // Delegated to renderer ANSI builder
128
- const { renderFrameToAnsi } = await import("./renderer.mjs");
129
- const ansiText = renderFrameToAnsi(frame, capabilities);
130
- output.write(ansiText);
131
- },
132
-
133
- async suspend(operation) {
134
- if (!isEntered || isClosed) return operation();
135
-
136
- // Temporarily restore cooked mode
137
- input.removeListener("data", handleData);
138
- process.removeListener("SIGWINCH", handleSigwinch);
139
- process.removeListener("SIGINT", handleSigint);
140
-
141
- if (capabilities.ansi && capabilities.outputIsTTY) {
142
- output.write("\u001b[?25h"); // Show cursor
143
- if (capabilities.alternateScreen) {
144
- output.write("\u001b[?1049l"); // Leave alternate screen
145
- }
146
- }
147
-
148
- if (capabilities.inputIsTTY && typeof input.setRawMode === "function") {
149
- input.setRawMode(wasRaw);
150
- }
151
-
152
- try {
153
- return await operation();
154
- } finally {
155
- // Re-enter raw mode
156
- if (capabilities.inputIsTTY && typeof input.setRawMode === "function") {
157
- input.setRawMode(true);
158
- }
159
- if (capabilities.ansi && capabilities.outputIsTTY) {
160
- if (capabilities.alternateScreen) {
161
- output.write("\u001b[?1049h");
162
- }
163
- output.write("\u001b[?25l");
164
- }
165
- input.on("data", handleData);
166
- process.on("SIGWINCH", handleSigwinch);
167
- process.on("SIGINT", handleSigint);
168
- }
169
- },
170
-
171
- async restore() {
172
- await session.close();
173
- },
174
-
175
- async close() {
176
- if (isClosed) return;
177
- isClosed = true;
178
- isEntered = false;
179
-
180
- input.removeListener("data", handleData);
181
- process.removeListener("SIGWINCH", handleSigwinch);
182
- process.removeListener("SIGINT", handleSigint);
183
-
184
- if (capabilities.ansi && capabilities.outputIsTTY) {
185
- output.write("\u001b[?25h"); // Show cursor
186
- if (capabilities.alternateScreen) {
187
- output.write("\u001b[?1049l"); // Leave alternate screen
188
- }
189
- }
190
-
191
- if (capabilities.inputIsTTY && typeof input.setRawMode === "function") {
192
- try {
193
- input.setRawMode(wasRaw);
194
- } catch {
195
- // Ignore state restoration failures on teardown
196
- }
197
- }
198
-
199
- keyListeners.clear();
200
- resizeListeners.clear();
201
- },
202
-
203
- onKey(listener) {
204
- keyListeners.add(listener);
205
- return () => keyListeners.delete(listener);
206
- },
207
-
208
- onResize(listener) {
209
- resizeListeners.add(listener);
210
- return () => resizeListeners.delete(listener);
211
- },
212
- };
213
-
214
- return session;
215
- }
@@ -1,323 +0,0 @@
1
- import { getStringWidth, clipText } from "./layout.mjs";
2
- import { getSymbols } from "./capabilities.mjs";
3
-
4
- /**
5
- * Filter items by string filter using token subsequence matching.
6
- * @template T
7
- * @param {T[]} items
8
- * @param {string} filterText
9
- * @param {(item: T) => string} getText
10
- * @returns {T[]}
11
- */
12
- export function filterItems(items, filterText, getText) {
13
- if (!filterText || !filterText.trim()) return items;
14
- const tokens = filterText.trim().toLowerCase().split(/\s+/);
15
-
16
- return items.filter((item) => {
17
- const text = getText(item).toLowerCase();
18
- return tokens.every((token) => text.includes(token));
19
- });
20
- }
21
-
22
- /**
23
- * Create list widget state and reducers.
24
- * @template T
25
- * @param {Object} opts
26
- * @param {T[]} opts.items
27
- * @param {(item: T) => string} opts.getId
28
- * @param {(item: T) => string} opts.getText
29
- * @param {number} [opts.pageSize=10]
30
- * @param {boolean} [opts.multiSelect=false]
31
- */
32
- export function createListState(opts) {
33
- const { items, getId, getText, pageSize = 10, multiSelect = false } = opts;
34
-
35
- let filter = "";
36
- let selectedIndex = 0;
37
- let offset = 0;
38
- const selectedIds = new Set();
39
-
40
- function getFilteredItems() {
41
- return filterItems(items, filter, getText);
42
- }
43
-
44
- function clampSelection() {
45
- const filtered = getFilteredItems();
46
- if (filtered.length === 0) {
47
- selectedIndex = 0;
48
- offset = 0;
49
- return;
50
- }
51
- if (selectedIndex < 0) selectedIndex = 0;
52
- if (selectedIndex >= filtered.length) selectedIndex = filtered.length - 1;
53
-
54
- // Adjust scroll offset viewport
55
- if (selectedIndex < offset) {
56
- offset = selectedIndex;
57
- } else if (selectedIndex >= offset + pageSize) {
58
- offset = selectedIndex - pageSize + 1;
59
- }
60
- }
61
-
62
- clampSelection();
63
-
64
- return {
65
- get filter() {
66
- return filter;
67
- },
68
- set filter(val) {
69
- filter = val;
70
- selectedIndex = 0;
71
- offset = 0;
72
- clampSelection();
73
- },
74
- get selectedIndex() {
75
- return selectedIndex;
76
- },
77
- get offset() {
78
- return offset;
79
- },
80
- get selectedIds() {
81
- return Array.from(selectedIds);
82
- },
83
-
84
- setItems(newItems) {
85
- opts.items = newItems;
86
- clampSelection();
87
- },
88
-
89
- move(delta) {
90
- const filtered = getFilteredItems();
91
- if (filtered.length === 0) return;
92
- selectedIndex += delta;
93
- clampSelection();
94
- },
95
-
96
- page(direction) {
97
- const step = Math.max(1, pageSize - 2);
98
- this.move(direction * step);
99
- },
100
-
101
- toStart() {
102
- selectedIndex = 0;
103
- clampSelection();
104
- },
105
-
106
- toEnd() {
107
- const filtered = getFilteredItems();
108
- selectedIndex = Math.max(0, filtered.length - 1);
109
- clampSelection();
110
- },
111
-
112
- toggleCurrent() {
113
- if (!multiSelect) return;
114
- const filtered = getFilteredItems();
115
- const current = filtered[selectedIndex];
116
- if (!current) return;
117
- const id = getId(current);
118
- if (selectedIds.has(id)) {
119
- selectedIds.delete(id);
120
- } else {
121
- selectedIds.add(id);
122
- }
123
- },
124
-
125
- selectAll() {
126
- if (!multiSelect) return;
127
- for (const item of getFilteredItems()) {
128
- selectedIds.add(getId(item));
129
- }
130
- },
131
-
132
- clearSelection() {
133
- selectedIds.clear();
134
- },
135
-
136
- getSelectedItem() {
137
- const filtered = getFilteredItems();
138
- return filtered[selectedIndex] || null;
139
- },
140
-
141
- getFilteredItems,
142
- getId,
143
- getText,
144
- pageSize,
145
- multiSelect,
146
- };
147
- }
148
-
149
- /**
150
- * Render paginated list lines.
151
- * @template T
152
- * @param {ReturnType<typeof createListState<T>>} state
153
- * @param {import("./layout.mjs").computeResponsiveLayout extends (...args: any[]) => infer R ? R : never} layout
154
- * @param {import("./capabilities.mjs").TerminalCapabilities} caps
155
- * @returns {import("./renderer.mjs").ScreenLine[]}
156
- */
157
- export function renderListLines(state, layout, caps) {
158
- const symbols = getSymbols(caps);
159
- const filtered = state.getFilteredItems();
160
- const lines = [];
161
-
162
- const total = filtered.length;
163
- const startIdx = state.offset;
164
- const endIdx = Math.min(total, startIdx + state.pageSize);
165
-
166
- // Scroll indicator top
167
- if (startIdx > 0) {
168
- const aboveCount = startIdx;
169
- lines.push({
170
- runs: [
171
- {
172
- text: `${symbols.arrowUp} ${aboveCount} more items above`,
173
- style: { fg: "cyan", dim: true },
174
- },
175
- ],
176
- });
177
- }
178
-
179
- for (let i = startIdx; i < endIdx; i++) {
180
- const item = filtered[i];
181
- const isSelected = i === state.selectedIndex;
182
- const itemText = state.getText(item);
183
- const itemId = state.getId(item);
184
- const isChecked = state.selectedIds.includes(itemId);
185
-
186
- let prefix = " ";
187
- if (state.multiSelect) {
188
- prefix = isChecked ? `${symbols.checked} ` : `${symbols.unchecked} `;
189
- }
190
-
191
- let pointer = isSelected ? `${symbols.select} ` : " ";
192
- let lineText = `${pointer}${prefix}${itemText}`;
193
-
194
- /** @type {import("./renderer.mjs").TextStyle} */
195
- let style = { fg: "default" };
196
- if (isSelected) {
197
- style = { fg: "cyan", bold: true };
198
- }
199
-
200
- lines.push({
201
- runs: [{ text: clipText(lineText, layout.mainWidth || layout.width), style }],
202
- });
203
- }
204
-
205
- // Scroll indicator bottom
206
- if (endIdx < total) {
207
- const belowCount = total - endIdx;
208
- lines.push({
209
- runs: [
210
- {
211
- text: `${symbols.arrowDown} ${belowCount} more items below`,
212
- style: { fg: "cyan", dim: true },
213
- },
214
- ],
215
- });
216
- }
217
-
218
- return lines;
219
- }
220
-
221
- /**
222
- * Create text input state and cursor manager.
223
- * @param {Object} [opts]
224
- * @param {string} [opts.initialValue=""]
225
- * @param {boolean} [opts.secret=false]
226
- */
227
- export function createTextInputState(opts = {}) {
228
- let value = opts.initialValue || "";
229
- let cursor = value.length;
230
- let visible = !opts.secret;
231
-
232
- return {
233
- get value() {
234
- return value;
235
- },
236
- set value(v) {
237
- value = String(v);
238
- cursor = value.length;
239
- },
240
- get cursor() {
241
- return cursor;
242
- },
243
- get visible() {
244
- return visible;
245
- },
246
- toggleVisible() {
247
- visible = !visible;
248
- },
249
-
250
- insert(text) {
251
- if (!text) return;
252
- value = value.slice(0, cursor) + text + value.slice(cursor);
253
- cursor += text.length;
254
- },
255
-
256
- backspace() {
257
- if (cursor > 0) {
258
- value = value.slice(0, cursor - 1) + value.slice(cursor);
259
- cursor--;
260
- }
261
- },
262
-
263
- delete() {
264
- if (cursor < value.length) {
265
- value = value.slice(0, cursor) + value.slice(cursor + 1);
266
- }
267
- },
268
-
269
- moveLeft() {
270
- if (cursor > 0) cursor--;
271
- },
272
-
273
- moveRight() {
274
- if (cursor < value.length) cursor++;
275
- },
276
-
277
- toStart() {
278
- cursor = 0;
279
- },
280
-
281
- toEnd() {
282
- cursor = value.length;
283
- },
284
-
285
- clear() {
286
- value = "";
287
- cursor = 0;
288
- },
289
-
290
- getDisplayValue(caps) {
291
- if (!visible && opts.secret) {
292
- const maskChar = caps.unicode ? "•" : "*";
293
- return maskChar.repeat(value.length);
294
- }
295
- return value;
296
- },
297
- };
298
- }
299
-
300
- /**
301
- * Render sticky status bar footer.
302
- * @param {Array<{ key: string, label: string }>} bindings
303
- * @param {number} width
304
- * @param {import("./capabilities.mjs").TerminalCapabilities} caps
305
- * @returns {import("./renderer.mjs").ScreenLine}
306
- */
307
- export function renderStatusBar(bindings, width, _caps) {
308
- const runs = [];
309
- let currentWidth = 0;
310
-
311
- for (const b of bindings) {
312
- const itemStr = `${b.key} ${b.label} `;
313
- const itemWidth = getStringWidth(itemStr);
314
-
315
- if (currentWidth + itemWidth > width) break;
316
-
317
- runs.push({ text: `${b.key} `, style: { fg: "cyan", bold: true } });
318
- runs.push({ text: `${b.label} `, style: { fg: "white", dim: true } });
319
- currentWidth += itemWidth;
320
- }
321
-
322
- return { runs };
323
- }
File without changes