@cspeach/cli 1.1.29 → 1.1.31

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.
Files changed (105) hide show
  1. package/.cspeach/project.json +4 -0
  2. package/README.md +11 -1
  3. package/dist/agent/loop.js +127 -11
  4. package/dist/auth/auth-file.js +2 -2
  5. package/dist/cli.js +39 -13
  6. package/dist/commands/config-set.js +1 -1
  7. package/dist/commands/login.js +1 -1
  8. package/dist/commands/logout.js +1 -1
  9. package/dist/commands/plan-continue.js +1 -1
  10. package/dist/commands/plan-gate.js +8 -1
  11. package/dist/commands/plan-resume.js +24 -18
  12. package/dist/commands/plan-startup.js +49 -6
  13. package/dist/commands/team-discover.js +195 -0
  14. package/dist/commands/team-plan.js +585 -0
  15. package/dist/commands/team.js +652 -46
  16. package/dist/commands/whoami.js +1 -1
  17. package/dist/config/paths.js +15 -1
  18. package/dist/journey/chain.js +728 -0
  19. package/dist/journey/clean-core.js +213 -0
  20. package/dist/journey/flow.js +166 -0
  21. package/dist/journey/run.js +435 -0
  22. package/dist/journey/session-deps.js +77 -0
  23. package/dist/journey/trigger.js +153 -0
  24. package/dist/lane/app-chain.js +9 -7
  25. package/dist/lane/chain-token.js +9 -5
  26. package/dist/lane/roots.js +97 -41
  27. package/dist/one-shot.js +21 -1
  28. package/dist/projects/clean-core-confirm.js +192 -0
  29. package/dist/projects/detail-path.js +9 -4
  30. package/dist/projects/extract-clean-core.js +82 -0
  31. package/dist/projects/migration.js +4 -105
  32. package/dist/projects/plan-lane.js +67 -17
  33. package/dist/projects/plan-sources.js +13 -7
  34. package/dist/projects/project-folder.js +131 -0
  35. package/dist/projects/promote-command.js +39 -0
  36. package/dist/projects/save-command.js +115 -15
  37. package/dist/projects/saved-lines.js +17 -3
  38. package/dist/projects/sort-command.js +499 -0
  39. package/dist/projects/sort-core.js +248 -0
  40. package/dist/projects/validate.js +2 -1
  41. package/dist/projects/workspace-retire.js +48 -0
  42. package/dist/projects/workspace.js +182 -120
  43. package/dist/register/atc-file.js +9 -1
  44. package/dist/register/discover.js +901 -0
  45. package/dist/register/findings.js +82 -0
  46. package/dist/register/measure.js +60 -1
  47. package/dist/register/offer.js +125 -0
  48. package/dist/register/picture.js +25 -0
  49. package/dist/register/plan-import.js +325 -0
  50. package/dist/register/records.js +57 -0
  51. package/dist/register/status-file/estate-parts.js +168 -0
  52. package/dist/register/status-file/plan-parts.js +68 -0
  53. package/dist/register/status-file/progress-chart.js +110 -0
  54. package/dist/register/status-file/register-map.js +221 -0
  55. package/dist/register/status-file/register-model.js +341 -0
  56. package/dist/register/status-file/register-parts.js +555 -0
  57. package/dist/register/status-file/register-status-file.js +189 -0
  58. package/dist/register/status-file/team-css.js +5 -0
  59. package/dist/register/status-file-sync.js +22 -0
  60. package/dist/register/status-html.js +19 -0
  61. package/dist/register/store.js +40 -2
  62. package/dist/register/workbooks.js +124 -0
  63. package/dist/register/write-whole.js +22 -0
  64. package/dist/register/xlsx.js +127 -0
  65. package/dist/register/xref.js +117 -0
  66. package/dist/renderer/startup-lines.js +11 -0
  67. package/dist/renderer/tool-labels.js +3 -1
  68. package/dist/renderer/tool-widget.js +15 -1
  69. package/dist/repl/at-completer.js +6 -2
  70. package/dist/repl/at-picker.js +1 -1
  71. package/dist/repl/journey-entry.js +25 -0
  72. package/dist/repl.js +158 -39
  73. package/dist/session/backfill.js +342 -0
  74. package/dist/session/project-match.js +63 -0
  75. package/dist/session/resume.js +72 -24
  76. package/dist/session/schema.js +3 -1
  77. package/dist/session/store.js +96 -51
  78. package/dist/skill-catalog.js +2 -2
  79. package/dist/skills/bundled-skills.js +45 -45
  80. package/dist/tools/_filesystem-shared.js +190 -21
  81. package/dist/tools/_team-work-path.js +133 -0
  82. package/dist/tools/approval.js +7 -1
  83. package/dist/tools/filesystem/extract-xlsx.js +27 -3
  84. package/dist/tools/filesystem/file-edit.js +12 -4
  85. package/dist/tools/filesystem/file-read.js +8 -4
  86. package/dist/tools/filesystem/file-write.js +12 -4
  87. package/dist/tools/filesystem/glob.js +33 -8
  88. package/dist/tools/filesystem/grep.js +9 -4
  89. package/dist/tools/filesystem/read-document.js +6 -3
  90. package/dist/tools/project/cca_merge.js +8 -2
  91. package/dist/tools/project/team_estate.js +91 -0
  92. package/dist/tools/project/team_findings.js +145 -0
  93. package/dist/tools/project/upgrade_merge_progress.js +8 -2
  94. package/dist/tools/sap-read.js +21 -4
  95. package/dist/tools/sap-write.js +203 -5
  96. package/dist/tools/subagent/read_agent.js +3 -0
  97. package/dist/tools/tool-loading.js +2 -2
  98. package/dist/tools/transport-fit-check.js +117 -0
  99. package/dist/tools/transport-fit.js +15 -7
  100. package/dist/tools/transport.js +57 -2
  101. package/dist/ui/app.js +2 -2
  102. package/dist/ui/approval-modal.js +74 -6
  103. package/dist/ui/widgets/confirm-card.js +68 -5
  104. package/dist/util/open-in-browser.js +143 -0
  105. package/package.json +120 -120
@@ -7,6 +7,7 @@ import { extractUpgradeScan, extractUpgradeProgress, extractUpgradeReport } from
7
7
  import { extractCcaAssessment } from './extract-cca.js';
8
8
  import { extractModernizeResult } from './extract-modernize.js';
9
9
  import { extractTestCoverage } from './extract-test-coverage.js';
10
+ import { extractCleanCoreDecision } from './extract-clean-core.js';
10
11
  import { extractPlan, laneConflictsWithPrior, LANE_NEW_FAMILY } from './extract-plan.js';
11
12
  import { buildEnvelope } from './build.js';
12
13
  import { buildPlanRevision } from './plan-run.js';
@@ -17,6 +18,7 @@ import { saveProject } from './save.js';
17
18
  import { formatSavedLines } from './saved-lines.js';
18
19
  import { writePlanHandover } from './handover-md.js';
19
20
  import { ensureWorkspace, resolveWorkspacePath } from './workspace.js';
21
+ import { resolveProjectFolderSync, ensureProjectId } from './project-folder.js';
20
22
  import { askYesNo, isYes } from './yes-no.js';
21
23
  import { checkLaneConsistency, checkRequiredDeploy, LANE_CHANGED_AT_CREATE, readPlanLane } from './plan-lane.js';
22
24
  import { specGapRefForSave } from './plan-sources.js';
@@ -103,6 +105,14 @@ const SKILL_REGISTRY = {
103
105
  itemNoun: 'phases',
104
106
  manifestMarker: 'plan-manifest',
105
107
  },
108
+ 'abap-clean-core': {
109
+ // 2026-10-06 (clean-core mission control): a triage table. The rows become
110
+ // team project decisions only after one Yes per row (clean-core-confirm.ts).
111
+ artefactType: 'clean-core-decision',
112
+ extract: (md) => extractCleanCoreDecision(md),
113
+ itemNoun: 'decisions',
114
+ manifestMarker: 'clean-core-manifest',
115
+ },
106
116
  };
107
117
  /**
108
118
  * C1 (2026-06-11, defect D22) — map an inline artifact-manifest block in the
@@ -128,6 +138,8 @@ export function detectArtifactSkill(markdown) {
128
138
  return 'abap-modernize';
129
139
  if (/<!--\s*(?:csforge|cspeach):test-coverage-manifest\b/.test(markdown))
130
140
  return 'abap-test';
141
+ if (/<!--\s*(?:csforge|cspeach):clean-core-manifest\b/.test(markdown))
142
+ return 'abap-clean-core';
131
143
  const up = /<!--\s*(?:csforge|cspeach):upgrade-manifest\s*\n([\s\S]*?)\n\s*-->/.exec(markdown);
132
144
  if (up) {
133
145
  const art = /(?:^|\n)\s*artefact:\s*(\S+)/.exec(up[1] ?? '');
@@ -360,22 +372,54 @@ export async function runSaveCommand(args) {
360
372
  }
361
373
  // C1 review fix — name the revision target BEFORE the confirm so the user
362
374
  // knows what a 'y' will do. Log line only, deliberately NOT a second
363
- // question: the B5 headless contract (below) allows at most one prompt.
375
+ // question: the B5 headless contract (below) allows one `prompt` question.
364
376
  if (planExtract && prior) {
365
377
  const priorFilename = prior.path.split(/[\\\/]/).pop();
366
378
  args.log(`[save] this will save as v${prior.envelope.version + 1} of existing plan '${prior.envelope.title}' (${priorFilename})`);
367
379
  }
368
- // CONTRACT (B5): runSaveCommand may ask the user at most ONE question —
369
- // this save confirm. The headless path in loop.ts (maybeOfferSave) installs
370
- // a prompt that blanket-answers 'y' to WHATEVER it is asked; any second
371
- // question added here would silently receive 'y' too. If you need another
372
- // prompt, give maybeOfferSave's headless prompt a question-aware answer
373
- // first.
374
- const answer = await askYesNo({ question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt });
380
+ // CONTRACT (B5): runSaveCommand asks 1+N cards — this save confirm, then
381
+ // (clean-core decisions only) one card per row. Only the save confirm may
382
+ // fall back to `prompt`: the headless path in loop.ts (maybeOfferSave)
383
+ // installs a prompt that blanket-answers 'y' to WHATEVER it is asked, so
384
+ // the row cards go through `confirm` only (never `prompt`), and a headless
385
+ // run confirms no row. Any further question routed through `prompt` would
386
+ // silently receive 'y' too: give maybeOfferSave's headless prompt a
387
+ // question-aware answer first.
388
+ // Inside a journey a triage is saved without the question: the journey needs the rows.
389
+ const journey = spec.artefactType === 'clean-core-decision' ? args.journey : undefined;
390
+ const answer = journey ? 'y' : await askYesNo({ question: 'Save as project file?', confirm: args.confirm, prompt: args.prompt });
375
391
  if (!isYes(answer)) {
376
392
  args.onNotSaved?.('declined');
377
393
  return null;
378
394
  }
395
+ // 2026-10-06 — a clean-core triage becomes team project decisions only row by
396
+ // row, each on its own Yes. Without a joined team project nothing is asked.
397
+ let tally;
398
+ if (spec.artefactType === 'clean-core-decision') {
399
+ const { confirmCleanCoreRows, decideCleanCoreRows } = await import('./clean-core-confirm.js');
400
+ const { activeProject } = await import('../register/store.js');
401
+ const joined = activeProject() !== null;
402
+ if (journey && joined) {
403
+ // Guided journey: [Yes] [Keep as is] [Skip] per row.
404
+ const r = await decideCleanCoreRows(extract, {
405
+ card: journey.card, ask: journey.ask, skip: journey.skip, log: args.log,
406
+ });
407
+ extract = r.extract;
408
+ tally = r.tally;
409
+ }
410
+ else {
411
+ if (journey) {
412
+ // Inside a journey: no team words, no command to type (Task 7).
413
+ const { JOURNEY_NOT_IN_PROJECT_LINE } = await import('./clean-core-confirm.js');
414
+ args.log(JOURNEY_NOT_IN_PROJECT_LINE);
415
+ }
416
+ else {
417
+ extract = await confirmCleanCoreRows(extract, {
418
+ ...(args.confirm ? { confirm: args.confirm } : {}), log: args.log, joined,
419
+ });
420
+ }
421
+ }
422
+ }
379
423
  let env;
380
424
  if (planExtract && prior) {
381
425
  // C1 (D30) — the 49dd duplicate: the generic save used to mint a fresh
@@ -414,6 +458,17 @@ export async function runSaveCommand(args) {
414
458
  let outDir;
415
459
  try {
416
460
  outDir = await ensureWorkspace();
461
+ // 2026-10-08 — the first save writes .cspeach/project.json (solo/home);
462
+ // a team folder uses its team id. Never fails the save.
463
+ try {
464
+ const pf = resolveProjectFolderSync(process.cwd());
465
+ const projectId = pf.kind === 'team' ? pf.projectId : ensureProjectId(pf.workDir);
466
+ try {
467
+ args.onProjectId?.(projectId);
468
+ }
469
+ catch { /* tagging is best-effort */ }
470
+ }
471
+ catch { /* id file is best-effort */ }
417
472
  }
418
473
  catch {
419
474
  outDir = args.cwd;
@@ -428,7 +483,9 @@ export async function runSaveCommand(args) {
428
483
  writePlanHandover(path, args.log);
429
484
  // 1.1.28 — two plain lines; the old suggested-email block is gone (saved-lines.ts).
430
485
  const [savedLine, reviewLine] = formatSavedLines(path, args.cwd);
431
- args.log(savedLine);
486
+ // Inside a journey the file is plumbing: the closing lines say what was recorded.
487
+ if (!journey)
488
+ args.log(savedLine);
432
489
  // Project Register (2026-09-18): when this laptop has joined a project, a
433
490
  // decision / fix record is written to the LOCAL outbox as a by-product of the
434
491
  // save. No project joined → `activeProject()` is one missing-file read and
@@ -438,20 +495,63 @@ export async function runSaveCommand(args) {
438
495
  const joined = activeProject();
439
496
  if (joined) {
440
497
  const { recordFromEnvelope } = await import('../register/records.js');
441
- const res = recordFromEnvelope(env, path, joined, { name: args.author.name }, args.cwd);
442
- if (res?.file)
443
- args.log(`Recorded for team project ${joined.projectId}: ${res.written} objects → ${res.file}`);
498
+ // Task 15 — detail files are read from where the skill wrote them: the
499
+ // seat work root in a team folder, the start folder otherwise. The
500
+ // record itself still goes to the shared outbox.
501
+ const res = recordFromEnvelope(env, path, joined, { name: joined.name ?? args.author.name }, resolveProjectFolderSync(args.cwd).workRoot);
502
+ if (tally) {
503
+ const { journeyRowLines } = await import('./clean-core-confirm.js');
504
+ for (const line of journeyRowLines(tally, res?.refused ?? []))
505
+ args.log(line);
506
+ }
507
+ else if (res?.file)
508
+ args.log(`Recorded for team project ${joined.projectId}: ${res.written} objects → ${basename(res.file)}`);
509
+ if (res?.file) {
510
+ const { regenerateWorkingList } = await import('../register/workbooks.js');
511
+ // The name the team project knows this laptop by (init --lead), as the team verbs use.
512
+ const sheet = regenerateWorkingList(joined, joined.name ?? args.author.name);
513
+ if (journey) {
514
+ const { journeyListLine } = await import('./clean-core-confirm.js');
515
+ const line = journeyListLine(sheet.kind);
516
+ if (line)
517
+ args.log(line);
518
+ }
519
+ else if (sheet.line)
520
+ args.log(sheet.line.trimStart());
521
+ }
444
522
  if (res?.refused.length) {
445
- args.log(`Not recorded (${res.refused.length} not in the team project's object list): ${res.refused.slice(0, 5).join(', ')}${res.refused.length > 5 ? ', …' : ''}`);
523
+ args.log(`Not recorded (${res.refused.length} not in ${tally ? 'your project' : 'the team project'}'s object list): ${res.refused.slice(0, 5).join(', ')}${res.refused.length > 5 ? ', …' : ''}`);
446
524
  }
447
525
  }
448
526
  }
449
527
  catch (e) {
450
- args.log(`(team project record not written: ${e instanceof Error ? e.message : String(e)})`);
528
+ if (journey) {
529
+ const { notRecordedLine } = await import('../journey/run.js');
530
+ args.log(notRecordedLine(e));
531
+ }
532
+ else
533
+ args.log(`(team project record not written: ${e instanceof Error ? e.message : String(e)})`);
451
534
  }
452
535
  if (planExtract && prior) {
453
536
  args.log(`(revision v${env.version} of the existing plan — version chain preserved, no new id)`);
454
537
  }
455
- args.log(reviewLine);
538
+ if (!journey)
539
+ args.log(reviewLine);
540
+ if (journey && tally) {
541
+ if (tally.cancelled) {
542
+ journey.pause();
543
+ }
544
+ else if (journey.page?.length) {
545
+ // One triage per page: a row of the page the triage left out is put off
546
+ // until "carry on", so the journey moves on instead of asking it again — and says so.
547
+ const answered = new Set([...tally.yes, ...tally.kept.map((k) => k.key), ...tally.skipped]);
548
+ const left = journey.page.filter((k) => !answered.has(k.toUpperCase()));
549
+ if (left.length) {
550
+ journey.leaveForLater(left);
551
+ const { leftOutLine } = await import('./clean-core-confirm.js');
552
+ args.log(leftOutLine(left));
553
+ }
554
+ }
555
+ }
456
556
  return path;
457
557
  }
@@ -10,7 +10,8 @@
10
10
  *
11
11
  * Never prints a user name.
12
12
  */
13
- import { basename, dirname, join, resolve } from 'node:path';
13
+ import { basename, dirname, resolve } from 'node:path';
14
+ import { resolveProjectFolderSync } from './project-folder.js';
14
15
  function editorLabel() {
15
16
  const url = process.env.CSPEACH_EDITOR_URL?.trim();
16
17
  return url ? url : 'cspeach.dev/edit';
@@ -22,10 +23,23 @@ function sameDir(a, b) {
22
23
  };
23
24
  return norm(a) === norm(b);
24
25
  }
25
- /** `<file name> (in this project's .cspeach folder)` — or the folder, when elsewhere. */
26
+ /**
27
+ * `<file name> (in <where>)` — names the folder kind (2026-10-08): this
28
+ * project's .cspeach folder (solo), your own working folder (team), or the
29
+ * home fallback folder with a hint. Anywhere else: the folder itself.
30
+ */
26
31
  export function savedLocation(savedPath, cwd) {
27
32
  const dir = dirname(savedPath);
28
- const where = sameDir(dir, join(cwd, '.cspeach')) ? "this project's .cspeach folder" : dir;
33
+ const pf = resolveProjectFolderSync(cwd);
34
+ let where = dir;
35
+ if (sameDir(dir, pf.workDir)) {
36
+ if (pf.kind === 'solo')
37
+ where = "this project's .cspeach folder";
38
+ else if (pf.kind === 'team')
39
+ where = 'your own working folder for this team project, not shared';
40
+ else
41
+ where = `${dir} — start CSPeach in a project folder to keep projects apart`;
42
+ }
29
43
  return `${basename(savedPath)} (in ${where})`;
30
44
  }
31
45
  export function formatSavedLines(savedPath, cwd) {