@ctrl-spc/cs 0.7.1 → 0.7.2
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/codex-home.js +10 -6
- package/dist/config.js +17 -0
- package/dist/index.js +93 -12
- package/dist/mcp.js +15 -6
- package/dist/panel3/answer.js +12 -12
- package/dist/panel3/checkout.js +522 -2
- package/dist/panel3/cli.js +49 -62
- package/dist/panel3/client.js +53 -16
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/prompt.js +146 -3
- package/dist/panel3/run.js +638 -71
- package/dist/panel3/say.js +10 -10
- package/dist/panel3/show.js +195 -22
- package/dist/panel3/spawn.js +5 -2
- package/dist/panel3/tools.js +326 -5
- package/dist/presence.js +8 -0
- package/dist/skills.js +165 -0
- package/package.json +2 -3
package/dist/panel3/say.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ═══ AGENT PANEL v3: `
|
|
2
|
+
* ═══ AGENT PANEL v3: `cs say`, the only way anything reaches the record. ═══
|
|
3
3
|
*
|
|
4
4
|
* THIS FILE BELONGS TO AGENT PANEL v3. Nothing outside `src/panel3/` may
|
|
5
5
|
* import it.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* So this command's whole job is to put the card and the turn on the record and
|
|
15
15
|
* return. It does not spawn, does not take, does not wait for anything to be
|
|
16
16
|
* ready, and there is nothing here for a daemon to be up for. Nothing answers
|
|
17
|
-
* the turn yet, which is why `
|
|
17
|
+
* the turn yet, which is why `cs show` says out loud that nothing is acting on
|
|
18
18
|
* the card: that is the record being honest, not this command being unfinished.
|
|
19
19
|
*
|
|
20
20
|
* ---------------------------------------------------------------------------
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
// and takes the leading comment with it, so a file whose first statement is
|
|
47
47
|
// `import type` loses its v3 header in the published `dist/`.
|
|
48
48
|
import { at, out, returned, signedInClient } from './client.js';
|
|
49
|
-
const USAGE = 'usage:
|
|
49
|
+
const USAGE = 'usage: cs say [--card <id>] [--project <id or name>] "<text>"';
|
|
50
50
|
/** Long enough to recognise a request on a board, short enough to be a name. */
|
|
51
51
|
const TITLE_LIMIT = 80;
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
/**
|
|
54
54
|
* The arguments after `say`. A bare word that is not a flag is the message, and
|
|
55
|
-
* a SECOND one is refused rather than joined: `
|
|
55
|
+
* a SECOND one is refused rather than joined: `cs say hello there` is a message
|
|
56
56
|
* the shell already split, and quietly gluing it back together would be this
|
|
57
57
|
* command guessing at what the user typed. Quoting is the fix and the error says
|
|
58
58
|
* so.
|
|
@@ -67,7 +67,7 @@ function parseArgs(args) {
|
|
|
67
67
|
i += 1;
|
|
68
68
|
const value = args[i];
|
|
69
69
|
// A FLAG IS NOT A CARD ID. Swallowing the next token whatever it is sends
|
|
70
|
-
// `
|
|
70
|
+
// `cs say --card --help "x"` to Postgres and surfaces "invalid input
|
|
71
71
|
// syntax for type uuid", which is a real error and the wrong one: the
|
|
72
72
|
// truth is that no card was named.
|
|
73
73
|
if (value === undefined || value.startsWith('--')) {
|
|
@@ -141,8 +141,8 @@ async function existingCard(client, cardId) {
|
|
|
141
141
|
if (error)
|
|
142
142
|
throw new Error(`could not read card ${cardId}: ${error.message}`);
|
|
143
143
|
if (!data) {
|
|
144
|
-
throw new Error(`no card with id ${cardId}. Run \`
|
|
145
|
-
+ 'or `
|
|
144
|
+
throw new Error(`no card with id ${cardId}. Run \`cs say "<text>"\` with no --card to start a new one, `
|
|
145
|
+
+ 'or `cs show` to list the cards there are.');
|
|
146
146
|
}
|
|
147
147
|
return data;
|
|
148
148
|
}
|
|
@@ -160,7 +160,7 @@ async function existingCard(client, cardId) {
|
|
|
160
160
|
* what they typed, and having the product classify a message at send time is the
|
|
161
161
|
* move ux.md spends a section forbidding: "the moment the product classifies, it
|
|
162
162
|
* has to be right, and being wrong silently discards something the user said."
|
|
163
|
-
* So it stays null, `
|
|
163
|
+
* So it stays null, `cs show` says "no project yet", and that is the truth.
|
|
164
164
|
*
|
|
165
165
|
* IT IS A LABEL, NOT A WALL. ux.md: "Project is a filter the coordinator applies,
|
|
166
166
|
* never a wall it lives inside." Nothing downstream narrows a read to this
|
|
@@ -250,7 +250,7 @@ export async function say(args) {
|
|
|
250
250
|
const reason = error instanceof Error ? error.message : String(error);
|
|
251
251
|
if (isNew) {
|
|
252
252
|
throw new Error(`${reason}\ncard ${card.id} was created and carries no turn. `
|
|
253
|
-
+ `Retry with:
|
|
253
|
+
+ `Retry with: cs say --card ${card.id} "<text>"`);
|
|
254
254
|
}
|
|
255
255
|
throw error;
|
|
256
256
|
}
|
|
@@ -258,5 +258,5 @@ export async function say(args) {
|
|
|
258
258
|
out(`title ${card.title}`);
|
|
259
259
|
out(`turn ${turn.id} ${at(turn.created_at)} ${turn.addressed_at ? `addressed ${at(turn.addressed_at)}` : 'NOT ADDRESSED'}`);
|
|
260
260
|
out();
|
|
261
|
-
out(`
|
|
261
|
+
out(` cs show ${card.id}`);
|
|
262
262
|
}
|
package/dist/panel3/show.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ═══ AGENT PANEL v3: `
|
|
2
|
+
* ═══ AGENT PANEL v3: `cs show`, the whole record in a terminal. ═══
|
|
3
3
|
*
|
|
4
4
|
* THIS FILE BELONGS TO AGENT PANEL v3. Nothing outside `src/panel3/` may
|
|
5
5
|
* import it.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ═══ AN EMPTY RECORD AND A FAILED READ MUST NEVER LOOK ALIKE. ═══
|
|
17
17
|
*
|
|
18
18
|
* A read that comes back empty prints "No cards.". A read that FAILS throws, so
|
|
19
|
-
* `
|
|
19
|
+
* `cs` prints the reason on stderr and exits non-zero. `returned()` in
|
|
20
20
|
* `client.ts` is the one place that distinction is made, for every v3 command,
|
|
21
21
|
* and it also rejects the shape that would otherwise slip through: no error and
|
|
22
22
|
* no data, which is not "nothing is there" but "the client returned something
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
import { at, out, returned, signedInClient } from './client.js';
|
|
38
38
|
import { coordination, elsewhereMismatch, listeningFleet, } from './presence.js';
|
|
39
39
|
import { designatedCoordinator } from './coordinator.js';
|
|
40
|
+
import { DEFAULT_BRANCH_PATTERN, worktreeFolder } from './checkout.js';
|
|
41
|
+
import { existsSync } from 'node:fs';
|
|
40
42
|
import { listCodebaseLocations, listCodebases } from '../codebases.js';
|
|
41
43
|
/** The columns of `panel3_asks` that carry content, plus the reference that says
|
|
42
44
|
* whether they are the ones to read. */
|
|
@@ -80,8 +82,8 @@ export async function withAskContent(client, asks) {
|
|
|
80
82
|
const read = (query, what) => returned(query, 'read', what);
|
|
81
83
|
const CARD_COLUMNS = 'id, project_id, title, state, created_at, archived_at';
|
|
82
84
|
const TURN_COLUMNS = 'id, card_id, role, body, created_at, addressed_at, run_id';
|
|
83
|
-
const RUN_COLUMNS = 'id, card_id, codebase_id, parent_run_id, level, state, brief, report, failed_because,
|
|
84
|
-
+ 'pid, started_at, resumed_at, read_back_at, ended_at';
|
|
85
|
+
const RUN_COLUMNS = 'id, card_id, codebase_id, branch, parent_run_id, level, state, brief, report, failed_because, '
|
|
86
|
+
+ 'activity, machine_id, pid, started_at, resumed_at, read_back_at, ended_at';
|
|
85
87
|
const ASK_COLUMNS = `id, card_id, run_id, pending_run_id, answered_at, delivered_at, created_at, ${ASK_CONTENT_COLUMNS}`;
|
|
86
88
|
const OUTPUT_COLUMNS = 'id, card_id, run_id, kind, ref_id, label, created_at';
|
|
87
89
|
const ATTACHMENT_COLUMNS = 'id, card_id, kind, ref_id, label, created_at';
|
|
@@ -180,7 +182,7 @@ export async function loadOutputNames(client, outputs) {
|
|
|
180
182
|
* ═══ WHAT WAS ATTACHED TO ONE CARD. ═══
|
|
181
183
|
*
|
|
182
184
|
* One card at a time, unlike `loadChildren`'s batch of every card on the list
|
|
183
|
-
* view: `
|
|
185
|
+
* view: `cs show` prints attachments only in the single-card view (the plan's
|
|
184
186
|
* own line: the list view already says one thing per card, and this is
|
|
185
187
|
* per-item detail), and level 1's prompt and a dispatched brief are both about
|
|
186
188
|
* one card's work, never many at once. A batched loader with exactly one
|
|
@@ -190,10 +192,149 @@ export async function loadAttachments(client, cardId) {
|
|
|
190
192
|
return read(client.from('panel3_attachments').select(ATTACHMENT_COLUMNS).eq('card_id', cardId)
|
|
191
193
|
.order('created_at'), 'panel3_attachments');
|
|
192
194
|
}
|
|
195
|
+
export async function standingRulesFor(client, runId) {
|
|
196
|
+
const rows = await read(client
|
|
197
|
+
.from('panel3_runs')
|
|
198
|
+
.select('codebase_id, card:panel3_cards!panel3_runs_card_id_fkey(project_id)')
|
|
199
|
+
.eq('id', runId), `the standing rules run ${runId} is under`);
|
|
200
|
+
const run = rows[0];
|
|
201
|
+
const projectId = run?.card?.project_id ?? null;
|
|
202
|
+
if (!projectId)
|
|
203
|
+
return [];
|
|
204
|
+
const scoped = client
|
|
205
|
+
.from('agent_instructions')
|
|
206
|
+
.select('title, content, codebase_id')
|
|
207
|
+
.eq('project_id', projectId);
|
|
208
|
+
return read((run.codebase_id === null
|
|
209
|
+
? scoped.is('codebase_id', null)
|
|
210
|
+
: scoped.or(`codebase_id.is.null,codebase_id.eq.${run.codebase_id}`))
|
|
211
|
+
/* ═══ MOST SPECIFIC FIRST, WHICH IS THE ORDER ux.md GIVES. ═══ "A
|
|
212
|
+
codebase's documents beat the project's for work in that codebase", so
|
|
213
|
+
the codebase's are rendered above the project's. `nulls last` on a
|
|
214
|
+
descending-specificity sort is what puts the non-null rows first; within
|
|
215
|
+
each scope it is oldest-first, matching the list the person sees. */
|
|
216
|
+
.order('codebase_id', { nullsFirst: false })
|
|
217
|
+
.order('created_at'), `the standing rules on project ${projectId}`);
|
|
218
|
+
}
|
|
219
|
+
const NOTHING_STORED = {
|
|
220
|
+
base_branch: null, landing: null, branch_pattern: null, base_protected: null,
|
|
221
|
+
};
|
|
222
|
+
export async function storedGitRules(client, codebaseId) {
|
|
223
|
+
const rows = await read(client.from('codebase_git_rules')
|
|
224
|
+
.select('base_branch, landing, branch_pattern, base_protected')
|
|
225
|
+
.eq('codebase_id', codebaseId), `the git rules on codebase ${codebaseId}`);
|
|
226
|
+
return rows[0] ?? NOTHING_STORED;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* ═══ THE DETECTED DEFAULT, FROM TWO INPUTS, AND IT IS NEVER WRITTEN DOWN. ═══
|
|
230
|
+
*
|
|
231
|
+
* More than one member of the organization means a pull request; otherwise
|
|
232
|
+
* straight onto the main branch; and a PROTECTED base branch means a pull
|
|
233
|
+
* request whatever the organization's size, because on a protected branch the
|
|
234
|
+
* merge either fails or bypasses the review, and the second is worse.
|
|
235
|
+
*
|
|
236
|
+
* ═══ UNKNOWN PROTECTION FALLS BACK TO THE MEMBER COUNT AND SAYS SO. ═══ `null`
|
|
237
|
+
* is not `false`. Reading it as false would be the one wrong answer available
|
|
238
|
+
* here, because false is the answer that skips a review.
|
|
239
|
+
*
|
|
240
|
+
* Nothing here writes a setting. Detection produces an answer for this run and a
|
|
241
|
+
* sentence for the screen, and `landing` stays null until a person chooses.
|
|
242
|
+
*/
|
|
243
|
+
export function landingFor(stored, members) {
|
|
244
|
+
if (stored.landing !== null)
|
|
245
|
+
return { landing: stored.landing, detectedBecause: null };
|
|
246
|
+
if (stored.base_protected === true) {
|
|
247
|
+
return {
|
|
248
|
+
landing: 'pr',
|
|
249
|
+
detectedBecause: 'the main branch is protected on the remote, so a merge here would either '
|
|
250
|
+
+ 'fail or bypass the review',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return members > 1
|
|
254
|
+
? {
|
|
255
|
+
landing: 'pr',
|
|
256
|
+
detectedBecause: `this organization has ${members} members`
|
|
257
|
+
+ (stored.base_protected === null
|
|
258
|
+
? ', and nobody could check whether the main branch is protected'
|
|
259
|
+
: ''),
|
|
260
|
+
}
|
|
261
|
+
: {
|
|
262
|
+
landing: 'main',
|
|
263
|
+
detectedBecause: 'this organization has one member'
|
|
264
|
+
+ (stored.base_protected === null
|
|
265
|
+
? ', and nobody could check whether the main branch is protected'
|
|
266
|
+
: ', and the main branch is not protected'),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* How many people a change to these rules reaches, which is what the detected
|
|
271
|
+
* ending turns on.
|
|
272
|
+
*
|
|
273
|
+
* ═══ IT TAKES THE PROJECT, IT DOES NOT LOOK ONE UP. ═══ The project is on the
|
|
274
|
+
* run row already at every call site, and finding it from the codebase would
|
|
275
|
+
* mean naming `cliv2_codebases` from inside `panel3/`, which conventions.md's
|
|
276
|
+
* rule 4 forbids and `panel3-isolation.contract.test.mjs` fails on. `projects`
|
|
277
|
+
* and `memberships` are product tables, which v3 reads under the person's own
|
|
278
|
+
* RLS exactly as the web app does.
|
|
279
|
+
*/
|
|
280
|
+
async function orgMembers(client, projectId) {
|
|
281
|
+
const project = await read(client.from('projects').select('org_id').eq('id', projectId), `which organization project ${projectId} belongs to`);
|
|
282
|
+
const orgId = project[0]?.org_id;
|
|
283
|
+
/* ONE, NOT ZERO, when the project cannot be resolved: the count only ever
|
|
284
|
+
decides between "on your own" and "on a team", and a zero would read as
|
|
285
|
+
neither while behaving as the first. */
|
|
286
|
+
if (!orgId)
|
|
287
|
+
return 1;
|
|
288
|
+
return (await read(client.from('memberships').select('user_id').eq('org_id', orgId), `who is in organization ${orgId}`)).length;
|
|
289
|
+
}
|
|
290
|
+
export async function gitRulesFor(client, codebaseId, projectId) {
|
|
291
|
+
const stored = await storedGitRules(client, codebaseId);
|
|
292
|
+
/* THE MEMBER COUNT IS ONLY ASKED FOR WHEN IT DECIDES SOMETHING. A codebase
|
|
293
|
+
whose team chose an ending needs no count, and most do not. */
|
|
294
|
+
const members = stored.landing === null ? await orgMembers(client, projectId) : 1;
|
|
295
|
+
return {
|
|
296
|
+
...stored,
|
|
297
|
+
...landingFor(stored, members),
|
|
298
|
+
branchPattern: stored.branch_pattern ?? DEFAULT_BRANCH_PATTERN,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* WHAT WORK IN THIS CODEBASE STARTS FROM, recorded on the CODEBASE and not on
|
|
303
|
+
* the card that prompted the question (story 3 scenario 4), so the same question
|
|
304
|
+
* is never asked twice.
|
|
305
|
+
*
|
|
306
|
+
* Upsert on `codebase_id`, which is unique: the row may not exist yet, and a
|
|
307
|
+
* codebase nobody has configured is a codebase with no row rather than a row of
|
|
308
|
+
* nulls.
|
|
309
|
+
*/
|
|
310
|
+
export async function setMainBranch(client, codebaseId, branch) {
|
|
311
|
+
await returned(client.from('codebase_git_rules')
|
|
312
|
+
.upsert({ codebase_id: codebaseId, base_branch: branch }, { onConflict: 'codebase_id' })
|
|
313
|
+
.select('codebase_id'), 'record which branch work starts from', `codebase ${codebaseId}`);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* AN OBSERVATION, WRITTEN WHERE THE ANSWER WAS FOUND. Refreshed opportunistically
|
|
317
|
+
* by the machine that holds the checkout, and never turned into a setting:
|
|
318
|
+
* `landing` stays null until a person chooses, so detected and chosen stay
|
|
319
|
+
* distinguishable.
|
|
320
|
+
*
|
|
321
|
+
* A failure is SAID, not fatal. This is a fact about the remote, not about the
|
|
322
|
+
* work, and refusing to run a card because a `gh` call could not be recorded
|
|
323
|
+
* would be the observation deciding the outcome.
|
|
324
|
+
*/
|
|
325
|
+
export async function recordBaseProtection(client, codebaseId, isProtected) {
|
|
326
|
+
await returned(client.from('codebase_git_rules')
|
|
327
|
+
.upsert({
|
|
328
|
+
codebase_id: codebaseId,
|
|
329
|
+
base_protected: isProtected,
|
|
330
|
+
base_protected_at: new Date().toISOString(),
|
|
331
|
+
}, { onConflict: 'codebase_id' })
|
|
332
|
+
.select('codebase_id'), 'record whether the main branch is protected', `codebase ${codebaseId}`);
|
|
333
|
+
}
|
|
193
334
|
/**
|
|
194
335
|
* ═══ ONE LINE, EVERYWHERE AN ATTACHMENT IS NAMED. ═══
|
|
195
336
|
*
|
|
196
|
-
* `
|
|
337
|
+
* `cs show`'s ATTACHMENTS section, level 1's prompt and a dispatched brief all
|
|
197
338
|
* name the same fact about the same row, and ux.md's own warning about the
|
|
198
339
|
* outbound rules applies here too: two copies of "how an attachment reads"
|
|
199
340
|
* would drift, silently, in whichever caller nobody re-read. So there is one
|
|
@@ -202,7 +343,7 @@ export async function loadAttachments(client, cardId) {
|
|
|
202
343
|
*/
|
|
203
344
|
export const attachmentLine = (a) => `${a.kind} ${a.label} id ${a.ref_id}`;
|
|
204
345
|
/**
|
|
205
|
-
* ═══ THE WHOLE ATTACHMENTS SECTION, EXACTLY AS `
|
|
346
|
+
* ═══ THE WHOLE ATTACHMENTS SECTION, EXACTLY AS `cs show` PRINTS IT, OR
|
|
206
347
|
* NOTHING AT ALL. ═══
|
|
207
348
|
*
|
|
208
349
|
* Every other section on a card prints its own header and a "none" line at
|
|
@@ -215,7 +356,7 @@ export const attachmentLine = (a) => `${a.kind} ${a.label} id ${a.ref_id}`;
|
|
|
215
356
|
* PURE AND EXPORTED for the same reason `nothingWillActOn` is: the "prints
|
|
216
357
|
* nothing when there is nothing" rule is a fact about this function's return
|
|
217
358
|
* value, and a test asserting it should not need a live client or a running
|
|
218
|
-
* `
|
|
359
|
+
* `cs show` to see it.
|
|
219
360
|
*/
|
|
220
361
|
export function attachmentsSection(attachments) {
|
|
221
362
|
if (attachments.length === 0)
|
|
@@ -223,7 +364,19 @@ export function attachmentsSection(attachments) {
|
|
|
223
364
|
return [`ATTACHMENTS ${attachments.length}`, ...attachments.map((a) => ` ${attachmentLine(a)}`)];
|
|
224
365
|
}
|
|
225
366
|
async function codebaseSection(client, card, attachments, runs) {
|
|
226
|
-
|
|
367
|
+
/* ═══ WHAT THE CARD IS ATTACHED TO, AND WHAT ITS RUNS ARE ACTUALLY IN. ═══
|
|
368
|
+
They are not the same set: a codebase the coordinator chose is on the run
|
|
369
|
+
row and on no attachment, and keying this section off attachments alone
|
|
370
|
+
printed nothing at all for the ordinary path — including the branch and the
|
|
371
|
+
folder, which are the two things `cs show` exists to say here. */
|
|
372
|
+
const attached = attachments.filter((attachment) => attachment.kind === 'codebase');
|
|
373
|
+
const selected = [...new Set([
|
|
374
|
+
...attached.map((attachment) => attachment.ref_id),
|
|
375
|
+
...runs.map((run) => run.codebase_id).filter((id) => id !== null),
|
|
376
|
+
])].map((id) => ({
|
|
377
|
+
ref_id: id,
|
|
378
|
+
label: attached.find((attachment) => attachment.ref_id === id)?.label ?? id,
|
|
379
|
+
}));
|
|
227
380
|
if (selected.length === 0)
|
|
228
381
|
return [];
|
|
229
382
|
const codebases = card.project_id ? await listCodebases(client, card.project_id) : [];
|
|
@@ -252,10 +405,29 @@ async function codebaseSection(client, card, attachments, runs) {
|
|
|
252
405
|
const located = locations.some((location) => (location.machineId === machineId && location.gitRemoteUrl === codebase.gitRemoteUrl));
|
|
253
406
|
return `${located ? 'located on' : 'NOT LOCATED on'} ${machineName}`;
|
|
254
407
|
});
|
|
255
|
-
return ` ${name} ${availability.join(' · ')}`;
|
|
408
|
+
return ` ${name} ${availability.join(' · ')}${branchOf(runs, codebase.id)}`;
|
|
256
409
|
}),
|
|
257
410
|
];
|
|
258
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* ═══ THE BRANCH, AND — HERE AND ONLY HERE — THE FOLDER. ═══
|
|
414
|
+
*
|
|
415
|
+
* worktrees-8: no absolute path may reach the cloud or a hosted browser, so the
|
|
416
|
+
* card and the run details name the branch and stop. `cs show` runs on the
|
|
417
|
+
* person's own machine, which is the one surface where the path is already
|
|
418
|
+
* theirs, so this is where a power user reads it.
|
|
419
|
+
*
|
|
420
|
+
* The folder is printed only when it is really here: the copy is machine-local,
|
|
421
|
+
* and naming one on a machine that does not hold it would be an invitation to
|
|
422
|
+
* `cd` into nothing.
|
|
423
|
+
*/
|
|
424
|
+
function branchOf(runs, codebaseId) {
|
|
425
|
+
const branch = runs.find((run) => run.codebase_id === codebaseId && run.branch)?.branch;
|
|
426
|
+
if (!branch)
|
|
427
|
+
return '';
|
|
428
|
+
const folder = worktreeFolder(codebaseId, branch);
|
|
429
|
+
return `\n branch ${branch}${existsSync(folder) ? ` in ${folder}` : ''}`;
|
|
430
|
+
}
|
|
259
431
|
// ---------------------------------------------------------------------------
|
|
260
432
|
// WHAT THE RECORD MEANS.
|
|
261
433
|
const isLive = (run) => run.state === 'running' && !run.ended_at;
|
|
@@ -288,7 +460,7 @@ const isOutstanding = (ask) => !ask.answered_at || !ask.delivered_at;
|
|
|
288
460
|
*
|
|
289
461
|
* WHAT IT CHANGES HERE IS ONE SENTENCE, AND THE SENTENCE MATTERS: a stalled
|
|
290
462
|
* question normally has a move, and this is the one case where it has none.
|
|
291
|
-
* Printing `
|
|
463
|
+
* Printing `cs answer` at somebody whose answer would start nothing sends them
|
|
292
464
|
* to type a command that cannot do what it says.
|
|
293
465
|
*/
|
|
294
466
|
const askWasStopped = (ask, runs) => {
|
|
@@ -382,13 +554,13 @@ function notesAboutQuestions(asks, runs) {
|
|
|
382
554
|
const notes = [];
|
|
383
555
|
for (const ask of asks.filter((a) => isStalled(a, runs))) {
|
|
384
556
|
/* ═══ UNLESS ANSWERING IT WOULD DO NOTHING. ═══ A question asked by work the
|
|
385
|
-
user stopped is never delivered again, so `
|
|
557
|
+
user stopped is never delivered again, so `cs answer` settles the row and
|
|
386
558
|
starts nobody. Naming the command anyway would be this screen sending
|
|
387
559
|
somebody to type something that cannot do what it says, which is the same
|
|
388
560
|
failure as the command itself claiming an effect it did not have. */
|
|
389
561
|
if (askWasStopped(ask, runs)) {
|
|
390
562
|
notes.push(`question ${ask.id} was asked by work you stopped: answering it starts nothing, and `
|
|
391
|
-
+ `nothing is waiting for it. Send a message instead:
|
|
563
|
+
+ `nothing is waiting for it. Send a message instead: cs say --card ${ask.card_id} `
|
|
392
564
|
+ '"<text>".');
|
|
393
565
|
continue;
|
|
394
566
|
}
|
|
@@ -398,7 +570,7 @@ function notesAboutQuestions(asks, runs) {
|
|
|
398
570
|
to. Saying only that nothing will act on it would leave them reading a
|
|
399
571
|
dead end that is not one. */
|
|
400
572
|
notes.push(`question ${ask.id} went to an agent that has ended without answering it or passing it on: `
|
|
401
|
-
+ `nothing will act on it until you do, with
|
|
573
|
+
+ `nothing will act on it until you do, with cs answer ${ask.id} "<text>".`);
|
|
402
574
|
}
|
|
403
575
|
return notes;
|
|
404
576
|
}
|
|
@@ -508,7 +680,7 @@ designation, fleet) {
|
|
|
508
680
|
if (status !== 'here' && card.state === 'working') {
|
|
509
681
|
return [
|
|
510
682
|
status === 'nobody'
|
|
511
|
-
? 'no machine is running `
|
|
683
|
+
? 'no machine is running `cs start`, so nothing can pick this up: it is waiting for a '
|
|
512
684
|
+ 'machine, not being worked on. Start the CLI on a machine and it is taken from where '
|
|
513
685
|
+ 'it is.'
|
|
514
686
|
/* ═══ `'elsewhere'`, WHICH USED TO READ AS `'nobody'` AND CLAIMED NO
|
|
@@ -582,7 +754,7 @@ designation, fleet) {
|
|
|
582
754
|
}
|
|
583
755
|
// ---------------------------------------------------------------------------
|
|
584
756
|
// RENDERING. Plain text, aligned, and every id printed in full so it can be
|
|
585
|
-
// pasted straight back into `
|
|
757
|
+
// pasted straight back into `cs show`. `out` and `at` come from `client.ts`,
|
|
586
758
|
// so two commands cannot print the same instant two different ways.
|
|
587
759
|
const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
588
760
|
/** A body of text under a heading, indented, with its blank lines kept. */
|
|
@@ -647,7 +819,7 @@ export function machinesSection(designation, fleet) {
|
|
|
647
819
|
const lines = [
|
|
648
820
|
`coordinator ${designation ? `${designation.machineId} ${designation.agent}` : 'none designated, any machine may coordinate'}`,
|
|
649
821
|
fleet.length === 0
|
|
650
|
-
? 'listening none, so nothing can be picked up until a machine runs `
|
|
822
|
+
? 'listening none, so nothing can be picked up until a machine runs `cs start`'
|
|
651
823
|
: `listening ${count(fleet.length, 'machine')}`,
|
|
652
824
|
];
|
|
653
825
|
for (const m of fleet) {
|
|
@@ -711,7 +883,7 @@ async function showAllCards(client) {
|
|
|
711
883
|
}
|
|
712
884
|
}
|
|
713
885
|
out();
|
|
714
|
-
out('
|
|
886
|
+
out(' cs show <card-id> for one card in full.');
|
|
715
887
|
}
|
|
716
888
|
async function showCard(client, card) {
|
|
717
889
|
const [children, names, designation, fleet, attachments] = await Promise.all([
|
|
@@ -833,7 +1005,7 @@ async function showCard(client, card) {
|
|
|
833
1005
|
renderNotes(nothingWillActOn(card, turns, runs, asks, status, designation, fleet));
|
|
834
1006
|
if (runs.length > 0) {
|
|
835
1007
|
out();
|
|
836
|
-
out('
|
|
1008
|
+
out(' cs show <run-id> for a run\'s brief and report.');
|
|
837
1009
|
}
|
|
838
1010
|
}
|
|
839
1011
|
/**
|
|
@@ -884,8 +1056,9 @@ function renderRunTree(runs, readingBack) {
|
|
|
884
1056
|
const pad = ' '.repeat(depth + 1);
|
|
885
1057
|
const liveness = isLive(run) ? 'LIVE' : run.state;
|
|
886
1058
|
out(`${pad}L${run.level} ${liveness} ${run.id}`);
|
|
887
|
-
if (run.codebase_id)
|
|
888
|
-
out(`${pad} codebase ${run.codebase_id}`);
|
|
1059
|
+
if (run.codebase_id) {
|
|
1060
|
+
out(`${pad} codebase ${run.codebase_id}${run.branch ? ` branch ${run.branch}` : ''}`);
|
|
1061
|
+
}
|
|
889
1062
|
out(`${pad} machine ${run.machine_id}${run.pid ? ` pid ${run.pid}` : ''}`);
|
|
890
1063
|
out(`${pad} started ${at(run.started_at)}`
|
|
891
1064
|
+ (run.ended_at ? ` ended ${at(run.ended_at)}` : ' not ended'));
|
|
@@ -978,7 +1151,7 @@ async function showRun(client, run) {
|
|
|
978
1151
|
}
|
|
979
1152
|
// ---------------------------------------------------------------------------
|
|
980
1153
|
/**
|
|
981
|
-
* `
|
|
1154
|
+
* `cs show` with no id lists every card. With an id it shows that card in
|
|
982
1155
|
* full, or that run with its brief and report — one argument, resolved against
|
|
983
1156
|
* the record, because a `--run` flag would make the caller say which kind of id
|
|
984
1157
|
* they are holding when the record already knows.
|
package/dist/panel3/spawn.js
CHANGED
|
@@ -243,7 +243,10 @@ function codexArgs(level, toolsUrl, platform, ownerSession) {
|
|
|
243
243
|
'--ignore-user-config',
|
|
244
244
|
'-c', 'model="gpt-5.5"',
|
|
245
245
|
'-c', 'features.apps=false',
|
|
246
|
-
|
|
246
|
+
// The same closing `codex-home.ts` writes into the per-run config, and
|
|
247
|
+
// for the same measured reason: `features.multi_agent=false` parses and
|
|
248
|
+
// leaves the tool reachable.
|
|
249
|
+
'-c', 'agents.enabled=false',
|
|
247
250
|
'-c', `mcp_servers.${SERVER}.url=${JSON.stringify(toolsUrl)}`,
|
|
248
251
|
'-c', `mcp_servers.${SERVER}.default_tools_approval_mode="approve"`,
|
|
249
252
|
'-c', 'windows.sandbox="unelevated"',
|
|
@@ -331,7 +334,7 @@ const MAX_OUTPUT_CHARS = 1_000_000;
|
|
|
331
334
|
* spawn can fail hand back a message that carries one — node's own spawn error
|
|
332
335
|
* reads `spawn /Users/…/claude ENOENT`, and an EACCES from the synchronous throw
|
|
333
336
|
* names the file too — and both of these become a run's `failed_because`, which
|
|
334
|
-
* `
|
|
337
|
+
* `cs show` prints and a coordinator can repeat to the person.
|
|
335
338
|
*
|
|
336
339
|
* `workingCopy()` above already makes this argument for the checkout and the
|
|
337
340
|
* fix is the same one: the CODE is what a person acts on, it is not a path, and
|