@cspeach/cli 1.1.47 → 1.1.49

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 (110) hide show
  1. package/dist/agent/level.js +1 -1
  2. package/dist/agent/loop.js +7 -6
  3. package/dist/agent/sap-connection-adapter.js +2 -0
  4. package/dist/agent/system-prompt.js +2 -2
  5. package/dist/agent/tool-dispatch.js +13 -1
  6. package/dist/approvals/op-labels.js +49 -10
  7. package/dist/approvals/render.js +14 -23
  8. package/dist/approvals/risk-floor.js +196 -19
  9. package/dist/approvals/visible-changes.js +35 -0
  10. package/dist/commands/config-set.js +5 -5
  11. package/dist/commands/cost.js +2 -1
  12. package/dist/commands/help.js +1 -1
  13. package/dist/commands/project.js +301 -0
  14. package/dist/commands/team-plan.js +16 -6
  15. package/dist/commands/team.js +295 -68
  16. package/dist/config/loader.js +13 -0
  17. package/dist/cost/cost-log.js +7 -4
  18. package/dist/cost/credits-wire.js +11 -6
  19. package/dist/cost/credits.js +15 -0
  20. package/dist/cost/display-mode.js +16 -4
  21. package/dist/doctor/checks/zcspeach.js +96 -0
  22. package/dist/journey/chain.js +489 -61
  23. package/dist/journey/clean-core.js +61 -36
  24. package/dist/journey/flow.js +75 -2
  25. package/dist/journey/people.js +37 -0
  26. package/dist/journey/rebuild.js +7 -6
  27. package/dist/journey/run.js +129 -42
  28. package/dist/journey/trigger.js +16 -10
  29. package/dist/one-shot.js +1 -1
  30. package/dist/project-context/render.js +7 -0
  31. package/dist/projects/clean-core-confirm.js +166 -44
  32. package/dist/projects/email-template.js +56 -0
  33. package/dist/projects/extract-cca.js +8 -7
  34. package/dist/projects/index.js +0 -1
  35. package/dist/projects/merge-cca.js +0 -19
  36. package/dist/projects/merge-upgrade.js +2 -2
  37. package/dist/projects/project-folder.js +2 -1
  38. package/dist/projects/save-command.js +51 -14
  39. package/dist/register/decision-rules.js +2 -1
  40. package/dist/register/export-files.js +18 -0
  41. package/dist/register/picture.js +3 -3
  42. package/dist/register/project-transport.js +125 -0
  43. package/dist/register/records.js +25 -13
  44. package/dist/register/rules-edit.js +5 -0
  45. package/dist/register/status-file/progress-chart.js +17 -6
  46. package/dist/register/status-file/register-map.js +8 -5
  47. package/dist/register/status-file/register-model.js +55 -36
  48. package/dist/register/status-file/register-parts.js +97 -82
  49. package/dist/register/status-file/register-status-file.js +16 -7
  50. package/dist/register/status-file/report-model.js +293 -205
  51. package/dist/register/status-file/team-css.js +1 -1
  52. package/dist/register/status-html.js +11 -4
  53. package/dist/register/store.js +2 -2
  54. package/dist/register/workbooks.js +24 -13
  55. package/dist/renderer/footer-line.js +10 -2
  56. package/dist/renderer/status-footer.js +2 -1
  57. package/dist/renderer/tool-labels.js +2 -2
  58. package/dist/repl/builtin-commands.js +1 -1
  59. package/dist/repl/continue-skill.js +5 -0
  60. package/dist/repl/level-command.js +6 -15
  61. package/dist/repl.js +21 -8
  62. package/dist/router/router-core.js +5 -3
  63. package/dist/session/audit-export.js +4 -1
  64. package/dist/skill-catalog.js +4 -7
  65. package/dist/tools/_team-work-path.js +53 -0
  66. package/dist/tools/approval.js +97 -19
  67. package/dist/tools/filesystem/file-edit.js +8 -1
  68. package/dist/tools/filesystem/file-write.js +8 -1
  69. package/dist/tools/local-files.js +31 -0
  70. package/dist/tools/project/_merge-shared.js +2 -2
  71. package/dist/tools/project/_team-tool-shared.js +5 -0
  72. package/dist/tools/project/cca_merge.js +1 -11
  73. package/dist/tools/project/rules_propose.js +2 -1
  74. package/dist/tools/project/team_findings.js +81 -28
  75. package/dist/tools/project/team_lane_move.js +127 -0
  76. package/dist/tools/project/upgrade_merge_progress.js +8 -4
  77. package/dist/tools/subagent/schedule_create.js +131 -0
  78. package/dist/tools/text-elements/tools.js +7 -1
  79. package/dist/tools/tool-loading.js +1 -1
  80. package/dist/tools/update-method-intercept.js +18 -1
  81. package/dist/tools/write-pipeline.js +19 -5
  82. package/dist/ui/approval-modal.js +6 -3
  83. package/dist/ui/error-emitter.js +21 -0
  84. package/dist/ui/footer-line.js +19 -1
  85. package/dist/ui/header.js +41 -0
  86. package/dist/ui/ink-classifier-route.js +19 -0
  87. package/dist/ui/rich-error-box.js +9 -0
  88. package/dist/ui/sidebar.js +12 -0
  89. package/dist/ui/status-emitter.js +19 -0
  90. package/dist/ui/status-line.js +62 -0
  91. package/dist/ui/status-row.js +38 -0
  92. package/dist/ui/todo-panel.js +85 -0
  93. package/dist/ui/widgets/ask-form.js +250 -0
  94. package/dist/ui/widgets/ask-question-modal.js +178 -0
  95. package/fiori/zlas-inspection-lr/package.json +26 -0
  96. package/fiori/zlas-inspection-lr/ui5-local.yaml +45 -0
  97. package/fiori/zlas-inspection-lr/ui5.yaml +41 -0
  98. package/fiori/zlas-inspection-lr/webapp/Component.js +12 -0
  99. package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +9 -0
  100. package/fiori/zlas-inspection-lr/webapp/index.html +35 -0
  101. package/fiori/zlas-inspection-lr/webapp/manifest.json +145 -0
  102. package/live-check-2026-10-17-1343.txt +41 -0
  103. package/package.json +121 -121
  104. package/test-harness/fe-draft/app.ts +0 -188
  105. package/test-harness/fe-draft/fe-draft.e2e.test.ts +0 -461
  106. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +0 -51
  107. package/test-harness/fe-draft/routes.ts +0 -224
  108. package/test-harness/fe-draft/routes.unit.test.ts +0 -45
  109. package/test-harness/fe-draft/server.ts +0 -225
  110. package/test-harness/fe-draft/variants.ts +0 -196
@@ -12,13 +12,16 @@
12
12
  * inside a slash command the journey queues or names.
13
13
  */
14
14
  import path from 'node:path';
15
- import { estateSummaryOf, provenForTargetWords, usageLineWithWindow } from '@cspeach/register-core';
16
- import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, ownerMatch, runTeamCommand, severalLine, whoAmI, writeExports, writeReports } from '../commands/team.js';
17
- import { EXPORTS_DIR, exportsOn, setExportsAlways } from '../register/export-files.js';
15
+ import { estateSummaryOf, PART_ORDER, PART_WORDS, provenForTargetWords, usageLineWithWindow } from '@cspeach/register-core';
16
+ import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, NO_CHECK_ALL_LINE, ownerMatch, runTeamCommand, severalLine, whoAmI, writeExports, writeReports } from '../commands/team.js';
17
+ import { EXPORTS_DIR, exportsOff, exportsOn, exportsPresent, setExportsAlways } from '../register/export-files.js';
18
18
  import { shown, shownUpTo } from '../register/atc-file.js';
19
+ import { withoutRuleId } from '../register/records.js';
19
20
  import { activeProject, readEstate, readLanes, readProjectFile, VARIANT_NAME } from '../register/store.js';
20
21
  import { registerFor } from '../register/picture.js';
21
- import { claimedSinceCheck, fixFailedCheck, myObjects, provenForTarget, readJourneyFacts, stepOf, toDecide, toFix, toProve, toRedesign, unreadableReason, STEPS, } from './clean-core.js';
22
+ import { TRANSPORT_NONE_WORDS, TRANSPORT_REQUEST, transportWords } from '../register/project-transport.js';
23
+ import { claimedSinceCheck, fixFailedCheck, myObjects, newestRecordAt, provenForTarget, readJourneyFacts, stepOf, toDecide, toFix, toProve, unreadableReason, STEPS, } from './clean-core.js';
24
+ import { doneFingerprint, setDoneFingerprint } from './flow.js';
22
25
  /* ── words ──────────────────────────────────────────────────────────────── */
23
26
  export const UNREADABLE_LINES = {
24
27
  'object-list': 'The project\'s object list in this folder can\'t be read right now — it may still be syncing. Try again in a minute.',
@@ -79,8 +82,14 @@ export function notRecordedLine(error) {
79
82
  export const objectShown = (name) => (OBJECT_NAME.test(name.toUpperCase()) ? name.toUpperCase() : shown(name));
80
83
  /** An object marked fix whose name CSPeach never puts in a command: one plain line, and it is passed over. */
81
84
  export const passedOverLine = (name) => `${objectShown(name)}: the name is not an ABAP name, so CSPeach passed it over.`;
85
+ /** Fix round 1: an object marked fix that the project's own view does not hold right now: one plain line, and it is passed over. */
86
+ export const notInViewLine = (name) => `${objectShown(name)}: it is not in the project's list right now, so CSPeach passed it over.`;
82
87
  /** F16: the check a project measures with, said as a line. */
83
88
  export const checkedWithLine = (variant) => ` Checked with: ${variant}.`;
89
+ /** Task 8 (B11): a project that holds no transport request yet, said as a line (headless, or None yet on the card). The words live in project-transport.ts. */
90
+ export const TRANSPORT_NONE_LINE = ` ${TRANSPORT_NONE_WORDS}`;
91
+ /** Task 8 (B11): the request a project's fixes are recorded in, said as a line. */
92
+ export const transportLine = (request) => ` ${transportWords(request)}`;
84
93
  /* ── commands the journey queues ────────────────────────────────────────── */
85
94
  export const OBJECT_TYPE = /^[A-Z]{4}$/;
86
95
  /** An ABAP object name: letters, digits, underscores, an optional /NAMESPACE/ in front. */
@@ -129,13 +138,17 @@ export function cleanReason(reason) {
129
138
  export function triageCommand(laneId) {
130
139
  return LANE_ID.test(laneId) ? `/abap-clean-core triage lane ${laneId}` : null;
131
140
  }
132
- /** `/abap-refactor <TYPE> <NAME> — <reason>`, names upper-cased; null when a name is not an ABAP name. */
141
+ /**
142
+ * `/abap-refactor <TYPE> <NAME> — <reason>`, names upper-cased; null when a
143
+ * name is not an ABAP name. A "Rule <id>:" an older record carries in its
144
+ * reason is left out (project view Task 4: rule ids never sit inside a reason).
145
+ */
133
146
  export function refactorCommand(o) {
134
147
  const type = o.type.trim().toUpperCase();
135
148
  const name = o.name.trim().toUpperCase();
136
149
  if (!OBJECT_TYPE.test(type) || !OBJECT_NAME.test(name))
137
150
  return null;
138
- const reason = cleanReason(o.reason);
151
+ const reason = cleanReason(withoutRuleId(String(o.reason ?? '')));
139
152
  return `/abap-refactor ${type} ${name}${reason ? ` — ${reason}` : ''}`;
140
153
  }
141
154
  /* ── the runner ─────────────────────────────────────────────────────────── */
@@ -196,6 +209,7 @@ function nextStepOnly(stage, ctx) {
196
209
  return result(stage, [
197
210
  `Next: ${step(stage, `start your project${named.length ? ` on ${named.join(', ')}` : ''} in this folder.`)}`,
198
211
  checkedWithLine(DEFAULT_ATC_VARIANT),
212
+ ...(ctx.transportAsked ? [TRANSPORT_NONE_LINE] : []),
199
213
  // I15: above 500 objects the first check is per package, said as a line.
200
214
  ...(objects !== null && objects > FIRST_CHECK_WHOLE_MAX ? [FIRST_CHECK_PER_PACKAGE_LINE] : []),
201
215
  ], { kind: 'stop' });
@@ -252,28 +266,68 @@ async function start(ctx) {
252
266
  const asWith = ctx.variant && VARIANT_NAME.test(ctx.variant) ? ['--atc-variant', ctx.variant] : [];
253
267
  // I15: above 500 objects the start measures nothing; the chain offers the per-package first check.
254
268
  const big = isBigStart(ctx.cwd, packages);
255
- const { code, said } = await quiet(ctx, ['start', ...packages, '--folder', ctx.cwd, ...asWith, ...(big ? ['--no-measure'] : [])]);
269
+ const developers = ctx.developers?.length ? ['--developers', ctx.developers.join(', ')] : [];
270
+ const transport = ctx.transport && TRANSPORT_REQUEST.test(ctx.transport) ? ['--transport', ctx.transport] : [];
271
+ const { code, said } = await quiet(ctx, ['start', ...packages, '--folder', ctx.cwd, ...asWith, ...(big ? ['--no-measure'] : []), ...developers, ...transport]);
256
272
  if (code !== 0)
257
273
  return stop(failure(said, 'The project could not be started.'));
258
274
  let name = packages.join(', ');
259
275
  let variant = asWith[1] ?? DEFAULT_ATC_VARIANT;
276
+ let stored;
260
277
  try {
261
278
  const project = readProjectFile(ctx.cwd);
262
279
  name = shown(project.name);
263
280
  if (project.atcVariant && VARIANT_NAME.test(project.atcVariant))
264
281
  variant = project.atcVariant;
282
+ const r = project.transport?.request;
283
+ if (typeof r === 'string' && TRANSPORT_REQUEST.test(r))
284
+ stored = r;
265
285
  }
266
286
  catch { /* the name and the check it was started with */ }
267
- const objects = said.map((l) => /^\s*([\d,]+) objects?, matched against/.exec(l)?.[1]).find(Boolean);
268
- const check = said.map((l) => /First check \([^)]*\): ([\d,]+) of ([\d,]+) objects? with findings/.exec(l)).find(Boolean);
287
+ // Project view Task 5: the counts are read from the folder's view, never from the lines the start printed.
288
+ const counts = startCounts(ctx);
289
+ const objects = counts?.objects;
290
+ const check = counts?.check;
269
291
  const lines = [`✓ Your project on ${name} started here.`, checkedWithLine(variant)];
292
+ // Task 8 (B11): the transport, said only when the start asked for it.
293
+ if (stored)
294
+ lines.push(transportLine(stored));
295
+ else if (ctx.transportAsked)
296
+ lines.push(TRANSPORT_NONE_LINE);
270
297
  if (objects)
271
- lines.push(` ${objects} ${objects === '1' ? 'object' : 'objects'}, matched against the system's count.`);
272
- lines.push(check ? ` First check: ${check[1]} of ${check[2]} objects with findings.` : ' No check yet.');
298
+ lines.push(` ${plural(objects, 'object', 'objects')}, matched against the system's count.`);
299
+ lines.push(check ? ` First check: ${num(check.withFindings)} of ${plural(check.checked, 'object', 'objects')} with findings.` : ' No check yet.');
300
+ // B1: each person's share, as the start wrote it (never the lane-move or results lines).
301
+ if (developers.length)
302
+ lines.push(...said.map(shareLine).filter((l) => l !== null));
273
303
  // Review T7 finding 2: init decides "big" from the live pull, which may differ from the inventory.
274
- const perPackage = !check && said.includes(FIRST_CHECK_PER_PACKAGE_LINE);
304
+ // B1: a start with people named measures nothing at all, so the first check runs per package too.
305
+ const perPackage = !check && (said.includes(FIRST_CHECK_PER_PACKAGE_LINE) || said.includes(NO_CHECK_ALL_LINE));
275
306
  return { ...result('START', lines, { kind: 'continue' }), ...(perPackage ? { firstCheckPerPackage: true } : {}) };
276
307
  }
308
+ /**
309
+ * What the start leaves in the folder, read from its project view: how many
310
+ * objects, and — when the start measured — how many of the objects checked
311
+ * have findings. Null when the folder cannot be read.
312
+ */
313
+ function startCounts(ctx) {
314
+ const view = facts(ctx).view;
315
+ if (!view)
316
+ return null;
317
+ const { all } = view;
318
+ return { objects: all.total, check: all.coverage.checked ? { checked: all.coverage.checked, withFindings: all.coverage.withFindings } : null };
319
+ }
320
+ /** ` James Miller (james): ZSD_ORDERS, ZMM_STOCK · 10 objects` and ` James Miller: no package yet.` as team start prints them. */
321
+ const SHARE = /^ {2}(.+?) \([A-Za-z0-9_-]+\): (.+) · (\d[\d,]* objects?)$/;
322
+ const NO_SHARE = /^ {2}(.+): no package yet\.$/;
323
+ /** One person's share for the journey: ` James Miller: ZSD_ORDERS, ZMM_STOCK · 10 objects.`; null for any other line. */
324
+ function shareLine(l) {
325
+ const m = SHARE.exec(l);
326
+ if (m)
327
+ return ` ${shown(m[1])}: ${shownUpTo(m[2], 200)} · ${m[3]}.`;
328
+ const none = NO_SHARE.exec(l);
329
+ return none ? ` ${shown(none[1])}: no package yet.` : null;
330
+ }
277
331
  /** Code objects the folder's inventory holds for these packages; null when it does not know them all. */
278
332
  function inventoryObjects(cwd, packages) {
279
333
  const found = readEstate(cwd);
@@ -406,7 +460,7 @@ function unowned(ctx) {
406
460
  /* DECIDE — the triage of my objects, a model step. */
407
461
  function decideStage(ctx) {
408
462
  const f = facts(ctx);
409
- const left = f.register ? toDecide(f.register, f.me, f.skip) : [];
463
+ const left = f.register ? toDecide(f.register, f.me, f.skip, f.view) : [];
410
464
  const lane = left[0]?.lane;
411
465
  const command = lane ? triageCommand(lane) : null;
412
466
  if (!command)
@@ -416,22 +470,33 @@ function decideStage(ctx) {
416
470
  /* FIX — the next object to fix, a model step. */
417
471
  function fixStage(ctx) {
418
472
  const f = facts(ctx);
419
- const left = f.register ? toFix(f.register, f.me, f.skip) : [];
420
- // A name that is not an ABAP name is passed over with one line; the next one is queued.
473
+ return fixStep(f.register ? toFix(f.register, f.me, f.skip, f.view) : [], f.view);
474
+ }
475
+ /**
476
+ * The fix step over the objects left to fix, in order: a name that is not an
477
+ * ABAP name, or an object the project view does not hold (fix round 2), is
478
+ * passed over with one line and put off for the session; the next one is queued.
479
+ */
480
+ export function fixStep(left, view) {
421
481
  const passed = [];
482
+ const skip = () => (passed.length ? { fix: new Set(passed.map((x) => x.key)) } : undefined);
422
483
  for (const o of left) {
423
484
  // The reason comes from the recorded decision only, never from the model's words.
424
485
  const command = refactorCommand({ type: o.type, name: o.name, ...(o.decision?.reason ? { reason: o.decision.reason } : {}) });
425
486
  if (!command) {
426
- passed.push(o);
487
+ passed.push({ key: o.key, line: passedOverLine(o.name) });
488
+ continue;
489
+ }
490
+ if (view && !view.object(o.key)) {
491
+ passed.push({ key: o.key, line: notInViewLine(o.name) });
427
492
  continue;
428
493
  }
429
- const lines = [...passed.map((x) => passedOverLine(x.name)), `${step('FIX', `fix ${o.name.toUpperCase()}.`)}`];
430
- if (fixFailedCheck(o))
494
+ const lines = [...passed.map((x) => x.line), `${step('FIX', `fix ${o.name.toUpperCase()}.`)}`];
495
+ if (view?.object(o.key)?.part === 'failed-check')
431
496
  lines.push(' Its last fix failed the check.');
432
- return result('FIX', lines, { kind: 'queue', command, key: o.key }, passed.length ? { fix: new Set(passed.map((x) => x.key)) } : undefined);
497
+ return result('FIX', lines, { kind: 'queue', command, key: o.key }, skip());
433
498
  }
434
- return result('FIX', passed.map((x) => passedOverLine(x.name)), { kind: 'continue' }, passed.length ? { fix: new Set(passed.map((x) => x.key)) } : undefined);
499
+ return result('FIX', passed.map((x) => x.line), { kind: 'continue' }, skip());
435
500
  }
436
501
  /** What a check of my claimed fixes measures: my one area, or the packages of the claimed objects. */
437
502
  function proveScope(f, waiting) {
@@ -439,7 +504,7 @@ function proveScope(f, waiting) {
439
504
  const joined = activeProject();
440
505
  const lanes = joined ? readLanes(joined.projectDir).filter((l) => l.owner.trim().toLowerCase() === f.me.trim().toLowerCase()) : [];
441
506
  if (lanes.length === 1)
442
- return { args: ['refresh'], objects: myObjects(r, f.me).length };
507
+ return { args: ['refresh'], objects: myObjects(r, f.me, f.view).length };
443
508
  const packages = [...new Set(waiting.map((o) => o.package))];
444
509
  return {
445
510
  args: ['refresh', '--packages', packages.join(',')],
@@ -469,7 +534,7 @@ async function prove(ctx) {
469
534
  const f = facts(ctx);
470
535
  if (!f.register)
471
536
  return result('PROVE', [UNREADABLE_LINES[unreadableReason(f) ?? 'other']], { kind: 'stop' });
472
- const waiting = toProve(f.register, f.me);
537
+ const waiting = toProve(f.register, f.me, f.view);
473
538
  if (!waiting.length)
474
539
  return result('PROVE', [], { kind: 'continue' });
475
540
  const scope = proveScope(f, waiting);
@@ -555,23 +620,29 @@ async function report(ctx) {
555
620
  return result('REPORT', lines, { kind: 'continue' }, { report: true });
556
621
  }
557
622
  const CSV_SAVED = '✓ Saved the CSV files for your own tools in the exports folder.';
623
+ const CSV_UPDATED = '✓ Updated the CSV files in the exports folder.';
558
624
  /**
559
625
  * The CSV export at REPORT (spec §3.4, D2): written when the project file's
560
626
  * switch is on; otherwise one card. "Every time" turns the switch on (visible
561
627
  * in the project file), "Just this once" writes them, "No" or a card put away
562
- * writes nothing. A headless chain never reaches REPORT (it stops at the
563
- * next-step line before it), so there is no card and no file there.
628
+ * writes nothing. CSV files already in the exports folder (G5) are rewritten
629
+ * without a card, so they never go stale. A headless chain never reaches
630
+ * REPORT (it stops at the next-step line before it), so there is no card and
631
+ * no file there.
564
632
  */
565
633
  async function csvExport(ctx, projectDir, project) {
566
- const write = () => {
567
- const { saved, held } = writeExports(projectDir, project, path.join(projectDir, 'reports', EXPORTS_DIR), ctx.team.deps);
634
+ const dir = path.join(projectDir, 'reports', EXPORTS_DIR);
635
+ const write = (savedLine = CSV_SAVED) => {
636
+ const { saved, held } = writeExports(projectDir, project, dir, ctx.team.deps);
568
637
  return [
569
- ...(saved.length ? [CSV_SAVED] : []),
638
+ ...(saved.length ? [savedLine] : []),
570
639
  ...held.map((name) => ` "${name}" is open in another program, so it was not updated. Close it and say carry on.`),
571
640
  ];
572
641
  };
573
642
  if (exportsOn(project))
574
643
  return write();
644
+ if (!exportsOff(project) && exportsPresent(dir))
645
+ return write(CSV_UPDATED);
575
646
  const res = await ctx.team.confirm({
576
647
  id: 'journey-export',
577
648
  tag: 'QUESTION',
@@ -587,11 +658,26 @@ async function csvExport(ctx, projectDir, project) {
587
658
  setExportsAlways(projectDir);
588
659
  return [...write(), ' They are written with every report from now on.'];
589
660
  }
590
- /** The closing count of objects kept: "1 kept as is", "2 kept as they are". */
591
- export const keptPart = (n) => `${n} ${n === 1 ? 'kept as is' : 'kept as they are'}`;
661
+ const num = (n) => n.toLocaleString('en-US');
662
+ /** The closing count of objects kept: "1 kept as is", "2 kept as they are" (the view's words). */
663
+ export const keptPart = (n) => PART_WORDS.kept(n);
664
+ /**
665
+ * G2, project view Task 5: the DONE count, ` 18 objects: 4 clean, …` — my
666
+ * objects (`view.me`, retired and out of scope included), each in exactly one
667
+ * of the view's parts, said in the view's order and words, the zero parts left
668
+ * out. Null when there are no objects.
669
+ */
670
+ export function doneLine(mine) {
671
+ if (!mine.total)
672
+ return null;
673
+ const parts = PART_ORDER.filter((p) => mine.parts[p]).map((p) => PART_WORDS[p](mine.parts[p]));
674
+ return ` ${plural(mine.total, 'object', 'objects')}${parts.length ? `: ${parts.join(', ')}` : ''}.`;
675
+ }
592
676
  /* DONE — a short summary. */
593
677
  /** Said instead of "Done for now." when the project has no counted check at all. */
594
678
  export const NOTHING_MEASURED_LINE = 'Nothing is measured yet.';
679
+ /** B8: said instead of "Done for now." when "carry on" finds nothing new since the last DONE. */
680
+ export const NOTHING_LEFT_LINE = 'Nothing left to carry on.';
595
681
  /** The folder's project has no counted check run (I15, review T7 finding 1). */
596
682
  export function nothingMeasured(f) {
597
683
  return !!f.register && f.register.runs.length === 0;
@@ -607,19 +693,20 @@ function done(ctx) {
607
693
  if (nothingMeasured(f))
608
694
  return nothingMeasuredResult('DONE');
609
695
  const lines = ['Done for now.'];
610
- if (f.register) {
611
- const mine = myObjects(f.register, f.me);
612
- const count = (pred) => mine.filter(pred).length;
613
- const parts = [
614
- [count((o) => o.state === 'clean'), 'clean'],
615
- [count((o) => o.state === 'verified'), 'fix proven'],
616
- ].filter(([n]) => n > 0).map(([n, w]) => `${n} ${w}`);
617
- const kept = count((o) => o.decision?.classification === 'keep' && o.state !== 'clean');
618
- if (kept)
619
- parts.push(keptPart(kept));
620
- if (mine.length)
621
- lines.push(` ${plural(mine.length, 'object', 'objects')}${parts.length ? `: ${parts.join(', ')}` : ''}.`);
622
- const rebuild = toRedesign(f.register, f.me).length;
696
+ if (f.register && f.view) {
697
+ const mine = f.view.me(f.me);
698
+ // B8: "carry on" after DONE with nothing new since says so, then the same counts.
699
+ if (ctx.session) {
700
+ const print = `${newestRecordAt(f.register) ?? ''}|${f.register.runs.length}|${mine.active}`;
701
+ if (doneFingerprint(ctx.session) === print)
702
+ lines[0] = NOTHING_LEFT_LINE;
703
+ setDoneFingerprint(ctx.session, print);
704
+ }
705
+ // The view's parts (built with this session's skipped rows): retired and out of scope are parts of their own, so it adds up.
706
+ const counts = doneLine(mine);
707
+ if (counts)
708
+ lines.push(counts);
709
+ const rebuild = mine.parts.rebuild;
623
710
  if (rebuild)
624
711
  lines.push(` ${plural(rebuild, 'object', 'objects')} planned for a rebuild — handled separately.`);
625
712
  }
@@ -17,17 +17,18 @@
17
17
  * A pattern (*, %, ?) in such a sentence is refused in one line; no journey.
18
18
  *
19
19
  * Resume, in a folder that holds a project with no plan step pending: any of
20
- * "carry on" / "continue" / "continue clean core" picks up a journey this
21
- * session paused; "continue clean core" also resumes after a clean turn with
22
- * no answer pending; bare "carry on" / "continue" only as the session's first
23
- * words. Otherwise "continue" keeps its old meaning.
20
+ * "carry on" / "continue clean core" resumes when no turn was interrupted and
21
+ * no answer is pending (live re-run L2, 2026-10-21: after any turn, not only
22
+ * after DONE, a pause or as the first words). A bare "continue" often answers
23
+ * the turn before, so it resumes only after a pause or DONE, or as the first
24
+ * words. Right after a pause line any of the words resume. In a folder
25
+ * without a project, "continue" keeps its old meaning.
24
26
  */
25
27
  import { folderKind } from './clean-core.js';
26
- import { isJourneyPaused, isJourneyPausedJustNow, userTurnCount } from './flow.js';
28
+ import { isJourneyEndedJustNow, isJourneyPaused, isJourneyPausedJustNow, userTurnCount } from './flow.js';
27
29
  /** The runner's own words for a pattern (run.ts START). */
28
30
  export const PATTERN_LINE = 'Name each package in full, for example ZFI_AR — not a pattern.';
29
31
  const CC = String.raw `clean[\s-]?core`;
30
- const CLEAN_CORE = new RegExp(CC, 'i');
31
32
  /** Words in front of the verb that do not change what is asked. */
32
33
  const FILLER = new RegExp(String.raw `^(?:(?:let'?s|let us|please|now|ok(?:ay)?|so|then|and|right|go ahead and|time to|(?:i|we)(?:'d| would)? (?:like|want) to|(?:i|we) need to)[,]?\s+)+`, 'i');
33
34
  const QUESTION = /^(?:what|why|how|is|are|was|were|can|could|would|should|does|did|will|explain|tell|describe|which|when|where|who|whether|show)\b/i;
@@ -129,9 +130,8 @@ export function isResumePhrase(input) {
129
130
  * Resume needs a project in `cwd` and no plan step in flight. Then:
130
131
  * - right after a pause line, with no turn of the person's since, any of the
131
132
  * words resumes — even after an interrupted step (that is what paused it);
132
- * - otherwise nothing interrupted, no answer pending, and: a journey this
133
- * session paused, or the words name clean core, or it is the session's
134
- * first turn (text and image turns count; tool results do not).
133
+ * - otherwise nothing interrupted and no answer pending: the words resume,
134
+ * whatever turn came before (L2).
135
135
  */
136
136
  export function detectJourney(input, ctx) {
137
137
  const start = parseJourneyStart(input);
@@ -149,5 +149,11 @@ export function detectJourney(input, ctx) {
149
149
  return { kind: 'resume' };
150
150
  if (s.turnInterrupted || s.awaitingSkillAnswer)
151
151
  return null;
152
- return isJourneyPaused(ctx.session) || CLEAN_CORE.test(input) || userTurnCount(ctx.session) === 0 ? { kind: 'resume' } : null;
152
+ // A bare "continue" often answers the turn before, so it keeps its old rule:
153
+ // after a pause or DONE, or as the session's first words.
154
+ if (/^continue[.!]*$/i.test(input.trim())) {
155
+ return isJourneyPaused(ctx.session) || isJourneyEndedJustNow(ctx.session) || userTurnCount(ctx.session) === 0 ? { kind: 'resume' } : null;
156
+ }
157
+ // L2: "carry on" and "continue clean core" always resume here (after DONE, a pause, or any other turn).
158
+ return { kind: 'resume' };
153
159
  }
package/dist/one-shot.js CHANGED
@@ -78,10 +78,10 @@ import './tools/web/web_search.js';
78
78
  import './tools/project/project_context_get.js';
79
79
  import './tools/project/playbook_get.js';
80
80
  import './tools/project/convention_get.js';
81
- import './tools/project/cca_merge.js';
82
81
  import './tools/project/upgrade_merge_progress.js';
83
82
  import './tools/project/team_findings.js';
84
83
  import './tools/project/team_estate.js';
84
+ import './tools/project/team_lane_move.js';
85
85
  import './tools/subagent/_background-shared.js';
86
86
  import './tools/subagent/background_run.js';
87
87
  import './tools/subagent/monitor_emit.js';
@@ -97,6 +97,13 @@ function renderSapConnection(s) {
97
97
  if (s.currentTransport) {
98
98
  lines.push(` <current_transport>${escapeXml(s.currentTransport)}</current_transport>`);
99
99
  }
100
+ // Task 6 (project view): the request the project in this folder records its fixes in. Absent outside one.
101
+ if (s.projectTransport) {
102
+ lines.push(` <project_transport>${escapeXml(s.projectTransport)}</project_transport>`);
103
+ // Fix round 1: say plainly where a write with no request goes (the write tools' order).
104
+ lines.push(` <transport_rule>A write that names no transport goes to the project request ${escapeXml(s.projectTransport)}, `
105
+ + 'not to the current transport (a request that already holds the object, or the request of an approved plan, still wins).</transport_rule>');
106
+ }
100
107
  if (s.activeScope) {
101
108
  if (s.activeScope.package) {
102
109
  lines.push(` <active_package>${escapeXml(s.activeScope.package)}</active_package>`);