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/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.ended
293
- ? String(view.state ?? 'ended').toLowerCase().replace(/_/g, ' ')
294
- : meta.state ?? 'running';
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
- return state === 'FINISHED' ? 'done' : 'stopped';
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
- parts.push(`/develop starts ${view.entryRef}`);
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.join(' ').trim();
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. SHIPPED and DECLINED are not columns: they only grow, and the
594
- * console's own board folds them to a count (R68) for the same reason — so
595
- * here they are counts beside the project's name.
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 finished work,
607
- * then the ideas, then what is ready, and what is being built last of all —
608
- * the order of how much a person at this terminal is about to act on it.
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 = ['MERGED', 'CONSIDERING', 'PLANNED', 'IN_DEVELOPMENT'];
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
+ }