cawdev-cli 1.0.5-beta → 1.0.7-beta
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/lib/agents.mjs +38 -0
- package/lib/background-shells.mjs +108 -0
- package/lib/codex.mjs +5 -0
- package/lib/roadmap-format.mjs +52 -0
- package/mcp/server.mjs +62 -11
- package/package.json +1 -1
- package/runner/README.md +191 -22
- package/runner/attach.mjs +708 -69
- package/runner/cawdev.mjs +17 -2
- package/runner/configure.mjs +34 -0
- package/runner/input.mjs +26 -9
- package/runner/prefs.mjs +52 -0
- package/runner/restart.mjs +191 -0
- package/runner/runner.mjs +323 -27
- package/runner/sign-in.mjs +11 -0
- package/runner/split.mjs +109 -16
package/runner/split.mjs
CHANGED
|
@@ -282,6 +282,20 @@ export function isPullRequest(url) {
|
|
|
282
282
|
* `view` of the run, which is where the card and the final state come from.
|
|
283
283
|
* @param waiting a word when the session is stopped on a person, else null.
|
|
284
284
|
*/
|
|
285
|
+
/**
|
|
286
|
+
* A dev session that ended FINISHED having committed nothing — R24: IMPLEMENT
|
|
287
|
+
* stopped on a plan it would not follow, TEST and MEMORY ran on an untouched
|
|
288
|
+
* branch, and the run finished. "finished" in green with `/pr` offered read
|
|
289
|
+
* as work done, and `/pr` then said there was nothing to propose. Known only
|
|
290
|
+
* once the terminal has read the run's commits (`meta.commits`); until then
|
|
291
|
+
* nothing is claimed either way.
|
|
292
|
+
*/
|
|
293
|
+
export function builtNothing(meta) {
|
|
294
|
+
const view = meta.view;
|
|
295
|
+
return Boolean(meta.ended && view?.state === 'FINISHED'
|
|
296
|
+
&& (view.profile ?? 'CODE') === 'CODE' && meta.commits === 0);
|
|
297
|
+
}
|
|
298
|
+
|
|
285
299
|
export function titleFor(meta, waiting = null) {
|
|
286
300
|
const view = meta.view ?? {};
|
|
287
301
|
const profile = view.profile ?? meta.profile ?? null;
|
|
@@ -289,9 +303,10 @@ export function titleFor(meta, waiting = null) {
|
|
|
289
303
|
const what = view.entryRef
|
|
290
304
|
? `${view.entryRef} ${view.entryTitle ?? view.label ?? meta.label ?? ''}`.trim()
|
|
291
305
|
: (view.label ?? meta.label ?? 'a session');
|
|
292
|
-
const state = meta
|
|
293
|
-
|
|
294
|
-
|
|
306
|
+
const state = builtNothing(meta) ? 'finished · nothing built'
|
|
307
|
+
: meta.ended
|
|
308
|
+
? String(view.state ?? 'ended').toLowerCase().replace(/_/g, ' ')
|
|
309
|
+
: meta.state ?? 'running';
|
|
295
310
|
// What it is and where it stands FIRST: a narrow window clips from the
|
|
296
311
|
// right, and "plan · finished" is what decides what to do next. The project
|
|
297
312
|
// is the window's subtitle, a line of its own, and not in here.
|
|
@@ -374,14 +389,15 @@ export function toneFor(meta) {
|
|
|
374
389
|
if (!state || meta.view?.live) {
|
|
375
390
|
return null;
|
|
376
391
|
}
|
|
377
|
-
|
|
392
|
+
// Finished having built nothing is not done: amber, not green.
|
|
393
|
+
return state === 'FINISHED' && !builtNothing(meta) ? 'done' : 'stopped';
|
|
378
394
|
}
|
|
379
395
|
|
|
380
396
|
/**
|
|
381
397
|
* What can be done about a session now — the window's last line. Empty while
|
|
382
398
|
* it runs: the prompt, the answer and the permission keys are the prompt's.
|
|
383
399
|
*
|
|
384
|
-
* A finished PLAN on a card offers its development. A finished session that
|
|
400
|
+
* A finished PLAN on a card offers its development — `d`, in its place. A finished session that
|
|
385
401
|
* wrote a branch offers the pull request until one is known, then shows it.
|
|
386
402
|
* Every ended window can be closed.
|
|
387
403
|
*/
|
|
@@ -397,12 +413,17 @@ export function actionsFor(meta) {
|
|
|
397
413
|
parts.push('continue asked for — back when this machine takes it');
|
|
398
414
|
}
|
|
399
415
|
if (view?.state === 'FINISHED' && view.profile === 'PLAN' && view.entryRef) {
|
|
400
|
-
|
|
416
|
+
// `d` (or /develop) develops it with the agent that planned it, and the
|
|
417
|
+
// development's window takes this one's place.
|
|
418
|
+
parts.push(`d develops ${view.entryRef} here, in place of this plan`);
|
|
419
|
+
parts.push('/open reads the plan on cawdev');
|
|
401
420
|
}
|
|
402
421
|
if (isPullRequest(view?.prUrl)) {
|
|
403
422
|
parts.push(`PR ${view.prUrl} · /open`);
|
|
404
423
|
} else if (meta.prAsked) {
|
|
405
424
|
parts.push(meta.prAsked === true ? 'pull request asked for…' : meta.prAsked);
|
|
425
|
+
} else if (builtNothing(meta)) {
|
|
426
|
+
parts.push(`nothing was committed${view.entryRef ? ` — /new plan ${view.entryRef} to re-plan it` : ''}`);
|
|
406
427
|
} else if (view?.branch && (view.profile ?? 'CODE') === 'CODE' && view.state === 'FINISHED') {
|
|
407
428
|
parts.push('/pr opens the pull request');
|
|
408
429
|
}
|
|
@@ -455,6 +476,20 @@ export function isAutoCheckout(path) {
|
|
|
455
476
|
* `{ needsProject, projects }` when no `@project` was written — the project
|
|
456
477
|
* is ALWAYS asked, never guessed — or `{ error }` in words.
|
|
457
478
|
*/
|
|
479
|
+
/**
|
|
480
|
+
* A command line as words — on spaces and tabs, with every line break kept as
|
|
481
|
+
* a word of its own (`'\n'`). Splitting on all whitespace turned a pasted list
|
|
482
|
+
* of requirements into one long line; kept here, `wordsText` puts it back.
|
|
483
|
+
*/
|
|
484
|
+
export function commandWords(text) {
|
|
485
|
+
return String(text).split(/[ \t]+/).flatMap((token) => token.split(/(\n)/)).filter(Boolean);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The words of a task back into its text, line breaks where they were. */
|
|
489
|
+
export function wordsText(words) {
|
|
490
|
+
return words.join(' ').replace(/ *\n */g, '\n').trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
458
493
|
export function parseNew(words, { projects = [] } = {}) {
|
|
459
494
|
const named = words.find((word) => word.startsWith('@'));
|
|
460
495
|
const rest = words.filter((word) => word !== named && word !== '');
|
|
@@ -469,7 +504,7 @@ export function parseNew(words, { projects = [] } = {}) {
|
|
|
469
504
|
if (slug && projects.length && !projects.includes(slug)) {
|
|
470
505
|
return { error: `this machine does not serve ${slug} — it serves ${projects.join(', ')}` };
|
|
471
506
|
}
|
|
472
|
-
const text = args
|
|
507
|
+
const text = wordsText(args);
|
|
473
508
|
if (!text) {
|
|
474
509
|
return { error: kind === 'ask' ? 'ask what? /new ask <question>'
|
|
475
510
|
: `${kind} what? /new ${kind} R3, or /new ${kind} <the task in words>` };
|
|
@@ -590,24 +625,25 @@ export function wordWrap(text, width) {
|
|
|
590
625
|
|
|
591
626
|
/**
|
|
592
627
|
* The board's columns, in the board's order, and the words the console uses
|
|
593
|
-
* for them
|
|
594
|
-
*
|
|
595
|
-
*
|
|
628
|
+
* for them — the work still to be done, which is what somebody at this
|
|
629
|
+
* terminal acts on. MERGED, SHIPPED and DECLINED are not columns: the work
|
|
630
|
+
* is over, the three only grow, and the console's own board folds the last
|
|
631
|
+
* two to a count (R68) for the same reason — so here all three are counts
|
|
632
|
+
* beside the project's name.
|
|
596
633
|
*/
|
|
597
634
|
export const BOARD_COLUMNS = [
|
|
598
635
|
['CONSIDERING', 'Considering'],
|
|
599
636
|
['PLANNED', 'Planned'],
|
|
600
637
|
['IN_DEVELOPMENT', 'In development'],
|
|
601
|
-
['MERGED', 'Merged'],
|
|
602
638
|
];
|
|
603
|
-
const FOLDED = [['SHIPPED', 'shipped'], ['DECLINED', 'declined']];
|
|
639
|
+
const FOLDED = [['MERGED', 'merged'], ['SHIPPED', 'shipped'], ['DECLINED', 'declined']];
|
|
604
640
|
|
|
605
641
|
/**
|
|
606
|
-
* Which column gives way first when the width runs out: the
|
|
607
|
-
*
|
|
608
|
-
*
|
|
642
|
+
* Which column gives way first when the width runs out: the ideas, then what
|
|
643
|
+
* is ready, and what is being built last of all — the order of how much a
|
|
644
|
+
* person at this terminal is about to act on it.
|
|
609
645
|
*/
|
|
610
|
-
const GIVES_WAY = ['
|
|
646
|
+
const GIVES_WAY = ['CONSIDERING', 'PLANNED', 'IN_DEVELOPMENT'];
|
|
611
647
|
|
|
612
648
|
/** Narrower than this, a column's card lines say nothing. */
|
|
613
649
|
export const MIN_COLUMN = 18;
|
|
@@ -828,3 +864,60 @@ export function sessionTag(n, meta) {
|
|
|
828
864
|
: (view.label ?? meta?.label ?? 'a session'), 32);
|
|
829
865
|
return `${n ? `${n} · ` : ''}${type} · ${what}${meta?.projectSlug ? ` — ${meta.projectSlug}` : ''}`;
|
|
830
866
|
}
|
|
867
|
+
|
|
868
|
+
// --- a card, in the terminal ---------------------------------------------------
|
|
869
|
+
|
|
870
|
+
/** How many lines of a card's body `/view` prints before pointing at the web. */
|
|
871
|
+
export const CARD_BODY_LINES = 30;
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* `/view R29`: a card as lines for the transcript — what it is, where it
|
|
875
|
+
* stands, who has it, its branch and pull request, then its body. Pure, so it
|
|
876
|
+
* is read at every combination rather than guessed from a live platform.
|
|
877
|
+
* Only what the card SAYS: an empty field draws no line, because a row of
|
|
878
|
+
* "none"s is read instead of scanned.
|
|
879
|
+
*
|
|
880
|
+
* @param card the platform's RoadmapEntryView (`GET …/roadmap/{ref}`).
|
|
881
|
+
* @param url the card's page on the console, or null.
|
|
882
|
+
*/
|
|
883
|
+
export function cardLines(card, ink, url = null) {
|
|
884
|
+
const lines = [];
|
|
885
|
+
const field = (label, value) => {
|
|
886
|
+
if (value) lines.push(` ${ink.muted(label.padEnd(9))} ${value}`);
|
|
887
|
+
};
|
|
888
|
+
lines.push(` ${ink.bold(`${card.ref ?? ''} ${card.title ?? ''}`.trim())}`);
|
|
889
|
+
const kind = card.kind === 'ISSUE'
|
|
890
|
+
? `issue${card.severity ? ` · ${String(card.severity).toLowerCase()}` : ''}` : null;
|
|
891
|
+
field('status', [card.statusDisplay ?? card.status, kind].filter(Boolean).join(' · '));
|
|
892
|
+
field('section', card.section);
|
|
893
|
+
field('tags', card.tags?.length ? card.tags.join(', ') : null);
|
|
894
|
+
field('sprint', card.sprint ? `${card.sprint.ref ?? card.sprint.number} ${card.sprint.name ?? ''}`.trim() : null);
|
|
895
|
+
const item = card.workItem;
|
|
896
|
+
field('branch', card.branch ?? item?.branch);
|
|
897
|
+
field('work', item ? [item.statusDisplay ?? item.status, item.runnerName && `on ${item.runnerName}`]
|
|
898
|
+
.filter(Boolean).join(' · ') : null);
|
|
899
|
+
const pr = item?.prUrl ?? card.review?.prUrl;
|
|
900
|
+
field('pr', isPullRequest(pr) ? pr : null);
|
|
901
|
+
const run = card.activeRun;
|
|
902
|
+
field('session', run ? [String(run.state ?? '').toLowerCase().replace(/_/g, ' '),
|
|
903
|
+
run.runnerName && `on ${run.runnerName}`].filter(Boolean).join(' · ') : null);
|
|
904
|
+
field('coding', card.codingBy?.email);
|
|
905
|
+
field('review', card.reviewingBy?.email);
|
|
906
|
+
field('rejected', card.rejection?.note ? `${card.rejection.note}${card.rejection.decidedByEmail ? ` — ${card.rejection.decidedByEmail}` : ''}` : null);
|
|
907
|
+
field('merged', card.merge);
|
|
908
|
+
field('version', card.version);
|
|
909
|
+
field('declined', card.declinedReason);
|
|
910
|
+
field('related', card.relatedRefs?.length ? card.relatedRefs.join(', ') : null);
|
|
911
|
+
field('after', card.after?.length ? card.after.map((each) => each.ref).join(', ') : null);
|
|
912
|
+
field('web', url);
|
|
913
|
+
const body = String(card.body ?? '').replace(/\s+$/, '');
|
|
914
|
+
if (body) {
|
|
915
|
+
const all = body.split('\n');
|
|
916
|
+
lines.push('');
|
|
917
|
+
for (const line of all.slice(0, CARD_BODY_LINES)) lines.push(` ${line}`);
|
|
918
|
+
if (all.length > CARD_BODY_LINES) {
|
|
919
|
+
lines.push(ink.muted(` … ${all.length - CARD_BODY_LINES} more line(s)${url ? ' — the rest on the web' : ''}`));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return lines;
|
|
923
|
+
}
|